In our previous guide, we explored how to create a VPC using Terraform. Now, let's dive deeper into AWS by creating public subnets using Terraform. Our focus will be on understanding the function of individual AWS resources rather than the syntax of Terraform.
By the end of this tutorial, you will have set up the following components in your AWS account:
- VPC
- Internet Gateway (IGW)
- Route Tables
- Subnets
- IAM Role
- Security Groups (SG)
- EC2 Instances
Prerequisites
Make sure you have the following installed and configured:
- The Terraform CLI (1.2.0+).
- The AWS CLI.
- An AWS account and credentials with sufficient permissions
Begin by setting up your AWS credentials. Export the following environment variables with your actual AWS credentials:
export AWS_ACCESS_KEY_ID=yourAccessKeyID
export AWS_SECRET_ACCESS_KEY=yourSecretAccessKey
export AWS_DEFAULT_REGION=yourAWSRegion
Next, create a variables.tf
file in your project directory and add the following:
variable "aws_region" {
type = string
default = "us-east-1"
}
variable "vpc_name" {
type = string
default = "my_vpc"
}
variable "environment" {
description = "The deployment environment (e.g., dev, qa, prod)"
type = string
validation {
condition = contains(["dev", "qa", "prod"], var.environment)
error_message = "The environment must be one of: dev, qa, or prod."
}
}
variable "env_cidr_map" {
description = "Map of environment names to CIDR block second octet"
type = map(string)
default = {
"dev" = "0"
"qa" = "10"
"prod" = "20"
}
}
variable "public_subnets" {
default = {
"public_subnet_1" = 1
"public_subnet_2" = 2
"public_subnet_3" = 3
}
}
Also, create a main.tf
file in the same directory with this initial content:
provider "aws" {
region = "us-east-1"
}
data "aws_availability_zones" "available" {}
data "aws_region" "current" {}
Create Public Subnets
Now, let's create the infrastructure as depicted in the above chart. Make sure to add the code examples to your main.tf
file.
VPC Creation
Configure the VPC with a specific IPv4 CIDR block based on the environment variable. For example, if you choose dev
as the environment, the IPv4 CIDR block will be 10.0.0.0/16
.
Refer to our previous post for more details on VPC creation.
resource "aws_vpc" "vpc" {
cidr_block = "10.${lookup(var.env_cidr_map, var.environment, "0")}.0.0/16"
assign_generated_ipv6_cidr_block = true
tags = {
Name = var.vpc_name
Environment = var.environment
Terraform = "true"
}
}
Default NACL
A Network Access Control List (NACL) is automatically created with the VPC.
Internet Gateway
Establish an Internet Gateway to allow internet access to the VPC.
resource "aws_internet_gateway" "internet_gateway" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "grit_coding_igw"
}
}
Subnets
Create a subnet in each of the three Availability Zones (AZs). Subnets will use the default NACL automatically. For custom NACL applications, you can modify NACL associations.
resource "aws_subnet" "public_subnets" {
for_each = var.public_subnets
vpc_id = aws_vpc.vpc.id
cidr_block = cidrsubnet(aws_vpc.vpc.cidr_block, 8, each.value)
availability_zone = tolist(data.aws_availability_zones.available.names)[each.value]
map_public_ip_on_launch = true
assign_ipv6_address_on_creation = true
ipv6_cidr_block = cidrsubnet(aws_vpc.vpc.ipv6_cidr_block, 8, each.value)
tags = {
Name = each.key
Terraform = "true"
}
}
Route Table
After creating the route table, add a route to the Internet Gateway. Subnets linked to this route table with the IGW route are defined as public subnets, facilitating access to and from the internet.
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
}
route {
ipv6_cidr_block = "::/0"
gateway_id = aws_internet_gateway.internet_gateway.id
}
tags = {
Name = "grit_coding_public_rtb"
Terraform = "true"
}
}
resource "aws_route_table_association" "public" {
depends_on = [aws_subnet.public_subnets]
route_table_id = aws_route_table.public_route_table.id
for_each = aws_subnet.public_subnets
subnet_id = each.value.id
}
With the public subnets in place, letโs add a few more resources to test the public subnet connection. We will need IAM Role, a Security Group, and EC2 instances.
IAM Role with AmazonSSMManagedInstanceCore
This role, when attached to an EC2 instance, provides the specified permissions. The AmazonSSMManagedInstanceCore
policy is necessary for access via the session manager, which is part of the AWS Systems Manager.
resource "aws_iam_role" "ec2_ssm_role" {
name = "ec2-ssm-role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Action = "sts:AssumeRole",
Effect = "Allow",
Principal = {
Service = "ec2.amazonaws.com"
}
}
]
})
tags = {
Terraform = "true"
}
}
resource "aws_iam_role_policy_attachment" "ssm_managed_instance_core" {
role = aws_iam_role.ec2_ssm_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_instance_profile" "ec2_ssm_instance_profile" {
name = "ec2-ssm-instance-profile"
role = aws_iam_role.ec2_ssm_role.name
}
Alternative Option: If you prefer using a key pair for EC2 access, create one in the AWS console, then specify its ID when creating the EC2 instance. Make sure to add an ingress rule for SSH (port 22) to the security group if you choose this method.
Security Group
We'll set up a security group with only egress rules. Ingress rules are not required since we are using the Systems Manager.
resource "aws_security_group" "ec2_sg" {
name = "EC2_Instance_SG"
description = "Security group for EC2 instances with default outbound rules"
vpc_id = aws_vpc.vpc.id
}
resource "aws_security_group_rule" "ec2_sg_egress_rule" {
type = "egress"
to_port = 0
protocol = "-1"
from_port = 0
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
security_group_id = aws_security_group.ec2_sg.id
}
EC2 Instance Creation
Create EC2 instances in each public subnet, attaching the security group and IAM role.
resource "aws_instance" "public_instance" {
for_each = aws_subnet.public_subnets
ami = "ami-0c101f26f147fa7fd"
instance_type = "t2.micro"
subnet_id = each.value.id
associate_public_ip_address = true
vpc_security_group_ids = [aws_security_group.ec2_sg.id]
iam_instance_profile = aws_iam_instance_profile.ec2_ssm_instance_profile.name
tags = {
Name = "Public_Instance_${each.key}"
Terraform = "true"
}
}
Deploy and Test
Execute the following commands in your project directory to deploy your Terraform configuration.
terraform init
terraform apply -auto-approve
Enter dev
as the environment value.
Connect to the EC2 instance using Session Manager. Run the following commands to retrieve the ID of the connected EC2 instance. You can find more information about the Session Manager in this post.
TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"`
curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id
Wrap-up
Today, we successfully set up public subnets and an EC2 instance, demonstrating how Infrastructure as Code (IaC) can make infrastructure deployment efficient, repeatable, and scalable.
For those new to cloud technology, these concepts can initially seem complex. However, I've found that hands-on experience, such as replicating these setups in the AWS console, makes learning more intuitive.
For further guidance, please watch the below demo video, which provides a manual walkthrough of the same setup in the AWS console.
You can find the source code from today's session in my GitHub Repository.
Note
If you're just beginning or not yet familiar with AWS terminology and concepts, consider looking into the AWS Cloud Practitioner certification. It provides comprehensive and clear insights into cloud concepts, making it an excellent starting point for beginners.
Top comments (1)
That is too complex, I need to take your note intro concideration