# AWS Three-Tier Web Architecture: Reference Design with Terraform

> A reference architecture for a highly available three-tier web application on AWS: VPC, ALB, EC2 auto scaling and RDS, with the Terraform modules to build each layer.

- Source: https://www.itwonderlab.com/aws-three-tier-architecture-terraform/
- Published: 2026-04-03
- Updated: 2026-04-03
- Author: Javier Ruiz Jiménez (https://www.javierruizjimenez.com/)
- Site: IT Wonder Lab (https://www.itwonderlab.com/)

---

## 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](https://www.itwonderlab.com/tutorials/aws/).

```
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](https://www.itwonderlab.com/aws-vpc/), [subnets](https://www.itwonderlab.com/aws-subnets/), route tables | [VPC](https://www.itwonderlab.com/aws-terraform-tutorial-aws-vpc/), [subnets](https://www.itwonderlab.com/aws-terraform-tutorial-aws-subnets/), [routing](https://www.itwonderlab.com/aws-terraform-tutorial-aws-routing-tables/) |
| Internet access | [Internet gateway](https://www.itwonderlab.com/aws-internet-gateway/) and [NAT gateway](https://www.itwonderlab.com/aws-nat-gateway/) | [IGW](https://www.itwonderlab.com/aws-terraform-tutorial-aws-internet-gateway/), [NAT](https://www.itwonderlab.com/aws-terraform-tutorial-aws-nat-gateway/) |
| Entry point | [Route 53](https://www.itwonderlab.com/aws-route-53/), [ALB](https://www.itwonderlab.com/aws-elastic-load-balancing/), [ACM](https://www.itwonderlab.com/aws-acm/) | [Route 53](https://www.itwonderlab.com/aws-with-terraform-tutorial-aws-route-53/), [load balancers](https://www.itwonderlab.com/aws-terraform-tutorial-aws-load-balancers/) |
| Compute | [EC2](https://www.itwonderlab.com/aws-ec2/), [Auto Scaling](https://www.itwonderlab.com/aws-auto-scaling/), [AMI](https://www.itwonderlab.com/aws-ami/) | [EC2](https://www.itwonderlab.com/aws-terraform-tutorial-aws-ec2/), [Auto Scaling](https://www.itwonderlab.com/aws-terraform-tutorial-aws-auto-scaling/) |
| Data | [RDS](https://www.itwonderlab.com/aws-rds/) Multi-AZ | [RDS](https://www.itwonderlab.com/aws-terraform-tutorial-aws-rds/) |
| Access control | [Security groups](https://www.itwonderlab.com/aws-security-groups/), [IAM](https://www.itwonderlab.com/aws-iam/) | [Security groups](https://www.itwonderlab.com/aws-terraform-tutorial-aws-security-groups/), [IAM](https://www.itwonderlab.com/aws-terraform-tutorial-aws-iam-roles-policies/) |

### Design decisions

**High availability.** Use at least two [Availability Zones](https://www.itwonderlab.com/aws-regions-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:

```hcl title="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](https://www.itwonderlab.com/aws-terraform-tutorial-aws-iam-roles-policies/) and [Session Manager](https://www.itwonderlab.com/aws-systems-manager/) for access, with [VPC endpoints](https://www.itwonderlab.com/terraform-aws-vpc-endpoints/) if there is no NAT.

**Secrets outside the code.** The database password is managed by [Secrets Manager](https://www.itwonderlab.com/terraform-aws-kms-secrets-manager/).

### How to organize the Terraform code

One [module](https://www.itwonderlab.com/terraform-module/) per layer, called from an environment directory ([project structure](https://www.itwonderlab.com/terraform-project-structure/)):

```hcl title="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](https://www.itwonderlab.com/terraform-variables-outputs-locals/)).

### Operations

- [CloudWatch alarms](https://www.itwonderlab.com/terraform-aws-cloudwatch-alarms/) on ALB 5xx errors, unhealthy hosts, CPU and database storage.
- [Security monitoring](https://www.itwonderlab.com/terraform-aws-security-monitoring/) with CloudTrail and GuardDuty.
- Deploy through [CI/CD with OIDC](https://www.itwonderlab.com/terraform-github-actions-aws-oidc/).
- Backups: RDS automated backups, snapshots and a tested restore.

### 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](https://www.itwonderlab.com/terraform-cost-estimation-infracost/).

### Evolution

When the application is containerized, replace the EC2 tier with [ECS and Fargate](https://www.itwonderlab.com/containers-aws-ecs-terraform-fargate/) or [EKS](https://www.itwonderlab.com/terraform-eks/). For event-driven parts consider [Lambda](https://www.itwonderlab.com/terraform-aws-lambda-api-gateway/) and [SQS](https://www.itwonderlab.com/terraform-aws-sqs-sns/). Static content moves to [S3 and CloudFront](https://www.itwonderlab.com/terraform-s3-static-website-cloudfront/).
