Skip to content

Provider Terraform Cloudflare v4 → v5 — Runbook de Migração

Escopo: as quatro roots Terraform que pinam o provider cloudflare/cloudflare. Issue #1679. A reescrita da config vai no repo; a migração de state abaixo deve ser executada por um operador com um CLOUDFLARE_API_TOKEN real — essas roots gerenciam infraestrutura VIVA de DNS / WAF / status-page na edge, então leia o runbook inteiro antes de rodar qualquer coisa.

Root Providers após a migração
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

O bump do aws para ~> 6.53 em multi-region e dns-subzone-delegation alinha essas duas roots com o resto do repo (PR #1681) — elas só estavam travadas pela quebra do Cloudflare v5.

O que mudou por root

pkg/octopus/deploy/terraform/multi-region

  • cloudflare_record.connect_anycast / cloudflare_record.signal_anycast renomeados para cloudflare_dns_record (rename de resource do v5). Blocos moved estão no main.tf, então o state acompanha automaticamente.
  • Ambos os resources cloudflare_ruleset (octopus_waf_managed, octopus_waf_custom): os blocos repetidos rules { … } viraram um único list attribute rules = [ { … } ]; action_parameters virou um objeto aninhado. Os valores dos atributos não mudaram — mesmas actions, mesmas expressions, mesmo id de managed-ruleset.

platform/terraform/status-edge

  • cloudflare_record.status_acloudflare_dns_record.status_a (+ bloco moved). Todos os atributos do record inalterados (A record proxied, ttl = 1, mesmo comment).
  • cloudflare_ruleset.cache: rules virou list attribute; action_parameters, edge_ttl e browser_ttl viraram objetos aninhados. Semântica inalterada (edge TTL override 15 s, browser TTL bypass).

platform/terraform/dns-caa

  • cloudflare_record.caa_issue["…"] (×3) e cloudflare_record.caa_iodefcloudflare_dns_record (+ blocos moved cobrindo todas as instâncias do for_each). O bloco CAA data { … } virou um objeto data = { … } — mesmo payload flags / tag / value.

platform/terraform/dns-subzone-delegation

  • cloudflare_zone.customer: argumento v4 zone → v5 name; v4 account_id → v5 account = { id = … }. Mesmo nome de resource type — o state faz upgrade in place, sem mudança de address.
  • O argumento v4 plan foi removido do cloudflare_zone upstream. O rate plan agora vive num NOVO resource companheiro cloudflare_zone_subscription.customer com exatamente o mesmo mapeamento de tier (tier freefree, todo o resto → pro). Para zonas que já existem, importe a subscription antes do primeiro apply (ver abaixo).
  • cloudflare_zone_dnssec.customer: o v5 expõe a chave explicitamente — status = "active" preserva o comportamento v4 de habilitar na criação.
  • cloudflare_api_token.customer: blocos v4 policy { … } → v5 policies = [ { … } ]; ids de permission-group viraram objetos (permission_groups = [{ id = … }]) e resources agora é uma string JSON-encoded (jsonencode({ … })). Mesmo permission group id de DNS-Write, mesmo escopo por zona.
  • O output customer_zones[*].zone_name agora lê cloudflare_zone.customer[k].name (o v5 renomeou o atributo zone para name); o formato do output não mudou.

Migração de state

Resources renomeados — automático via blocos moved

O provider v5 (≥ 5.19; pinamos ~> 5.21) implementa o provider-defined move do Terraform (MoveResourceState) para cloudflare_recordcloudflare_dns_record. Com Terraform ≥ 1.8 (o repo usa 1.15.x), os blocos moved já commitados em cada root migram as entradas de state in place no próximo plan/apply — sem cirurgia manual de state, sem destroy/create. É o mesmo mecanismo que a ferramenta tf-migrate da própria Cloudflare emite.

Resources com mesmo nome — automático via state upgraders

cloudflare_ruleset, cloudflare_zone, cloudflare_zone_dnssec e cloudflare_api_token mantiveram seus type names; o provider v5 embute state upgraders que convertem o formato de state v4 na primeira vez que o provider v5 o lê. Nada a executar.

Resource novo — cloudflare_zone_subscription (só dns-subzone-delegation)

Para cada zona de customer que já existe no state, importe a subscription para o Terraform não emitir uma chamada redundante de set de plano:

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 — cirurgia manual de state (só se um bloco moved for rejeitado)

Se alguma ferramenta do pipeline não honrar o provider-defined move (ex.: um CLI que não é o Terraform), o caminho manual equivalente é remove + import. Os ids dos records já estão documentados no 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>"

Procedimento de execução (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 do apply — o plan DEVE mostrar:

  • linhas # cloudflare_record.X has moved to cloudflare_dns_record.X para cada record renomeado;
  • 0 to add, 0 to change, 0 to destroy (multi-region/dns-subzone-delegation: adições são aceitáveis SOMENTE para imports de cloudflare_zone_subscription que você pulou intencionalmente);
  • NENHUM destroy, NENHUM replace (-/+) em qualquer cloudflare_dns_record, cloudflare_ruleset, cloudflare_zone ou cloudflare_api_token.

Checagem rápida:

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

Se (e somente se) o gate passar:

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

Rollback

  • Antes do apply: nada foi escrito — git revert no commit da migração, terraform init -upgrade de volta para o lockfile v4, pronto. Blocos moved são só de plan-time.
  • Depois do apply: o state agora usa addresses/formatos v5. Restaure o snapshot de state feito acima (terraform state push pre-v5-<date>.tfstate.backup) e faça git revert da config na mesma mudança; depois rode terraform plan com v4 para confirmar No changes. O backend S3 mantém objetos versionados como segunda fonte de recuperação.
  • Os records/rulesets em si nunca são tocados por uma migração correta — rollback é uma operação de config/state, não uma mudança de DNS.

Pós-migração

  • Remover a regra de ignore do cloudflare no .github/dependabot.yml para os minors v5.x voltarem a fluir (rastreado em #1679 — deliberadamente NÃO faz parte desta mudança; só acontece depois da migração de state viva ser executada).
  • terraform plan em cada root no próximo drift check agendado deve reportar No changes.