Skip to main content

AWS Virtual Private Cloud (VPC): Create VPC and Build a Private and Public Subnet Scheme, Verify the Network Connectivity with EC2 Instances, Part 3: Terraform Version

2068 words·
AWS AWS CLI Terraform Virtual Private Cloud (VPC) Internet Gateway NAT Gateway Routing Tables EC2
Table of Contents
AWS-VPC - This article is part of a series.
Part 3: This Article
GitHub Repository Available

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
  • terraform.tf
# Terraform Provider
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

# Provider AWS Region
provider "aws" {
  alias  = "aws_region"
  region = var.aws_region
}

Variables
#

  • variables.tf
## AWS Region & Availability Zones

variable "aws_region" {
  description = "AWS Region"
  type        = string
  default     = "us-east-1"
}

variable "availability_zone_1" {
  description = "The availability zone for the subnet"
  type        = string
  default     = "us-east-1a"
}

variable "availability_zone_2" {
  description = "The availability zone for the subnet"
  type        = string
  default     = "us-east-1b"
}

variable "availability_zone_3" {
  description = "The availability zone for the subnet"
  type        = string
  default     = "us-east-1c"
}


## VPC: CIDR Blocks

variable "vpc_cidr" {
  description = "VPC CIDR block"
  type        = string
  default     = "10.10.0.0/16"
}

variable "vpc_subnet_cidr_1" {
  description = "Subnet CIDR block"
  type        = string
  default     = "10.10.0.0/24"
}

variable "vpc_subnet_cidr_2" {
  description = "Subnet CIDR block"
  type        = string
  default     = "10.10.1.0/24"
}

variable "vpc_subnet_cidr_3" {
  description = "Subnet CIDR block"
  type        = string
  default     = "10.10.2.0/24"
}

VPC & Subnets
#

  • vpc.tf
# VPC
resource "aws_vpc" "vpc" {
  provider = aws.aws_region
  cidr_block           = var.vpc_cidr
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name       = "vpc"
    Env        = "Production"
  }
}


# Public Subnet "10.10.0.0/24"
resource "aws_subnet" "vpc_subnet_public1" {
  provider = aws.aws_region
  vpc_id                  = aws_vpc.vpc.id
  cidr_block              = var.vpc_subnet_cidr_1
  availability_zone       = var.availability_zone_1
  map_public_ip_on_launch = true

  tags = {
    Name      = "VPC Subnet-Public-1"
    Env       = "Production"
  }
}

# Private Subnet "10.10.1.0/24"
resource "aws_subnet" "vpc_subnet_private1" {
  provider = aws.aws_region
  vpc_id            = aws_vpc.vpc.id
  cidr_block        = var.vpc_subnet_cidr_2
  availability_zone = var.availability_zone_1

  tags = {
    Name      = "VPC Subnet-Private-1"
    Env       = "Production"
  }
}

# Private Subnet "10.10.2.0/24"
resource "aws_subnet" "vpc_subnet_private2" {
  provider = aws.aws_region
  vpc_id            = aws_vpc.vpc.id
  cidr_block        = var.vpc_subnet_cidr_3
  availability_zone = var.availability_zone_2

  tags = {
    Name      = "VPC Subnet-Private-2"
    Env       = "Production"
  }
}
  • map_public_ip_on_launch = true Ensures instances launched in these subnets automatically get public IP addresses assigned

NAT & Internet Gateway
#

  • gateways.tf
# Elastic IP (EIP) for NAT Gateway
resource "aws_eip" "vpc_nat_gw_eip" {
  provider = aws.aws_region
  domain = "vpc"

  tags = {
    Name        = "VPC NAT-GW EIP"
    Env         = "Production"
  }
}

# NAT Gateway
resource "aws_nat_gateway" "vpc_nat_gw" {
  provider = aws.aws_region
  allocation_id = aws_eip.vpc_nat_gw_eip.id
  subnet_id     = aws_subnet.vpc_subnet_public1.id

  tags = {
    Name        = "VPC NAT-GW"
    Env         = "Production"
  }

  depends_on = [aws_internet_gateway.vpc_igw]
}

# Internet Gateway
resource "aws_internet_gateway" "vpc_igw" {
  provider = aws.aws_region
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name        = "VPC IGW"
    Env         = "Production"
  }
}

Routing Tables
#

  • routetable.tf
# Private Routing Table
resource "aws_route_table" "vpc_private_routetable" {
  provider = aws.aws_region
  vpc_id = aws_vpc.vpc.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.vpc_nat_gw.id
  }

  tags = {
    Name        = "VPC Private Route Table"
    Env         = "Production"
  }
}

