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.

14bloques
~45 hde estudio
7simuladores
SemanaBloquesObjetivo
101–04Entender el modelo y crear/destruir tu primera infraestructura
205–08Escribir configuraciones reales: dependencias, variables, state y bucles
309–11Reutilizar con módulos y trabajar con state remoto y varios entornos
412–14Tests, seguridad, CI/CD y proyecto final de principio a fin

Marca cada bloque como completado al final: el progreso se guarda en este navegador.

~/infra/envs/dev

    

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.

Al terminar sabrás
  • 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/aws sabe crear una instancia EC2; integrations/github sabe 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.web es i-0b7d4e19a2). Sin él, Terraform no sabría qué gestiona. Tiene un bloque entero dedicado (07).

Terraform frente a otras herramientas

HerramientaEnfoqueLenguajePlataformasCuándo elegirla
TerraformDeclarativo, state propioHCLCualquiera con providerEstándar de facto, multi-nube, enorme ecosistema
OpenTofuFork abierto de TerraformHCLLas mismasSi necesitas licencia open source (MPL 2.0). Casi todo lo de esta guía aplica igual
PulumiDeclarativo con lenguajes generalesTypeScript, Python, Go, C#…Multi-nubeEquipos que prefieren bucles y tipos de un lenguaje de programación
CloudFormation / CDKDeclarativo, state gestionado por AWSYAML/JSON (CDK: TS, Python…)Solo AWSOrganizaciones 100 % AWS que no quieren gestionar state
AnsibleConfiguración de servidores, procedimentalYAMLServidores y algo de nubeInstalar y configurar software dentro de máquinas. Complementa a Terraform
Licencia y OpenTofu

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

  1. 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.
  2. 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.

Al terminar sabrás
  • 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: :LazyExtras y activa lang.terraform. Instala terraform-ls y el formateo.
  • Autocompletado en la terminal: terraform -install-autocomplete y 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.

Sin Docker

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.

Antes de tocar una nube real

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

  1. Ejecuta el proyecto de Docker completo y abre http://localhost:8000.
  2. Cambia external = 8000 por 8080 y ejecuta terraform 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 (~).

  3. Ejecuta terraform destroy y comprueba con docker ps -a que 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.

Al terminar sabrás
  • 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

Interactivo

Pulsa cada parte del código

Cada fragmento resaltable tiene un nombre y una función. Pulsa o navega con el tabulador.

resource "aws_instance" "web" {
  ami           = "ami-0abc123"
  instance_type = var.instance_type

  tags = {
    Name = "web-${var.env}"
  }

  root_block_device {
    volume_size = 20
  }
}  # comentario
Elige una parte
La explicación aparecerá aquí.

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

TipoEjemploNotas
string"eu-west-1"Siempre con comillas dobles
number3, 0.5Enteros y decimales comparten tipo
booltrue, 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
nullnull“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ónQué devuelve
var.regionValor de la variable de entrada region
local.nameValor calculado en un bloque locals
aws_instance.web.idAtributo id del recurso web de tipo aws_instance
data.aws_ami.ubuntu.idAtributo de un data source
module.network.vpc_idOutput vpc_id de un módulo
path.moduleRuta del módulo actual (para leer ficheros)
terraform.workspaceNombre del workspace activo
count.index, each.key, each.value, selfValores 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

  1. En terraform console, prueba lower, join(",", ["a","b"]), split("-", "a-b-c"), max(3, 7, 1) y merge({a=1}, {b=2}).
  2. Escribe un bloque locals con un mapa de etiquetas comunes (Project, Owner) y un heredoc que interpole uno de esos valores. Comprueba el resultado con terraform console escribiendo local.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.

Al terminar sabrás
  • 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.
Simulador

Ejecuta los comandos y observa ficheros e infraestructura

Pruébalos en orden y también desordenados (por ejemplo, plan sin init) para ver qué error da Terraform.

bash: ~/demo

Ficheros en el directorio

  • main.tf
  • .terraform/ (providers)
  • .terraform.lock.hcl
  • terraform.tfstate

Infraestructura real (AWS)

aws_s3_bucket.assets
aws_security_group.web
aws_instance.web

Qué hace cada comando

