Infrastructure as Code
Up and Running with the Cloudflare Terraform Provider
Quick-start guide to managing Cloudflare infrastructure with OpenTofu or Terraform.
The Cloudflare Terraform provider gives you a clean way to manage DNS, security, redirects, and edge services using the same Infrastructure as Code workflow you already apply elsewhere. Keeping that configuration in version control makes changes easier to review, repeat, and roll back.
This post covers the basics of getting up and running, then highlights a few of the common edge cases that are worth understanding early.
Authentication and Setup
Modern Cloudflare authentication uses API tokens rather than legacy API keys. Tokens are easier to scope and far safer to manage in CI or local development.
Creating an API token
- Open the Cloudflare dashboard and go to
My Profile→API Tokens - Select
Create Token - Start from a template or define a custom token with the minimum required permissions
- Store the token securely
The provider can read credentials from environment variables, which helps keep sensitive values out of your repository and out of local configuration files:
export CLOUDFLARE_API_TOKEN="your-api-token-here"
terraform {
required_version = "1.10.6"
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 5.0"
}
}
}
provider "cloudflare" {
# Reads CLOUDFLARE_API_TOKEN from the environment
}
The same configuration works cleanly in OpenTofu by keeping the provider definition identical.
Managing Zones and DNS
A common pattern with Cloudflare is to treat the zone as the root object, then drive settings and records from variables so the configuration stays maintainable as it grows.
Zone
resource "cloudflare_zone" "example" {
account = {
id = var.cloudflare_account_id
}
zone = "example.com"
type = "full"
}
Zone settings
variable "zone_settings" {
description = "Cloudflare zone settings to configure"
type = map(any)
default = {
automatic_https_rewrites = "on"
ssl = "full"
always_use_https = "on"
min_tls_version = "1.2"
browser_check = "on"
brotli = "on"
}
}
resource "cloudflare_zone_setting" "settings" {
for_each = var.zone_settings
zone_id = cloudflare_zone.example.id
setting_id = each.key
value = each.value
}
DNS records
variable "cloudflare_dns_records" {
description = "DNS records to create in Cloudflare"
type = map(object({
type = string
name = string
content = string
ttl = optional(number, 1)
proxied = optional(bool, false)
priority = optional(number)
}))
default = {
root = {
type = "CNAME"
name = "example.com"
content = "site.pages.dev"
ttl = 1
proxied = true
}
www = {
type = "CNAME"
name = "www"
content = "site.pages.dev"
ttl = 1
proxied = true
}
blog = {
type = "CNAME"
name = "blog"
content = "blog.pages.dev"
ttl = 3600
proxied = false
}
}
}
resource "cloudflare_dns_record" "dns_records" {
for_each = var.cloudflare_dns_records
zone_id = cloudflare_zone.example.id
type = each.value.type
name = each.value.name
content = each.value.content
ttl = each.value.ttl
proxied = each.value.proxied
priority = each.value.priority
}
That variable-driven approach is usually easier to scale than scattering individual record resources throughout a repository.
Page Rules and Rulesets
Cloudflare’s older Page Rules feature still exists in some environments, but for new work you should prefer Rulesets. They offer a more flexible expression-based model and are where most new Cloudflare traffic behaviour is being built.
A simple Page Rule might still look like this:
resource "cloudflare_page_rule" "static_cache" {
zone_id = cloudflare_zone.example.id
target = "static.example.com/*"
priority = 1
status = "active"
actions {
cache_level = "cache_everything"
edge_cache_ttl = 86400
}
}
For new infrastructure, a Ruleset is usually the better fit:
resource "cloudflare_ruleset" "redirects" {
zone_id = cloudflare_zone.example.id
kind = "zone"
phase = "http_request_dynamic_redirect"
name = "redirects"
rules = [
{
action = "redirect"
action_parameters = {
from_value = {
preserve_query_string = false
status_code = 301
target_url = {
expression = "wildcard_replace(http.request.full_uri, r\"http://*\", r\"https://$${1}\")"
}
}
}
description = "Redirect HTTP to HTTPS"
enabled = true
expression = "(http.request.full_uri wildcard r\"http://*\")"
},
{
action = "redirect"
action_parameters = {
from_value = {
preserve_query_string = true
status_code = 301
target_url = {
expression = "wildcard_replace(http.request.full_uri, r\"https://www.*\", r\"https://$${1}\")"
}
}
}
description = "Redirect www to apex"
enabled = true
expression = "(http.request.full_uri wildcard r\"https://www.*\")"
}
]
}
Using R2 as a Terraform Backend
Cloudflare R2 can also be a useful place to hold remote state, particularly if you are already operating inside the Cloudflare ecosystem.
Create the R2 bucket
resource "cloudflare_r2_bucket" "terraform_state" {
account_id = var.cloudflare_account_id
name = "terraform-state-bucket"
location = "WEUR"
}
Configure the backend
terraform {
backend "s3" {
region = "WEUR"
bucket = "terraform-state-bucket"
key = "example/terraform.tfstate"
endpoint = "https://xxx.r2.cloudflarestorage.com"
# Configure credentials locally, in CI, or through environment variables.
}
}
That works because R2 exposes an S3-compatible API, which makes backend adoption much simpler than introducing a completely separate state system.
Common Pitfalls
Two issues come up often when teams first start managing Cloudflare with Terraform or OpenTofu.
Default zone rulesets
If you hit an error like:
'zone' is not a valid value for kind because exceeded maximum number of zone rulesets
Cloudflare may already have a default zone ruleset for that phase. In that case, import and manage the existing entrypoint instead of trying to create a second one.
curl -X GET \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets/phases/http_request_dynamic_redirect/entrypoint"
tofu import cloudflare_ruleset.redirects <zone-id>/<ruleset-id>
Provider v5 resource names
If you are upgrading from an older provider version, some resource names have changed. One common example is:
cloudflare_record→cloudflare_dns_record
Versioning the provider explicitly makes these migrations far easier to reason about.
Implementation Best Practices
The patterns that tend to hold up well are:
- use API tokens instead of legacy API keys
- keep credentials in environment variables or a secure secret manager
- pin the provider version
- use variable-driven structures for repeatable DNS and settings management
- use Rulesets for new traffic logic rather than building on Page Rules
Conclusion
The Cloudflare Terraform provider gives you a reliable way to treat edge infrastructure like the rest of your platform code. Once the basics are in place, you can manage zones, DNS, redirects, and supporting storage through the same reviewable IaC workflow you use elsewhere.
That consistency matters. It makes Cloudflare changes easier to audit, easier to reuse, and much less dependent on one person clicking around the dashboard.