Back to Blog

Disaster Recovery Made Simple: Multi-Cloud Website Hosting with AWS, Azure, Terraform, and Route 53

Built a multi-cloud weather tracker with AWS S3 + CloudFront (primary) and Azure Blob (standby). Route 53 health checks switch traffic automatically during outages. Infra is 100% Terraform.

Imagine a weather tracking app that people rely on daily. What happens if the AWS region hosting it goes down? You don't want users staring at error pages. That's the challenge I wanted to solve — a multi-cloud weather tracker that stays online even if one provider fails.

AWS S3 CloudFront Azure Blob Storage Route 53 Terraform ACM Namecheap

Why I Built This

So I built an app hosted on AWS S3 + CloudFront and backed up on Azure Blob Storage. Using Route 53 DNS Failover, the app can automatically redirect users to Azure if AWS goes down. All of this is automated with Terraform, so redeploying is as simple as running a command.

Key Goals

Services in Play

Multi-Cloud Disaster Recovery Architecture Diagram

Architecture — Route 53 DNS failover between AWS CloudFront/S3 (primary) and Azure Blob Storage (secondary)


Phase 1: Prerequisites & Setup

1. Install Terraform

Visit the Terraform Downloads page and install for your OS. On macOS:

brew install terraform
terraform --version

2. Configure AWS & Azure CLI

Install and configure the AWS CLI:

aws configure
# Provide Access Key ID, Secret Access Key, and default region

Install and log in with Azure CLI:

az login
az account show  # Note your Subscription ID and Tenant ID

Create a Service Principal for Terraform:

az ad sp create-for-rbac --role="Contributor" --scopes="/subscriptions/<subscription-id>"
# Note the appId (Client ID), password (Client Secret), and tenant (Tenant ID)

3. Create the Terraform Project Structure

mkdir multi-cloud-weather-tracker
cd multi-cloud-weather-tracker

Create the following files: main.tf, variables.tf, aws_credentials.tfvars, azure_credentials.tfvars.

variables.tf:

variable "aws_access_key" {}
variable "aws_secret_key" {}
variable "azure_client_id" {}
variable "azure_client_secret" {}
variable "azure_subscription_id" {}
variable "azure_tenant_id" {}

main.tf providers:

provider "aws" {
  access_key = var.aws_access_key
  secret_key = var.aws_secret_key
  region     = "us-east-1"
}

provider "azurerm" {
  client_id       = var.azure_client_id
  client_secret   = var.azure_client_secret
  subscription_id = var.azure_subscription_id
  tenant_id       = var.azure_tenant_id
}
Security: Add *.tfvars to .gitignore to prevent credentials from being pushed to version control.

4. Initialize Terraform

terraform init
terraform validate

Phase 2: AWS S3 Static Hosting

The website code is available at: github.com/Eweka01/Cloud-Projects. Download and rename the folder to website/ in your project root.

S3 bucket + static hosting (main.tf):

resource "aws_s3_bucket" "weather_app" {
  bucket = "weather-tracker-app-bucket-345382"

  website {
    index_document = "index.html"
    error_document = "error.html"
  }

  lifecycle {
    prevent_destroy = true
  }
}

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
}

resource "aws_s3_object" "website_index" {
  bucket       = aws_s3_bucket.weather_app.id
  key          = "index.html"
  source       = "website/index.html"
  content_type = "text/html"
}

resource "aws_s3_object" "website_assets" {
  for_each = fileset("website/assets", "*")
  bucket   = aws_s3_bucket.weather_app.id
  key      = "assets/${each.value}"
  source   = "website/assets/${each.value}"
}

resource "aws_s3_bucket_policy" "bucket_policy" {
  bucket = aws_s3_bucket.weather_app.id
  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}/*"
    }]
  })
}
terraform apply -var-file="aws_credentials.tfvars" -var-file="azure_credentials.tfvars"

Phase 3: Azure Blob Storage (Standby)

resource "azurerm_resource_group" "rg" {
  name     = "rg-static-website"
  location = "East US"
}

resource "azurerm_storage_account" "storage" {
  name                     = "mystorageaccount345382"
  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"
  }
}

resource "azurerm_storage_blob" "index_html" {
  name                   = "index.html"
  storage_account_name   = azurerm_storage_account.storage.name
  storage_container_name = "$web"
  type                   = "Block"
  content_type           = "text/html"
  source                 = "website/index.html"
}

resource "azurerm_storage_blob" "assets" {
  for_each = fileset("website/assets", "**/*")

  name                   = "assets/${each.value}"
  storage_account_name   = azurerm_storage_account.storage.name
  storage_container_name = "$web"
  type                   = "Block"
  content_type           = "application/octet-stream"
  source                 = "website/assets/${each.value}"
}

Phase 4: Disaster Recovery with Route 53 DNS Failover

1. Create a Hosted Zone

resource "aws_route53_zone" "main" {
  name = "yourdomain.com"
}

2. Request an SSL Certificate (ACM)

Go to AWS Certificate Manager → Request a public certificate for yourdomain.com and www.yourdomain.com. Use DNS validation. Once issued, attach to your CloudFront distribution.

3. Define Health Checks

resource "aws_route53_health_check" "aws_health_check" {
  type              = "HTTPS"
  fqdn              = "your-aws-cloudfront.cloudfront.net"
  port              = 443
  request_interval  = 30
  failure_threshold = 3
}

resource "aws_route53_health_check" "azure_health_check" {
  type              = "HTTPS"
  fqdn              = "your-azure-site.web.core.windows.net"
  port              = 443
  request_interval  = 30
  failure_threshold = 3
}

4. Setup Failover Records

resource "aws_route53_record" "primary" {
  zone_id = aws_route53_zone.main.zone_id
  name    = "yourdomain.com"
  type    = "A"

  alias {
    name                   = "your-aws-cloudfront.cloudfront.net"
    zone_id                = "Z2FDTNDATAQYW2"
    evaluate_target_health = true
  }

  failover_routing_policy { type = "PRIMARY" }
  set_identifier  = "primary"
  health_check_id = aws_route53_health_check.aws_health_check.id
}

resource "aws_route53_record" "secondary" {
  zone_id = aws_route53_zone.main.zone_id
  name    = "www.yourdomain.com"
  type    = "CNAME"
  records = ["your-azure-site.web.core.windows.net"]
  ttl     = 300

  failover_routing_policy { type = "SECONDARY" }
  set_identifier  = "secondary"
  health_check_id = aws_route53_health_check.azure_health_check.id
}

5. Update Namecheap Nameservers

Log in to Namecheap → Domain List → Manage → Nameservers → Custom DNS. Enter the 4 Route 53 nameservers from your hosted zone and save. DNS propagation can take up to 30 minutes.


Testing Failover

  1. Restrict S3 bucket public access (or disable CloudFront)
  2. Visit your domain — it should redirect to Azure automatically
  3. You'll see a "Not secure" warning on Azure — this is expected
  4. The site remains functional despite the warning
Expected behavior: Azure Blob Storage static websites don't support HTTPS with custom domains by default. In production, configure Azure CDN to enable HTTPS. For this setup, the HTTP fallback on Azure is normal and doesn't indicate a configuration issue.
Read Full Article on Medium

Lessons Learned

Multi-cloud IaC works. A single terraform apply provisions resources across both AWS and Azure simultaneously — no manual console work required.
View Code on GitHub Back to Blog