This skill guides provisioning Cloudflare infrastructure with OpenTofu/Terraform. Use when managing zones, DNS records, WAF rules, SSL settings, Page Rules, or cache configuration.
This skill is limited to using the following tools:
resources/load-balancer.mdresources/production-zone.mdresources/waf.mdCloudflare provides CDN, DNS, security, and edge computing services. This skill covers OpenTofu/Terraform patterns for Cloudflare resources.
terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.0"
}
}
}
provider "cloudflare" {
# API Token from environment: CLOUDFLARE_API_TOKEN
# Or explicitly (not recommended):
# api_token = var.cloudflare_api_token
}
# Preferred: API Token (scoped permissions)
export CLOUDFLARE_API_TOKEN="your-api-token"
# Or with 1Password
CLOUDFLARE_API_TOKEN=op://Infrastructure/Cloudflare/api_token
# Legacy: Global API Key (full access - avoid if possible)
export CLOUDFLARE_API_KEY="your-global-api-key"
export CLOUDFLARE_EMAIL="your-email@example.com"
API Token Permissions (minimum required):
| Resource | Permission | Use Case |
|---|---|---|
| Zone | Read | List zones, read settings |
| Zone | Edit | Modify zone settings |
| DNS | Edit | Manage DNS records |
| Firewall | Edit | WAF, firewall rules |
| SSL | Edit | Certificate management |
| Cache | Purge | Cache invalidation |
data "cloudflare_zone" "main" {
name = "example.com"
}
# Use in resources
resource "cloudflare_record" "www" {
zone_id = data.cloudflare_zone.main.id
# ...
}
data "cloudflare_accounts" "main" {
name = "My Account"
}
output "account_id" {
value = data.cloudflare_accounts.main.accounts[0].id
}
# A Record
resource "cloudflare_record" "root" {
zone_id = data.cloudflare_zone.main.id
name = "@"
type = "A"
content = var.server_ip
ttl = 1 # 1 = automatic
proxied = true
}
# CNAME Record
resource "cloudflare_record" "www" {
zone_id = data.cloudflare_zone.main.id
name = "www"
type = "CNAME"
content = "@"
ttl = 1
proxied = true
}
# MX Records
resource "cloudflare_record" "mx_primary" {
zone_id = data.cloudflare_zone.main.id
name = "@"
type = "MX"
content = "mail.example.com"
ttl = 3600
priority = 10
proxied = false # MX cannot be proxied
}
# TXT Record (SPF)
resource "cloudflare_record" "spf" {
zone_id = data.cloudflare_zone.main.id
name = "@"
type = "TXT"
content = "v=spf1 include:_spf.google.com ~all"
ttl = 3600
}
locals {
dns_records = {
api = {
type = "A"
content = var.api_server_ip
proxied = true
}
staging = {
type = "A"
content = var.staging_server_ip
proxied = true
}
mail = {
type = "A"
content = var.mail_server_ip
proxied = false
}
}
}
resource "cloudflare_record" "subdomain" {
for_each = local.dns_records
zone_id = data.cloudflare_zone.main.id
name = each.key
type = each.value.type
content = each.value.content
ttl = 1
proxied = each.value.proxied
}
resource "cloudflare_zone_settings_override" "ssl" {
zone_id = data.cloudflare_zone.main.id
settings {
# SSL mode: off, flexible, full, strict (recommended)
ssl = "strict"
# Minimum TLS version
min_tls_version = "1.2"
# Always use HTTPS
always_use_https = "on"
# Automatic HTTPS Rewrites
automatic_https_rewrites = "on"
# HTTP Strict Transport Security
security_header {
enabled = true
max_age = 31536000 # 1 year
include_subdomains = true
preload = true
nosniff = true
}
# TLS 1.3
tls_1_3 = "on"
# Opportunistic Encryption
opportunistic_encryption = "on"
}
}
resource "cloudflare_origin_ca_certificate" "origin" {
csr = tls_cert_request.origin.cert_request_pem
hostnames = ["example.com", "*.example.com"]
request_type = "origin-rsa"
requested_validity = 5475 # 15 years (max)
}
# Generate private key and CSR
resource "tls_private_key" "origin" {
algorithm = "RSA"
rsa_bits = 2048
}
resource "tls_cert_request" "origin" {
private_key_pem = tls_private_key.origin.private_key_pem
subject {
common_name = "example.com"
organization = "Example Inc"
}
}
output "origin_certificate" {
value = cloudflare_origin_ca_certificate.origin.certificate
sensitive = true
}
See resources/waf.md for IP access rules, WAF custom rules with rate limiting, and managed rulesets (Cloudflare + OWASP).
resource "cloudflare_ruleset" "cache" {
zone_id = data.cloudflare_zone.main.id
name = "Cache Rules"
description = "Caching configuration"
kind = "zone"
phase = "http_request_cache_settings"
# Cache static assets aggressively
rules {
action = "set_cache_settings"
action_parameters {
cache = true
edge_ttl {
mode = "override_origin"
default = 2592000 # 30 days
}
browser_ttl {
mode = "override_origin"
default = 86400 # 1 day
}
}
expression = "(http.request.uri.path.extension in {\"css\" \"js\" \"jpg\" \"jpeg\" \"png\" \"gif\" \"svg\" \"woff\" \"woff2\"})"
description = "Cache static assets"
enabled = true
}
# Bypass cache for API
rules {
action = "set_cache_settings"
action_parameters {
cache = false
}
expression = "(starts_with(http.request.uri.path, \"/api/\"))"
description = "Bypass cache for API"
enabled = true
}
# Bypass cache for admin
rules {
action = "set_cache_settings"
action_parameters {
cache = false
}
expression = "(starts_with(http.request.uri.path, \"/admin\"))"
description = "Bypass cache for admin"
enabled = true
}
}
# Cache everything for static paths
resource "cloudflare_page_rule" "cache_static" {
zone_id = data.cloudflare_zone.main.id
target = "example.com/assets/*"
priority = 1
actions {
cache_level = "cache_everything"
edge_cache_ttl = 2592000 # 30 days
}
}
# Bypass cache for API
resource "cloudflare_page_rule" "bypass_api" {
zone_id = data.cloudflare_zone.main.id
target = "example.com/api/*"
priority = 2
actions {
cache_level = "bypass"
}
}
# Force HTTPS
resource "cloudflare_page_rule" "force_https" {
zone_id = data.cloudflare_zone.main.id
target = "http://example.com/*"
priority = 3
actions {
always_use_https = true
}
}
resource "cloudflare_ruleset" "redirects" {
zone_id = data.cloudflare_zone.main.id
name = "Redirects"
description = "URL redirects"
kind = "zone"
phase = "http_request_dynamic_redirect"
# www to apex redirect
rules {
action = "redirect"
action_parameters {
from_value {
status_code = 301
target_url {
expression = "concat(\"https://example.com\", http.request.uri.path)"
}
preserve_query_string = true
}
}
expression = "(http.host eq \"www.example.com\")"
description = "Redirect www to apex"
enabled = true
}
# Old URL to new URL
rules {
action = "redirect"
action_parameters {
from_value {
status_code = 301
target_url {
value = "https://example.com/new-page"
}
}
}
expression = "(http.request.uri.path eq \"/old-page\")"
description = "Redirect old page to new"
enabled = true
}
}
resource "cloudflare_zone_settings_override" "performance" {
zone_id = data.cloudflare_zone.main.id
settings {
# Compression
brotli = "on"
# Speed optimizations
minify {
css = "on"
html = "on"
js = "on"
}
# Early Hints
early_hints = "on"
# HTTP/2 and HTTP/3
http2 = "on"
http3 = "on"
# 0-RTT
zero_rtt = "on"
# Rocket Loader (async JS loading)
rocket_loader = "on"
# Image optimization (requires Pro+)
# polish = "lossless"
# mirage = "on"
# Mobile redirect (if needed)
# mobile_redirect {
# mobile_subdomain = "m"
# status = "on"
# strip_uri = false
# }
}
}
resource "cloudflare_zone_settings_override" "security" {
zone_id = data.cloudflare_zone.main.id
settings {
# Security level: off, essentially_off, low, medium, high, under_attack
security_level = "medium"
# Challenge Passage TTL
challenge_ttl = 1800
# Browser Integrity Check
browser_check = "on"
# Email Obfuscation
email_obfuscation = "on"
# Server Side Excludes
server_side_exclude = "on"
# Hotlink Protection
hotlink_protection = "on"
# IP Geolocation
ip_geolocation = "on"
}
}
See resources/load-balancer.md for Argo Smart Routing and Load Balancer configuration with origin pools, health checks, and session affinity.
# Deploy Worker via Terraform (see wrangler-coder for full Worker management)
resource "cloudflare_worker_route" "api" {
zone_id = data.cloudflare_zone.main.id
pattern = "example.com/api/*"
script_name = cloudflare_worker_script.api.name
}
resource "cloudflare_worker_script" "api" {
account_id = var.cloudflare_account_id
name = "api-worker"
content = file("${path.module}/workers/api.js")
module = true
plain_text_binding {
name = "ENVIRONMENT"
text = var.environment
}
secret_text_binding {
name = "API_KEY"
text = var.api_key
}
}
See resources/production-zone.md for a complete Terraform configuration with DNS, SSL/TLS, WAF, caching, redirects, and variables template.