Saltar a contenido

🚀 Importar recursos en Terraform con import {}: El caso real de un SPF en Cloudflare

🤔 Por qué te vas a encontrar importando recursos

Porque la vida real no empieza con terraform apply. Empieza con alguien (a veces tú, hace seis meses) creando un registro DNS a mano en el panel de Cloudflare. O migras de otra herramienta. O heredas infraestructura clickops que ahora hay que poner bajo control de versión.

El drift existe. La importación es cómo le dices a Terraform: "esto ya existe, adopta su estado y gestiónalo tú a partir de ahora".


⚔️ import {} vs terraform import legacy

Característica terraform import (CLI) Bloque import {} (1.5+)
Dónde vive Solo en state local En tu código .tf (versionado)
Revisión en PR Imposible Nativa: git diff lo muestra
Plan preview No (ciego hasta apply) Sí: terraform plan lo valida
Generar config Manual terraform plan -generate-config-out=...
Limpieza posterior Manual (state rm) Borra el bloque y apply
Idempotencia Frágil Declarativa

Veredicto: 🏆 El bloque import {} gana por goleada. Es infrastructure as code de verdad, no un comando one-shot que se pierde en el historial de bash.


🛠️ Paso a paso: Importar el SPF de figueiral.net

1️⃣ Config first: el recurso ya existe en tu código

module "spf_figueiral-net" {
  source   = "./modules/dns_config"
  zone_id  = var.cloudflare_zone_id_figueiral_net
  content  = "v=spf1 ~all"
  proxied  = false
  settings = {}
  tags     = []
  ttl      = 1
  name     = "figueiral.net"
  type     = "TXT"
}

El módulo dns_config crea un cloudflare_dns_record. El nombre del recurso dentro del módulo es this, así que la dirección completa es module.spf_figueiral-net.cloudflare_dns_record.this.

2️⃣ Averigua el ID del proveedor

Formato Cloudflare DNS: <zone_id>/<record_id>.

  • zone_id: lo tienes en var.cloudflare_zone_id_figueiral_net
  • record_id: lo sacas de la UI de Cloudflare (detalles del registro) o con API:
    curl -s -H "Authorization: Bearer $CF_API_TOKEN" \
      "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?name=figueiral.net&type=TXT" | jq -r '.result[0].id'
    
    Resultado: 64e61777e8403b71dbfebe51c2866dfd

3️⃣ Añade el bloque import {} temporal

import {
  to = module.spf_figueiral-net.cloudflare_dns_record.this
  id = "${var.cloudflare_zone_id_figueiral_net}/64e61777e8403b71dbfebe51c2866dfd"
}

Nota: El bloque va fuera del módulo, a nivel de root module (o donde tengas la llamada al módulo). Terraform 1.5+ lo entiende nativamente.

4️⃣ Flujo completo de comandos

# 1. Inicializa (descarga providers, módulos) 🚀
terraform init

# 2. Formatea y valida ANTES de planear ✨
terraform fmt -recursive
terraform validate

# 3. Plan: previsualiza la importación SIN tocar state real 🔍
terraform plan
# Deberías ver: "import block will import existing resource..."

# 4. Apply: ejecuta la importación y sincroniza state ✅
terraform apply
# Confirma con 'yes'

# 5. Limpia: BORRA el bloque import {} del código 🧹
#    (sí, de verdad: bórralo, guárdalo en git si quieres historial)
terraform apply
# Ahora el recurso está gestionado sin bloque de importación

✅ Buenas prácticas que te ahorran dolores

Práctica Por qué
Config first 📝 Escribes el recurso antes de importar. Así plan detecta drift real.
fmt + validate siempre Evita sorpresas de sintaxis en el plan.
-generate-config-out opcional 🤖 Si no tienes el recurso en código: terraform plan -generate-config-out=imported.tf te genera un esqueleto. Revisa, mueve a tu módulo, borra el archivo temporal.
Limpia el bloque después 🧹 El bloque import {} es transitorio. Déjalo y el siguiente apply intentará re-importar (error o no-op según versión).
Commitea el state tras apply 💾 Si usas backend remoto, el lock lo gestiona Terraform. Si local, git add terraform.tfstate (o mejor: usa backend remoto).

💡 Tips pro: bulk import y cf-terraforming

🔄 for_each + import {} para múltiples registros

# Mapa de registros a importar
locals {
  spf_records = {
    "figueiral.net"     = "64e61777e8403b71dbfebe51c2866dfd"
    "mail.figueiral.net" = "a1b2c3d4e5f67890123456789abcdef0"
  }
}

# Recurso con for_each
module "spf_records" {
  for_each = local.spf_records
  source   = "./modules/dns_config"
  zone_id  = var.cloudflare_zone_id_figueiral_net
  name     = each.key
  type     = "TXT"
  content  = "v=spf1 ~all"
  ttl      = 1
  proxied  = false
}

# Bloques import generados dinámicamente (Terraform 1.6+)
import {
  for_each = local.spf_records
  to       = module.spf_records[each.key].cloudflare_dns_record.this
  id       = "${var.cloudflare_zone_id_figueiral_net}/${each.value}"
}

Terraform 1.6 permite for_each en bloques import {}. Antes tenías que escribir uno a mano por registro.

🧰 cf-terraforming: la navaja suiza para Cloudflare

go install github.com/cloudflare/cf-terraforming@latest
cf-terraforming import --resources=dns_records --zone-id=<ZONE_ID> --output=cf_import.tf

Te genera: - 📄 Recursos cloudflare_dns_record completos - 📦 Bloques import {} listos para apply - ⚙️ Variables y outputs coherentes

Úsalo para la importación inicial masiva. Luego refactoriza a tus módulos.


🎯 Conclusión

El bloque import {} convierte la importación en código revisable, testeable y versionado. Ya no es un comando arcano que corres a oscuras: es parte de tu flujo de PR, de tu plan, de tu historia.

Para el SPF de figueiral.net:

  1. ✍️ Escribes el módulo (config first)
  2. 🏷️ Añades el import {} con el ID <zone>/<record>
  3. fmtvalidateplanapply
  4. 🧹 Borras el bloque y apply de nuevo
  5. 🎉 El recurso es tuyo. Gestionado. Versionado. Sin drift.

Y la próxima vez que alguien pregunte "¿quién creó este registro?", git blame te responde. Eso, amigo, es infraestructura como código de verdad.