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 unCLOUDFLARE_API_TOKENreal — 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_anycastrenombrados acloudflare_dns_record(rename de resource de v5). Los bloquesmovedestán enmain.tf, así que el state los sigue automáticamente.- Ambos resources
cloudflare_ruleset(octopus_waf_managed,octopus_waf_custom): los bloques repetidosrules { … }pasaron a ser un único list attributerules = [ { … } ];action_parameterspasó 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_a→cloudflare_dns_record.status_a(+ bloquemoved). Todos los atributos del record sin cambios (A record proxied,ttl = 1, mismo comment).cloudflare_ruleset.cache:rulespasó a list attribute;action_parameters,edge_ttlybrowser_ttlpasaron a objetos anidados. Semántica sin cambios (edge TTL override 15 s, browser TTL bypass).
platform/terraform/dns-caa¶
cloudflare_record.caa_issue["…"](×3) ycloudflare_record.caa_iodef→cloudflare_dns_record(+ bloquesmovedcubriendo todas las instancias delfor_each). El bloque CAAdata { … }pasó a un objetodata = { … }— mismo payloadflags/tag/value.
platform/terraform/dns-subzone-delegation¶
cloudflare_zone.customer: argumento v4zone→ v5name; v4account_id→ v5account = { id = … }. Mismo nombre de resource type — el state hace upgrade in place, sin cambio de address.- El argumento v4
planfue eliminado delcloudflare_zoneupstream. El rate plan ahora vive en un NUEVO resource compañerocloudflare_zone_subscription.customercon exactamente el mismo mapeo de tier (tierfree→free, 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 v4policy { … }→ v5policies = [ { … } ]; los ids de permission-group pasaron a objetos (permission_groups = [{ id = … }]) yresourcesahora es una string JSON-encoded (jsonencode({ … })). Mismo permission group id de DNS-Write, mismo alcance por zona.- El output
customer_zones[*].zone_nameahora leecloudflare_zone.customer[k].name(v5 renombró el atributozoneaname); 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_record → cloudflare_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.Xpara cada record renombrado; 0 to add, 0 to change, 0 to destroy(multi-region/dns-subzone-delegation: adiciones aceptables SOLO para imports decloudflare_zone_subscriptionque saltaste intencionalmente);- NINGÚN
destroy, NINGÚNreplace(-/+) en ningúncloudflare_dns_record,cloudflare_ruleset,cloudflare_zonenicloudflare_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 revertdel commit de la migración,terraform init -upgradede vuelta al lockfile v4, listo. Los bloquesmovedson 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 hazgit revertde la config en el mismo cambio; luego ejecutaterraform plancon v4 para confirmarNo 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
cloudflareen.github/dependabot.ymlpara 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 planen cada root en el próximo drift check programado debe reportarNo changes.