Skip to main content

Advanced VPC with ALB and EC2 Simplified

Updated Jan 24, 2021 ·

Overview

This is a continuation of the previous lab on Advanced VPC with ALB and EC2 instances. In this lab, we'll be simplifying the configuration by using "count meta" argument.

As a references, here's the diagram that we'll use.

Terraform files

We'll still use the same set of files.

provider.tf
terraform {
required_version = ">= 0.12"

required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 4.16.0"
}
}
}

provider "aws" {
region = var.aws_region
shared_credentials_files = var.my_credentials
profile = var.my_profile
}

variables.tf
# Variables for setting up terraform

variable "aws_region" {
description = "AWS region"
type = string
}

variable "my_credentials" {
description = "Credentials to be used to connect to AWS"
type = list(string)
}

variable "my_profile" {
description = "Profile to be used to connect to AWS"
type = string
}

variable "my_ip" {
type = string
}

# Variables for creating the VPC and EC2 instances

variable "instance_type" {
type = string
}

variable "avail_zones" {
type = list(string)
}

variable "cidr_block" {
type = string
}



terraform.tfvars
# Variables for setting up terraform
aws_region = "ap-southeast-1"
my_credentials = ["/mnt/c/Users/Eden.Jose/.aws/credentials"]
my_profile = "vscode-dev"

# Variables for creating the VPC and EC2 instances
avail_zones = ["ap-southeast-1a", "ap-southeast-1b", "ap-southeast-1c"]
instance_type = "t3.micro"
cidr_block = "10.0.0.0/16"

Here's the modified main file.

main.tf
### main.tf
#---------------------------------------------------------------------
# This terraform template deploys a VPC with 2 public subnets that has
# a security group, an internet gateway, a NAT gateway, and an
# Application loadbalancer. Traffic will be loadbalanced between the
# EC2 instances in the autoscaling group. Finally, the instances are
# bootstrapped with an NGINX webserver.
#---------------------------------------------------------------------

resource "aws_vpc" "lab04-vpc" {
cidr_block = var.cidr_block
instance_tenancy = "default"
enable_dns_support = "true"
enable_dns_hostnames = "true"

tags = {
Name = "lab04-vpc"
}
}

# Creates the public subnet 1 and 2
resource "aws_subnet" "lab04-public-subnet" {
map_public_ip_on_launch = true
vpc_id = aws_vpc.lab04-vpc.id
count = length(var.avail_zones)
cidr_block = cidrsubnet(var.cidr_block, 8, count.index)
availability_zone = element(var.avail_zones, count.index)

tags = {
Name = "lab04-public-subnet-${element(var.avail_zones, count.index)}"
Type = "Public"
}
}

# Creates the private subnet 1 and 2
resource "aws_subnet" "lab04-private-subnet" {
map_public_ip_on_launch = true
vpc_id = aws_vpc.lab04-vpc.id
count = length(var.avail_zones)
cidr_block = cidrsubnet(var.cidr_block, 8, count.index + length(var.avail_zones))
availability_zone = element(var.avail_zones, count.index)

tags = {
Name = "lab04-private-subnet-${element(var.avail_zones, count.index)}"
Type = "Public"
}
}

resource "aws_internet_gateway" "lab04-igw" {
vpc_id = aws_vpc.lab04-vpc.id

tags = {
Name = "lab04-igw"
}
}

# Create the Elastic IPs per availability zone
resource "aws_eip" "lab04-eip-nat" {
count = length(var.avail_zones)
vpc = true
}

# Creates the NAT gateway - Public NAT.
# This creates a NAT gateway in each availability zone.
resource "aws_nat_gateway" "lab04-natgw" {
count = length(var.avail_zones)
allocation_id = element(aws_eip.lab04-eip-nat.*.id, count.index)
subnet_id = element(aws_subnet.lab04-public-subnet.*.id, count.index)

tags = {
Name = "lab04-natgw-${element(var.avail_zones, count.index)}"
}

# To ensure proper ordering, it is recommended to add an
# explicit dependency on the Internet Gateway for the VPC.
depends_on = [aws_internet_gateway.lab04-igw]
}

