AWS Three-Tier Web Architecture: Reference Design with Terraform

· 2 min read · AWS Tutorials

The classic three-tier architecture on AWS #

The three-tier pattern separates a web application into presentation (what receives traffic), application (the business logic) and data (the database). Each tier lives in its own subnets, scales on its own and only talks to its neighbor. It is the base design of the AWS with Terraform series.

Internet
   │
[ Route 53 ] ──► [ ALB ]            public subnets  (2+ AZs)   <- presentation
                    │
              [ Auto Scaling EC2 ]  private subnets (2+ AZs)   <- application
                    │
                 [ RDS ]            isolated subnets (2+ AZs)  <- data

Components and where to learn them #

Layer Service Tutorial
Network VPC, subnets, route tables VPC, subnets, routing
Internet access Internet gateway and NAT gateway IGW, NAT
Entry point Route 53, ALB, ACM Route 53, load balancers
Compute EC2, Auto Scaling, AMI EC2, Auto Scaling
Data RDS Multi-AZ RDS
Access control Security groups, IAM Security groups, IAM

Design decisions #

High availability. Use at least two Availability Zones for every tier. The load balancer spreads traffic, the Auto Scaling group replaces failed instances, and RDS Multi-AZ fails over automatically.

Three layers of subnets. Public subnets only hold the load balancer and NAT gateways. Application instances are private, and the database subnets have no route to the Internet at all.

Security groups chained by reference. Instead of opening CIDR ranges, each tier only accepts traffic from the security group of the previous one:

security.tf
resource "aws_security_group" "alb" {
  name   = "ditwl-pro-alb"
  vpc_id = aws_vpc.main.id
}

resource "aws_vpc_security_group_ingress_rule" "alb_https" {
  security_group_id = aws_security_group.alb.id
  cidr_ipv4         = "0.0.0.0/0"
  ip_protocol       = "tcp"
  from_port         = 443
  to_port           = 443
}

resource "aws_security_group" "app" {
  name   = "ditwl-pro-app"
  vpc_id = aws_vpc.main.id
}

resource "aws_vpc_security_group_ingress_rule" "app_from_alb" {
  security_group_id            = aws_security_group.app.id
  referenced_security_group_id = aws_security_group.alb.id
  ip_protocol                  = "tcp"
  from_port                    = 8080
  to_port                      = 8080
}

resource "aws_security_group" "db" {
  name   = "ditwl-pro-db"
  vpc_id = aws_vpc.main.id
}

resource "aws_vpc_security_group_ingress_rule" "db_from_app" {
  security_group_id            = aws_security_group.db.id
  referenced_security_group_id = aws_security_group.app.id
  ip_protocol                  = "tcp"
  from_port                    = 5432
  to_port                      = 5432
}

Egress rules are omitted for brevity: security groups created with Terraform have no egress rules unless you add them.

No keys, no SSH. Instances use an IAM role and Session Manager for access, with VPC endpoints if there is no NAT.

Secrets outside the code. The database password is managed by Secrets Manager.

How to organize the Terraform code #

One module per layer, called from an environment directory (project structure):

environments/pro/main.tf
module "network" {
  source   = "../../modules/network"
  vpc_cidr = "10.10.0.0/16"
  az_count = 2
}

module "database" {
  source             = "../../modules/database"
  vpc_id             = module.network.vpc_id
  subnet_ids         = module.network.database_subnet_ids
  allowed_sg_id      = module.app.security_group_id
  multi_az           = true
}

module "app" {
  source              = "../../modules/app"
  vpc_id              = module.network.vpc_id
  private_subnet_ids  = module.network.private_subnet_ids
  public_subnet_ids   = module.network.public_subnet_ids
  domain_name         = "www.example.com"
}

Wire the outputs of each module to the inputs of the next (variables and outputs).

Operations #

Costs #

The biggest items are the NAT gateways (one per AZ), the load balancer and Multi-AZ RDS, which are charged by the hour even when idle. A single NAT gateway saves money in development but is a single point of failure. See cost estimation.

Evolution #

When the application is containerized, replace the EC2 tier with ECS and Fargate or EKS. For event-driven parts consider Lambda and SQS. Static content moves to S3 and CloudFront.

#AWS #Terraform #OpenTofu #Network