ComandoQué haceCuándo
terraform initDescarga providers y módulos, configura el backend y crea o respeta .terraform.lock.hclAl clonar, al añadir providers o módulos, al cambiar el backend. Es seguro repetirlo
terraform validateComprueba sintaxis, tipos y referenciasAntes de cada plan; en CI
terraform fmtFormatea el códigoSiempre. En CI con -check -recursive
terraform planLee la infraestructura real (refresh), la compara con el código y muestra los cambios. No modifica nadaAntes de cada apply
terraform applyCalcula un plan, pide confirmación y lo ejecuta. Actualiza el statePara hacer los cambios
terraform destroyDestruye todo lo gestionado por esta configuraciónEntornos temporales y laboratorios
terraform outputMuestra los outputs del statePara obtener IPs, URLs, IDs
terraform showMuestra el state o un plan guardado de forma legiblePara inspeccionar

Cómo leer un plan

SímboloSignificadoRiesgo
+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 replacementAlto: 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 applyNinguno

(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é.

Aplica exactamente lo que revisaste

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

  1. Con el proyecto de Docker: plan -out=tfplan, inspecciona con terraform show tfplan y aplica con apply tfplan.
  2. 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 replacement en name: 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.

  3. Para el contenedor a mano con docker stop tutorial y ejecuta terraform 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.

Al terminar sabrás
  • 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ónPermiteUso
= 6.2.0Solo esa versiónRaro; congela demasiado
>= 6.0Cualquier versión desde 6.0, incluida 7, 8…Arriesgado en providers
~> 6.0>= 6.0 y < 7.0Lo habitual: acepta mejoras, no cambios mayores
~> 6.2.0>= 6.2.0 y < 6.3.0Solo 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.

Simulador

Así recorre Terraform el grafo

Cada flecha es una referencia en el código. Observa qué se crea a la vez y cuánto tiempo se ahorra frente a hacerlo en serie.

Pulsa “Simular apply”.
0 stiempo simulado

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

  1. Con la lista de infraestructura del bloque 01, dibuja el grafo: ¿qué se podría crear en paralelo?
  2. 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. Ejecuta terraform graph.
  3. Lee la página de aws_s3_bucket en 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 para prevent_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.

Al terminar sabrás
  • 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

Interactivo

Precedencia de variables

Activa y desactiva fuentes. Están ordenadas de menor a mayor prioridad: la última activa 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 no cifra nada

    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

    1. Parametriza el proyecto de Docker: variable external_port (number, default 8000, validación entre 1024 y 65535) y variable container_name. Añade un output url.
      Ver solución
    2. Prueba terraform plan -var="external_port=80" y lee el mensaje de error de la validación.
    3. Crea dev.tfvars y prod.tfvars con 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.

    Al terminar sabrás
    • 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.

    Simulador

    Código, state y realidad

    Empieza con apply. Después cambia el código, provoca cambios “a mano” en la nube o borra el state, y ejecuta plan para ver cómo razona Terraform.

    Código

    main.tf: lo que quieres

    Cambiar ami fuerza reemplazo; instance_type se actualiza en el sitio.

    State

    terraform.tfstate

    Realidad

    lo que hay en AWS

    Comandos

    Alguien toca la nube a mano

    salida

    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.

    Reglas de oro del state

    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

    1. Con Docker: terraform state list y terraform state show docker_container.nginx.
    2. Borra el contenedor a mano (docker rm -f tutorial) y ejecuta terraform plan. Explica el resultado.
    3. Renombra docker_container.nginx a docker_container.web usando un bloque moved y 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.web y Plan: 0 to add, 0 to change, 0 to destroy. Tras aplicarlo en todos los entornos, puedes borrar el bloque moved.

    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.

    Al terminar sabrás
    • Cuándo usar count y cuándo for_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

    Simulador

    count frente a for_each al cambiar una lista

    Cada nombre es un bucket. Quita staging o añade uno al principio y compara los planes. Pulsa “Aplicar” para consolidar el estado actual.

    count = length(var.envs)

      for_each = toset(var.envs)

        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ónEjemploResultado
        formatformat("web-%02d", 3)"web-03"
        join / splitjoin(",", ["a","b"])"a,b"
        replacereplace("a_b", "_", "-")"a-b"
        lengthlength(["a","b"])2
        mergemerge({a=1}, {b=2}){a=1, b=2}
        lookuplookup(var.sizes, "prod", "t3.micro")Valor o default
        concat / flattenflatten([["a"], ["b","c"]])["a","b","c"]
        keys / valueskeys({a=1, b=2})["a","b"]
        toset / tolisttoset(["a","a","b"])["a","b"] (set)
        coalescecoalesce(var.name, "default")Primer valor no nulo ni vacío
        try / cantry(var.cfg.port, 80)Valor alternativo si la expresión falla
        jsonencode / yamlencodejsonencode({Version = "2012-10-17"})String JSON (políticas IAM)
        file / templatefiletemplatefile("init.sh.tftpl", {port = 80})Fichero con variables sustituidas
        cidrsubnetcidrsubnet("10.0.0.0/16", 8, 1)"10.0.1.0/24"
        Calculadora

        cidrsubnet(prefix, newbits, netnum)

        newbits son los bits que añades a la máscara (de /16 a /24 son 8). netnum elige cuál de las subredes resultantes quieres, empezando en 0.

        Practica

        1. Con Docker, crea tres contenedores nginx con for_each a partir de un mapa nombre => puerto. Quita uno del mapa y observa el plan.
          Ver solución
        2. Haz lo mismo con count y una lista, quita el elemento del medio y compara los planes.
        3. Con cidrsubnet y una expresión for, genera 4 subredes /24 a partir de 10.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.

        Al terminar sabrás
        • 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-argumentoPara quéVisto en
        depends_onDependencias que no se expresan con referencias05
        count / for_eachVarias instancias de un recurso o módulo08
        providerElegir una configuración con alias05
        lifecycleCambiar cómo se crea, actualiza y destruyeEste bloque

        lifecycle

        OpciónProblema que resuelve
        create_before_destroyUn reemplazo deja el servicio caído entre el destroy y el create
        prevent_destroyUn cambio inocente obliga a reemplazar una base de datos o un bucket con datos
        ignore_changesEl autoescalado cambia desired_capacity y cada plan quiere revertirlo
        replace_triggered_byQuieres reemplazar un recurso cuando cambie otro (desde 1.2)
        precondition / postconditionFallar 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:

        1. Imágenes preconstruidas con Packer: la máquina arranca ya configurada.
        2. user_data / cloud-init para configuración al arrancar.
        3. Gestión de configuración (Ansible) para lo que cambia a menudo dentro de la máquina.
        4. El argumento o recurso nativo del provider, si existe: casi siempre existe.

        Practica

        1. En el contenedor de Docker, añade create_before_destroy = true y 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_destroy exige que el recurso nuevo pueda convivir con el viejo: nombres y puertos únicos. Por eso se suele combinar con nombres generados (name_prefix).

        2. Añade prevent_destroy = true a la imagen y ejecuta terraform destroy. Lee el error.
        3. Añade una precondition que exija que var.external_port no 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.

        Al terminar sabrás
        • 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_bucket y reexpone todos sus argumentos solo añade una capa.
        • Interfaz pequeña con defaults sensatos y description en cada variable y output.
        • Sin bloques provider dentro. El que llama configura los providers; el módulo solo declara required_providers. Si necesita un alias, se pasa con providers = { 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

        1. Convierte el ejercicio de Docker con for_each en un módulo modules/site que reciba name y port y devuelva url. 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 admiten count y for_each desde la versión 0.13.

        2. Lee el README y los variables.tf de terraform-aws-modules/vpc/aws y 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.

        Al terminar sabrás
        • 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):

        Dos detalles que confunden a todos

        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

        Simulador

        Ana y Luis lanzan apply a la vez

        Terminal de Ana
        terraform.tfstateserial 42sin bloquear
        Terminal de Luis

        Pulsa el botón con el locking activado y después desactivado. (Simplificado.)

        Varios entornos: workspaces o directorios

        Workspaces (CLI)Un directorio por entorno
        StatesUno por workspace, mismo backendUno por directorio, backend propio
        CódigoIdéntico; diferencias con terraform.workspace y tfvarsCada entorno llama a los mismos módulos con valores distintos
        AislamientoDébil: mismas credenciales y backendFuerte: cuentas, roles y buckets distintos
        RiesgoAplicar en prod creyendo estar en devAlgo de repetición entre directorios
        Ideal paraCopias 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

        1. 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.
        2. Abre dos terminales y lanza terraform apply en ambas. Lee el error de la segunda.
        3. Reorganiza tu proyecto en modules/ y envs/dev + envs/prod con 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.

        Al terminar sabrás
        • Montar la cadena fmt → validate → tflint → escáner de seguridad.
        • Escribir tests nativos con terraform test y providers simulados.
        • Gestionar secretos sin que acaben en el código.

        La cadena de comprobaciones

        1. fmtEstilo uniforme
        2. validateSintaxis, tipos, referencias
        3. tflintErrores del provider y malas prácticas
        4. Trivy / CheckovConfiguración insegura
        5. terraform testComportamiento esperado
        6. 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

        MalBien
        Contraseña escrita en main.tf o en un tfvars commiteadoLeerla 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 providerPerfiles locales, roles IAM y OIDC en CI
        State en un bucket sin cifrar y con acceso amplioBucket cifrado, versionado, con acceso mínimo y registro de accesos
        Ficheros de secretos en claro en el repositorioCifrados con SOPS (hay provider para leerlos) o fuera del repositorio

        Practica

        1. Instala tflint y trivy y ejecútalos sobre tu proyecto de AWS. Corrige al menos dos avisos.
        2. Escribe un test con mock_provider que compruebe que todas tus instancias tienen la etiqueta Environment.
          Ver solución
        3. 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.

        Al terminar sabrás
        • 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

        1. Rama + PRCambio en el código
        2. Checksfmt, validate, tflint, trivy, test
        3. PlanSe publica en el PR
        4. RevisiónUna persona lee el plan
        5. MergeA main
        6. ApplyDel plan revisado
        7. DriftPlan programado diario

        GitHub Actions

        Bitbucket Pipelines

        Por qué OIDC

        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_version y .terraform-version), providers (lock file commiteado) y módulos (version o ref).
        • 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-exitcode devuelve 2 cuando hay cambios.
        • Convención de etiquetas (default_tags) para saber de quién es cada recurso y cuánto cuesta.

        Practica

        1. Crea en AWS el proveedor de identidad OIDC de tu plataforma de CI y un rol con confianza limitada a tu repositorio.
        2. Monta el pipeline de tu plataforma: plan en cada PR y apply manual en main.
        3. Añade un pipeline programado que ejecute terraform plan -detailed-exitcode y 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.

        Sin cuenta de AWS

        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

        Libros

        Práctica y certificación

        Herramientas y comunidad

        Chuleta de comandos

        ComandoPara qué
        terraform initPreparar el directorio: providers, módulos y backend
        terraform init -upgradeActualizar providers y módulos dentro de las restricciones
        terraform init -migrate-stateMover el state al backend nuevo
        terraform fmt -recursiveFormatear todo el árbol
        terraform validateComprobar sintaxis y tipos
        terraform plan -out=tfplanCalcular y guardar el plan
        terraform apply tfplanAplicar exactamente el plan guardado
        terraform apply -replace=ADDRForzar el reemplazo de un recurso
        terraform plan -refresh-onlyVer el drift sin proponer cambios
        terraform plan -destroyVer qué destruiría destroy
        terraform plan -detailed-exitcodeSalida 0 = sin cambios, 1 = error, 2 = hay cambios
        terraform plan -target=ADDRLimitar a un recurso. Solo para emergencias: deja el resto sin aplicar
        terraform plan -generate-config-out=f.tfGenerar HCL para los bloques import
        terraform destroyDestruir todo lo gestionado
        terraform output -jsonLeer outputs en JSON
        terraform state list / show ADDRInspeccionar el state
        terraform state mv / rmMover u olvidar recursos (mejor moved / removed)
        terraform consoleProbar expresiones y funciones
        terraform graphExportar el grafo en formato DOT
        terraform workspace new / select / listGestionar workspaces
        terraform providers lock -platform=linux_amd64Añadir hashes de otras plataformas al lock file
        terraform force-unlock IDLiberar un lock colgado (con cuidado)
        terraform testEjecutar los tests .tftest.hcl
        terraform show -json tfplanPlan 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.