Skip to content
ExponaBLOG
TERRAFORM, IAC & CLOUD AUTOMATION · DAY 064 / 100

Managing Cloudflare with Terraform: a step-by-step guide

DNS, WAF and caching as code.

Expona Admin8 min read

Introduction

DNS management, WAF rules, and caching strategies are critical for any production web application, yet they're often managed manually through dashboards—fragile, error-prone, and difficult to version control. Managing Cloudflare with Terraform: a step-by-step guide brings infrastructure-as-code discipline to your edge layer, letting you define DNS records, security policies, and performance settings alongside your broader cloud infrastructure.

In this guide, we'll walk through the Cloudflare provider for Terraform, covering zones and records, WAF rules and rate limiting, page rules and cache configuration, and how to import existing Cloudflare setup into your Terraform state. By the end, you'll have a repeatable, auditable approach to managing your Cloudflare estate.

Setting up the Cloudflare Provider

Before you can manage any Cloudflare resource, you need to authenticate and configure the provider. The Cloudflare provider requires an API token (preferred) or email and API key combination.

Generate an API token in your Cloudflare dashboard under Account Settings > API Tokens. Create a token with at least Zone:Edit and DNS:Edit permissions, then store it securely in your environment or a secrets manager.

hcl
terraform {
  required_providers {
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "~> 4.0"
    }
  }
}

provider "cloudflare" {
  api_token = var.cloudflare_api_token
}

variable "cloudflare_api_token" {
  description = "Cloudflare API token"
  type        = string
  sensitive   = true
}

variable "cloudflare_zone_id" {
  description = "Cloudflare zone ID for your domain"
  type        = string
}

Store your API token in a .tfvars file (excluded from version control) or pass it via environment variable CLOUDFLARE_API_TOKEN. Never commit credentials to your repository.

Cloudflare Provider for Zones and Records

A zone represents your domain in Cloudflare. You can either reference an existing zone or create a new one via Terraform. Most teams reference existing zones; however, Terraform can still manage all records within that zone.

Here's a practical scenario: you're managing api.example.com and app.example.com subdomains, both pointing to load-balanced endpoints. Rather than manually creating A records in the dashboard, define them as code.

hcl
data "cloudflare_zone" "main" {
  name = var.domain_name  # e.g., "example.com"
}

resource "cloudflare_record" "api" {
  zone_id = data.cloudflare_zone.main.id
  name    = "api"
  type    = "A"
  value   = var.api_lb_ip
  ttl     = 3600
  proxied = true  # orange cloud: traffic routes through Cloudflare
}

resource "cloudflare_record" "app" {
  zone_id = data.cloudflare_zone.main.id
  name    = "app"
  type    = "A"
  value   = var.app_lb_ip
  ttl     = 3600
  proxied = true
}

resource "cloudflare_record" "mx" {
  zone_id  = data.cloudflare_zone.main.id
  name     = "@"
  type     = "MX"
  priority = 10
  value    = "mail.example.com"
  ttl      = 3600
  proxied  = false  # gray cloud: DNS only, no proxying
}

When proxied = true, traffic flows through Cloudflare's edge, enabling WAF, caching, and DDoS protection. When proxied = false, Cloudflare simply resolves DNS without intercepting traffic. For mail records, always use proxied = false.

WAF Rules and Rate Limits

Web Application Firewall (WAF) rules protect against common attacks. Terraform lets you define WAF rules as code, making it easy to test, version, and replicate security policies across zones.

Consider a real-world scenario: you need to block traffic from specific countries during a DDoS event, allow only specific user agents for your API, and rate-limit login endpoints to prevent brute force attacks.

hcl
resource "cloudflare_firewall_rule" "block_countries" {
  zone_id     = data.cloudflare_zone.main.id
  description = "Block traffic from high-risk countries"
  filter_id   = cloudflare_firewall_filter.geo_block.id
  action      = "block"
  priority    = 1
}

resource "cloudflare_firewall_filter" "geo_block" {
  zone_id     = data.cloudflare_zone.main.id
  description = "Geo-blocking filter"
  expression  = "(cf.country in {\"CN\" \"RU\"})"
}

resource "cloudflare_rate_limit" "api_login" {
  zone_id      = data.cloudflare_zone.main.id
  disabled     = false
  description  = "Rate limit login endpoint"
  threshold    = 5
  period       = 60
  match {
    request {
      url {
        path {
          matches = "/login"
        }
      }
    }
  }
  action {
    mode    = "challenge"  # CAPTCHA
    timeout = 86400
  }
}

resource "cloudflare_rate_limit" "api_general" {
  zone_id      = data.cloudflare_zone.main.id
  disabled     = false
  description  = "Rate limit general API"
  threshold    = 100
  period       = 60
  match {
    request {
      url {
        path {
          matches = "/api/*"
        }
      }
    }
  }
  action {
    mode = "block"
  }
}

