I recently completed a multi-cloud project where I deployed a weather tracking website on AWS as primary and Azure as secondary, with automatic failover using Route 53 health checks โ all provisioned with Terraform. In this post, I'll walk you through the full architecture, the steps I followed, and the real challenges I hit along the way (with fixes!).
๐ What We're Building
A static weather tracking website that:
- Runs on AWS S3 + CloudFront as the primary endpoint (HTTPS โ )
- Automatically fails over to Azure Blob Storage if AWS goes down
- Uses Route 53 for DNS management and health-check-based failover
- Is fully provisioned using Terraform
- Uses a custom domain registered on Namecheap
๐๏ธ Architecture Overview
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ AWS Cloud (PRIMARY) โ
โ โ
User โโโบ Route 53 โโโบโ CloudFront CDN โโโบ S3 Bucket โ
Hosted Zone โ (HTTPS + SSL) (Static Files) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
Health Check Fails?
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Azure Cloud (SECONDARY) โ
โ โ
โ Resource Group โ
โ โโโ Storage Account โ
โ โโโ Static Website โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The flow:
- User visits domain โ DNS request hits Route 53
- Route 53 checks health of primary (AWS CloudFront)
- If healthy โ serves from AWS S3 via CloudFront (fast, HTTPS)
- If unhealthy โ automatically reroutes to Azure Blob Storage
๐ ๏ธ Tech Stack
| Service | Role |
|---|---|
| AWS S3 | Static file hosting |
| AWS CloudFront | CDN + HTTPS |
| AWS Route 53 | DNS + failover routing |
| AWS ACM | SSL/TLS certificate |
| Azure Blob Storage | Secondary failover site |
| Terraform | Infrastructure as Code |
| Namecheap | Domain registrar |
๐ Step-by-Step Implementation
Step 1 โ Buy a Domain on Namecheap
Head to Namecheap and grab a domain. Extensions like .site, .tech, or .online can go as low as $1/year if you're on a budget.
Step 2 โ Request an SSL Certificate in ACM
โ ๏ธ Critical: Request the certificate in us-east-1 (N. Virginia) โ CloudFront only accepts certs from this region.
- Go to AWS Certificate Manager โ Request public certificate
- Add
yourdomain.comandwww.yourdomain.com - Choose DNS validation
- Click Create records in Route 53 (auto-validates via CNAME)
- Wait until status shows Issued โ
Step 3 โ Create CloudFront Distribution
In the AWS Console:
-
Origin domain: Use the S3 website endpoint โ
bucket-name.s3-website-us-east-1.amazonaws.com
โ ๏ธ Do NOT pick the default S3 REST endpoint. You must use the website endpoint for static hosting to work.
-
Viewer protocol policy:
Redirect HTTP to HTTPS -
Alternate domain names:
yourdomain.comandwww.yourdomain.com - Custom SSL certificate: Select your ACM cert
-
Default root object:
index.html
Deploy and copy your CloudFront domain (e.g., xxxxx.cloudfront.net).
Step 4 โ Terraform AWS Resources
Here's the core Terraform for S3, health checks, and Route 53:
# S3 Bucket for static website
resource "aws_s3_bucket" "weather_app" {
bucket = "weather-tracker-app-bucket-8jh7"
website {
index_document = "index.html"
error_document = "error.html"
}
lifecycle {
prevent_destroy = true
}
}
# Disable block public access BEFORE applying bucket policy
resource "aws_s3_bucket_public_access_block" "public_access" {
bucket = aws_s3_bucket.weather_app.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
# Bucket policy โ depends_on ensures block is lifted first
resource "aws_s3_bucket_policy" "bucket_policy" {
bucket = aws_s3_bucket.weather_app.id
depends_on = [aws_s3_bucket_public_access_block.public_access]
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "PublicReadGetObject"
Effect = "Allow"
Principal = "*"
Action = "s3:GetObject"
Resource = "arn:aws:s3:::${aws_s3_bucket.weather_app.id}/*"
}
]
})
}
# Route 53 Hosted Zone (imported โ already existed)
resource "aws_route53_zone" "main" {
name = "yourdomain.com"
lifecycle {
prevent_destroy = true
}
}
# Health check for CloudFront (primary)
resource "aws_route53_health_check" "aws_health_check" {
type = "HTTPS"
fqdn = "xxxxx.cloudfront.net"
port = 443
request_interval = 30
failure_threshold = 3
}
# Primary A record โ apex domain โ CloudFront
resource "aws_route53_record" "root" {
zone_id = aws_route53_zone.main.zone_id
name = "yourdomain.com"
type = "A"
alias {
name = "xxxxx.cloudfront.net"
zone_id = "Z2FDTNDATAQYW2"
evaluate_target_health = true
}
failover_routing_policy { type = "PRIMARY" }
set_identifier = "primary-root"
health_check_id = aws_route53_health_check.aws_health_check.id
}
# Primary CNAME โ www โ CloudFront
resource "aws_route53_record" "primary_www" {
zone_id = aws_route53_zone.main.zone_id
name = "www.yourdomain.com"
type = "CNAME"
records = ["xxxxx.cloudfront.net"]
ttl = 300
failover_routing_policy { type = "PRIMARY" }
set_identifier = "primary-www"
health_check_id = aws_route53_health_check.aws_health_check.id
}
Step 5 โ Terraform Azure Resources
resource "azurerm_resource_group" "rg" {
name = "rg-static-website"
location = "East US"
}
resource "azurerm_storage_account" "storage" {
name = "mystorageaccount0045"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
account_tier = "Standard"
account_replication_type = "LRS"
account_kind = "StorageV2"
static_website {
index_document = "index.html"
}
}
# Health check for Azure secondary โ use HTTPS
resource "aws_route53_health_check" "azure_health_check" {
type = "HTTPS"
fqdn = "mystorageaccount0045.z13.web.core.windows.net"
port = 443
request_interval = 30
failure_threshold = 3
}
# Secondary CNAME โ www โ Azure (failover)
resource "aws_route53_record" "secondary_www" {
zone_id = aws_route53_zone.main.zone_id
name = "www.yourdomain.com"
type = "CNAME"
records = ["mystorageaccount0045.z13.web.core.windows.net"]
ttl = 300
failover_routing_policy { type = "SECONDARY" }
set_identifier = "secondary-www"
health_check_id = aws_route53_health_check.azure_health_check.id
}
Step 6 โ Update Namecheap Nameservers
- Log in to Namecheap โ Domain List โ Manage
- Nameservers โ Custom DNS
- Enter Route 53 NS records from your hosted zone:
ns-123.awsdns-45.com
ns-234.awsdns-56.org
ns-345.awsdns-67.net
ns-456.awsdns-78.co.uk
- Save and wait up to 30 minutes
Step 7 โ Apply Terraform
terraform init
terraform apply -var-file="aws_credentials.tfvars" -var-file="azure_credentials.tfvars"
๐ฅ Real Challenges I Faced (and How I Fixed Them)
This is the part most tutorials skip. Here are the actual errors I hit and how I solved them.
โ Challenge 1: S3 Bucket Policy โ 403 AccessDenied
Error:
Error: putting S3 Bucket Policy: AccessDenied: User is not authorized
to perform: s3:PutBucketPolicy because public policies are prevented
by the BlockPublicPolicy setting
Cause: Terraform tried to apply the bucket policy before the BlockPublicPolicy setting was lifted.
Fix: Add depends_on to force the correct execution order:
resource "aws_s3_bucket_policy" "bucket_policy" {
depends_on = [aws_s3_bucket_public_access_block.public_access]
...
}
โ Challenge 2: Route 53 CNAME Conflict
Error:
InvalidChangeBatch: RRSet of type CNAME with DNS name www.yourdomain.com
is not permitted as it conflicts with other records with the same DNS name
Cause: You cannot have a CNAME and an A record on the same DNS name in Route 53. My primary was A type and secondary was CNAME โ same name, different types = conflict.
Fix: Use CNAME for both primary and secondary www records. Only the apex domain (yourdomain.com) can use an A alias record.
# www primary โ CNAME (not A)
resource "aws_route53_record" "primary_www" {
type = "CNAME"
records = ["xxxxx.cloudfront.net"]
...
}
# www secondary โ CNAME (matches primary type)
resource "aws_route53_record" "secondary_www" {
type = "CNAME"
records = ["yourstorageaccount.z13.web.core.windows.net"]
...
}
โ Challenge 3: Route 53 Alias Not Supported for Azure
Error:
Invalid combination of arguments: only one of alias, records can be
specified, but alias, records were specified.
Cause: I tried using a Route 53 alias block pointing to Azure. Route 53 alias records only work with AWS services (CloudFront, ELB, S3, etc.) โ there is no hosted zone ID for Azure.
Fix: Use records + ttl (standard CNAME) instead of an alias block for Azure:
# โ Wrong โ alias doesn't work for Azure
alias {
name = "yourstorageaccount.z13.web.core.windows.net"
zone_id = "some-azure-zone-id" # doesn't exist
}
# โ
Correct โ use records + ttl
records = ["yourstorageaccount.z13.web.core.windows.net"]
ttl = 300
โ Challenge 4: Type A Record with Domain Name Value
Error:
FATAL problem: ARRDATAIllegalIPv4Address: Value is not a valid IPv4
address encountered with 'yourstorageaccount.z13.web.core.windows.net'
Cause: I accidentally set type = "A" while passing a domain name in records. Type A only accepts IP addresses.
Fix: Change type to CNAME when the value is a domain name, not an IP.
โ Challenge 5: Imported Route 53 Zone Gets Destroyed
Cause: After importing the existing Route 53 hosted zone into Terraform state, running terraform destroy would delete it โ even though it was pre-existing.
Fix: Add prevent_destroy lifecycle rule to the imported resource:
resource "aws_route53_zone" "main" {
name = "yourdomain.com"
lifecycle {
prevent_destroy = true
}
}
Or remove it from state before destroying everything else:
terraform state rm aws_route53_zone.main
๐งช How to Test Failover
- Go to S3 โ Block public access โ Enable (breaks CloudFront origin)
- Wait 60โ90 seconds (3 failed checks ร 30s interval)
- Visit your domain โ traffic should now hit Azure
- Re-enable S3 public access to restore primary
๐ Expected Behavior
| Scenario | Result |
|---|---|
| Normal operation | AWS CloudFront โ HTTPS โ |
| AWS primary fails | Azure Blob Storage โ HTTP โ ๏ธ (expected) |
| AWS recovers | Automatically returns to primary โ |
The HTTP warning on Azure failover is expected behavior. Azure Blob Storage doesn't support HTTPS with custom domains without Azure CDN. In production, you'd configure Azure CDN โ but for this project, the disaster recovery mechanism is working correctly.
๐ Security Tips
Never commit credentials to GitHub. Add this to your .gitignore:
*credentials.tfvars
.terraform/
*.tfstate
*.tfstate.backup
๐ก Key Takeaways
- Route 53 alias records are AWS-only โ use CNAME for external services like Azure
- ACM certificates for CloudFront must be in us-east-1 โ regardless of your app's region
-
Terraform execution order matters โ use
depends_onwhen resource creation order is critical - A records only accept IPs โ use CNAME when pointing to a domain name
-
Protect imported resources โ always add
prevent_destroyto resources that existed before Terraform - Primary and secondary Route 53 records must share the same type on the same DNS name
๐ GitHub Repository
Check out the full source code here ๐ [https://github.com/awais684]
If this helped you, drop a โค๏ธ and feel free to ask questions in the comments. Happy cloud building! โ๏ธ
Top comments (0)