Build a Multi-Region Active-Active Architecture on AWS with Terraform

Provision identical app stacks in Mumbai and Singapore with Terraform workspaces, Route53 latency routing, RDS Global Database, and sub-60-second failover.

Domains & Technologies

Domains
ROUTE53RDSAWS-CLOUD-ENGINEERING
Technologies
TERRAFORMAWS

Blueprint Walkthrough

Architecture Overview

This project builds the highest level of cloud availability — an active-active multi-region architecture where your application runs simultaneously in two AWS regions. Users in India get served from Mumbai (ap-south-1). Users in Southeast Asia get served from Singapore (ap-southeast-1). If Mumbai goes down completely, all traffic automatically shifts to Singapore within 60 seconds — no manual intervention, no hotline calls at 3am.

This is how Hotstar serves cricket streaming to 50 million concurrent viewers during IPL — no single region failure can take down the platform because both regions are always actively serving traffic.

◈ DIAGRAM
Indian Users SE Asian Users
| |
v v
Route53 (Global DNS with latency routing)
| |
+----------------+ +------------------+
| Mumbai | | Singapore |
| ap-south-1 | | ap-southeast-1 |
| | | |
| ALB | | ALB |
| EC2 ASG | | EC2 ASG |
| RDS Primary |--->| RDS Read Replica |
| (read/write) | | (read, promoted |
| | | if Mumbai fails) |
+----------------+ +------------------+
| |
+----------+-----------+
|
RDS Global Database
(replication lag < 1 second)
Problem Solved

A single-region application has a single point of failure. AWS regions do have outages — ap-south-1 has had incidents that affected multiple availability zones simultaneously. When that happens to a single-region application, it goes completely offline until AWS resolves the incident — sometimes for hours.

With multi-region active-active, a regional failure means your application loses capacity but stays online. Route53 health checks detect the failing region within 30 seconds and stop routing traffic there. Singapore continues serving all users within 60 seconds of the Mumbai failure beginning.

Step-by-Step Implementation Guide

Step 1: Set Up Terraform Workspaces for Multi-Region

Bash
mkdir multi-region-app && cd multi-region-app
## Terraform workspaces let you use the same code for multiple environments
## Here we use them for different regions
terraform workspace new mumbai
terraform workspace new singapore
terraform workspace list
## Expected: mumbai, singapore
## Switch to mumbai workspace
terraform workspace select mumbai

Create variables.tf:

HCL
variable "region_config" {
type = map(object({
region = string
az_suffixes = list(string)
is_primary = bool
}))
default = {
mumbai = {
region = "ap-south-1"
az_suffixes = ["a", "b"]
is_primary = true # Mumbai hosts the RDS primary
}
singapore = {
region = "ap-southeast-1"
az_suffixes = ["a", "b"]
is_primary = false # Singapore has the read replica
}
}
}
locals {
# Automatically use the right config based on the workspace
config = var.region_config[terraform.workspace]
}

Create main.tf:

HCL
provider "aws" {
region = local.config.region
}
## The same VPC, ASG, and ALB code runs in both workspaces
## Only the region and is_primary flag differ
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
tags = {
Name = "${terraform.workspace}-vpc"
Region = local.config.region
}
}
resource "aws_subnet" "public" {
count = length(local.config.az_suffixes)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 4, count.index)
availability_zone = "${local.config.region}${local.config.az_suffixes[count.index]}"
map_public_ip_on_launch = true
tags = { Name = "${terraform.workspace}-public-${count.index}" }
}
resource "aws_autoscaling_group" "app" {
name = "${terraform.workspace}-app-asg"
vpc_zone_identifier = aws_subnet.public[*].id
target_group_arns = [aws_lb_target_group.app.arn]
health_check_type = "ELB"
min_size = 2
max_size = 10
desired_capacity = 2
launch_template {
id = aws_launch_template.app.id
version = "$Latest"
}
tag {
key = "Name"
value = "${terraform.workspace}-app-instance"
propagate_at_launch = true
}
}

Step 2: Set Up RDS Global Database

HCL
## This runs only in the mumbai workspace (primary region)
## The Singapore workspace gets a read replica automatically through Global Database
resource "aws_rds_global_cluster" "main" {
count = local.config.is_primary ? 1 : 0 # Only create in primary workspace
global_cluster_identifier = "production-global-db"
engine = "aurora-postgresql"
engine_version = "15.2"
database_name = "appdb"
storage_encrypted = true
}
resource "aws_rds_cluster" "app" {
cluster_identifier = "${terraform.workspace}-aurora-cluster"
engine = "aurora-postgresql"
engine_version = "15.2"
database_name = local.config.is_primary ? "appdb" : null
master_username = local.config.is_primary ? "postgres" : null
master_password = local.config.is_primary ? var.db_password : null
# Link to global cluster
global_cluster_identifier = local.config.is_primary ? aws_rds_global_cluster.main[0].id : var.global_cluster_id
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.rds.id]
# Automatic backups
backup_retention_period = 7
preferred_backup_window = "02:00-03:00"
deletion_protection = true
skip_final_snapshot = false
tags = {
Role = local.config.is_primary ? "primary" : "replica"
Region = local.config.region
}
}
resource "aws_rds_cluster_instance" "app" {
count = 2 # 2 instances per region
cluster_identifier = aws_rds_cluster.app.id
instance_class = "db.r6g.large"
engine = aws_rds_cluster.app.engine
engine_version = aws_rds_cluster.app.engine_version
performance_insights_enabled = true
monitoring_interval = 60
}

