Comandos útiles de Terraform: validate, fmt, show, providers, output, refresh y graph
Qué hace cada uno de estos comandos, cómo se usan en la práctica y cuáles de ellos tienen sentido en una pipeline de CI (y cuáles no).
- terraform
- iac
- ci
- buenas-practicas
Hasta ahora en la serie hemos ido viendo bloques de configuración: providers, variables, atributos, outputs. Este post es distinto, va sobre la propia CLI de Terraform: comandos que no cambian infraestructura, pero que acabas usando todos los días para depurar, formatear e inspeccionar lo que tienes escrito.
terraform validate
Comprueba que la sintaxis HCL y los argumentos de cada bloque son correctos, sin llegar a planificar nada. No necesita credenciales ni conectividad con el backend, solo los providers ya descargados (terraform init antes).
$ terraform validate
Success! The configuration is valid.
Si metes un argumento que no existe, por ejemplo file_permissions en vez de file_permission, el error señala la línea exacta y sugiere el nombre correcto:
$ terraform validate
Error: Unsupported argument
on main.tf line 9, in resource "local_file" "notice":
9: file_permissions = "0700"
An argument named "file_permissions" is not expected here. Did you mean "file_permission"?
validate no detecta todo: si el argumento existe pero el valor es inválido para la API del proveedor (un tipo de máquina que no existe en GCP, por ejemplo), eso no lo pilla validate, lo pilla plan al hablar con la API.
terraform fmt
Reescribe los ficheros .tf del directorio actual con el formato estándar de Terraform: indentación, alineación de =, espacios. No cambia lógica, solo estilo.
$ terraform fmt -recursive
main.tf
La salida lista los ficheros que ha tocado. Con -check no reescribe nada, solo dice si algo estaría mal formateado y devuelve un código de salida distinto de cero si es así, que es la forma de usarlo en CI (más abajo).
terraform show
Muestra el state actual: todos los recursos que Terraform gestiona con sus atributos calculados.
$ terraform show
# local_file.notice:
resource "local_file" "notice" {
content = "Office pet of the month: clever-lynx"
directory_permission = "0777"
file_permission = "0777"
filename = "./notice.txt"
id = "a1b2c3..."
}
# random_pet.office:
resource "random_pet" "office" {
id = "clever-lynx"
keepers = null
length = 2
separator = "_"
}
Con -json la misma información sale en JSON, pensada para que la consuma otra herramienta en vez de un humano:
$ terraform show -json | jq '.values.root_module.resources[0].values.filename'
"./notice.txt"
Esta forma JSON es la que consumen herramientas como Sentinel, OPA, Checkov o tfsec para revisar el plan antes del apply: en vez de parsear texto, parsean una estructura estable.
terraform providers
Lista qué providers necesita la configuración y cuáles hay ya en el state, sin tener que abrir versions.tf a mano.
$ terraform providers
Providers required by configuration:
.
├── provider[registry.terraform.io/hashicorp/local]
└── provider[registry.terraform.io/hashicorp/random]
Tiene un subcomando útil para entornos sin salida a internet: providers mirror descarga los binarios de los providers a un directorio local, para que otra máquina (o el runner de CI) pueda hacer init apuntando ahí en vez de a registry.terraform.io.
$ terraform providers mirror ./mirror
- Mirroring hashicorp/local...
- Mirroring hashicorp/random...
Esto conecta con lo que vimos en el post de providers: el .terraform.lock.hcl fija versiones y hashes, providers mirror es para cuando esos binarios no se pueden descargar directamente del registry.
terraform output
Ya lo vimos en el post de atributos y outputs.
$ terraform output
office-pet = "clever-lynx"
$ terraform output office-pet
"clever-lynx"
$ terraform output -json office-pet
"clever-lynx"
terraform refresh (y -refresh-only)
Sincroniza el state con lo que hay realmente desplegado, sin cambiar ningún recurso. Por defecto no hace falta llamarlo aparte: plan y apply ya refrescan el state en memoria antes de calcular el diff.
$ terraform plan
random_pet.office: Refreshing state... [id=clever-lynx]
local_file.notice: Refreshing state... [id=a1b2c3...]
No changes. Your infrastructure matches the configuration.
El comando terraform refresh suelto está deprecado desde Terraform 1.x a favor de terraform apply -refresh-only, que hace lo mismo pero mostrando primero un plan de los cambios que va a persistir en el state y pidiendo confirmación, en vez de escribir directamente:
$ terraform apply -refresh-only
random_pet.office: Refreshing state... [id=clever-lynx]
Terraform will perform the following actions:
...
Would you like to update the Terraform state to reflect these detected changes?
Tiene sentido cuando alguien ha tocado algo fuera de Terraform (un clic en la consola, un script suelto) y quieres que el state refleje la realidad sin tocar la infraestructura. Si lo que quieres es lo contrario, que plan/apply no pierdan tiempo comprobando el estado real, está -refresh=false; útil para acelerar un plan cuando sabes que nada ha cambiado por fuera, pero con el riesgo de que el state ya no refleje la realidad y el plan salga mal.
terraform graph
Genera el grafo de dependencias en formato DOT.
$ terraform graph
digraph {
compound = "true"
newrank = "true"
subgraph "root" {
"[root] local_file.notice (expand)" [label = "local_file.notice", shape = "box"]
"[root] random_pet.office (expand)" [label = "random_pet.office", shape = "box"]
"[root] local_file.notice (expand)" -> "[root] random_pet.office (expand)"
...
}
}
El DOT en crudo no dice mucho a simple vista. Con Graphviz instalado (apt install graphviz) se convierte en una imagen:
$ terraform graph | dot -Tsvg > graph.svg
Para dos recursos como este lab el grafo es trivial, pero en un módulo con decenas de recursos es la forma más rápida de ver qué depende de qué sin leer todo el código.
Cuáles de estos comandos tienen sentido en CI
No todos. Una pipeline de CI típica para un repo de Terraform hace esto, en este orden:
terraform fmt -check -recursive
terraform init -input=false -lockfile=readonly
terraform validate
terraform plan -input=false -out=plan.tfplan
terraform show -json plan.tfplan > plan.json # opcional, para políticas
terraform apply -input=false plan.tfplan # solo tras aprobación, en otro job
fmt -check -recursive:-checkno reescribe nada, solo comprueba y devuelve error si algo no está formateado;-recursivemira también los subdirectorios (módulos locales, por ejemplo), no solo el directorio actual. No necesitainitni credenciales, así que es el primer paso de la pipeline.init -lockfile=readonly: usa el.terraform.lock.hcldel repo tal cual, sin tocarlo. Si alguien necesita una versión de provider distinta, lo cambia en local y lo commitea; el CI no debe reescribir el lock file por su cuenta.validate: rápido, sin backend real ni credenciales de más, buen filtro antes de gastar tiempo en unplan.plan -out=plan.tfplan: el plan se guarda como artefacto. Elapplyposterior usa ese fichero exacto (apply plan.tfplan), no vuelve a planificar. Así lo que se aprueba en la revisión es exactamente lo que se aplica, sin volver a preguntarle a la API por si algo cambió mientras tanto.show -json: si usas OPA, Checkov, tfsec o Sentinel para revisar cambios, el punto de entrada es el plan en JSON, no el.tf. Evalúa el cambio real, no solo el código estático.-input=falseen todos los comandos: en CI no hay terminal interactiva para responder un prompt; si falta una variable sin default, mejor que el comando falle explícito que se quede colgado esperando input.- La variable de entorno
TF_IN_AUTOMATION=trueavisa a Terraform de que se está ejecutando en una pipeline: ajusta algunos mensajes (por ejemplo, deja de sugerir comandos interactivos en los errores) pero no cambia el comportamiento real de ningún comando.
Lo que normalmente no entra en la pipeline:
refresh/apply -refresh-onlysueltos: si el state y la realidad se han desincronizado, es una señal de que algo cambió fuera de Terraform, y eso se investiga a mano, no se corrige automáticamente en cada ejecución de la pipeline.graph: sirve para que una persona depure un módulo a ojo, no aporta nada dentro de una pipeline automatizada.providers mirror: solo hace falta si el runner de CI no tiene salida aregistry.terraform.io; en ese caso elinitapunta al mirror local en vez de aregistry.terraform.io.
En resumen
validatecomprueba sintaxis y argumentos sin planificar;fmtnormaliza el estilo; ambos son baratos y van primero en cualquier pipeline.show(con-json) inspecciona el state o un plan guardado; es el formato que consumen OPA, Checkov, tfsec o Sentinel.providerslista qué providers usa la configuración;providers mirrorresuelve el caso sin acceso directo al registry.outputconsulta valores expuestos sin volver a aplicar nada.refreshestá deprecado a favor deapply -refresh-only; ninguno de los dos va en una pipeline, son para sincronizar el state a mano cuando algo cambió por fuera.graphvisualiza dependencias, útil para depurar un módulo grande, no para CI.- Una pipeline de CI razonable:
fmt -check→init -lockfile=readonly→validate→plan -out=→ (aprobación) →applydel plan guardado, todo con-input=false.
El ejemplo completo está en el repositorio terraform-zero-to-hero, en la carpeta labs/06-terraform-commands.