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
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.
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
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 regionsterraform workspace new mumbaiterraform workspace new singaporeterraform workspace list## Expected: mumbai, singapore ## Switch to mumbai workspaceterraform workspace select mumbaiCreate variables.tf:
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:
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
## 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
## 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 ALBresource "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 ALBresource "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 regionresource "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
## 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 Databaseaws rds failover-global-cluster \ --global-cluster-identifier production-global-db \ --target-db-cluster-identifier singapore-aurora-cluster ## Watch the failover progressaws 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 trafficcurl https://api.devopsnetwork.in/health## Expected: 200 response from Singapore regionValidation & Testing
## 1. Verify infrastructure in both regionsterraform workspace select mumbai && terraform outputterraform 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 Mumbaidig 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 listeneraws 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 5done## Expected: After ~30-60 seconds, all requests show Singapore ## 4. Verify RDS replication lagaws 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) measuredecho "Multi-region active-active architecture validated"Videos & Guides
AWS Multi-Region Architecture with Route53 — Full Tutorial
Complete multi-region AWS architecture tutorial covering Route53 latency-based routing, health checks, failover configuration, and RDS Global Database promotion.
Amazon Route53 Routing Policies Documentation
Official Route53 documentation for all routing policies including latency-based routing, health checks, failover routing, and geolocation — essential for multi-region architectures.
Amazon Aurora Global Database Documentation
Official AWS Aurora Global Database documentation covering cross-region replication, managed planned failover, unplanned failover, and recovery time objectives.