The firewall_rule and rate_limit resources integrate with Cloudflare's native WAF. Use challenge mode for interactive verification (CAPTCHA) or block to deny outright. Adjust thresholds and periods based on your traffic baseline and security posture.

Page Rules and Cache Settings

Page rules allow you to set cache behavior, security levels, and other settings per URL pattern. While Cloudflare encourages the newer ruleset engine for new deployments, page rules remain widely used for granular control.

A common scenario: you want aggressive caching for static assets (images, CSS, JS), no caching for API responses, and always-on HTTPS for your admin panel.

hcl
resource "cloudflare_page_rule" "cache_static" {
  zone_id = data.cloudflare_zone.main.id
  target  = "example.com/static/*"
  priority = 1

  actions {
    cache_level         = "cache_everything"
    edge_cache_ttl      = 31536000  # 1 year
    browser_cache_ttl   = 31536000
  }
}

resource "cloudflare_page_rule" "api_no_cache" {
  zone_id = data.cloudflare_zone.main.id
  target  = "api.example.com/v1/*"
  priority = 2

  actions {
    cache_level = "bypass"
  }
}

resource "cloudflare_page_rule" "admin_https" {
  zone_id = data.cloudflare_zone.main.id
  target  = "example.com/admin/*"
  priority = 3

  actions {
    always_use_https = true
    security_level   = "high"
  }
}

resource "cloudflare_zone_settings_override" "main" {
  zone_id = data.cloudflare_zone.main.id

  settings {
    ssl                = "strict"
    min_tls_version    = "1.2"
    opportunistic_https = "on"
    automatic_https_rewrites = "on"
    caching_level      = "standard"
    purge_on_cache_by_default_on_upload = true
  }
}

Zone-level settings apply globally; page rules override them for specific URL patterns. The cache_level = "cache_everything" with a long TTL is ideal for immutable assets. For dynamic content, use bypass to skip caching entirely or rely on cache headers from your origin.

Importing Existing Cloudflare Configuration

If you have existing zones, records, WAF rules, or page rules already configured in Cloudflare, you need to import them into Terraform state before you can manage them as code. The terraform import command is your tool.

Import syntax varies by resource type. Here's how to import common resources:

hcl
# Import a DNS record
terraform import cloudflare_record.api zone_id/record_id

# Import a page rule
terraform import cloudflare_page_rule.cache_static zone_id/page_rule_id

# Import a rate limit
terraform import cloudflare_rate_limit.api_login zone_id/rate_limit_id

# Import a firewall rule
terraform import cloudflare_firewall_rule.block_countries zone_id/firewall_rule_id

To find resource IDs, use the Cloudflare API or dashboard inspect tools. For example, list all DNS records in a zone via the API:

hcl
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/zones/ZONE_ID/dns_records" | jq '.result[] | {name, type, id}'

Once imported, Terraform's state file tracks that resource. Then write resource blocks in your Terraform code to match the existing configuration, run terraform plan to verify no changes are needed, and commit everything to version control.

For large migrations, write a script to parse your Cloudflare API responses and generate Terraform resource blocks automatically, then review and refine by hand.

Common Mistakes

  • Forgetting proxied = false for non-proxied records: MX, TXT, and NS records should never be proxied. Setting proxied = true breaks mail delivery and domain verification.
  • Storing API tokens in code or state files: Always use environment variables or a secrets manager. Mark sensitive variables with sensitive = true to prevent them from appearing in logs.
  • Not importing before managing existing resources: If you create a Terraform resource for a DNS record that already exists, you'll attempt to recreate it and may trigger unnecessary changes. Always import first.
  • Neglecting cache header conflicts: If your origin sends Cache-Control: no-cache but Terraform sets cache_level = "cache_everything", the origin wins. Align cache settings across your stack.
  • Mixing page rules and rulesets: Cloudflare's newer ruleset engine coexists with legacy page rules. Don't use both for the same URL pattern; standardize on one approach.

Key Takeaways

  • Use the Cloudflare provider to version control DNS records, WAF rules, rate limits, and cache policies alongside your infrastructure.
  • Authenticate securely with an API token; never commit credentials and mark sensitive variables with sensitive = true.
  • Distinguish between proxied = true (orange cloud: full Cloudflare protection) and proxied = false (gray cloud: DNS only).
  • WAF rules and rate limits provide edge-layer security; adjust thresholds and actions based on your threat model and traffic patterns.
  • Page rules and zone settings control caching and SSL behavior; ensure they align with origin cache headers to avoid unexpected behavior.
  • Always import existing Cloudflare resources into Terraform state before writing resource blocks to prevent accidental recreation.
  • Test configuration changes in a staging zone or with terraform plan before applying to production.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles