Cloudflare and Terraform integration illustration

Up and Running with the Cloudflare Terraform Provider

The Cloudflare Terraform provider enables you to manage your Cloudflare resources using Infrastructure as Code principles. By storing your configuration in version control, you gain auditable changes, easy rollbacks, and the ability to use the same refined processes you already have for your cloud infrastructure.

Authentication and Setup

Modern authentication with the Cloudflare provider uses API tokens instead of the legacy API keys. API tokens are more secure and can be scoped to specific resources and permissions.

Creating an API Token

  1. Go to your Cloudflare dashboard → My Profile → API Tokens
  2. Click “Create Token”
  3. Use a preset template or create a custom token with specific permissions
  4. Copy the generated token securely

Provider Configuration

Configure the Terraform provider to authenticate with your Cloudflare account. Using environment variables prevents credentials from being committed to version control or stored in state 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" {
  # API token will be read from CLOUDFLARE_API_TOKEN environment variable
}

Managing DNS and Zones

Rather than creating individual resource blocks for each DNS record or zone setting, we define our configuration as variables and use for_each to create resources. Throughout the examples below, you’ll see this pattern repeatedly used. This approach helps keep configurations consistent, reduce duplication, and make scaling changes easier. While you can split variables into dedicated resources when it makes sense, managing them in this way provides a cleaner, more maintainable infrastructure-as-code workflow.

Zones

A zone represents a domain in Cloudflare. Once created, it becomes the container for all your domain’s DNS records, security settings, and performance features like caching and WAF rules.

resource "cloudflare_zone" "example" {
  account = {
    id = var.cloudflare_account_id
  }
  zone = "example.com"
  type = "full"
}

Zone Settings

Zone settings control how Cloudflare handles traffic for your domain, including SSL/TLS modes, HTTPS enforcement, compression, and performance optimisations. These settings apply globally to the entire zone.

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"
    "rocket_loader"            = "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

DNS records map your domain names to IP addresses or other destinations. Cloudflare supports all standard record types (A, AAAA, CNAME, MX, TXT) and allows you to toggle proxying to route traffic through Cloudflare’s network.

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
}

Page Rules

Page Rules allow you to configure caching, redirects, and other behaviours based on URL patterns. However, Cloudflare is phasing these out in favour of the more powerful Rulesets system below.

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
  }
}

Rulesets

Rulesets replace Page Rules with a more flexible expression-based system. Instead of simple URL patterns, you can write complex logic for redirects, caching, transforms, and security rules. For new infrastructure, use Rulesets rather than Page Rules.

Dynamic Redirects

Dynamic redirects allow you to create URL redirects based on patterns and expressions. Common uses include enforcing HTTPS, removing www subdomains, or redirecting old URLs to new locations.

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 from 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 subdomain to apex domain"
      enabled     = true
      expression  = "(http.request.full_uri wildcard r\"https://www.*\")"
    }
  ]
}

Using R2 as a Terraform Backend

R2 is Cloudflare’s S3-compatible object storage with zero egress fees, making it significantly cheaper than S3.

Create the R2 Bucket

Define the bucket where your Terraform state will be stored.

resource "cloudflare_r2_bucket" "terraform_state" {
  account_id = var.cloudflare_account_id
  name       = "terraform-state-bucket"
  location   = "WEUR" # or other R2 locations
}

Configure Backend

Configure Terraform to use R2 for remote state storage. The S3 backend type works seamlessly with R2’s S3-compatible API:

terraform {
  backend "s3" {
    region   = "WEUR"
    bucket   = "terraform-state-bucket"
    key      = "example/terraform.tfstate"
    endpoint = "https://xxx.r2.cloudflarestorage.com"

    # Configured locally, in CI, or through environment variables:
    # access_key = "your-r2-access-key"
    # secret_key = "your-r2-secret-key"
  }
}

Generate R2 Access Credentials

R2 uses separate access credentials from your Cloudflare API token. Generate these through the R2 dashboard:

  1. Go to Cloudflare dashboard → R2 → Manage R2 API Tokens
  2. Create API token with read/write permissions for your bucket
  3. Copy the Access Key ID and Secret Access Key

Common Pitfalls and Gotchas

Default Zone Ruleset

When you encounter the error 'zone' is not a valid value for kind because exceeded maximum number of zone rulesets..., it indicates that Cloudflare has reached the limit for the number of zone-level rulesets in a specific phase; to manage this, you should import and modify the existing default zone ruleset for that phase.

Solution: Import the Existing Ruleset

  1. Get your zone ID from the Cloudflare dashboard

  2. Get the ruleset ID using the Cloudflare API, for example, if you’re trying to deploy a http_request_dynamic_redirect phase:

    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"
  3. Import the ruleset before applying:

    tofu import cloudflare_ruleset.redirects <zone-id>/<ruleset-id>

Version 5.x Resource Name Changes

If you’re migrating from provider v4 to v5, be aware of resource name changes:

  • cloudflare_recordcloudflare_dns_record
  • Check the provider documentation for the complete migration guide and up-to-date resource names

Implementation Best Practices

  1. Use API tokens instead of legacy API keys for better security
  2. Store credentials securely and use environment variables if possible to prevent sensitive data getting into your backend configuration as this can leak sensitive credentials if improperly configured.
  3. Version your provider to ensure consistent behaviour
  4. Use variable-driven configuration for managing multiple similar resources
  5. Consider R2 for state storage to keep everything within the Cloudflare ecosystem

Conclusion

The Cloudflare Terraform provider offers comprehensive management of your Cloudflare infrastructure. With modern API token authentication, extensive resource coverage, and excellent integration with existing Terraform workflows, you can confidently manage DNS, security, performance, and edge computing resources as code. Whether you’re just getting started or optimising existing infrastructure, the Cloudflare Terraform provider provides the tools you need to manage your edge infrastructure effectively.

Was this post helpful?

Related articles