Skip to content

cloudflare_worker_version: placement field silently ignored — sent to wrong API endpoint, causes infinite forced replacement #7206

Description

@gabrielgiordan

Summary

cloudflare_worker_version accepts a placement block in its schema, but placement is never actually applied. The provider sends it to the versions endpoint (POST /accounts/{id}/workers/scripts/{name}/versions), which does not support placement. The correct endpoint is PATCH /accounts/{id}/workers/scripts/{name}/settings (script-and-version-settings). Additionally, the Read() call to Versions.Get() never returns placement (not part of the version resource at the API level), so state always shows placement = null after every read — causing a forced replacement on every subsequent tofu plan.

Provider version

cloudflare/cloudflare 5.20.0 (OpenTofu 1.12)

Reproduction

resource "cloudflare_worker_version" "api_proxy" {
  account_id         = var.cloudflare_account_id
  worker_id          = "my-worker"
  compatibility_date = "2026-06-17"
  main_module        = "worker.js"

  placement = {
    region = "gcp:us-east1"
  }

  modules = [{
    name           = "worker.js"
    content_type   = "application/javascript+module"
    content_base64 = filebase64("worker.js")
  }]
}
  1. tofu apply — succeeds, no error
  2. CF dashboard → Worker → Settings → Placement → still shows Default (not Region)
  3. tofu plan immediately after apply → shows forced replacement due to placement drift
  4. Repeat forever

Root cause

Create() in internal/services/worker_version/resource.go calls only Workers.Beta.Workers.Versions.New(), which maps to POST /accounts/{id}/workers/scripts/{name}/versions. This endpoint does not accept placement configuration — placement is not a documented field in the versions upload metadata.

The correct endpoint for placement is PATCH /accounts/{id}/workers/scripts/{name}/settings (script-and-version-settings), which the resource never calls.

Additionally, Read() calls Versions.Get(), which returns no placement data (placement is a script-level setting, not version-level), so placement is written as null to state after every read. Since placement is marked Optional only (not Computed), Terraform sees config ≠ state and marks the resource for forced replacement on every plan.

Contrast with cloudflare_workers_script

cloudflare_workers_script uses PUT /accounts/{id}/workers/scripts/{name} (multipart), which does accept placement in the metadata blob. Placement is marked Optional+Computed on that resource, so it round-trips correctly. This confirms the API supports placement — the issue is specific to cloudflare_worker_version using the wrong endpoint.

Expected behavior

cloudflare_worker_version should either:

  1. Make a secondary PATCH .../settings call after Versions.New() to set placement (matching what wrangler does), or
  2. Clearly document that placement on cloudflare_worker_version is unsupported and remove the field from the schema to prevent silent misconfiguration

Observed behavior

  • Placement silently ignored — CF dashboard always shows Default regardless of config
  • Every tofu plan after apply shows forced replacement due to placement drifting to null in state
  • No error or warning surfaced to the user

Workaround

Remove placement from cloudflare_worker_version and set it via null_resource + local-exec:

resource "null_resource" "worker_placement" {
  triggers = {
    worker_version = cloudflare_worker_version.api_proxy.id
  }
  provisioner "local-exec" {
    command = <<-EOT
      curl -s -X PATCH \
        "https://api.cloudflare.com/client/v4/accounts/${var.cloudflare_account_id}/workers/scripts/${var.worker_id}/settings" \
        -H "Authorization: Bearer ${var.cloudflare_api_token}" \
        -H "Content-Type: application/json" \
        -d '{"placement":{"region":"gcp:us-east1"}}'
    EOT
  }
}

Or set placement manually in the CF dashboard and add lifecycle { ignore_changes = [placement] } to suppress the drift.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions