DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Terraform Practice pt2: Security Groups, NAT & Internet Gateways, and Route Tables with Terraform on AWS
Andrey Frol
Andrey Frol

Posted on • Updated on

Terraform Practice pt2: Security Groups, NAT & Internet Gateways, and Route Tables with Terraform on AWS

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.

Β 

Security Groups

Β 

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.

Β 

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.

Β 

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



Add public security group and rules to the main.tf file:

# 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
}
Enter fullscreen mode Exit fullscreen mode



Next we will add our private security group to main.tf:

# 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
}
Enter fullscreen mode Exit fullscreen mode



It is a good time to add output blocks to be able to access security group ids in the future:

# outputs.tf

output "security_group_public" {
  value = aws_security_group.public.id
}

output "security_group_private" {
  value = aws_security_group.private.id
}
Enter fullscreen mode Exit fullscreen mode



Format the code and check for syntax errors with validate:

$ terraform ftm
$ terraform validate
# Output
Success! The configuration is valid.
Enter fullscreen mode Exit fullscreen mode



Next we run plan to see what Terraform intends to deploy using the configuration:

$ terraform plan
# Output
Plan: 11 to add, 0 to change, 0 to destroy.
Enter fullscreen mode Exit fullscreen mode

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

Β 

Let's deploy these resources with apply and verify deployment:

$ 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.
Enter fullscreen mode Exit fullscreen mode



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

Β 

Internet Gateway

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

# 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"
  }
}
Enter fullscreen mode Exit fullscreen mode



Format and validate:

$ terraform fmt
$ terraform validate
Enter fullscreen mode Exit fullscreen mode

Β 

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:

# 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"
  }
}
Enter fullscreen mode Exit fullscreen mode



Format, validate, and move on to Route Tables

Β 

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.

# 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
}
Enter fullscreen mode Exit fullscreen mode

As usual, format and validate.

Β 

Now we run plan command:

$ terraform plan
# Output
Plan: 18 to add, 0 to change, 0 to destroy.
Enter fullscreen mode Exit fullscreen mode



Full code for this article can be found HERE



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.

Top comments (0)

🌚 Life is too short to browse without dark mode