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.
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.
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.
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.
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:
# 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_idTo find resource IDs, use the Cloudflare API or dashboard inspect tools. For example, list all DNS records in a zone via the API:
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 = falsefor non-proxied records: MX, TXT, and NS records should never be proxied. Settingproxied = truebreaks 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 = trueto 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-cachebut Terraform setscache_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) andproxied = 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 planbefore applying to production.






Comments
Loading comments…