Step 3: Configure Route53 with Latency Routing and Health Checks

HCL
## Route53 is global — this runs once, not per workspace
## Put this in a separate route53 directory
provider "aws" {
region = "us-east-1" # Route53 is global but API is in us-east-1
alias = "global"
}
## Health check for Mumbai ALB
resource "aws_route53_health_check" "mumbai" {
provider = aws.global
fqdn = var.mumbai_alb_dns
port = 80
type = "HTTP"
resource_path = "/health"
failure_threshold = 3 # 3 consecutive failures = unhealthy
request_interval = 10 # Check every 10 seconds (fastest possible)
tags = { Name = "mumbai-health-check" }
}
## Health check for Singapore ALB
resource "aws_route53_health_check" "singapore" {
provider = aws.global
fqdn = var.singapore_alb_dns
port = 80
type = "HTTP"
resource_path = "/health"
failure_threshold = 3
request_interval = 10
tags = { Name = "singapore-health-check" }
}
## Latency-based routing — users go to the nearest region
resource "aws_route53_record" "mumbai" {
provider = aws.global
zone_id = var.hosted_zone_id
name = "api.devopsnetwork.in"
type = "CNAME"
latency_routing_policy {
region = "ap-south-1"
}
set_identifier = "mumbai"
health_check_id = aws_route53_health_check.mumbai.id
ttl = 30 # Low TTL = faster failover
records = [var.mumbai_alb_dns]
}
resource "aws_route53_record" "singapore" {
provider = aws.global
zone_id = var.hosted_zone_id
name = "api.devopsnetwork.in"
type = "CNAME"
latency_routing_policy {
region = "ap-southeast-1"
}
set_identifier = "singapore"
health_check_id = aws_route53_health_check.singapore.id
ttl = 30
records = [var.singapore_alb_dns]
}

Step 4: Implement Failover — Promote Singapore Replica

Bash
## This procedure runs when Mumbai has a major outage
## Document this BEFORE the outage — not during it
## Step 1: Verify Mumbai is actually failing (not a false alarm)
aws route53 get-health-check-status \
--health-check-id MUMBAI_HEALTH_CHECK_ID
## Expected during outage: HealthCheckObservations show failure
## Step 2: Promote Singapore read replica to standalone primary
## This takes 1-2 minutes for Aurora Global Database
aws rds failover-global-cluster \
--global-cluster-identifier production-global-db \
--target-db-cluster-identifier singapore-aurora-cluster
## Watch the failover progress
aws rds describe-global-clusters \
--global-cluster-identifier production-global-db \
--query 'GlobalClusters[0].GlobalClusterMembers[*].[DBClusterArn,IsWriter]'
## Expected: singapore-aurora-cluster shows IsWriter=true after 1-2 minutes
## Step 3: Update application config to point to Singapore primary
## If using a CNAME for the DB endpoint, update it:
aws route53 change-resource-record-sets \
--hosted-zone-id YOUR_ZONE_ID \
--change-batch '{
"Changes": [{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "db.devopsnetwork.in",
"Type": "CNAME",
"TTL": 60,
"ResourceRecords": [{"Value": "SINGAPORE_CLUSTER_ENDPOINT"}]
}
}]
}'
## Step 4: Verify Singapore is serving all traffic
curl https://api.devopsnetwork.in/health
## Expected: 200 response from Singapore region
Validation & Testing
Bash
## 1. Verify infrastructure in both regions
terraform workspace select mumbai && terraform output
terraform workspace select singapore && terraform output
## Expected: Both workspaces show ALB DNS names and ASG names
## 2. Test latency-based routing (from different locations)
## From India — should resolve to Mumbai
dig api.devopsnetwork.in
## From Singapore — should resolve to Singapore
## Use an online DNS propagation checker like dnschecker.org
## 3. Test health check failover
## Temporarily stop the Mumbai ALB listener
aws elbv2 modify-listener \
--listener-arn MUMBAI_LISTENER_ARN \
--default-actions '[{"Type":"fixed-response","FixedResponseConfig":{"StatusCode":"503"}}]'
## Wait 30-60 seconds for Route53 to detect the failure
## Run this in a loop and watch when it switches:
for i in {1..30}; do
REGION=$(curl -s https://api.devopsnetwork.in/region)
echo "$(date): Serving from $REGION"
sleep 5
done
## Expected: After ~30-60 seconds, all requests show Singapore
## 4. Verify RDS replication lag
aws cloudwatch get-metric-statistics \
--namespace AWS/RDS \
--metric-name AuroraGlobalDBReplicationLag \
--dimensions Name=DBClusterIdentifier,Value=singapore-aurora-cluster \
--start-time $(date -u -d '-1 hour' '+%Y-%m-%dT%H:%M:%SZ') \
--end-time $(date -u '+%Y-%m-%dT%H:%M:%SZ') \
--period 300 \
--statistics Average \
--region ap-southeast-1
## Expected: AuroraGlobalDBReplicationLag < 1000ms (1 second)
## 5. Run a full failover drill
## Time the entire failover from Mumbai failure detection to Singapore serving all traffic
## Document the actual RTO (Recovery Time Objective) measured
echo "Multi-region active-active architecture validated"