🚀 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 envar.cloudflare_zone_id_figueiral_netrecord_id: lo sacas de la UI de Cloudflare (detalles del registro) o con API:Resultado: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'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_eachen bloquesimport {}. 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:
- ✍️ Escribes el módulo (
config first) - 🏷️ Añades el
import {}con el ID<zone>/<record> - ✨
fmt→validate→plan→apply - 🧹 Borras el bloque y
applyde nuevo - 🎉 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.