Curso completo · 14 bloques · en orden
Terraform desde cero hasta trabajar con él en equipo
Una sola página con la teoría, simuladores interactivos, ejemplos copiables, autoevaluaciones, ejercicios con solución y un proyecto final. Sigue los bloques en orden: cada uno usa lo que aprendiste en el anterior.
| Semana | Bloques | Objetivo |
|---|---|---|
| 1 | 01–04 | Entender el modelo y crear/destruir tu primera infraestructura |
| 2 | 05–08 | Escribir configuraciones reales: dependencias, variables, state y bucles |
| 3 | 09–11 | Reutilizar con módulos y trabajar con state remoto y varios entornos |
| 4 | 12–14 | Tests, seguridad, CI/CD y proyecto final de principio a fin |
Marca cada bloque como completado al final: el progreso se guarda en este navegador.
Bloque 01 · Fundamentos · ~2 h
Qué es la infraestructura como código y qué hace Terraform
Antes de escribir una línea, entiende el problema que resuelve Terraform y las tres piezas con las que trabaja: el código, los providers y el state.
- Por qué gestionar infraestructura a mano no escala.
- La diferencia entre un enfoque declarativo y uno imperativo.
- Cómo se reparten el trabajo Terraform Core, los providers y el state.
- Dónde encaja Terraform frente a OpenTofu, Pulumi, CloudFormation y Ansible.
El problema: infraestructura hecha a mano
Imagina que montas un entorno en la consola de AWS: una red, dos subredes, un balanceador, tres servidores y una base de datos. Funciona. Seis meses después te piden un entorno idéntico para pruebas. Nadie recuerda qué casillas se marcaron, qué puerto se abrió “un momento” para depurar ni por qué la base de datos tiene ese tamaño.
- No es reproducible: cada entorno se parece al anterior, pero no es igual.
- No hay historial: no sabes quién cambió qué, cuándo ni por qué.
- No hay revisión: un cambio en producción no pasa por ningún pull request.
- Deriva: la realidad se aleja de la documentación desde el primer día.
La solución: describir la infraestructura en ficheros
Infraestructura como código (IaC) significa que la infraestructura se define en ficheros de texto que viven en git, se revisan en pull requests y se aplican con una herramienta. Terraform es la herramienta de IaC más usada para esto, y funciona con casi cualquier plataforma que tenga API: AWS, Azure, Google Cloud, Kubernetes, GitHub, Cloudflare, Datadog y miles más.
Esta es la idea central de todo el curso. En un enfoque imperativo escribes los pasos. En uno declarativo escribes el estado final que quieres, y la herramienta calcula los pasos necesarios para llegar desde donde estás. Por eso Terraform es idempotente: si la realidad ya coincide con el código, no hace nada.
Cómo funciona Terraform por dentro
- Terraform Core es el binario
terraform. Lee tus ficheros, construye un grafo de dependencias, compara lo que quieres con lo que existe y decide qué crear, cambiar o borrar. No sabe nada de AWS ni de Azure. - Providers son plugins que traducen recursos a llamadas de API.
hashicorp/awssabe crear una instancia EC2;integrations/githubsabe crear un repositorio. Se descargan del Terraform Registry. - State es un fichero JSON donde Terraform apunta qué objeto real corresponde a cada recurso de tu código (por ejemplo, que
aws_instance.webesi-0b7d4e19a2). Sin él, Terraform no sabría qué gestiona. Tiene un bloque entero dedicado (07).
Terraform frente a otras herramientas
| Herramienta | Enfoque | Lenguaje | Plataformas | Cuándo elegirla |
|---|---|---|---|---|
| Terraform | Declarativo, state propio | HCL | Cualquiera con provider | Estándar de facto, multi-nube, enorme ecosistema |
| OpenTofu | Fork abierto de Terraform | HCL | Las mismas | Si necesitas licencia open source (MPL 2.0). Casi todo lo de esta guía aplica igual |
| Pulumi | Declarativo con lenguajes generales | TypeScript, Python, Go, C#… | Multi-nube | Equipos que prefieren bucles y tipos de un lenguaje de programación |
| CloudFormation / CDK | Declarativo, state gestionado por AWS | YAML/JSON (CDK: TS, Python…) | Solo AWS | Organizaciones 100 % AWS que no quieren gestionar state |
| Ansible | Configuración de servidores, procedimental | YAML | Servidores y algo de nube | Instalar y configurar software dentro de máquinas. Complementa a Terraform |
Desde agosto de 2023 (versión 1.6), Terraform se publica con la licencia Business Source License 1.1. Para usarlo en tu empresa para gestionar vuestra propia infraestructura no cambia nada; la restricción afecta a quien vende productos que compiten con HashiCorp. Como respuesta nació OpenTofu, un fork mantenido bajo la Linux Foundation. HashiCorp forma parte de IBM desde 2025. Lo que aprendas aquí sirve para ambos: en OpenTofu el comando es tofu en lugar de terraform.
Practica
- Elige un sistema que conozcas (el de tu trabajo o un proyecto personal) y lista en un fichero de texto qué piezas de infraestructura tiene: redes, máquinas, bases de datos, DNS, buckets, colas. Al lado de cada una, escribe de qué otra pieza depende. Lo usarás en el bloque 05.
- Lee la introducción oficial What is Terraform? (10 minutos).
Bloque 02 · Entorno · ~3 h
Instalación, editor y tu primer proyecto sin gastar dinero
Instala Terraform, configura el editor y crea y destruye un contenedor nginx con Docker. No necesitas cuenta en ninguna nube para este bloque.
- Instalar Terraform y fijar versiones por proyecto.
- Ejecutar un ciclo completo con el provider de Docker.
- Qué ficheros forman un proyecto y cuáles van a git.
- Cómo dar credenciales de nube de forma segura.
Instalar Terraform
Instrucciones oficiales para todos los sistemas en developer.hashicorp.com/terraform/install.
Fijar la versión por proyecto con tfenv
En equipo, todos deben usar la misma versión. tfenv instala varias versiones y elige la correcta leyendo un fichero .terraform-version del repositorio. Si prefieres una herramienta para todos tus lenguajes, mise o asdf hacen lo mismo. Usa un único método: no mezcles el paquete de apt con tfenv.
Editor
- VS Code: extensión oficial HashiCorp Terraform (autocompletado, documentación al pasar el ratón, formateo al guardar).
- Neovim / LazyVim:
:LazyExtrasy activalang.terraform. Instalaterraform-lsy el formateo. - Autocompletado en la terminal:
terraform -install-autocompletey reinicia la shell.
Tu primer proyecto: nginx en Docker
Necesitas Docker en marcha. Crea una carpeta y dentro un fichero main.tf:
Fíjate en image = docker_image.nginx.image_id: el contenedor usa un atributo de la imagen. Esa referencia hace que Terraform cree primero la imagen y después el contenedor. Lo verás a fondo en el bloque 05.
El provider hashicorp/local no necesita nada instalado y sirve para practicar el ciclo completo creando un fichero:
Credenciales de nube (cuando pases a AWS)
Terraform usa las mismas credenciales que la CLI de cada nube. Nunca las escribas en ficheros .tf.
Crea una alerta de presupuesto (AWS Budgets, por ejemplo de 5 USD), usa una cuenta de pruebas separada y ejecuta terraform destroy al terminar cada sesión. Si prefieres no gastar nada, LocalStack emula muchos servicios de AWS en tu máquina.
Estructura de un proyecto y qué va a git
Terraform carga todos los ficheros .tf del directorio como si fueran uno solo. Los nombres de fichero son convención, no reglas. Esta es la convención más extendida:
mi-proyecto/ ├── versions.tf # bloque terraform {}: versión y required_providers ├── providers.tf # configuración de cada provider ├── main.tf # recursos ├── variables.tf # variables de entrada ├── outputs.tf # valores de salida ├── terraform.tfvars # valores de las variables (cuidado con secretos) ├── .terraform.lock.hcl # versiones exactas de providers: SÍ va a git └── .terraform/ # plugins descargados: NO va a git
Practica
- Ejecuta el proyecto de Docker completo y abre
http://localhost:8000. - Cambia
external = 8000por8080y ejecutaterraform plan. ¿Qué símbolo aparece junto al contenedor y por qué?Ver solución
Aparece -/+: el contenedor se debe reemplazar, porque Docker no permite cambiar los puertos de un contenedor existente. El provider marca ese argumento como
# forces replacement. Cambiar ciertos argumentos obliga a destruir y crear; otros se pueden actualizar en el sitio (~). - Ejecuta
terraform destroyy comprueba condocker ps -aque no queda nada.
Bloque 03 · Lenguaje · ~3 h
El lenguaje HCL: bloques, argumentos, tipos y referencias
HCL (HashiCorp Configuration Language) es pequeño. En una tarde conoces toda su sintaxis; el resto del curso es aprender qué bloques existen y cómo combinarlos.
- Leer cualquier bloque e identificar tipo, etiquetas y argumentos.
- Usar los tipos de datos y las referencias entre objetos.
- Interpolar strings y escribir textos multilínea.
- Experimentar con expresiones en
terraform console.
Anatomía de un bloque
La forma general de todo HCL es siempre la misma: TIPO "etiqueta" "etiqueta" { argumento = valor }. El número de etiquetas depende del tipo de bloque: resource lleva dos, variable una y locals ninguna.
Tipos de datos
| Tipo | Ejemplo | Notas |
|---|---|---|
string | "eu-west-1" | Siempre con comillas dobles |
number | 3, 0.5 | Enteros y decimales comparten tipo |
bool | true, false | |
list(T) | ["a", "b"] | Ordenada, admite duplicados, índice desde 0 |
set(T) | toset(["a", "b"]) | Sin orden ni duplicados; lo usa for_each |
map(T) | { env = "dev" } | Claves string, todos los valores del mismo tipo |
object({...}) | { name = "web", port = 80 } | Atributos con nombre y tipo propio cada uno |
tuple([...]) | ["web", 80] | Lista con un tipo por posición |
null | null | “Sin valor”: el argumento se omite y aplica el default del provider |
Strings, interpolación y textos multilínea
Fuera de un string no hace falta ${}: escribe instance_type = var.instance_type, no "${var.instance_type}". La segunda forma es de versiones antiguas (0.11) y terraform fmt no la corrige por ti.
Referencias: cómo se nombra cada cosa
| Expresión | Qué devuelve |
|---|---|
var.region | Valor de la variable de entrada region |
local.name | Valor calculado en un bloque locals |
aws_instance.web.id | Atributo id del recurso web de tipo aws_instance |
data.aws_ami.ubuntu.id | Atributo de un data source |
module.network.vpc_id | Output vpc_id de un módulo |
path.module | Ruta del módulo actual (para leer ficheros) |
terraform.workspace | Nombre del workspace activo |
count.index, each.key, each.value, self | Valores especiales dentro de bucles y provisioners (bloque 08) |
Operadores disponibles: aritméticos + - * / %, comparación == != < > <= >=, lógicos && || ! y el condicional condición ? si_true : si_false.
Tu laboratorio: terraform console
Ejecuta terraform console en cualquier carpeta para probar expresiones y funciones sin crear nada. Úsalo cada vez que dudes de lo que devuelve algo.
Dos comandos más que usarás a diario: terraform fmt formatea el código según el estilo oficial (alinea los =, la sangría a 2 espacios) y terraform validate comprueba que la sintaxis y los tipos son correctos sin conectarse a ninguna nube.
Practica
- En
terraform console, pruebalower,join(",", ["a","b"]),split("-", "a-b-c"),max(3, 7, 1)ymerge({a=1}, {b=2}). - Escribe un bloque
localscon un mapa de etiquetas comunes (Project,Owner) y un heredoc que interpole uno de esos valores. Comprueba el resultado conterraform consoleescribiendolocal.nombre.Ver solución
Bloque 04 · Flujo de trabajo · ~3 h
El ciclo de vida: init, plan, apply y destroy
Cuatro comandos cubren el 90 % del trabajo diario. Lo más importante del bloque: aprender a leer un plan antes de aprobarlo.
- Qué hace exactamente cada comando y en qué orden van.
- Interpretar los símbolos
+ ~ - -/+de un plan. - Guardar un plan y aplicar exactamente ese plan.
Qué hace cada comando
| Comando | Qué hace | Cuándo |
|---|---|---|
terraform init | Descarga providers y módulos, configura el backend y crea o respeta .terraform.lock.hcl | Al clonar, al añadir providers o módulos, al cambiar el backend. Es seguro repetirlo |
terraform validate | Comprueba sintaxis, tipos y referencias | Antes de cada plan; en CI |
terraform fmt | Formatea el código | Siempre. En CI con -check -recursive |
terraform plan | Lee la infraestructura real (refresh), la compara con el código y muestra los cambios. No modifica nada | Antes de cada apply |
terraform apply | Calcula un plan, pide confirmación y lo ejecuta. Actualiza el state | Para hacer los cambios |
terraform destroy | Destruye todo lo gestionado por esta configuración | Entornos temporales y laboratorios |
terraform output | Muestra los outputs del state | Para obtener IPs, URLs, IDs |
terraform show | Muestra el state o un plan guardado de forma legible | Para inspeccionar |
Cómo leer un plan
| Símbolo | Significado | Riesgo |
|---|---|---|
| + | Se creará | Bajo |
| ~ | Se actualizará en el sitio (mismo objeto, mismo ID) | Medio: revisa qué cambia |
| - | Se destruirá | Alto: confirma que es intencionado |
| -/+ | Se destruirá y se creará uno nuevo (reemplazo). Busca el comentario # forces replacement | Alto: pérdida de datos o corte de servicio |
| +/- | Se creará el nuevo y después se destruirá el viejo (create_before_destroy) | Medio |
| <= | Un data source se leerá durante el apply | Ninguno |
(known after apply) significa que el valor lo decide la plataforma al crear el objeto (un ID, una IP, un ARN). Lee siempre la última línea primero: si esperabas “1 to change” y ves “3 to destroy”, para y averigua por qué.
terraform plan -out=tfplan guarda el plan en un fichero binario y terraform apply tfplan ejecuta ese plan sin recalcularlo ni pedir confirmación. Si el state cambió entre medias, Terraform rechaza el plan por obsoleto. Así funcionan todos los pipelines de CI serios (bloque 13).
Practica
- Con el proyecto de Docker:
plan -out=tfplan, inspecciona conterraform show tfplany aplica conapply tfplan. - Cambia el nombre del contenedor a
"tutorial-2". Antes de ejecutar el plan, apuesta qué símbolo verás. Compruébalo.Ver solución
Verás -/+ con
# forces replacementenname: un contenedor de Docker no se puede renombrar a través de su API de creación, así que el provider lo reemplaza. La documentación de cada recurso en el Registry indica qué argumentos fuerzan reemplazo. - Para el contenedor a mano con
docker stop tutorialy ejecutaterraform plan. Observa cómo Terraform detecta la diferencia con la realidad.
Bloque 05 · Piezas básicas · ~4 h
Providers, recursos, data sources y el grafo de dependencias
Aquí empiezas a escribir configuraciones reales. La habilidad clave: leer la documentación de un recurso en el Registry y entender en qué orden crea Terraform las cosas.
- Declarar y fijar versiones de providers, también con varias regiones.
- Distinguir argumentos de atributos y leer la documentación del Registry.
- Consultar recursos existentes con data sources.
- Cómo construye Terraform el grafo y por qué crea cosas en paralelo.
Providers y versiones
| Restricción | Permite | Uso |
|---|---|---|
= 6.2.0 | Solo esa versión | Raro; congela demasiado |
>= 6.0 | Cualquier versión desde 6.0, incluida 7, 8… | Arriesgado en providers |
~> 6.0 | >= 6.0 y < 7.0 | Lo habitual: acepta mejoras, no cambios mayores |
~> 6.2.0 | >= 6.2.0 y < 6.3.0 | Solo parches |
La restricción dice qué versiones aceptas; el fichero .terraform.lock.hcl registra cuál se eligió exactamente (con sus hashes). Por eso se commitea: todo el equipo y el CI usan los mismos binarios. Para subir de versión, terraform init -upgrade y revisa el changelog del provider.
Recursos: argumentos y atributos
Un recurso tiene una dirección única: TIPO.NOMBRE, por ejemplo aws_s3_bucket.logs. El tipo empieza siempre por el nombre del provider (aws_, docker_, github_).
- Argumentos son lo que tú escribes:
bucket = "acme-logs". - Atributos son lo que puedes leer después:
aws_s3_bucket.logs.arn. Incluyen los argumentos y los valores que calcula la plataforma.
Cada página de recurso en el Registry tiene la misma estructura: Example Usage (copia de aquí), Argument Reference (qué puedes configurar, cuáles son obligatorios), Attribute Reference (qué puedes leer) e Import (cómo adoptar uno existente). Leerla bien es la mitad del trabajo con Terraform.
Data sources: leer lo que ya existe
Un data consulta algo que no gestiona esta configuración: la AMI más reciente, una VPC creada por otro equipo, la cuenta actual. Nunca crea ni borra nada.
Dependencias y el grafo
Terraform no ejecuta los recursos en el orden en que aparecen en el fichero. Construye un grafo dirigido a partir de las referencias: si la subred usa aws_vpc.main.id, la VPC va antes. Todo lo que no depende entre sí se crea en paralelo (10 operaciones a la vez por defecto, ajustable con -parallelism=N). Al destruir, el orden se invierte.
Si una dependencia existe pero no se ve en el código (por ejemplo, la aplicación de la instancia necesita que una política IAM exista al arrancar, pero no la referencia), declárala con depends_on. Úsalo solo en ese caso: las referencias son siempre preferibles porque además transmiten valores.
Puedes ver el grafo real de tu configuración con terraform graph | dot -Tsvg > grafo.svg (necesita Graphviz).
Practica
- Con la lista de infraestructura del bloque 01, dibuja el grafo: ¿qué se podría crear en paralelo?
- En AWS (o LocalStack): crea una VPC, dos subredes en zonas distintas, un security group que permita el puerto 80 y una instancia Ubuntu con un data source
aws_ami. Ejecutaterraform graph. - Lee la página de
aws_s3_bucketen el Registry y localiza qué argumento obliga a reemplazar el bucket si cambia.Ver solución
bucket(el nombre). En S3 no se puede renombrar un bucket, así que cambiarlo implica crear uno nuevo y borrar el antiguo, con sus datos. Buen candidato paraprevent_destroy(bloque 09).
Bloque 06 · Parametrizar · ~3 h
Variables, outputs y locals
Las variables son las entradas de tu configuración, los outputs sus salidas y los locals sus cálculos internos. Juntos convierten un ejemplo fijo en código reutilizable.
- Declarar variables con tipo, valor por defecto y validación.
- Las siete formas de dar valor a una variable y cuál gana.
- Exponer resultados con outputs y evitar repetición con locals.
- Qué hace (y qué no hace)
sensitive.
Variables de entrada
Una variable sin default es obligatoria: si nadie le da valor, Terraform la pide por teclado (o falla con -input=false, como en CI). Escribe siempre description y type: son la documentación de tu módulo.
Cómo se asignan los valores y cuál gana
Locals: cálculos internos
Regla práctica: si un valor lo decide quien usa el código, es una variable. Si se deriva de otros valores o se repite, es un local. No conviertas en variable algo que nunca va a cambiar.
Outputs
Los outputs tienen dos usos: mostrar información tras el apply y, sobre todo, pasar valores de un módulo a otro (bloque 10).
sensitive = true solo oculta el valor en la salida de plan y apply ((sensitive value)). El valor se guarda en claro en el state. La protección real es cifrar y restringir el acceso al state (bloque 11). Desde la versión 1.10 existen valores ephemeral y, desde la 1.11, argumentos write-only en algunos recursos, que evitan que ciertos secretos lleguen al state.
Practica
- Parametriza el proyecto de Docker: variable
external_port(number, default 8000, validación entre 1024 y 65535) y variablecontainer_name. Añade un outputurl.Ver solución
- Prueba
terraform plan -var="external_port=80"y lee el mensaje de error de la validación. - Crea
dev.tfvarsyprod.tfvarscon puertos distintos y aplica cada uno con-var-file.
Bloque 07 · El concepto más importante · ~4 h
El state: cómo sabe Terraform qué existe
Casi todos los sustos con Terraform vienen de no entender el state. Dedica a este bloque el tiempo que necesites y juega con el simulador hasta que puedas predecir cada plan.
- Qué contiene el state y por qué es imprescindible.
- Cómo se calculan los planes a partir de código, state y realidad.
- Qué es el drift y cómo detectarlo.
- Renombrar, importar y dejar de gestionar recursos sin destruirlos.
Tres fuentes de verdad
En cada plan, Terraform mira tres cosas: el código (lo que quieres), el state (lo que Terraform cree que gestiona, con sus IDs) y la realidad (lo que devuelve la API). Primero refresca el state con la realidad y después compara el resultado con el código.
Experimentos que merecen la pena: aplica, cambia instance_type en la consola y haz plan (Terraform quiere devolverlo a lo que dice el código); borra el state y haz apply (la instancia vieja queda huérfana y el bucket choca con el existente); después importa en lugar de aplicar.
Qué hay dentro del state
serial aumenta con cada escritura y lineage identifica la “historia” del state; Terraform los usa para no mezclar states distintos. Los atributos incluyen todo lo que devuelve la API, contraseñas incluidas.
Drift: cambios fuera de Terraform
Cuando alguien cambia algo en la consola, el siguiente plan muestra Objects have changed outside of Terraform y propone volver a lo que dice el código. Tienes dos opciones: dejar que Terraform lo revierta, o aceptar el cambio actualizando el código. Si solo quieres ver el drift sin proponer cambios:
Comandos de state
Refactorizar sin destruir: moved, import y removed
Si renombras aws_instance.web a aws_instance.app en el código sin más, Terraform ve un recurso que ya no existe (destroy) y uno nuevo (create). Estos bloques declaran la intención en el código, se revisan en el pull request y funcionan igual para todo el equipo:
Con import no necesitas escribir la configuración a mano: terraform plan -generate-config-out=generated.tf la genera a partir del objeto real. Revísala y límpiala antes de commitear.
1. Nunca lo subas a git: contiene secretos y provoca conflictos. 2. Nunca lo edites a mano salvo emergencia y con copia. 3. En equipo, guárdalo en un backend remoto con locking y versionado (bloque 11). 4. Un state más pequeño significa planes más rápidos y menos daño si algo sale mal.
Practica
- Con Docker:
terraform state listyterraform state show docker_container.nginx. - Borra el contenedor a mano (
docker rm -f tutorial) y ejecutaterraform plan. Explica el resultado. - Renombra
docker_container.nginxadocker_container.webusando un bloquemovedy comprueba que el plan dice has moved to sin destruir nada.Ver solución
El plan muestra
# docker_container.nginx has moved to docker_container.webyPlan: 0 to add, 0 to change, 0 to destroy. Tras aplicarlo en todos los entornos, puedes borrar el bloquemoved.
Bloque 08 · Expresiones · ~4 h
Bucles, condicionales y funciones
Para crear diez buckets no escribes diez bloques. count, for_each, las expresiones for y los bloques dynamic generan configuración a partir de datos.
- Cuándo usar
county cuándofor_each, y el error clásico de mezclarlos. - Transformar y filtrar colecciones con expresiones
for. - Generar bloques anidados con
dynamic. - Las funciones que más se usan, incluida
cidrsubnet.
count: N copias
for_each: una instancia por clave
Regla: usa count para “cero o uno” y para copias idénticas e intercambiables. Usa for_each para colecciones de cosas distintas con identidad propia (usuarios, buckets, subredes). for_each acepta un map o un set(string); convierte listas con toset(). Las claves deben conocerse en el plan: no pueden depender de atributos (known after apply).
Expresiones for y splat
Bloques dynamic
Algunos argumentos son bloques anidados repetibles (ingress, setting, rule). dynamic los genera desde una colección. Úsalo con moderación: un bloque escrito a mano se lee mejor que uno dinámico.
Funciones imprescindibles
No hay funciones definidas por el usuario: solo las integradas (más las que exponen algunos providers desde la 1.8). La referencia completa está agrupada por categorías.
| Función | Ejemplo | Resultado |
|---|---|---|
format | format("web-%02d", 3) | "web-03" |
join / split | join(",", ["a","b"]) | "a,b" |
replace | replace("a_b", "_", "-") | "a-b" |
length | length(["a","b"]) | 2 |
merge | merge({a=1}, {b=2}) | {a=1, b=2} |
lookup | lookup(var.sizes, "prod", "t3.micro") | Valor o default |
concat / flatten | flatten([["a"], ["b","c"]]) | ["a","b","c"] |
keys / values | keys({a=1, b=2}) | ["a","b"] |
toset / tolist | toset(["a","a","b"]) | ["a","b"] (set) |
coalesce | coalesce(var.name, "default") | Primer valor no nulo ni vacío |
try / can | try(var.cfg.port, 80) | Valor alternativo si la expresión falla |
jsonencode / yamlencode | jsonencode({Version = "2012-10-17"}) | String JSON (políticas IAM) |
file / templatefile | templatefile("init.sh.tftpl", {port = 80}) | Fichero con variables sustituidas |
cidrsubnet | cidrsubnet("10.0.0.0/16", 8, 1) | "10.0.1.0/24" |
Practica
- Con Docker, crea tres contenedores nginx con
for_eacha partir de un mapanombre => puerto. Quita uno del mapa y observa el plan.Ver solución
- Haz lo mismo con
county una lista, quita el elemento del medio y compara los planes. - Con
cidrsubnety una expresiónfor, genera 4 subredes /24 a partir de10.20.0.0/16.Ver solución
Bloque 09 · Control fino · ~2 h
Meta-argumentos, lifecycle y comprobaciones
Los meta-argumentos funcionan en cualquier recurso, sea del provider que sea, y cambian cómo Terraform lo trata: cuántas copias, con qué provider, en qué orden y cómo se reemplaza.
- Evitar cortes de servicio y borrados accidentales con
lifecycle. - Convivir con sistemas que modifican tus recursos (autoescalado, parches).
- Validar supuestos con pre y postcondiciones y bloques
check. - Por qué los provisioners son el último recurso.
| Meta-argumento | Para qué | Visto en |
|---|---|---|
depends_on | Dependencias que no se expresan con referencias | 05 |
count / for_each | Varias instancias de un recurso o módulo | 08 |
provider | Elegir una configuración con alias | 05 |
lifecycle | Cambiar cómo se crea, actualiza y destruye | Este bloque |
lifecycle
| Opción | Problema que resuelve |
|---|---|
create_before_destroy | Un reemplazo deja el servicio caído entre el destroy y el create |
prevent_destroy | Un cambio inocente obliga a reemplazar una base de datos o un bucket con datos |
ignore_changes | El autoescalado cambia desired_capacity y cada plan quiere revertirlo |
replace_triggered_by | Quieres reemplazar un recurso cuando cambie otro (desde 1.2) |
precondition / postcondition | Fallar pronto con un mensaje claro si un supuesto no se cumple (desde 1.2) |
terraform_data (desde 1.4) es un recurso integrado que no crea nada en ninguna nube; sirve para guardar un valor y disparar reemplazos. Sustituye al antiguo null_resource.
Bloques check: verificar que todo funciona
Un check (desde 1.5) se evalúa en cada plan y apply. Si falla, muestra un aviso pero no detiene la operación. Es útil para comprobar que la web responde o que un certificado no está a punto de caducar.
Provisioners: el último recurso
local-exec y remote-exec ejecutan comandos al crear un recurso. Terraform no puede planificar lo que hacen, no detecta drift en ellos y, si fallan, el recurso queda marcado como tainted. La documentación oficial los desaconseja. Alternativas, en orden de preferencia:
- Imágenes preconstruidas con Packer: la máquina arranca ya configurada.
- user_data / cloud-init para configuración al arrancar.
- Gestión de configuración (Ansible) para lo que cambia a menudo dentro de la máquina.
- El argumento o recurso nativo del provider, si existe: casi siempre existe.
Practica
- En el contenedor de Docker, añade
create_before_destroy = truey cambia el nombre. ¿Qué símbolo aparece ahora y qué problema surge?Ver solución
Aparece +/-. El apply falla porque el nuevo contenedor intenta usar el mismo puerto 8000 que el viejo, que aún existe.
create_before_destroyexige que el recurso nuevo pueda convivir con el viejo: nombres y puertos únicos. Por eso se suele combinar con nombres generados (name_prefix). - Añade
prevent_destroy = truea la imagen y ejecutaterraform destroy. Lee el error. - Añade una
preconditionque exija quevar.external_portno sea 8080 y prueba a romperla.
Bloque 10 · Reutilizar · ~4 h
Módulos: componer infraestructura con piezas reutilizables
Un módulo es una carpeta con ficheros .tf. Ya llevas nueve bloques escribiendo uno sin saberlo: el módulo raíz. Ahora aprenderás a llamar a otros.
- Escribir un módulo con su interfaz de variables y outputs.
- Usar módulos locales, del Registry y de git con versiones fijas.
- Conectar módulos entre sí a través de outputs.
- Qué hace bueno (y malo) a un módulo.
La idea
Un módulo funciona como una función: recibe variables, crea recursos por dentro y devuelve outputs. Desde fuera solo se ve esa interfaz.
Escribir un módulo
infra/ ├── modules/ │ └── network/ │ ├── main.tf # recursos │ ├── variables.tf # interfaz de entrada │ ├── outputs.tf # interfaz de salida │ ├── versions.tf # required_providers, sin bloque provider │ └── README.md └── envs/ └── dev/ └── main.tf # llama a ../../modules/network
Llamar a módulos
Después de añadir o cambiar el source de un módulo, ejecuta terraform init para descargarlo. Las direcciones dentro de un módulo llevan prefijo: module.network.aws_vpc.this. Para mover recursos existentes a un módulo sin destruirlos, usa un bloque moved (bloque 07).
Qué hace bueno a un módulo
- Representa un concepto (una red, un servicio web, una base de datos con sus backups), no un único recurso. Un módulo que envuelve un solo
aws_s3_buckety reexpone todos sus argumentos solo añade una capa. - Interfaz pequeña con defaults sensatos y
descriptionen cada variable y output. - Sin bloques
providerdentro. El que llama configura los providers; el módulo solo declararequired_providers. Si necesita un alias, se pasa conproviders = { aws = aws.use1 }. - Versionado con tags semánticos si lo usan otros equipos. Documentación generada con terraform-docs.
- Poca profundidad: módulos que llaman a módulos que llaman a módulos son muy difíciles de depurar. Dos niveles suelen bastar.
Antes de escribir uno, busca en el Registry de módulos. Los de terraform-aws-modules son muy usados y una buena referencia de estilo, aunque leerlos enteros cuesta: son genéricos a propósito.
Practica
- Convierte el ejercicio de Docker con
for_eachen un módulomodules/siteque recibanameyporty devuelvaurl. Llámalo dos veces desde el raíz.Ver solución
También podrías llamar al módulo una sola vez con
for_each = var.sites: los módulos admitencountyfor_eachdesde la versión 0.13. - Lee el README y los
variables.tfdeterraform-aws-modules/vpc/awsy crea una VPC con él.
Bloque 11 · Trabajo en equipo · ~4 h
Backends remotos, locking y varios entornos
Un state en tu portátil vale para aprender. En un equipo, el state vive en un sitio compartido, cifrado, versionado y con bloqueo para que dos personas no apliquen a la vez.
- Configurar un backend S3 con locking nativo y migrar a él.
- Qué ocurre sin locking cuando dos personas aplican a la vez.
- Separar dev, staging y prod: workspaces frente a directorios.
- Compartir datos entre states sin acoplarlos.
El backend S3
El backend decide dónde se guarda el state. Los más comunes son s3, azurerm, gcs y HCP Terraform. Primero hay que crear el bucket del state, normalmente con una configuración pequeña aparte que usa state local (el problema del huevo y la gallina):
El bloque backend no admite variables ni locals, porque se lee antes que nada. Para cambiar valores por entorno, usa configuración parcial con -backend-config.
Antes de la versión 1.10, el locking en S3 necesitaba una tabla de DynamoDB (dynamodb_table). Esa opción está obsoleta: en proyectos nuevos usa use_lockfile = true.
Qué evita el locking
Varios entornos: workspaces o directorios
| Workspaces (CLI) | Un directorio por entorno | |
|---|---|---|
| States | Uno por workspace, mismo backend | Uno por directorio, backend propio |
| Código | Idéntico; diferencias con terraform.workspace y tfvars | Cada entorno llama a los mismos módulos con valores distintos |
| Aislamiento | Débil: mismas credenciales y backend | Fuerte: cuentas, roles y buckets distintos |
| Riesgo | Aplicar en prod creyendo estar en dev | Algo de repetición entre directorios |
| Ideal para | Copias temporales (una por rama o PR) | dev / staging / prod |
infra/
├── modules/
│ ├── network/
│ └── web/
└── envs/
├── dev/
│ ├── main.tf # module "network" { source = "../../modules/network" ... }
│ ├── backend.tf # key = "envs/dev/..."
│ └── terraform.tfvars # instance_type = "t3.micro"
└── prod/
├── main.tf
├── backend.tf # key = "envs/prod/...", a menudo otra cuenta
└── terraform.tfvars # instance_type = "m6i.large"
Además de por entorno, divide los states por capas con distinto ritmo de cambio: red, datos y aplicaciones. Un state con 2.000 recursos tarda minutos en cada plan y cualquier error afecta a todo. Herramientas como Terragrunt ayudan a orquestar muchos states, pero empieza sin ellas.
Compartir datos entre states
Practica
- Crea el bootstrap del bucket de state, configura el backend S3 en tu proyecto y migra con
terraform init -migrate-state. Comprueba en la consola de S3 que el fichero existe y tiene versiones. - Abre dos terminales y lanza
terraform applyen ambas. Lee el error de la segunda. - Reorganiza tu proyecto en
modules/yenvs/dev+envs/prodcon keys de state distintas.
Bloque 12 · Calidad · ~4 h
Calidad, tests y seguridad
El código de infraestructura merece las mismas garantías que el de aplicación: formato, análisis estático, tests y escaneo de seguridad, en local y en CI.
- Montar la cadena fmt → validate → tflint → escáner de seguridad.
- Escribir tests nativos con
terraform testy providers simulados. - Gestionar secretos sin que acaben en el código.
La cadena de comprobaciones
- fmtEstilo uniforme
- validateSintaxis, tipos, referencias
- tflintErrores del provider y malas prácticas
- Trivy / CheckovConfiguración insegura
- terraform testComportamiento esperado
- Revisar el planSiempre una persona
- tflint: linter con plugins por provider (AWS, Azure, GCP).
- Trivy y Checkov: escáneres de seguridad de IaC. Trivy absorbió a tfsec.
- Infracost: estima cuánto cuesta un plan antes de aplicarlo.
Tests nativos: terraform test
Desde la versión 1.6, los ficheros *.tftest.hcl contienen bloques run con aserciones. Con command = plan no se crea nada; con command = apply (el valor por defecto) se crea infraestructura real y se destruye al final. Desde la 1.7, mock_provider simula un provider para testear sin credenciales.
Para tests de integración más complejos (desplegar, hacer peticiones HTTP, destruir) existe Terratest, en Go.
Secretos
| Mal | Bien |
|---|---|
Contraseña escrita en main.tf o en un tfvars commiteado | Leerla de un gestor: data "aws_secretsmanager_secret_version", Vault, o generarla con random_password y guardarla en el gestor |
Access keys de AWS en el bloque provider | Perfiles locales, roles IAM y OIDC en CI |
| State en un bucket sin cifrar y con acceso amplio | Bucket cifrado, versionado, con acceso mínimo y registro de accesos |
| Ficheros de secretos en claro en el repositorio | Cifrados con SOPS (hay provider para leerlos) o fuera del repositorio |
Practica
- Instala tflint y trivy y ejecútalos sobre tu proyecto de AWS. Corrige al menos dos avisos.
- Escribe un test con
mock_providerque compruebe que todas tus instancias tienen la etiquetaEnvironment.Ver solución
- Configura pre-commit en el repositorio y haz un commit con un fichero mal formateado.
Bloque 13 · Automatizar · ~4 h
CI/CD y trabajo en equipo
En un equipo nadie aplica desde su portátil. Los cambios entran por pull request, el pipeline muestra el plan, alguien lo revisa y el apply se ejecuta de forma automática y auditada.
- El flujo estándar de pull request para infraestructura.
- Autenticar el CI en la nube sin claves de larga duración (OIDC).
- Montar pipelines en GitHub Actions y Bitbucket Pipelines.
- Las prácticas de equipo que evitan incidentes.
El flujo
- Rama + PRCambio en el código
- Checksfmt, validate, tflint, trivy, test
- PlanSe publica en el PR
- RevisiónUna persona lee el plan
- MergeA main
- ApplyDel plan revisado
- DriftPlan programado diario
GitHub Actions
Bitbucket Pipelines
Con OIDC, la plataforma de CI emite un token firmado para cada ejecución y AWS lo cambia por credenciales temporales de un rol concreto. No hay access keys guardadas en variables del pipeline que puedan filtrarse o caducar. Usa un rol de solo lectura para los planes de pull requests y otro con permisos de escritura solo para la rama principal.
Plataformas especializadas
Si no quieres montar el flujo a mano: Atlantis (open source, comenta el plan en el PR y aplica con un comentario atlantis apply), HCP Terraform (state, ejecuciones, políticas y registro privado de HashiCorp), y alternativas como Spacelift, env0 o Scalr.
Prácticas de equipo
- Nada de cambios en la consola en entornos gestionados. Si hay emergencia, después se refleja en el código.
- States pequeños por entorno y capa: limitan el radio de impacto de cada error.
- Versiones fijadas: Terraform (
required_versiony.terraform-version), providers (lock file commiteado) y módulos (versionoref). - Planes legibles en el PR y revisión obligatoria cuando hay - o -/+.
- Detección de drift con un plan programado que avise si el resultado no es “No changes”.
terraform plan -detailed-exitcodedevuelve 2 cuando hay cambios. - Convención de etiquetas (
default_tags) para saber de quién es cada recurso y cuánto cuesta.
Practica
- Crea en AWS el proveedor de identidad OIDC de tu plataforma de CI y un rol con confianza limitada a tu repositorio.
- Monta el pipeline de tu plataforma: plan en cada PR y apply manual en main.
- Añade un pipeline programado que ejecute
terraform plan -detailed-exitcodey falle si el código de salida es 2.
Bloque 14 · Proyecto final · ~10 h
Proyecto final: una web en AWS de principio a fin
Junta todo lo aprendido en un repositorio que podrías enseñar en una entrevista: red, servidores, dos entornos, state remoto, tests y pipeline. Marca cada tarea al terminarla; se guarda en este navegador.
tf-web/ ├── bootstrap/ # bucket del state (state local) ├── modules/ │ ├── network/ # VPC, 2 subredes públicas, IGW, rutas │ └── web/ # SG dinámico, instancias, user_data con templatefile ├── envs/ │ ├── dev/ # backend propio, t3.micro, 1 instancia │ └── prod/ # backend propio, 2 instancias, prevent_destroy ├── tests/ # terraform test con mock_provider ├── .pre-commit-config.yaml ├── .tflint.hcl ├── .terraform-version └── bitbucket-pipelines.yml # o .github/workflows/terraform.yml
Fase 0 · Preparación
Hecho cuando aws sts get-caller-identity muestra la cuenta correcta.
Fase 1 · State remoto
Hecho cuando ves los dos ficheros de state en S3.
Fase 2 · Módulo network
Hecho cuando terraform output muestra la VPC y dos subredes.
Fase 3 · Módulo web
Hecho cuando curl a cada URL devuelve “Entorno: dev”.
Fase 4 · Dos entornos
Hecho cuando un cambio en un módulo aparece en el plan de ambos entornos.
Fase 5 · Calidad
Hecho cuando terraform test pasa en verde.
Fase 6 · CI/CD
Hecho cuando un PR que cambia el tipo de instancia muestra ~ en el pipeline y se aplica al hacer merge.
Fase 7 · Operación
Hecho cuando el coste diario vuelve a cero.
Haz el mismo proyecto con LocalStack y tflocal, o cambia de dominio: un clúster local con kind y los providers kubernetes y helm desplegando nginx en dos namespaces (dev y prod). Los conceptos son exactamente los mismos.
Anexo · Para volver siempre
Recursos, chuleta de comandos y glosario
Recursos para seguir aprendiendo
Documentación oficial
- Tutoriales oficialesRutas guiadas por nube (AWS, Azure, GCP, Docker). El mejor complemento práctico de esta guía.
- Referencia del lenguajeTodos los bloques, expresiones y meta-argumentos.
- Referencia de la CLICada comando con todas sus opciones.
- Guía de estilo oficialNombres, estructura de ficheros y convenciones.
- Terraform RegistryDocumentación de cada provider y módulo.
Libros
- Terraform: Up & Running (3.ª ed.)Yevgeniy Brikman, O'Reilly. El libro de referencia, muy práctico sobre equipos y producción.
- Terraform in ActionScott Winkler, Manning. Proyectos completos y patrones avanzados.
- Terraform Best PracticesAnton Babenko. Gratuito, centrado en estructura de proyectos y convenciones.
Práctica y certificación
- Certificación Terraform AssociateExamen oficial de nivel inicial. Esta guía cubre su temario; repasa la guía de estudio oficial antes.
- LocalStackEmula AWS en local para practicar sin coste.
- Cursos en vídeo de freeCodeCampCursos largos y gratuitos, incluido uno de preparación para la certificación.
Herramientas y comunidad
- OpenTofuEl fork abierto; su documentación también es muy clara.
- pre-commit-terraformHooks para fmt, validate, tflint, docs y más.
- awesome-tfLista curada de herramientas, módulos y artículos.
- HashiCorp DiscussForo oficial para dudas concretas.
Chuleta de comandos
| Comando | Para qué |
|---|---|
| terraform init | Preparar el directorio: providers, módulos y backend |
| terraform init -upgrade | Actualizar providers y módulos dentro de las restricciones |
| terraform init -migrate-state | Mover el state al backend nuevo |
| terraform fmt -recursive | Formatear todo el árbol |
| terraform validate | Comprobar sintaxis y tipos |
| terraform plan -out=tfplan | Calcular y guardar el plan |
| terraform apply tfplan | Aplicar exactamente el plan guardado |
| terraform apply -replace=ADDR | Forzar el reemplazo de un recurso |
| terraform plan -refresh-only | Ver el drift sin proponer cambios |
| terraform plan -destroy | Ver qué destruiría destroy |
| terraform plan -detailed-exitcode | Salida 0 = sin cambios, 1 = error, 2 = hay cambios |
| terraform plan -target=ADDR | Limitar a un recurso. Solo para emergencias: deja el resto sin aplicar |
| terraform plan -generate-config-out=f.tf | Generar HCL para los bloques import |
| terraform destroy | Destruir todo lo gestionado |
| terraform output -json | Leer outputs en JSON |
| terraform state list / show ADDR | Inspeccionar el state |
| terraform state mv / rm | Mover u olvidar recursos (mejor moved / removed) |
| terraform console | Probar expresiones y funciones |
| terraform graph | Exportar el grafo en formato DOT |
| terraform workspace new / select / list | Gestionar workspaces |
| terraform providers lock -platform=linux_amd64 | Añadir hashes de otras plataformas al lock file |
| terraform force-unlock ID | Liberar un lock colgado (con cuidado) |
| terraform test | Ejecutar los tests .tftest.hcl |
| terraform show -json tfplan | Plan en JSON, para scripts y herramientas |
Glosario
- Provider
- Plugin que traduce recursos de Terraform a llamadas a la API de una plataforma.
- Resource
- Objeto que Terraform crea y gestiona: una instancia, un bucket, un registro DNS.
- Data source
- Consulta de solo lectura sobre algo que existe fuera de esta configuración.
- State
- Registro que relaciona cada dirección del código con su objeto real y sus atributos.
- Backend
- Dónde se guarda el state y cómo se bloquea.
- Plan
- Lista de acciones que Terraform propone para que la realidad coincida con el código.
- Drift
- Diferencia entre la realidad y el state provocada por cambios fuera de Terraform.
- Module
- Carpeta de ficheros .tf con variables de entrada y outputs. El directorio donde ejecutas es el módulo raíz.
- Address
- Nombre único de un recurso:
module.network.aws_subnet.public["eu-west-1a"]. - Lock file
.terraform.lock.hcl: versiones y hashes exactos de los providers elegidos.- Workspace
- Varios states para la misma configuración y el mismo backend.
- Idempotencia
- Aplicar lo mismo varias veces da el mismo resultado: sin cambios si ya coincide.
- Tainted
- Recurso marcado para reemplazo, por ejemplo porque falló su creación.
- HCL
- HashiCorp Configuration Language, el lenguaje de los ficheros .tf.