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
}
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
}
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
}
Format the code and check for syntax errors with validate
:
$ terraform ftm
$ terraform validate
# Output
Success! The configuration is valid.
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.
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.
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"
}
}
Format and validate:
$ terraform fmt
$ terraform validate
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"
}
}
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
}
As usual, format and validate.
Now we run plan
command:
$ terraform plan
# Output
Plan: 18 to add, 0 to change, 0 to destroy.
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)