#========================================================================
# Creates the route table. One route table per AZ
# This is a public route table that routes to the IGW.
resource "aws_route_table" "lab04-rt-public" {
vpc_id = aws_vpc.lab04-vpc.id

route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.lab04-igw.id
}

tags = {
Name = "lab04-rt-public"
}
}

# Associates the route table to the public subnet
resource "aws_route_table_association" "lab04-route-assoc-public" {
count = length(var.avail_zones)
subnet_id = element(aws_subnet.lab04-public-subnet.*.id, count.index)
route_table_id = aws_route_table.lab04-rt-public.id
}

#========================================================================
# This is a private route table that routes to the NAT-GW.
resource "aws_route_table" "lab04-rt-private" {
vpc_id = aws_vpc.lab04-vpc.id
count = length(var.avail_zones)

route {
cidr_block = "0.0.0.0/0"
gateway_id = element(aws_nat_gateway.lab04-natgw.*.id, count.index)
}

tags = {
Name = "lab04-rt-private-${element(var.avail_zones, count.index)}"
}
}

# Associates the route table to the subnets
resource "aws_route_table_association" "lab04-route-assoc-private" {
count = length(var.avail_zones)
subnet_id = element(aws_subnet.lab04-private-subnet.*.id, count.index)
route_table_id = element(aws_route_table.lab04-rt-private.*.id, count.index)
}
#========================================================================

# Creates the security group for the autoscaling group of wenservers
# Note that the egress traffic is routed to the ALB.
# This can be seen on the cidr_blocks of the second ingress.
resource "aws_security_group" "lab04-secgroup-1" {
name = "lab04-secgroup-1"
description = "Allow web server network traffic"
vpc_id = aws_vpc.lab04-vpc.id

ingress {
description = "SSH from my IP"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.my_ip]
}

ingress {
description = "HTTP from anywhere, through the ALB"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = [
cidrsubnet(var.cidr_block, 8, 1),
cidrsubnet(var.cidr_block, 8, 2)
]
}

egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}

tags = {
Name = "lab04-secgroup-1"
}
}

