Scenario: Multi-Region Web App with AWS & Cloudflare

Jeeva-AWSLabsJourney
4 min readJan 29, 2025

--

Use Case:
You’re deploying a highly available web application with a primary AWS region in us-east-1 and a disaster recovery region in eu-west-2. The app uses an S3 static website and a Cloudflare CDN for global caching.

Requirements to Implement

  1. Use required_providers to explicitly define AWS & Cloudflare providers.
  2. Deploy an S3 bucket for static hosting in two AWS regions.
  3. Use conditional expressions to enable versioning only if it's the primary region.
  4. Use variables for region selection, bucket name, and Cloudflare settings.
  5. Use built-in functions (e.g., lookup, lower, join) for dynamic resource naming.

Terraform Implementation Plan

1. Define Required Providers & Multi-Region AWS Setup

  • Configure AWS in us-east-1 (primary) & eu-west-2 (DR).
  • Use Cloudflare for CDN.

2. Use Variables for Flexibility

  • Allow users to enable multi-region via a Boolean flag.
  • Store the Cloudflare zone ID dynamically.

3. Deploy S3 Buckets with Conditional Configurations

  • Enable versioning only in the primary region.
  • Use count and lookup() to deploy the secondary region only if enabled.

4. Integrate with Cloudflare

  • Create a Cloudflare DNS record dynamically.

Industry-standard Terraform structure for the multi-provider, multi-region deployment

Modularized structure
Terraform state management (S3 + DynamoDB)
Dynamic backend configuration
Secrets management with tfvars
Optimized for reusability

File Structure

terraform-multi-region-app/
│── modules/
│ ├── s3/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ ├── cloudflare/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── terraform.tfvars
│ │ ├── backend.tf
│ ├── prod/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── terraform.tfvars
│ │ ├── backend.tf
│── global/
│ ├── providers.tf
│ ├── versions.tf
│── README.md
│── .gitignore

📌 1. Terraform Version & Providers (global/versions.tf)

terraform {
required_version = ">= 1.5.0"
  required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.0"
}
}
}

📌 2. Providers & Remote State (global/providers.tf)

provider "aws" {
region = var.primary_region
alias = "primary"
}
provider "aws" {
region = var.secondary_region
alias = "secondary"
}
provider "cloudflare" {
api_token = var.cloudflare_api_token
}

📌 3. Remote State Backend (environments/dev/backend.tf)

terraform {
backend "s3" {
bucket = "terraform-state-bucket"
key = "multi-region/dev/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-lock"
encrypt = true
}
}

📌 4. S3 Bucket Module (modules/s3/main.tf)

resource "aws_s3_bucket" "this" {
provider = aws.primary
bucket = "${var.bucket_name}-${var.region}"
  lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_versioning" "this" {
provider = aws.primary
bucket = aws_s3_bucket.this.id
versioning_configuration {
status = var.enable_versioning ? "Enabled" : "Suspended"
}
}

📌 Variables (modules/s3/variables.tf)

variable "bucket_name" {
type = string
description = "Base name for S3 bucket"
}
variable "region" {
type = string
description = "AWS region"
}
variable "enable_versioning" {
type = bool
description = "Enable versioning for the S3 bucket"
default = true
}

📌 5. Cloudflare Module (modules/cloudflare/main.tf)

resource "cloudflare_record" "cdn" {
zone_id = data.cloudflare_zone.selected.id
name = var.record_name
value = var.s3_endpoint
type = "CNAME"
proxied = true
}
data "cloudflare_zone" "selected" {
name = var.cloudflare_zone
}

📌 Variables (modules/cloudflare/variables.tf)

variable "record_name" {
type = string
description = "Cloudflare DNS record name"
}
variable "s3_endpoint" {
type = string
description = "S3 website endpoint"
}
variable "cloudflare_zone" {
type = string
description = "Cloudflare Zone Name"
}

📌 6. Environment-Specific Config (environments/dev/main.tf)

module "primary_s3" {
source = "../../modules/s3"
bucket_name = var.s3_bucket_name
region = var.primary_region
enable_versioning = true
}
module "secondary_s3" {
source = "../../modules/s3"
bucket_name = var.s3_bucket_name
region = var.secondary_region
enable_versioning = false
count = var.enable_multi_region ? 1 : 0
}
module "cloudflare" {
source = "../../modules/cloudflare"
record_name = "app"
s3_endpoint = "s3-website.${var.primary_region}.amazonaws.com"
cloudflare_zone = var.cloudflare_zone
}

📌 Variables (environments/dev/variables.tf)

variable "primary_region" {
type = string
description = "Primary AWS Region"
default = "us-east-1"
}
variable "secondary_region" {
type = string
description = "Secondary AWS Region"
default = "eu-west-2"
}
variable "enable_multi_region" {
type = bool
description = "Enable Multi-Region Disaster Recovery"
default = true
}
variable "s3_bucket_name" {
type = string
description = "Base name for S3 bucket"
default = "my-multi-region-app"
}
variable "cloudflare_zone" {
type = string
description = "Cloudflare Zone Name"
default = "example.com"
}

📌 7. Secrets Management (environments/dev/terraform.tfvars)

cloudflare_api_token = "your-cloudflare-token"

📌 8. Git Best Practices (.gitignore)

*.tfstate
*.tfvars
.terraform/
terraform.tfstate.backup

🚀 Industry-Standard Best Practices Applied

Modular Structure: Separates S3 & Cloudflare into reusable modules
Environment Segmentation: Different Terraform workspaces for dev/prod
State Locking: Uses S3 backend with DynamoDB to prevent state corruption
Security Best Practices: Secrets (tfvars) are excluded from Git
Scalability: Can easily extend to new AWS regions & services

📌 Next Steps

  1. Initialize Terraform:
  • cd environments/dev terraform init

2.Plan Deployment:

  • terraform plan

3.Apply Configuration:

  • terraform apply -auto-approve

Key Concepts Used in This Practice

Multiple Providers: AWS (2 regions) & Cloudflare
Multi-Region Deployment: Primary & DR (conditionally deployed)
Required Providers Block: Ensures correct versions are used
Variables & Conditional Expressions:

  • enable_multi_region ? 1 : 0 controls secondary bucket creation
  • lookup() and join() functions used for dynamic naming
    Built-in Functions:
  • join("-", [var.s3_bucket_name, var.primary_region]) → Creates dynamic bucket names
  • lower(var.s3_bucket_name) → Ensures lowercase names

--

--

Jeeva-AWSLabsJourney
Jeeva-AWSLabsJourney

Written by Jeeva-AWSLabsJourney

Exploring AWS, cloud, Linux & DevOps. Your guide to navigating the digital realm. Join me on the journey of discovery

No responses yet