Curated Skills
by lstudlo

cloudflare

references/terraform/patterns.md

.md 175 lines
Content
# Terraform Patterns & Use Cases

Architecture patterns, multi-environment setups, and real-world use cases.

## Recommended Directory Structure

```
terraform/
├── environments/
│   ├── production/
│   │   ├── main.tf
│   │   └── terraform.tfvars
│   └── staging/
│       ├── main.tf
│       └── terraform.tfvars
├── modules/
│   ├── zone/
│   ├── worker/
│   └── dns/
└── shared/          # Shared resources across envs
    └── main.tf
```

**Note:** Cloudflare recommends avoiding modules for provider resources due to v5 auto-generation complexity. Prefer environment directories + shared state instead.

## Multi-Environment Setup

```hcl
# Directory: environments/{production,staging}/main.tf + modules/{zone,worker,pages}
module "zone" {
  source = "../../modules/zone"; account_id = var.account_id; zone_name = "example.com"; environment = "production"
}
module "api_worker" {
  source = "../../modules/worker"; account_id = var.account_id; zone_id = module.zone.zone_id
  name = "api-worker-prod"; script = file("../../workers/api.js"); environment = "production"
}
```

## R2 State Backend

```hcl
terraform {
  backend "s3" {
    bucket = "terraform-state"
    key = "cloudflare.tfstate"
    region = "auto"
    endpoints = { s3 = "https://<account_id>.r2.cloudflarestorage.com" }
    skip_credentials_validation = true
    skip_region_validation = true
    skip_requesting_account_id = true
    skip_metadata_api_check = true
    skip_s3_checksum = true
  }
}
```

## Worker with All Bindings

```hcl
locals { worker_name = "full-stack-worker" }
resource "cloudflare_workers_kv_namespace" "app" { account_id = var.account_id; title = "${local.worker_name}-kv" }
resource "cloudflare_r2_bucket" "app" { account_id = var.account_id; name = "${local.worker_name}-bucket" }
resource "cloudflare_d1_database" "app" { account_id = var.account_id; name = "${local.worker_name}-db" }

resource "cloudflare_worker_script" "app" {
  account_id = var.account_id; name = local.worker_name; content = file("worker.js"); module = true
  compatibility_date = "2025-01-01"
  kv_namespace_binding { name = "KV"; namespace_id = cloudflare_workers_kv_namespace.app.id }
  r2_bucket_binding { name = "BUCKET"; bucket_name = cloudflare_r2_bucket.app.name }
  d1_database_binding { name = "DB"; database_id = cloudflare_d1_database.app.id }
  secret_text_binding { name = "API_KEY"; text = var.api_key }
}
```

## Wrangler Integration

**CRITICAL**: Wrangler and Terraform must NOT manage same resources.

**Terraform**: Zones, DNS, security rules, Access, load balancers, worker deployments (CI/CD), KV/R2/D1 resource creation  
**Wrangler**: Local dev (`wrangler dev`), manual deploys, D1 migrations, KV bulk ops, log streaming (`wrangler tail`)

### CI/CD Pattern

```hcl
# Terraform creates infrastructure
resource "cloudflare_workers_kv_namespace" "app" { account_id = var.account_id; title = "app-kv" }
resource "cloudflare_d1_database" "app" { account_id = var.account_id; name = "app-db" }
output "kv_namespace_id" { value = cloudflare_workers_kv_namespace.app.id }
output "d1_database_id" { value = cloudflare_d1_database.app.id }
```

```yaml
# GitHub Actions: terraform apply → envsubst wrangler.jsonc.template → wrangler deploy
- run: terraform apply -auto-approve
- run: |
    export KV_NAMESPACE_ID=$(terraform output -raw kv_namespace_id)
    envsubst < wrangler.jsonc.template > wrangler.jsonc
- run: wrangler deploy
```

## Use Cases

### Static Site + API Worker

```hcl
resource "cloudflare_pages_project" "frontend" {
  account_id = var.account_id; name = "frontend"; production_branch = "main"
  build_config { build_command = "npm run build"; destination_dir = "dist" }
}
resource "cloudflare_worker_script" "api" {
  account_id = var.account_id; name = "api"; content = file("api-worker.js")
  d1_database_binding { name = "DB"; database_id = cloudflare_d1_database.api_db.id }
}
resource "cloudflare_dns_record" "frontend" {
  zone_id = cloudflare_zone.main.id; name = "app"; content = cloudflare_pages_project.frontend.subdomain; type = "CNAME"; proxied = true
}
resource "cloudflare_worker_route" "api" {
  zone_id = cloudflare_zone.main.id; pattern = "api.example.com/*"; script_name = cloudflare_worker_script.api.name
}
```

### Multi-Region Load Balancing

```hcl
resource "cloudflare_load_balancer_pool" "us" {
  account_id = var.account_id; name = "us-pool"; monitor = cloudflare_load_balancer_monitor.http.id
  origins { name = "us-east"; address = var.us_east_ip }
}
resource "cloudflare_load_balancer_pool" "eu" {
  account_id = var.account_id; name = "eu-pool"; monitor = cloudflare_load_balancer_monitor.http.id
  origins { name = "eu-west"; address = var.eu_west_ip }
}
resource "cloudflare_load_balancer" "global" {
  zone_id = cloudflare_zone.main.id; name = "api.example.com"; steering_policy = "geo"
  default_pool_ids = [cloudflare_load_balancer_pool.us.id]
  region_pools { region = "WNAM"; pool_ids = [cloudflare_load_balancer_pool.us.id] }
  region_pools { region = "WEU"; pool_ids = [cloudflare_load_balancer_pool.eu.id] }
}
```

### Secure Admin with Access

```hcl
resource "cloudflare_pages_project" "admin" { account_id = var.account_id; name = "admin"; production_branch = "main" }
resource "cloudflare_access_application" "admin" {
  account_id = var.account_id; name = "Admin"; domain = "admin.example.com"; type = "self_hosted"; session_duration = "24h"
  allowed_idps = [cloudflare_access_identity_provider.google.id]
}
resource "cloudflare_access_policy" "allow" {
  account_id = var.account_id; application_id = cloudflare_access_application.admin.id
  name = "Allow admins"; decision = "allow"; precedence = 1; include { email = var.admin_emails }
}
```

### Reusable Module

```hcl
# modules/cloudflare-zone/main.tf
variable "account_id" { type = string }; variable "domain" { type = string }; variable "ssl_mode" { default = "strict" }
resource "cloudflare_zone" "main" { account = { id = var.account_id }; name = var.domain }
resource "cloudflare_zone_settings_override" "main" {
  zone_id = cloudflare_zone.main.id; settings { ssl = var.ssl_mode; always_use_https = "on" }
}
output "zone_id" { value = cloudflare_zone.main.id }

# Usage: module "prod" { source = "./modules/cloudflare-zone"; account_id = var.account_id; domain = "example.com" }
```

## See Also

- [README](./README.md) - Provider setup
- [Configuration Reference](./configuration.md) - All resource types
- [API Reference](./api.md) - Data sources
- [Troubleshooting](./gotchas.md) - Best practices, common issues