Prerequisites #
Install AWS CLI #
# Update packages
sudo apt update
# Unstall zip tool
sudo apt install unzip -y
# Download AWS CLI zip file
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
# Unzip
unzip awscliv2.zip
# Install
sudo ./aws/install
# Verify installation / check version
/usr/local/bin/aws --version
Configure AWS CLI #
# Start AWS CLI configuration
aws configure
Install Terraform #
# Install the HashiCorp GPG key
wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg > /dev/null
# Verify the GPG key fingerprint
gpg --no-default-keyring --keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg --fingerprint
# Add the official HashiCorp repository
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
# Install Terraform
sudo apt update && sudo apt-get install terraform
# Verify installation / check version
terraform version
Terraform Network Stack #
File and Folder Structure #
The file and folder structure of the Terraform project looks like this:
aws-vpc-stack
├── gateways.tf
├── outputs.tf
├── routetable.tf
├── subnets.tf
├── terraform.tf
└── vpc.tf
Terraform Configuration Files #
Project Folder & Terraform Provider #
# Create project folder and terraform.tf manifest
TF_PROJECT_NAME=aws-vpc-stack
mkdir $TF_PROJECT_NAME && cd $TF_PROJECT_NAME
cat << EOF >> "terraform.tf"
# Terraform Provider
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
# AWS Region
provider "aws" {
region = "us-east-1"
}
EOF
VPC #
- vpc.tf
# VPC
resource "aws_vpc" "us-east-vpc" {
cidr_block = "10.10.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "us-east-vpc"
Env = "Production"
}
}
Subnets #
- subnets.tf
# Public Subnet "us-east-1a", "10.10.0.0/24"
resource "aws_subnet" "public-us-east-1a" {
vpc_id = aws_vpc.us-east-vpc.id
cidr_block = "10.10.0.0/24"
availability_zone = "us-east-1a"
map_public_ip_on_launch = true
tags = {
Name = "public-us-east-1a-subnet"
Env = "Production"
}
}
# Private Subnet "us-east-1a", "10.10.1.0/24"
resource "aws_subnet" "private-us-east-1a" {
vpc_id = aws_vpc.us-east-vpc.id
cidr_block = "10.10.1.0/24"
availability_zone = "us-east-1a"
tags = {
Name = "private-us-east-1a-subnet"
Env = "Production"
}
}
# Private Subnet "us-east-1b", "10.10.2.0/24"
resource "aws_subnet" "private-us-east-1b" {
vpc_id = aws_vpc.us-east-vpc.id
cidr_block = "10.10.2.0/24"
availability_zone = "us-east-1b"
tags = {
Name = "private-us-east-1b-subnet"
Env = "Production"
}
}
map_public_ip_on_launch = true
Ensures instances launched in these subnets automatically get public IP addresses assigned
Routing Tables #
- routetable.tf
# Private Routing Table
resource "aws_route_table" "private-routetable" {
vpc_id = aws_vpc.us-east-vpc.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.nat-gw.id
}
tags = {
Name = "Private Route Table"
Env = "Production"
}
}
# Public Routing Table
resource "aws_route_table" "public-routetable" {
vpc_id = aws_vpc.us-east-vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.us-east-vpc-igw.id
}
tags = {
Name = "Public Route Table"
Env = "Production"
}
}
# Associate Routes with Subnets
resource "aws_route_table_association" "public-us-east-1a" {
subnet_id = aws_subnet.public-us-east-1a.id
route_table_id = aws_route_table.public-routetable.id
}
resource "aws_route_table_association" "private-us-east-1a" {
subnet_id = aws_subnet.private-us-east-1a.id
route_table_id = aws_route_table.private-routetable.id
}
resource "aws_route_table_association" "private-us-east-1b" {
subnet_id = aws_subnet.private-us-east-1b.id
route_table_id = aws_route_table.private-routetable.id
}
-
“Public Route Table”: Routes traffic to the Internet Gateway for public subnets
-
“Private Route Table” Routes traffic to the NAT Gateway for private subnets
NAT & Internet Gateway #
- gateways.tf
# Elastic IP (EIP) for NAT Gateway
resource "aws_eip" "nat-gw-eip" {
domain = "vpc"
tags = {
Name = "nat-gw-eip"
Env = "Production"
}
}
# NAT Gateway
resource "aws_nat_gateway" "nat-gw" {
allocation_id = aws_eip.nat-gw-eip.id
subnet_id = aws_subnet.public-us-east-1a.id
tags = {
Name = "nat-gw"
Env = "Production"
}
depends_on = [aws_internet_gateway.us-east-vpc-igw]
}
# Internet Gateway
resource "aws_internet_gateway" "us-east-vpc-igw" {
vpc_id = aws_vpc.us-east-vpc.id
tags = {
Name = "igw"
Env = "Production"
}
}
Outputs #
- outputs.tf
# VPC ID
output "vpc_id" {
value = aws_vpc.us-east-vpc.id
}
# Public Subnet ID
output "public_subnet_ids" {
value = [
aws_subnet.public-us-east-1a.id
]
}
# Private Subnet IDs
output "private_subnet_ids" {
value = [
aws_subnet.private-us-east-1a.id,
aws_subnet.private-us-east-1b.id
]
}
# Public Routing Table ID
output "public_route_table_id" {
value = aws_route_table.public-routetable.id
}
# Private Routing Table ID
output "private_route_table_id" {
value = aws_route_table.private-routetable.id
}
# Internet Gateway ID
output "internet_gateway_id" {
value = aws_internet_gateway.us-east-vpc-igw.id
}
# NAT Gateway ID
output "nat_gateway_id" {
value = aws_nat_gateway.nat-gw.id
}
Initialize Terraform Project #
This will download and install the AWS Terraform provider defined in the terraform.tf file with “hashicorp/aws”, as well as setting up the configuration files in the project directory.
# Initialize the Terraform project
terraform init
Validate Configuration Files #
# Validates the syntax and structure of Terraform configuration files
terraform validate
# Shell output:
Success! The configuration is valid.
Plan the Deployment #
# Dry run / preview changes before applying them
terraform plan
Apply the Configuration #
# Create network stack
terraform apply -auto-approve
# Shell output:
Apply complete! Resources: 12 added, 0 changed, 0 destroyed.
Outputs:
internet_gateway_id = "igw-025b1167fb41a3600"
nat_gateway_id = "nat-019e3e7d31efd0cfb"
private_route_table_id = "rtb-04b6018e1b09e5b77"
private_subnet_ids = [
"subnet-01025a39fc09b0eb4",
"subnet-0e1e8083740a9576b",
]
public_route_table_id = "rtb-05f48d1eab4588247"
public_subnet_ids = [
"subnet-09c34fbef36a7cb7d",
]
vpc_id = "vpc-01239b58cd289c4fc"
Verify Deployment State #
# Lists all resources tracked in the Terraform state file
terraform state list
# Shell output:
aws_eip.nat-gw-eip
aws_internet_gateway.us-east-vpc-igw
aws_nat_gateway.nat-gw
aws_route_table.private-routetable
aws_route_table.public-routetable
aws_route_table_association.private-us-east-1a
aws_route_table_association.private-us-east-1b
aws_route_table_association.public-us-east-1a
aws_subnet.private-us-east-1a
aws_subnet.private-us-east-1b
aws_subnet.public-us-east-1a
aws_vpc.us-east-vpc
Terraform Security Group & EC2 Instances Stack #
File and Folder Structure #
The file and folder structure of the Terraform project looks like this:
sg-ec2-stack
├── main.tf
├── outputs.tf
├── security-group.tf
├── terraform.tf
└── variables.tf
Terraform Configuration Files #
Project Folder & Terraform Provider #
# Create project folder and terraform.tf manifest
TF_PROJECT_NAME=sg-ec2-stack
mkdir $TF_PROJECT_NAME && cd $TF_PROJECT_NAME
cat << EOF >> "terraform.tf"
# Terraform Provider
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
# AWS Region
provider "aws" {
region = "us-east-1"
}
EOF
Variables #
- variables.tf
# Input Variables for VPC, Subnets, SSH key pair and EC2 image IDs
# VPC
variable "vpc_id" {
default = "vpc-01239b58cd289c4fc" # Define VPC ID
}
# Public Subnet
variable "public_subnet_id" {
default = "subnet-09c34fbef36a7cb7d" # Define public subnet ID
}
# Private Subnet 1
variable "private_subnet1_id" {
default = "subnet-01025a39fc09b0eb4" # Define private subnet 1 ID
}
# Private Subnet 2
variable "private_subnet2_id" {
default = "subnet-0e1e8083740a9576b" # Define private subnet 2 ID
}
# SSH key pair name
variable "key_name" {
default = "us-east-1-pc-le" # Define key pair name
}
# EC2 Image ID
variable "ami_id" {
default = "ami-0e2c8caa4b6378d8c" # Define EC2 AMI ID
}
Security Group #
- security-group.tf
# Security Group for SSH Access
resource "aws_security_group" "ssh_sg" {
name = "vpc-us-east-1-prod-sg"
description = "Security group for SSH access"
vpc_id = var.vpc_id
ingress {
description = "Allow SSH from anywhere"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
description = "Allow all outbound traffic"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "vpc-us-east-1-prod-sg"
Env = "Production"
}
}
EC2 Instances #
- main.tf
# EC2 Instance in Public Subnet
resource "aws_instance" "ec2_public_subnet" {
ami = var.ami_id
instance_type = "t2.micro"
subnet_id = var.public_subnet_id
key_name = var.key_name
vpc_security_group_ids = [aws_security_group.ssh_sg.id]
tags = {
Name = "public-subnet-vm"
Env = "Production"
}
}
# EC2 Instance in Private Subnet 1
resource "aws_instance" "ec2_private_subnet1" {
ami = var.ami_id
instance_type = "t2.micro"
subnet_id = var.private_subnet1_id
key_name = var.key_name
vpc_security_group_ids = [aws_security_group.ssh_sg.id]
tags = {
Name = "private-subnet-vm1"
Env = "Production"
}
}
# EC2 Instance in Private Subnet 2
resource "aws_instance" "ec2_private_subnet2" {
ami = var.ami_id
instance_type = "t2.micro"
subnet_id = var.private_subnet2_id
key_name = var.key_name
vpc_security_group_ids = [aws_security_group.ssh_sg.id]
tags = {
Name = "private-subnet-vm2"
Env = "Production"
}
}
Outputs #
- outputs.tf
# Security Group ID
output "security_group_id" {
description = "Security Group ID for SSH Access"
value = aws_security_group.ssh_sg.id
}
# EC2 Instance ID: VM in Public Subnet
output "public_instance_id" {
description = "EC2 instance ID in public subnet"
value = aws_instance.ec2_public_subnet.id
}
# EC2 Instance ID: VM in Private Subnet 1
output "private_instance1_id" {
description = "EC2 instance ID in private subnet 1"
value = aws_instance.ec2_private_subnet1.id
}
# EC2 Instance ID: VM in Private Subnet 2
output "private_instance2_id" {
description = "EC2 instance ID in private subnet 2"
value = aws_instance.ec2_private_subnet2.id
}
# EC2 Instance public IP: VM in Public Subnet
output "public_instance_public_ip" {
description = "Public IP address of the EC2 instance in the public subnet"
value = aws_instance.ec2_public_subnet.public_ip
}
# EC2 Instance private IP: VM in Public Subnet
output "public_instance_private_ip" {
description = "Private IP address of the EC2 instance in the public subnet"
value = aws_instance.ec2_public_subnet.private_ip
}
# EC2 Instance private IP: VM1 in Private Subnet
output "private_instance1_private_ip" {
description = "Private IP address of the EC2 instance in private subnet 1"
value = aws_instance.ec2_private_subnet1.private_ip
}
# EC2 Instance private IP: VM2 in Private Subnet
output "private_instance2_private_ip" {
description = "Private IP address of the EC2 instance in private subnet 2"
value = aws_instance.ec2_private_subnet2.private_ip
}
Initialize Terraform Project #
This will download and install the AWS Terraform provider defined in the terraform.tf file with “hashicorp/aws”, as well as setting up the configuration files in the project directory.
# Initialize the Terraform project
terraform init
Validate Configuration Files #
# Validates the syntax and structure of Terraform configuration files
terraform validate
# Shell output:
Success! The configuration is valid.
Plan the Deployment #
# Dry run / preview changes before applying them
terraform plan
Apply the Configuration #
# Create network stack
terraform apply -auto-approve
# Shell output:
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
Outputs:
private_instance1_id = "i-0db9361aed32b5926"
private_instance1_private_ip = "10.10.1.143"
private_instance2_id = "i-0c5358e163a4dc797"
private_instance2_private_ip = "10.10.2.27"
public_instance_id = "i-07fa9a5f8b310fb47"
public_instance_private_ip = "10.10.0.90"
public_instance_public_ip = "35.174.12.79"
security_group_id = "sg-01cfc6dbfaa99c467"
Verify Deployment State #
# Lists all resources tracked in the Terraform state file
terraform state list
# Shell output:
aws_instance.ec2_private_subnet1
aws_instance.ec2_private_subnet2
aws_instance.ec2_public_subnet
aws_security_group.ssh_sg
Verify Network Connectivity #
SSH into Public Subnet VM #
- SSH into the first VM in the public subnet via it’s public IPv4 address
# Copy the private SSH key to the VM in the public subnet
scp -i /home/ubuntu/.ssh/us-east-1-pc-le.pem /home/ubuntu/.ssh/us-east-1-pc-le.pem ubuntu@35.174.12.79:~/.ssh/
# SSH into the VM in the public subnet
ssh -i /home/ubuntu/.ssh/us-east-1-pc-le.pem ubuntu@35.174.12.79
- Use the first VM to SSH it into the VMs in the private subnets
SSH into Private Subnet VM 1 #
# SSH into the VM in the private subnet 1
ssh -i /home/ubuntu/.ssh/us-east-1-pc-le.pem ubuntu@10.10.1.143
# Verify the VM can reach the internet
ping www.google.com
# Shell output:
PING www.google.com (142.250.31.103) 56(84) bytes of data.
64 bytes from bj-in-f103.1e100.net (142.250.31.103): icmp_seq=1 ttl=57 time=2.69 ms
64 bytes from bj-in-f103.1e100.net (142.250.31.103): icmp_seq=2 ttl=57 time=1.96 ms
# Exit the VM in the private subnet 1
exit
SSH into Private Subnet VM 2 #
# SSH into the VM in the private subnet 2
ssh -i /home/ubuntu/.ssh/us-east-1-pc-le.pem ubuntu@10.10.2.27
# Verify the VM can reach the internet
ping www.google.com
# Shell output:
PING www.google.com (142.251.167.103) 56(84) bytes of data.
64 bytes from ww-in-f103.1e100.net (142.251.167.103): icmp_seq=1 ttl=105 time=3.11 ms
64 bytes from ww-in-f103.1e100.net (142.251.167.103): icmp_seq=2 ttl=105 time=3.11 ms
Cleanup #
# CD into the SG & EC2 Terraform project folder
cd sg-ec2-stack
# Delete resources
terraform destroy -auto-approve
# CD into the network stack Terraform project folder
cd aws-vpc-stack
# Delete resources
terraform destroy -auto-approve