Terraform Practice pt2: Security Groups, NAT & Internet Gateways, and Route Tables with Terraform on AWS
Tags: cloud, devops, terraform, aws
Published: 2022-06-15 07:23:31

Welcome to part 2 of the my practice with Terraform and HCL. In this article I will continue building my AWS infrastructure with Terraform. 

We will learn how to deploy security groups, gateways, and route tables. Let's start with security groups.

&nbsp;

## Security Groups

&nbsp;

There are 2 ways to secure subnets on AWS: `security groups` and `network ACLs` (also called NACLs). Security groups control inbound and outbound traffic for your instances, and network ACLs control inbound and outbound traffic for your subnets. In most cases security groups get the job done, but sometimes NACLs are required to provide additional level of security. In this tutorial we will only use security groups.

&nbsp;

One quirk of Terraform is that it disables `ALLOW ALL` rule for AWS Security Groups. If we want to enable it for our security group we need to specify it explicitly.

There will be 2 security groups: one for public subnet and one for private subnet. We will also define security group rules to allow traffic on certain ports.

&nbsp;

First, we will add public security group. It will allow all outgoing traffic and allow incoming traffic for ssh/http/https.

<br/>
Add public security group and rules to the `main.tf` file:
```terraform
# main.tf

# ... code above

# Public Security Group
resource "aws_security_group" "public_sg" {
  name = "Public Security Group"
  description = "Public internet access"
  vpc_id = aws_vpc.vpc.id
 
  tags = {
    Name        = "Security Group for Public Subnet"
    Teraform = "true"
  }
}

# rule for allowing all outgoing traffic
resource "aws_security_group_rule" "public_out" {
  type = "egress"
  from_port = 0
  to_port = 0
  protocol = "-1"
  cidr_blocks = ["0.0.0.0/0"]

  security_group_id = aws_security_group.public_sg.id
  
}

# rule for allowing ssh traffic for public sg
resource "aws_security_group_rule" "public_ssh_in" {
  type = "ingress"
  from_port = 22
  to_port = 22
  protocol = "tcp"
  cidr_blocks = ["0.0.0.0/0"]

  security_group_id = aws_security_group.public_sg.id
}

# rule for allowing http traffic for public sg
resource "aws_security_group_rule" "public_http_in" {
  type = "ingress"
  from_port = 80
  to_port = 80
  protocol = "tcp"
  cidr_blocks = ["0.0.0.0/0"]

  security_group_id = aws_security_group.public_sg.id
}

# rule for allowing https traffic for public sg
resource "aws_security_group_rule" "public_https_in" {
  type = "ingress"
  from_port = 443
  to_port = 443
  protocol = "tcp"
  cidr_blocks = ["0.0.0.0/0"]

  security_group_id = aws_security_group.public_sg.id
}
```

<br/>
Next we will add our private security group to `main.tf`:
```terraform
# main.tf

# ... some code above

# Private Security Group
resource "aws_security_group" "private_sg" {
  name        = "Private Security Group"
  description = "Private internet access"
  vpc_id      = aws_vpc.vpc.id

  tags = {
    Name     = "Security Group for Private Subnet"
    Teraform = "true"
  }
}

resource "aws_security_group_rule" "private_out" {
  type        = "egress"
  from_port   = 0
  to_port     = 0
  protocol    = "-1"
  cidr_blocks = ["0.0.0.0/0"]

  security_group_id = aws_security_group.private_sg.id
}

resource "aws_security_group_rule" "private_in" {
  type        = "ingress"
  from_port   = 0
  to_port     = 65535
  protocol    = "-1"
  cidr_blocks = [var.vpc_cidr]

  security_group_id = aws_security_group.private_sg.id
}
```

<br/>
It is a good time to add `output` blocks to be able to access security group ids in the future:
```terraform
# outputs.tf

output "security_group_public" {
  value = aws_security_group.public.id
}
 
output "security_group_private" {
  value = aws_security_group.private.id
}
```

<br/>
Format the code and check for syntax errors with `validate`:
```bash
$ terraform ftm
$ terraform validate
# Output
Success! The configuration is valid.
```
<br/>
Next we run plan to see what Terraform intends to deploy using the configuration:
```bash
$ terraform plan
# Output
Plan: 11 to add, 0 to change, 0 to destroy.
```
<br/>

As we can see there are 11 resources planned for deployment:
- 2 new security groups
- 2 rules for private sg
- 4 rules for public sg
- 2 subnets
- 1 vpc


&nbsp;


