Skip to content

Provider Terraform Cloudflare v4 → v5 — Runbook de Migración

Alcance: las cuatro roots de Terraform que fijan el provider cloudflare/cloudflare. Issue #1679. La reescritura de la config va en el repo; la migración de state de abajo debe ser ejecutada por un operador con un CLOUDFLARE_API_TOKEN real — estas roots gestionan infraestructura VIVA de DNS / WAF / status-page en el edge, así que lee el runbook completo antes de ejecutar nada.

Root Providers tras la migración
pkg/octopus/deploy/terraform/multi-region cloudflare ~> 5.21, aws ~> 6.53
platform/terraform/status-edge cloudflare ~> 5.21
platform/terraform/dns-caa cloudflare ~> 5.21
platform/terraform/dns-subzone-delegation cloudflare ~> 5.21, aws ~> 6.53

El bump de aws a ~> 6.53 en multi-region y dns-subzone-delegation alinea esas dos roots con el resto del repo (PR #1681) — solo estaban retenidas por la rotura del Cloudflare v5.

Qué cambió por root

pkg/octopus/deploy/terraform/multi-region

  • cloudflare_record.connect_anycast / cloudflare_record.signal_anycast renombrados a cloudflare_dns_record (rename de resource de v5). Los bloques moved están en main.tf, así que el state los sigue automáticamente.
  • Ambos resources cloudflare_ruleset (octopus_waf_managed, octopus_waf_custom): los bloques repetidos rules { … } pasaron a ser un único list attribute rules = [ { … } ]; action_parameters pasó a ser un objeto anidado. Los valores de los atributos no cambian — mismas actions, mismas expressions, mismo id de managed-ruleset.

platform/terraform/status-edge

  • cloudflare_record.status_acloudflare_dns_record.status_a (+ bloque moved). Todos los atributos del record sin cambios (A record proxied, ttl = 1, mismo comment).
  • cloudflare_ruleset.cache: rules pasó a list attribute; action_parameters, edge_ttl y browser_ttl pasaron a objetos anidados. Semántica sin cambios (edge TTL override 15 s, browser TTL bypass).

platform/terraform/dns-caa

  • cloudflare_record.caa_issue["…"] (×3) y cloudflare_record.caa_iodefcloudflare_dns_record (+ bloques moved cubriendo todas las instancias del for_each). El bloque CAA data { … } pasó a un objeto data = { … } — mismo payload flags / tag / value.

platform/terraform/dns-subzone-delegation

  • cloudflare_zone.customer: argumento v4 zone → v5 name; v4 account_id → v5 account = { id = … }. Mismo nombre de resource type — el state hace upgrade in place, sin cambio de address.
  • El argumento v4 plan fue eliminado del cloudflare_zone upstream. El rate plan ahora vive en un NUEVO resource compañero cloudflare_zone_subscription.customer con exactamente el mismo mapeo de tier (tier freefree, todo lo demás → pro). Para zonas que ya existen, importa la subscription antes del primer apply (ver abajo).
  • cloudflare_zone_dnssec.customer: v5 expone el interruptor explícitamente — status = "active" preserva el comportamiento v4 de habilitar al crear.
  • cloudflare_api_token.customer: bloques v4 policy { … } → v5 policies = [ { … } ]; los ids de permission-group pasaron a objetos (permission_groups = [{ id = … }]) y resources ahora es una string JSON-encoded (jsonencode({ … })). Mismo permission group id de DNS-Write, mismo alcance por zona.
  • El output customer_zones[*].zone_name ahora lee cloudflare_zone.customer[k].name (v5 renombró el atributo zone a name); la forma del output no cambia.

Migración de state

Resources renombrados — automático vía bloques moved

El provider v5 (≥ 5.19; fijamos ~> 5.21) implementa el provider-defined move de Terraform (MoveResourceState) para cloudflare_recordcloudflare_dns_record. Con Terraform ≥ 1.8 (el repo usa 1.15.x), los bloques moved ya commiteados en cada root migran las entradas de state in place en el próximo plan/apply — sin cirugía manual de state, sin destroy/create. Es el mismo mecanismo que emite la herramienta tf-migrate de la propia Cloudflare.

Resources con el mismo nombre — automático vía state upgraders

cloudflare_ruleset, cloudflare_zone, cloudflare_zone_dnssec y cloudflare_api_token mantuvieron sus type names; el provider v5 incluye state upgraders que convierten el formato de state v4 la primera vez que el provider v5 lo lee. Nada que ejecutar.

Resource nuevo — cloudflare_zone_subscription (solo dns-subzone-delegation)

Para cada zona de customer que ya existe en el state, importa la subscription para que Terraform no emita una llamada redundante de set de plan:

cd platform/terraform/dns-subzone-delegation
export AWS_PROFILE=tlsstress-prod
export CLOUDFLARE_API_TOKEN=...   # platform token
terraform init -upgrade

# one import per existing customer key; zone_id from the customer_zones output
terraform import 'cloudflare_zone_subscription.customer["<customer_key>"]' '<zone_id>'

Fallback — cirugía manual de state (solo si un bloque moved es rechazado)

Si alguna herramienta del pipeline no honra el provider-defined move (p. ej., un CLI que no es Terraform), el camino manual equivalente es remove + import. Los ids de los records ya están documentados en el README de cada root:

# status-edge
terraform state rm cloudflare_record.status_a
terraform import cloudflare_dns_record.status_a "$ZONE/31de68523f9ce3eb44d39f7dfa812dd5"

# dns-caa
terraform state rm 'cloudflare_record.caa_issue["letsencrypt.org"]' \
                   'cloudflare_record.caa_issue["pki.goog"]' \
                   'cloudflare_record.caa_issue["amazon.com"]' \
                   cloudflare_record.caa_iodef
terraform import 'cloudflare_dns_record.caa_issue["letsencrypt.org"]' "$ZONE/d9260a33cacf58447acffe40ddd17da0"
terraform import 'cloudflare_dns_record.caa_issue["pki.goog"]'        "$ZONE/ddc9f69aa37316c91004372bd921cacc"
terraform import 'cloudflare_dns_record.caa_issue["amazon.com"]'      "$ZONE/9367272b025960f809ba33a74a980413"
terraform import  cloudflare_dns_record.caa_iodef                     "$ZONE/0744823ec83f55ce38abf63e7c6395e4"

# multi-region (record ids: look up via the Cloudflare API / dashboard)
terraform state rm cloudflare_record.connect_anycast cloudflare_record.signal_anycast
terraform import cloudflare_dns_record.connect_anycast "$ZONE/<record_id>"
terraform import cloudflare_dns_record.signal_anycast  "$ZONE/<record_id>"

Procedimiento de ejecución (por root)

cd <root>
export CLOUDFLARE_API_TOKEN=...          # scoped per root README
terraform state pull > pre-v5-$(date +%Y%m%d).tfstate.backup   # rollback insurance
terraform init -upgrade
terraform plan -out=v5-migration.plan

Gate antes del apply — el plan DEBE mostrar:

  • líneas # cloudflare_record.X has moved to cloudflare_dns_record.X para cada record renombrado;
  • 0 to add, 0 to change, 0 to destroy (multi-region/dns-subzone-delegation: adiciones aceptables SOLO para imports de cloudflare_zone_subscription que saltaste intencionalmente);
  • NINGÚN destroy, NINGÚN replace (-/+) en ningún cloudflare_dns_record, cloudflare_ruleset, cloudflare_zone ni cloudflare_api_token.

Chequeo rápido:

terraform show -no-color v5-migration.plan | grep -E 'has moved to|must be replaced|will be destroyed|Plan:'

Si (y solo si) el gate pasa:

terraform apply v5-migration.plan
terraform plan   # second plan MUST be: No changes.

Rollback

  • Antes del apply: no se escribió nada — git revert del commit de la migración, terraform init -upgrade de vuelta al lockfile v4, listo. Los bloques moved son solo de plan-time.
  • Después del apply: el state ahora usa addresses/formatos v5. Restaura el snapshot de state hecho arriba (terraform state push pre-v5-<date>.tfstate.backup) y haz git revert de la config en el mismo cambio; luego ejecuta terraform plan con v4 para confirmar No changes. El backend S3 mantiene objetos versionados como segunda fuente de recuperación.
  • Los records/rulesets en sí nunca se tocan en una migración correcta — el rollback es una operación de config/state, no un cambio de DNS.

Post-migración

  • Eliminar la regla de ignore de cloudflare en .github/dependabot.yml para que los minors v5.x vuelvan a fluir (rastreado en #1679 — deliberadamente NO forma parte de este cambio; solo ocurre después de ejecutar la migración de state viva).
  • terraform plan en cada root en el próximo drift check programado debe reportar No changes.