DEV Community

Cover image for Building a Multi-Cloud Weather Website with Automatic Disaster Recovery using Terraform
Muhammad Awais Zahid
Muhammad Awais Zahid

Posted on

Building a Multi-Cloud Weather Website with Automatic Disaster Recovery using Terraform

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         โ”‚
                     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
Enter fullscreen mode Exit fullscreen mode

The flow:

  1. User visits domain โ†’ DNS request hits Route 53
  2. Route 53 checks health of primary (AWS CloudFront)
  3. If healthy โ†’ serves from AWS S3 via CloudFront (fast, HTTPS)
  4. 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.

  1. Go to AWS Certificate Manager โ†’ Request public certificate
  2. Add yourdomain.com and www.yourdomain.com
  3. Choose DNS validation
  4. Click Create records in Route 53 (auto-validates via CNAME)
  5. 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.com and www.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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Step 6 โ€” Update Namecheap Nameservers

  1. Log in to Namecheap โ†’ Domain List โ†’ Manage
  2. Nameservers โ†’ Custom DNS
  3. 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
Enter fullscreen mode Exit fullscreen mode
  1. 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"
Enter fullscreen mode Exit fullscreen mode

๐Ÿ”ฅ 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
Enter fullscreen mode Exit fullscreen mode

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]
  ...
}
Enter fullscreen mode Exit fullscreen mode

โŒ 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
Enter fullscreen mode Exit fullscreen mode

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"]
  ...
}
Enter fullscreen mode Exit fullscreen mode

โŒ 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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

โŒ 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'
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

Or remove it from state before destroying everything else:

terraform state rm aws_route53_zone.main
Enter fullscreen mode Exit fullscreen mode

๐Ÿงช How to Test Failover

  1. Go to S3 โ†’ Block public access โ†’ Enable (breaks CloudFront origin)
  2. Wait 60โ€“90 seconds (3 failed checks ร— 30s interval)
  3. Visit your domain โ€” traffic should now hit Azure
  4. 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
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’ก 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_on when resource creation order is critical
  • A records only accept IPs โ€” use CNAME when pointing to a domain name
  • Protect imported resources โ€” always add prevent_destroy to 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)