Let's deploy these resources with `apply` and verify deployment:
```bash 
$ terraform apply
# Output
aws_vpc.vpc: Creating...
aws_vpc.vpc: Creation complete after 2s 
aws_subnet.private_subnets: Creating...
aws_subnet.public_subnet: Creating...
aws_security_group.private_sg: Creating...
aws_security_group.public_sg: Creating...
aws_subnet.private_subnets: Creation complete after 1s 
aws_security_group.public_sg: Creation complete after 2s 
aws_security_group_rule.public_ssh_in: Creating...
aws_security_group_rule.public_https_in: Creating...
aws_security_group_rule.public_http_in: Creating...
aws_security_group_rule.public_out: Creating...
aws_security_group.private_sg: Creation complete after 2s 
aws_security_group_rule.private_out: Creating...
aws_security_group_rule.private_in: Creating...
aws_security_group_rule.public_https_in: Creation complete after 0s 
aws_security_group_rule.private_in: Creation complete after 0s 
aws_security_group_rule.private_out: Creation complete after 1s 
aws_security_group_rule.public_http_in: Creation complete after 1s 
aws_security_group_rule.public_ssh_in: Creation complete after 1s 
aws_security_group_rule.public_out: Creation complete after 2s 
aws_subnet.public_subnet: Still creating... [10s elapsed]
aws_subnet.public_subnet: Creation complete after 11s 

Apply complete! Resources: 11 added, 0 changed, 0 destroyed.
```

<br/>
Check AWS console to verify that security groups were indeed created along with corresponding rules.


&nbsp;


## Internet Gateway

Internet gateway is rather simple. This is how it will look in `main.tf`:
```terraform
# main.tf

# ... some code above

# Internet gateway
resource "aws_internet_gateway" "internet_gateway" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "demo_igw"
    Terraform = "true"
  }
}
```

<br/>
Format and validate:
```bash
$ terraform fmt
$ terraform validate
```


&nbsp;

## NAT Gateway

Instances in the private subnet can access the internet by using NAT Gateway that resides in the public subnet. It will also be used by database instance to connect to the internet for software updates. Connections originating from the internet would not be able to establish a connection to the instances in the private subnet.

NAT Gateway will also require an Elastic IP which we will add right beside it:

```terraform
# main.tf

# ... some code above

# NAT gateway EIP
resource "aws_eip" "nat_gateway_eip" {
  vpc = true
  depends_on = [
    aws_internet_gateway.internet_gateway
  ]

  tags = {
    Name      = "demo_nat_gateway"
    Terraform = "true"
  }
}

resource "aws_nat_gateway" "nat_gateway" {
  depends_on    = [aws_subnet.public_subnet]
  allocation_id = aws_eip.nat_gateway_eip.id
  subnet_id     = aws_subnet.public_subnet.id
  tags = {
    Name = "demo_nat_gateway"
    Terraform = "true"
  }
}
```

<br/>
Format, validate, and move on to Route Tables


&nbsp;

## Route Tables

In this section we will create 2 route tables: a public one and a private one. Then we will associate them with corresponding subnets.

```terraform
# main.tf

# ... some code above

# Public route table
resource "aws_route_table" "public_route_table" {
  vpc_id = aws_vpc.vpc.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.internet_gateway.id
  }
  tags = {
    Name      = "demo_public_rtb"
    Terraform = "true"
  }
}

resource "aws_route_table_association" "public" {
  depends_on     = [aws_subnet.public_subnet]
  route_table_id = aws_route_table.public_route_table.id
  subnet_id      = aws_subnet.public_subnet.id
}

# Private route table
resource "aws_route_table" "private_route_table" {
  vpc_id = aws_vpc.vpc.id

  route {
    cidr_block = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.nat_gateway.id
  }
  tags = {
    Name      = "demo_public_rtb"
    Terraform = "true"
  }
}

resource "aws_route_table_association" "private" {
  depends_on     = [aws_subnet.private_subnet]
  route_table_id = aws_route_table.private_route_table.id
  subnet_id      = aws_subnet.private_subnet.id
}
```
As usual, format and validate.

&nbsp;

Now we run `plan` command:
```bash
$ terraform plan
# Output
Plan: 18 to add, 0 to change, 0 to destroy.
```

<br/>
Full code for this article can be found [HERE](https://github.com/periwinkleFTW/terraform-path-to-cert/tree/master/practice-pt-2)


<br/>
It turned out to be quite lengthy article. Also, our `main.tf` file is getting too large. I think it is time to refactor it in the next chapter and make use of modules.

Thank you for reading and see you in the next chapter where I will attempt to use modules to compartmentalize my code better and start working on instances.