# Public Routing Table
resource "aws_route_table" "vpc_public_routetable" {
  provider = aws.aws_region
  vpc_id = aws_vpc.vpc.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.vpc_igw.id
  }

  tags = {
    Name        = "VPC Public Route Table"
    Env         = "Production"
  }
}


# Associate Routes with Subnets
resource "aws_route_table_association" "vpc_subnet_public1_ra" {
  provider = aws.aws_region
  subnet_id      = aws_subnet.vpc_subnet_public1.id
  route_table_id = aws_route_table.vpc_public_routetable.id
}

resource "aws_route_table_association" "vpc_subnet_private1_ra" {
  provider = aws.aws_region
  subnet_id      = aws_subnet.vpc_subnet_private1.id
  route_table_id = aws_route_table.vpc_private_routetable.id
}

resource "aws_route_table_association" "vpc_subnet_private2_ra" {
  provider = aws.aws_region
  subnet_id      = aws_subnet.vpc_subnet_private2.id
  route_table_id = aws_route_table.vpc_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


Outputs
#

  • outputs.tf
# VPC ID
output "vpc_ID" {
  value = aws_vpc.vpc.id
}

# Public Subnet ID
output "VPC_Public_Subnet_IDs" {
  value = [
    aws_subnet.vpc_subnet_public1.id
  ]
}

# Private Subnet IDs
output "VPC_Private_Subnet_IDs" {
  value = [
    aws_subnet.vpc_subnet_private1.id,
    aws_subnet.vpc_subnet_private2.id
  ]
}


# Public Routing Table ID
output "VPC_Public_Route_Table_ID" {
  value = aws_route_table.vpc_public_routetable.id
}

# Private Routing Table ID
output "VPC_Private_Route_Table_ID" {
  value = aws_route_table.vpc_private_routetable.id
}

# Internet Gateway ID
output "VPC_Internet_Gateway_ID" {
  value = aws_internet_gateway.vpc_igw.id
}

# NAT Gateway ID
output "VPC_NAT_Gateway_ID" {
  value = aws_nat_gateway.vpc_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:

VPC_Internet_Gateway_ID = "igw-0e99ced819b1dabbf"
VPC_NAT_Gateway_ID = "nat-05ec00d7bc6a35cd0"
VPC_Private_Route_Table_ID = "rtb-0580f5ea559a9ebe8"
VPC_Private_Subnet_IDs = [
  "subnet-018c8424f296095fd",
  "subnet-0a9f8507df98af596",
]
VPC_Public_Route_Table_ID = "rtb-03f72ea80989e3deb"
VPC_Public_Subnet_IDs = [
  "subnet-00f1d96a487a6ca6b",
]
vpc_ID = "vpc-0607851416f6722b5"

Verify Deployment State
#

# Lists all resources tracked in the Terraform state file
terraform state list

# Shell output:
aws_eip.vpc_nat_gw_eip
aws_internet_gateway.vpc_igw
aws_nat_gateway.vpc_nat_gw
aws_route_table.vpc_private_routetable
aws_route_table.vpc_public_routetable
aws_route_table_association.vpc_subnet_private1_ra
aws_route_table_association.vpc_subnet_private2_ra
aws_route_table_association.vpc_subnet_public1_ra
aws_subnet.vpc_subnet_private1
aws_subnet.vpc_subnet_private2
aws_subnet.vpc_subnet_public1
aws_vpc.vpc



Terraform Security Group & EC2 Instances Stack
#

File and Folder Structure
#

The file and folder structure of the Terraform project looks like this:

aws-compute-stack
├── main.tf
├── outputs.tf
├── security-groups.tf
├── terraform.tf
└── variables.tf

Terraform Configuration Files
#

Project Folder & Terraform Provider
#

# Create project folder and terraform.tf manifest
TF_PROJECT_NAME=aws-compute-stack
mkdir $TF_PROJECT_NAME && cd $TF_PROJECT_NAME
  • terraform.tf
# Terraform Provider
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

# Provider for AWS Region
provider "aws" {
  alias  = "aws_region"
  region = var.aws_region
}

Variables
#

  • variables.tf
# Input Variables for VPC, Subnets, SSH key pair and EC2 image IDs

## AWS Region

variable "aws_region" {
  description = "AWS Region"
  type        = string
  default     = "us-east-1"
}


## VPC

variable "vpc_id" {
  default = "vpc-0607851416f6722b5" # Define VPC ID
}

# Public Subnet
variable "vpc_public_subnet1_id" {
  default = "subnet-00f1d96a487a6ca6b" # Define public subnet ID
}

# Private Subnet 1
variable "vpc_private_subnet1_id" {
  default = "subnet-018c8424f296095fd" # Define private subnet 1 ID
}

# Private Subnet 2
variable "vpc_private_subnet2_id" {
  default = "subnet-0a9f8507df98af596" # Define private subnet 2 ID
}


## SSH Key and EC2 Image

# 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
# vpc: Security Group for SSH Access and Ping
resource "aws_security_group" "vpc_sg" {
  provider = aws.aws_region
  name        = "SG"
  description = "Security group for SSH access and ping"
  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"]
  }

  ingress {
    description = "Allow ping"
    from_port   = 8
    to_port     = -1
    protocol    = "icmp"
    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 = "SG"
    Env  = "Production"
  }
}

EC2 Instances
#

  • main.tf
# EC2 Instance in Public Subnet
resource "aws_instance" "ec2_public_subnet1" {
  provider = aws.aws_region
  ami                    = var.ami_id
  instance_type          = "t2.micro"
  subnet_id              = var.vpc_public_subnet1_id
  key_name               = var.key_name
  vpc_security_group_ids = [aws_security_group.vpc_sg.id]

  tags = {
    Name = "public-subnet-vm"
    Env  = "Production"
  }
}

# EC2 Instance in Private Subnet 1
resource "aws_instance" "ec2_private_subnet1" {
  provider = aws.aws_region
  ami                    = var.ami_id
  instance_type          = "t2.micro"
  subnet_id              = var.vpc_private_subnet1_id
  key_name               = var.key_name
  vpc_security_group_ids = [aws_security_group.vpc_sg.id]

  tags = {
    Name = "private-subnet-vm1"
    Env  = "Production"
  }
}

# EC2 Instance in Private Subnet 2
resource "aws_instance" "ec2_private_subnet2" {
  provider = aws.aws_region
  ami                    = var.ami_id
  instance_type          = "t2.micro"
  subnet_id              = var.vpc_private_subnet2_id
  key_name               = var.key_name
  vpc_security_group_ids = [aws_security_group.vpc_sg.id]

  tags = {
    Name = "private-subnet-vm2"
    Env  = "Production"
  }
}

Outputs
#

  • outputs.tf
## Security Group

# vpc: Security Group ID
output "SecurityGroup_ID" {
  description = "Security Group ID for SSH Access"
  value       = aws_security_group.vpc_sg.id
}


## EC2 Instance IDs

# EC2 Instance ID: VM in Public Subnet
output "Public_Instance_ID" {
  description = "EC2 instance ID in public subnet"
  value       = aws_instance.ec2_public_subnet1.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 IPs

# 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_subnet1.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_subnet1.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-0f9366b298997a90d"
Private_Instance1_Private_IP = "10.10.1.34"
Private_Instance2_ID = "i-040a6448d70890554"
Private_Instance2_Private_IP = "10.10.2.205"
Public_Instance_ID = "i-01618137a7f80c719"
Public_Instance_Private_IP = "10.10.0.215"
Public_Instance_Public_IP = "34.235.161.192"
SecurityGroup_ID = "sg-0897bc1d51a5993f3"

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@34.235.161.192:~/.ssh/

# SSH into the VM in the public subnet
ssh -i /home/ubuntu/.ssh/us-east-1-pc-le.pem ubuntu@34.235.161.192
  • 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.34


# Verify the VM can reach the internet
ping www.google.com

# Shell output:
PING www.google.com (172.253.122.105) 56(84) bytes of data.
64 bytes from bh-in-f105.1e100.net (172.253.122.105): icmp_seq=1 ttl=57 time=2.96 ms
64 bytes from bh-in-f105.1e100.net (172.253.122.105): icmp_seq=2 ttl=57 time=2.49 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.205


# Verify the VM can reach the internet
ping www.google.com

# Shell output:
PING www.google.com (64.233.180.99) 56(84) bytes of data.
64 bytes from on-in-f99.1e100.net (64.233.180.99): icmp_seq=1 ttl=105 time=3.52 ms
64 bytes from on-in-f99.1e100.net (64.233.180.99): icmp_seq=2 ttl=105 time=3.03 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



Links #

# GitHub Repository
https://github.com/jueklu/terraform-aws-vpc-subnet-scheme
AWS-VPC - This article is part of a series.
Part 3: This Article