The ADMA infrastructure uses a dedicated VPC with public and private subnets across multiple availability zones. This design provides high availability, security isolation, and cost optimization.
VPC Architecture
Network Design
┌─────────────────────────────────────────────────────────────────────┐
│ VPC: 10.42.0.0/16 │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ AZ-A │ │ AZ-B │ │
│ │ │ │ │ │
│ │ ┌─────────────────┐ │ │ ┌─────────────────┐ │ │
│ │ │ Public Subnet A │ │ │ │ Public Subnet B │ │ │
│ │ │ 10.42.0.0/24 │ │ │ │ 10.42.1.0/24 │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ ┌───────┐ │ │ │ │ │ │ │
│ │ │ │ ALB │ │ │ │ │ │ │ │
│ │ │ └───┬───┘ │ │ │ │ │ │ │
│ │ └───────┼─────────┘ │ │ └─────────────────┘ │ │
│ │ │ │ │ │ │
│ │ ┌───────▼─────────┐ │ │ ┌─────────────────┐ │ │
│ │ │ Private Subnet A│ │ │ │ Private Subnet B│ │ │
│ │ │ 10.42.10.0/24 │ │ │ │ 10.42.11.0/24 │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ ECS Tasks │ │ │ │ ECS Tasks │ │ │
│ │ │ RDS Instance │ │ │ │ RDS Standby │ │ │
│ │ │ VPC Endpoints │ │ │ │ VPC Endpoints │ │ │
│ │ └─────────────────┘ │ │ └─────────────────┘ │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
│ Internet Gateway │
│ └─ Routes to public subnets │
└─────────────────────────────────────────────────────────────────────┘
VPC Configuration
The VPC is created in the network module:
# modules/network/main.tf
resource "aws_vpc" "this" {
cidr_block = var . vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = merge (var . tags , {
Name = " ${ local . name_prefix } -vpc"
})
}
Key settings:
enable_dns_support = true — Required for VPC endpoints
enable_dns_hostnames = true — Enables DNS resolution for ECS service discovery
Default CIDR: 10.42.0.0/16 (65,536 IP addresses)
Design Decision : The 10.42.0.0/16 range avoids common overlaps with corporate networks (often 10.0.0.0/8 or 172.16.0.0/12).
Availability Zones
The infrastructure automatically selects available AZs in your region:
data "aws_availability_zones" "available" {
state = "available"
}
locals {
azs = slice (
data . aws_availability_zones . available . names ,
0 ,
min (var . availability_zone_cnt , length (data . aws_availability_zones . available . names ))
)
}
Result : If you configure 2 public and 2 private subnets, they’ll be distributed across 2 AZs automatically.
Public Subnets
Public subnets host the Application Load Balancer (ALB) only:
resource "aws_subnet" "public" {
count = length (var . public_subnet_cidrs )
vpc_id = aws_vpc . this . id
cidr_block = var . public_subnet_cidrs [ count . index ]
availability_zone = local . azs [ count . index % length (local . azs )]
map_public_ip_on_launch = false
tags = merge (var . tags , {
Name = " ${ local . name_prefix } -public- ${ count . index + 1 } "
Tier = "public"
})
}
map_public_ip_on_launch = false — Even public subnets don’t auto-assign public IPs. The ALB receives a public IP through AWS’s managed service.
Default configuration:
public_subnet_cidrs = [
"10.42.0.0/24" , # 256 IPs in AZ-A
"10.42.1.0/24" # 256 IPs in AZ-B
]
Private Subnets
Private subnets host ECS tasks, RDS database, and VPC endpoints:
resource "aws_subnet" "private" {
count = length (var . private_subnet_cidrs )
vpc_id = aws_vpc . this . id
cidr_block = var . private_subnet_cidrs [ count . index ]
availability_zone = local . azs [ count . index % length (local . azs )]
map_public_ip_on_launch = false
tags = merge (var . tags , {
Name = " ${ local . name_prefix } -private- ${ count . index + 1 } "
Tier = "private"
})
}
Default configuration:
private_subnet_cidrs = [
"10.42.10.0/24" , # 256 IPs in AZ-A
"10.42.11.0/24" # 256 IPs in AZ-B
]
What runs here:
ECS Fargate tasks (frontend + backend)
RDS PostgreSQL instance
Interface VPC endpoints (ECR, CloudWatch, Secrets Manager)
Internet Gateway
The Internet Gateway provides internet access to public subnets:
resource "aws_internet_gateway" "this" {
vpc_id = aws_vpc . this . id
tags = merge (var . tags , {
Name = " ${ local . name_prefix } -igw"
})
}
Traffic flow:
ALB in public subnet receives external HTTP/HTTPS requests
ALB forwards traffic to ECS tasks in private subnets
ECS tasks respond through ALB back to internet via IGW
Route Tables
Public Route Table
Public subnets share a single route table with internet access:
resource "aws_route_table" "public" {
vpc_id = aws_vpc . this . id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway . this . id
}
tags = merge (var . tags , {
Name = " ${ local . name_prefix } -public-rt"
})
}
resource "aws_route_table_association" "public" {
count = length (aws_subnet . public )
subnet_id = aws_subnet . public [ count . index ] . id
route_table_id = aws_route_table . public . id
}
Routes:
Destination Target Description 10.42.0.0/16local VPC-internal traffic 0.0.0.0/0IGW All internet traffic
Private Route Tables
Each private subnet gets its own route table (prepared for future NAT Gateway if needed):
resource "aws_route_table" "private" {
count = length (aws_subnet . private )
vpc_id = aws_vpc . this . id
tags = merge (var . tags , {
Name = " ${ local . name_prefix } -private-rt- ${ count . index + 1 } "
})
}
resource "aws_route_table_association" "private" {
count = length (aws_subnet . private )
subnet_id = aws_subnet . private [ count . index ] . id
route_table_id = aws_route_table . private [ count . index ] . id
}
Routes:
Destination Target Description 10.42.0.0/16local VPC-internal traffic only
No NAT Gateway : Private subnets have NO route to the internet. All AWS service access is via VPC endpoints. This saves ~$32/month per NAT Gateway.
VPC Endpoints
VPC endpoints allow private subnet resources to access AWS services without internet:
Interface Endpoints
Created in the vpc_endpoints module for private AWS API access:
# modules/vpc_endpoints/main.tf
locals {
interface_endpoint_services = {
ecr_api = "com.amazonaws. ${ var . region } .ecr.api"
ecr_dkr = "com.amazonaws. ${ var . region } .ecr.dkr"
logs = "com.amazonaws. ${ var . region } .logs"
secretsmanager = "com.amazonaws. ${ var . region } .secretsmanager"
}
}
resource "aws_vpc_endpoint" "interface" {
for_each = local . interface_endpoint_services
vpc_id = var . vpc_id
service_name = each . value
vpc_endpoint_type = "Interface"
subnet_ids = var . private_subnet_ids
security_group_ids = [ var . endpoint_security_group ]
private_dns_enabled = true
tags = merge (var . tags , {
Name = " ${ local . name_prefix } - ${ each . key } -endpoint"
})
}
Required endpoints:
ecr.api — ECR image manifest APIs
ecr.dkr — Docker image layer downloads
logs — CloudWatch Logs for ECS task logs
secretsmanager — Secrets Manager for DB password and JWT secret
S3 Gateway Endpoint
Gateway endpoint for S3 (ECR stores layers in S3):
resource "aws_vpc_endpoint" "s3" {
vpc_id = var . vpc_id
service_name = "com.amazonaws. ${ var . region } .s3"
vpc_endpoint_type = "Gateway"
route_table_ids = var . private_route_table_ids
tags = merge (var . tags , {
Name = " ${ local . name_prefix } -s3-endpoint"
})
}
Cost : Gateway endpoints are free. Interface endpoints cost 7 / m o n t h p e r e n d p o i n t p e r A Z . T h i s i n f r a s t r u c t u r e u s e s 4 i n t e r f a c e e n d p o i n t s a c r o s s 2 A Z s = 7/month per endpoint per AZ. This infrastructure uses 4 interface endpoints across 2 AZs = ~ 7/ m o n t h p ere n d p o in tp er A Z . T hi s in f r a s t r u c t u re u ses 4 in t er f a cee n d p o in t s a cross 2 A Z s = 56/month, still cheaper than 2 NAT Gateways ($64/month).
Network Flow Examples
External Request
Frontend → Backend
Backend → RDS
ECS Task Startup
User accesses https://go.example.com:
DNS resolves to ALB public IP
Request hits ALB in public subnet
ALB security group allows port 443
ALB forwards to frontend ECS task in private subnet
Frontend task processes request
Response flows back through ALB to user
Frontend container calls backend API:
Frontend uses service discovery DNS: backend.adma.internal:8080
AWS Cloud Map resolves to backend task IPs in private subnet
Frontend security group allows egress to backend SG on port 8080
Backend security group allows ingress from frontend SG
Backend responds directly (same VPC, no ALB)
Backend connects to database:
Backend uses RDS endpoint: adma-prod-postgres.xxxxx.eu-west-1.rds.amazonaws.com
DNS resolves to RDS instance private IP
Backend security group allows egress to DB SG on port 5432
DB security group allows ingress from backend SG
Connection established with TLS (enforced by rds.force_ssl=1)
ECS task pulls container image:
ECS agent needs image from ECR
Connects to ecr.api VPC endpoint (private DNS resolves to endpoint IP)
Endpoint security group allows ingress from task SG on port 443
ECR API returns image manifest
Agent downloads layers from S3 via S3 gateway endpoint
Task starts, reads secrets from Secrets Manager endpoint
High Availability Design
The network is designed for high availability:
Multi-AZ Deployment
Component AZ Distribution Purpose ALB All public subnets Automatic failover across AZs ECS Tasks All private subnets Tasks distributed by ECS scheduler RDS 2 AZs (if Multi-AZ enabled) Synchronous replication to standby VPC Endpoints All private subnets Redundant endpoint ENIs
Subnet Sizing Considerations
Each /24 subnet provides:
251 usable IPs (5 reserved by AWS)
AWS reserves: .0, .1 (router), .2 (DNS), .3 (future), .255
Private subnet IP usage:
ECS tasks: ~10-50 IPs (depends on scaling)
RDS: 1 IP (2 if Multi-AZ)
VPC endpoints: 4 endpoints × 1 IP each = 4 IPs
Total: ~60 IPs per subnet (leaves ~190 spare)
Public subnet IP usage:
ALB: ~8-10 IPs (AWS managed)
Total: ~10 IPs per subnet (leaves ~240 spare)
Network Security
Security is enforced at multiple layers:
Subnet Isolation : Private subnets have no internet route
Security Groups : See Security Groups →
NACLs : Default VPC NACLs allow all traffic (security enforced via SGs)
TLS in Transit :
ALB terminates TLS
Backend → RDS: TLS enforced via sslmode=require
ECS → VPC endpoints: TLS (port 443)
Cost Optimization
No NAT Gateway Strategy
Traditional approach:
Private subnet → NAT Gateway (public subnet) → Internet Gateway
Cost: $32.40/month per NAT Gateway + $0.045/GB processed
This infrastructure:
Private subnet → VPC Endpoints (interface/gateway) → AWS Services
Cost: Interface endpoints only (~$7/month per endpoint per AZ)
Savings : ~$40-50/month for typical workload
Limitation : ECS tasks cannot make arbitrary internet requests. All external service calls must go through VPC endpoints. If you need internet access (e.g., for third-party APIs), add a NAT Gateway to the root main.tf.
Endpoint Usage Patterns
Endpoint Used By Traffic Pattern ecr.api / ecr.dkr ECS Burst during task startup, idle otherwise logs ECS tasks Constant (application logs) secretsmanager ECS Once per task startup s3 ECS Burst during image pull (layers)
Module Outputs
The network module exposes these outputs for other modules:
# modules/network/outputs.tf
output "vpc_id" {
description = "ID of the created VPC"
value = aws_vpc . this . id
}
output "vpc_cidr" {
description = "CIDR block of the VPC"
value = aws_vpc . this . cidr_block
}
output "public_subnet_ids" {
description = "List of public subnet IDs"
value = aws_subnet . public [ * ] . id
}
output "private_subnet_ids" {
description = "List of private subnet IDs"
value = aws_subnet . private [ * ] . id
}
output "private_route_table_ids" {
description = "List of private route table IDs"
value = aws_route_table . private [ * ] . id
}
These outputs are used by:
security module (needs VPC ID and CIDR)
rds module (needs private subnet IDs)
ecs module (needs public and private subnet IDs)
vpc_endpoints module (needs VPC ID, private subnet IDs, route table IDs)