# Creates the security group for the ALB
# This allows inbound traffic from the internet and
# allows outbound traffic to go through only the webserber security group
resource "aws_security_group" "lab04-secgroup-2" {
name = "lab04-secgroup-2"
description = "Allow ALB network traffic"
vpc_id = aws_vpc.lab04-vpc.id

ingress {
description = "HTTP from anywhere"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

egress {
from_port = 0
to_port = 0
protocol = "-1"
security_groups = [aws_security_group.lab04-secgroup-1.id]
}

tags = {
Name = "lab04-secgroup-1"
}
}

# Launch template for the autoscaling group
# The "webserver.tpl" bootstraps the instances in the ASG wIth NGINX.
# This uses string interpolation to inject the current module path.
resource "aws_launch_template" "lab04-launchtemplate-webserver" {
name = "lab04-launchtemplate-webserver"

image_id = data.aws_ami.lab04_ami.id
instance_type = var.instance_type
key_name = aws_key_pair.lab04-keypair.id
vpc_security_group_ids = [aws_security_group.lab04-secgroup-1.id]

tag_specifications {
resource_type = "instance"

tags = {
Name = "lab04-webserver"
}
}
user_data = filebase64("${path.module}/webserver.tpl")
}

# Creates the external-facing ALB.
resource "aws_lb" "lab04-alb" {
name = "lab04-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.lab04-secgroup-2.id]
enable_deletion_protection = false
subnets = aws_subnet.lab04-public-subnet.*.id

tags = {
Environment = "PRD"
}
}

resource "aws_lb_target_group" "lab04-alb-target-group" {
name = "lab04-alb-target-group"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.lab04-vpc.id
}

resource "aws_lb_listener" "lab04-alb-front_end" {
load_balancer_arn = aws_lb.lab04-alb.arn
port = "80"
protocol = "HTTP"

default_action {
type = "forward"
target_group_arn = aws_lb_target_group.lab04-alb-target-group.arn
}
}

# Forwards the route path to the target group
resource "aws_lb_listener_rule" "lab04-alb-listener-rule-1" {
listener_arn = aws_lb_listener.lab04-alb-front_end.arn
priority = 100

action {
type = "forward"
target_group_arn = aws_lb_target_group.lab04-alb-target-group.arn
}

condition {
path_pattern {
values = ["/"]
}
}
}

# Creates the ASG of webserver instances.
resource "aws_autoscaling_group" "lab04-asg" {
name = "lab04-asg"
desired_capacity = 2
max_size = 5
min_size = 2
vpc_zone_identifier = aws_subnet.lab04-private-subnet.*.id

target_group_arns = [
aws_lb_target_group.lab04-alb-target-group.arn
]

launch_template {
id = aws_launch_template.lab04-launchtemplate-webserver.id
version = "$Latest"
}
}

# Imports the keypair
resource "aws_key_pair" "lab04-keypair" {
key_name = "lab04-keypair"
public_key = file("~/.ssh/tf-keypair.pub")
}

In here we can see that we now only have two resource aws_subnet, one for public and one for private. To understand what's assigned to the variables, we can use the console command.

terraform console

> var.avail_zones
tolist([
"ap-southeast-1a",
"ap-southeast-1b",
"ap-southeast-1c",

> cidrsubnet(var.cidr_block, 8, 0)
"10.0.0.0/24"
> cidrsubnet(var.cidr_block, 8, 1)
"10.0.1.0/24"
> cidrsubnet(var.cidr_block, 8, 2)
"10.0.2.0/24"
> cidrsubnet(var.cidr_block, 8, 3)
"10.0.3.0/24"
> cidrsubnet(var.cidr_block, 8, 4)
"10.0.4.0/24"

> element(var.avail_zones, 1)
"ap-southeast-1b"
> element(var.avail_zones, 2)
"ap-southeast-1c"
> element(var.avail_zones, 3)
"ap-southeast-1a"

Before running any terraform commands, we need to assign our IP to a variable. Make sure to append the "/32" after your IP address.

export TF_VAR_my_ip=1.2.3.4/32

Initialize the working directory.

terraform init

Check if there are errors with the formatting.

terraform fmt

Then check if the config files are valid.

terraform validate

Finally, do a review to check what changes will be introduce when you actually run the template.

terraform plan

If it doesn't return any error, you can now apply it.

terraform apply -auto-approve

It should return the Apply complete! message, along with the output values.

Apply complete! Resources: 6 added, 3 changed, 3 destroyed.

Outputs:

alb_dns_name = "lab04-alb-2074635167.ap-southeast-1.elb.amazonaws.com"
private_cidr_blocks = [
"10.0.3.0/24",
"10.0.4.0/24",
"10.0.5.0/24",
]
private_subnet_id = [
"subnet-0461d695d3f32a8d7",
"subnet-073703ca385d90aec",
"subnet-0b2e94adc05022505",
]
public_cidr_blocks = [
"10.0.0.0/24",
"10.0.1.0/24",
"10.0.2.0/24",
]
public_subnet_id = [
"subnet-02f1c37fbbe36d1e4",
"subnet-07d6f6e03c9283169",
"subnet-06ba41c814e38bd3e",
]
vpc_id = "vpc-03d74d1e4c0e55081"

Verify

Check in the AWS console if the resources are created. You may also copy the alb_dns_name link that's returned when you run the apply command and open it in your browser.

lab04-alb-2074635167.ap-southeast-1.elb.amazonaws.com

You should see the splash page for Nginx.

Cleanup

To delete all the resources, just run the destroy command.

terraform destroy -auto-approve

Resources