Part 2: Setting Up the Infrastructure
In a previous article, we looked at an introduction to the 3-tier architecture and outlined the design and benefits. In this part, we will be looking at setting up the infrastructure for the 3-tier architecture.
This project is made up of 9 parts:
The project code can be found here
Before building the three-tier architecture, we need a robust foundation. This involves creating the Virtual Private Cloud (VPC), subnets, internet gateways, and NAT gateways. These resources define the networking and connectivity of your environment, ensuring secure and efficient communication between tiers.
Step 1: Virtual Private Cloud (VPC)
What is a VPC?
VPC (Virtual Private Cloud) is an isolated private cloud within a public cloud. A public cloud is cloud infrastructure that is shared among multiple users. Creating a VPC is like having your own flat in a public building with several flats. Your space is private; you can set up your properties and decide who has access or not.
In AWS, a VPC is a logically isolated network where you can define resources such as subnets, gateways, and security groups. It acts as a private cloud for your infrastructure. It achieves this privacy and isolation by using virtual network tools like security groups and other network functionalities. That way, only permitted entities can gain access to whatever resources are created within the VPC.
Terraform Configuration for VPC
variable "vpcs" {
description = "Map of VPC configurations"
type = map(object({
cidr_block = string
enable_dns_hostnames = bool
assign_generated_ipv6_cidr_block = bool
tags = map(string)
}))
}
module "vpc" {
for_each = var.vpcs
source = "github.com/davidshare/terraform-aws-modules//vpc?ref=vpc-v1.0.0"
cidr_block = each.value.cidr_block
enable_dns_hostnames = each.value.enable_dns_hostnames
assign_generated_ipv6_cidr_block = each.value.assign_generated_ipv6_cidr_block
tags = each.value.tags
}
Explanation of Arguments
cidr_block
: Defines the IP address range for the VPC. Example:10.0.0.0/16
allows 65,536 IP addresses.enable_dns_hostnames
: Enables DNS hostnames for instances. Useful for accessing services using names instead of IPs.assign_generated_ipv6_cidr_block
: Assigns an IPv6 range to the VPC for dual-stack networking.tags
: Key-value pairs for identifying and organizing resources.
Example Input
vpcs = {
main = {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
assign_generated_ipv6_cidr_block = true
tags = {
Name = "MyVPC"
}
}
}
Example output
Step 2: Subnets
What are Subnets?
Again, for subnets, imagine that your flat in the public building of flats has several rooms. Each room can contain different furniture and can be used for different purposes. Also, you can control who goes into the room or not by using a lock on the door.
Subnets are smaller networks within the VPC. Each subnet exists within a specific availability zone. Whatever resources you create within that subnet will exist only within it’s availability zone, and you can control access to these resources.
In this project, we create 6 subnets. Two public subnets for the frontend tier, 2 private subnets for the backend tier, and 2 private subnets for the database tier.
Terraform Configuration for Subnets
variable "subnets" {
description = "Map of subnets with their configuration"
type = map(object({
cidr_block = string
availability_zone = string
public = bool
vpc_name = string
tags = map(string)
}))
}
# Module call for subnets
module "subnets" {
for_each = var.subnets
source = "github.com/davidshare/terraform-aws-modules//subnet?ref=subnet-v1.0.0"
vpc_id = module.vpc[each.value.vpc_name].id
cidr_block = each.value.cidr_block
availability_zone = each.value.availability_zone
map_public_ip_on_launch = each.value.public
tags = merge(each.value.tags, local.tags)
}
Explanation of Arguments
cidr_block
: The IP range for the subnet. Example:10.0.1.0/24
allows 256 IP addresses.availability_zone
: Specifies the AWS region’s zone (e.g.,us-east-1a
) for fault tolerance.public
: Indicates whether a public IP address will be automatically assigned to the EC2 instances created within the subnet. It is used to keep the subnet private by ensuring that there is no access to it from the internet.vpc_name
: Links the subnet to its parent VPC using themodule.vpc
output.tags
: Helps organize subnets.
Example Input
subnets = {
"primary_public_1" = {
cidr_block = "10.0.1.0/24"
availability_zone = "us-east-1a"
public = true
vpc_name = "main"
tags = {
Name = "PrimaryPublicSubnet1"
}
},
"primary_public_2" = {
cidr_block = "10.0.2.0/24"
availability_zone = "us-east-1b"
public = true
vpc_name = "main"
tags = {
Name = "PrimaryPublicSubnet2"
}
},
"primary_backend_private_1" = {
cidr_block = "10.0.3.0/24"
availability_zone = "us-east-1a"
public = false
vpc_name = "main"
tags = {
Name = "PrimaryBackendPrivateSubnet1"
}
},
"primary_backend_private_2" = {
cidr_block = "10.0.4.0/24"
availability_zone = "us-east-1b"
public = false
vpc_name = "main"
tags = {
Name = "PrimaryBackendPrivateSubnet2"
}
},
"primary_db_private_1" = {
cidr_block = "10.0.5.0/24"
availability_zone = "us-east-1a"
public = false
vpc_name = "main"
tags = {
Name = "PrimaryDBPrivateSubnet1"
}
},
"primary_db_private_2" = {
cidr_block = "10.0.6.0/24"
availability_zone = "us-east-1b"
public = false
vpc_name = "main"
tags = {
Name = "PrimaryDBPrivateSubnet2"
}
}
}
Example Output:
Step 3: Internet Gateway
What is an Internet Gateway?
An Internet Gateway (IGW) connects your VPC to the public internet, allowing resources in public subnets to access external services.
Remember that our VPC is private. So, the IGW is what helps us get internet access. For this project, we only need one IGW for our VPC.
Terraform Configuration for IGW
variable "internet_gateways" {
description = "Map of Internet Gateway configurations"
type = map(object({
vpc = string
tags = map(string)
}))
}
module "internet_gateway" {
for_each = var.internet_gateways
source = "github.com/davidshare/terraform-aws-modules//internet_gateway?ref=internet_gateway-v1.0.0"
vpc_id = module.vpc[each.value.vpc].id
tags = each.value.tags
}
Explanation of Arguments
vpc
: Links the Internet Gateway to a specific VPC.tags
: Metadata to identify the gateway.
Example Input
internet_gateways = {
main = {
vpc = "main"
tags = {
Name = "Main Internet Gateway"
}
}
}
Example output
Step 4: NAT Gateway
What is a NAT Gateway?
A NAT Gateway enables private subnets to access the internet for updates or external APIs while preventing inbound traffic. The NAT Gateways are always created in public subnets since the public subnets have access to the internet.
In this project, we created two NAT gateways, one for each availability zone. This is because the subnets are limited to availability zones.
Terraform Configuration for NAT Gateway
variable "nat_gateways" {
description = "Map of NAT Gateway configurations"
type = map(object({
vpc = string
elastic_ip = string
subnet = string
tags = map(string)
}))
}
module "nat_gateway" {
for_each = var.nat_gateways
source = "github.com/davidshare/terraform-aws-modules//nat_gateway?ref=nat_gateway-v1.0.0"
allocation_id = module.elastic_ip[each.value.elastic_ip].allocation_id
subnet_id = module.subnets[each.value.subnet].id
tags = each.value.tags
depends_on = [module.elastic_ip]
}
Explanation of Arguments
elastic_ip
: Allocates a static public IP for the NAT Gateway.subnet
: Places the NAT Gateway in a specific subnet (usually public).tags
: Helps identify the gateway.
Example Input
nat_gateways = {
primary_backend_private_1 = {
vpc = "main"
elastic_ip = "primary_backend_private_1"
subnet = "primary_public_1"
tags = {
Name = "Backend NAT 1"
Description = "Natgateway 1"
}
},
primary_backend_private_2 = {
vpc = "main"
elastic_ip = "primary_backend_private_2"
subnet = "primary_public_2"
tags = {
Name = "Backend NAT 2"
Description = "Natgateway 2"
}
}
}
Example Output:
Step 5: Route Tables
What are Route Tables?
Route tables define how traffic is routed within a VPC and between the internet, subnets, and other resources.
A route table is like a waiter in a restaurant that directs people to their seats. When traffic gets to the VPC, based on already set rules, the route table will direct the traffic to the appropriate subnet and resources.
Terraform Configuration for Route Tables
variable "route_tables" {
description = "Map of route table configurations"
type = map(object({
vpc = string
tags = map(string)
}))
}
module "route_table" {
for_each = var.route_tables
source = "github.com/davidshare/terraform-aws-modules//route_table?ref=route_table-v1.0.0"
vpc_id = module.vpc[each.value.vpc].id
tags = each.value.tags
}
Explanation of Arguments
vpc
: The VPC that the route table will be created in.tags
: Metadata to identify the gateway.
Example Input
route_tables = {
"primary_public" = {
vpc = "main"
tags = {
Name = "Frontend"
Description = "route table for the public subnets"
}
},
"primary_backend_private" = {
vpc = "main"
tags = {
Name = "Backend in AZ A"
Description = "route table for the private subnets in one AZ A"
}
},
"primary_db_private" = {
vpc = "main"
tags = {
Name = "Backend in AZ B"
Description = "route table for the private subnets in one AZ B"
}
}
}
Example outputs
Step 6: Routes
What are Routes?
Routes are the rules that determine how traffic is routed in your network. Each route is part of a Route Table, and the table determines where traffic is directed.
As you can see from the terraform configuration below, we separated the rules for the NAT gateway (private traffic) from those for the Internet gateway (public traffic). Each rule has an associated route table, the CIDR range, and the gateway.
Terraform Configuration for Routes
variable "internet_gateway_routes" {
description = "Map of routes for Internet Gateway"
type = map(object({
route_table = string
cidr = string
gateway = string
}))}
variable "nat_gateway_routes" {
description = "Map of routes for NAT Gateway"
type = map(object({
route_table = string
cidr = string
gateway = string
}))}
module "internet_gateway_routes" {
for_each = var.internet_gateway_routes
source = "github.com/davidshare/terraform-aws-modules//route?ref=route-v1.0.0"
route_table_id = module.route_table[each.value.route_table].id
destination_cidr_block = each.value.cidr
gateway_id = module.internet_gateway[each.value.gateway].id
depends_on = [module.internet_gateway, module.route_table]
}
module "nat_gateway_routes" {
for_each = var.nat_gateway_routes
source = "github.com/davidshare/terraform-aws-modules//route?ref=route-v1.0.0"
route_table_id = module.route_table[each.value.route_table].id
destination_cidr_block = each.value.cidr
nat_gateway_id = module.nat_gateway[each.value.gateway].id
depends_on = [module.nat_gateway, module.route_table]
}
Explanation of Key Arguments
route_table
: Specifies which route table the route is associated with.destination_cidr_block
: The CIDR range that matches the traffic for this route.0.0.0.0/0
represents all IPv4 traffic.gateway_id
: The ID of the Internet Gateway for public subnets.nat_gateway_id
: The ID of the NAT Gateway for private subnets.destination_ipv6_cidr_block
: Same asdestination_cidr_block
, but for IPv6 traffic.vpc_peering_connection_id
: Used to route traffic to a peered VPC.transit_gateway_id
: Used to route traffic through a Transit Gateway.
Example Input
internet_gateway_routes = {
public-web = {
route_table = "primary_public"
cidr = "0.0.0.0/0"
gateway = "main"
}
}
nat_gateway_routes = {
primary_backend_private_1 = {
route_table = "primary_backend_private"
cidr = "0.0.0.0/0"
gateway = "primary_backend_private_1"
},
primary_backend_private_2 = {
route_table = "primary_db_private"
cidr = "0.0.0.0/0"
gateway = "primary_backend_private_2"
},
}
Step 7: Route table association
What is a route table association?
A Route Table Association is used to assign subnets to route tables within a VPC. By associating a route table with specific subnets, you define how traffic is routed for those subnets. For example:
- Private subnets are associated with route tables that use a NAT Gateway for internet access or have no internet access at all.
- Public subnets are associated with route tables that have routes to an Internet Gateway (IGW).
Terraform Configuration for Routes Table Association
variable "subnets_route_table_association" {
description = "Map of subnet to route table associations"
type = map(object({
subnet = string
route_table = string
}))
}
module "subnets_route_table_associations" {
for_each = var.subnets_route_table_association
source = "github.com/davidshare/terraform-aws-modules//route_table_association?ref=route_table_association-v1.0.0"
subnet_id = module.subnets[each.value.subnet].id
route_table_id = module.route_table[each.value.route_table].id
depends_on = [module.subnets, module.route_table]
}
Explanation of Arguments
subnet_id
: The ID of the subnet that the route table will be associated with. This is obtained from the subnet module.route_table_id
: The ID of the route table to be associated with the subnet.
Example Input
subnets_route_table_association = {
primary_public_1 = {
route_table = "primary_public"
subnet = "primary_public_1"
},
primary_public_2 = {
route_table = "primary_public"
subnet = "primary_public_2"
},
primary_backend_private_1 = {
route_table = "primary_backend_private"
subnet = "primary_backend_private_1"
},
primary_backend_private_2 = {
route_table = "primary_backend_private"
subnet = "primary_backend_private_2"
},
primary_db_private_1 = {
route_table = "primary_db_private"
subnet = "primary_db_private_1"
},
primary_db_private_2 = {
route_table = "primary_db_private"
subnet = "primary_db_private_2"
},
}
Example output
Conclusion
In this section, we’ve established the foundation of our three-tier architecture by creating:
- A VPC to isolate our resources.
- Subnets for each tier of the architecture.
- An Internet Gateway for public communication.
- A NAT Gateway for secure outbound traffic from private subnets.
- Route Tables to direct network traffic appropriately.
- Routes for defining rules for flow of traffic
- Route table associations to associate route tables with subnets.
Next, we’ll move on to setting up security layers to protect our resources and define communication rules between tiers.