⏱️ Lectura: 16 min
El 10 de agosto de 2023 HashiCorp cambió la licencia de Terraform de MPL 2.0 a Business Source License, y en cuestión de semanas un grupo de empresas lanzó OpenTofu como fork abierto bajo la Linux Foundation. El episodio confirmó algo que ya sabían los equipos de infraestructura: declarar servidores, redes y bases de datos como infraestructura como código dejó de ser una curiosidad para convertirse en la forma estándar de operar la nube.
📑 En este artículo
- TL;DR
- Qué es la infraestructura como código y por qué importa
- Cómo funciona Terraform por dentro
- Ejemplos prácticos: de un archivo local a infraestructura real
- Cómo empezar paso a paso
- Casos de uso reales
- Errores comunes y buenas prácticas
- Comparativa con alternativas
- Profundizando: el protocolo de providers y el fork OpenTofu
- Preguntas frecuentes
- ¿Terraform y OpenTofu son compatibles entre sí?
- ¿Necesito una cuenta en AWS o GCP para aprender Terraform?
- ¿Por qué no debo editar el state file a mano?
- ¿Cómo evito que dos personas apliquen cambios al mismo tiempo?
- ¿Terraform sirve para instalar paquetes dentro de un servidor?
- ¿Qué pasa si alguien borra un recurso manualmente en la consola?
- Referencias
Este artículo explica cómo Terraform y su fork OpenTofu implementan ese modelo con el ciclo plan/apply, qué pasa por dentro cuando corrés esos comandos, y cómo escribir tu primer módulo reproducible con ejemplos de código progresivos.
TL;DR
- HCL declara el estado final de tu infraestructura: Terraform calcula el plan para llegar ahí, no los pasos manuales para lograrlo.
terraform plancompara el código con el state file y muestra qué recursos crea, cambia o destruye antes de tocar nada real.- Un grafo dirigido acíclico ordena la creación de recursos según sus dependencias, sin que nadie escriba ese orden a mano.
- El fork OpenTofu mantiene a Terraform en licencia abierta desde que HashiCorp migró a Business Source License en 2023.
- Los backends remotos con locking evitan que dos personas apliquen cambios sobre el mismo state al mismo tiempo.
- Cada módulo empaqueta recursos reutilizables: el mismo código levanta staging y producción cambiando solo variables.
terraform importincorpora al state recursos creados a mano en la consola, sin recrearlos desde cero.
Qué es la infraestructura como código y por qué importa
La infraestructura como código consiste en describir servidores, redes, bases de datos y permisos en archivos de texto versionados, en lugar de crearlos con clics en una consola web. Terraform lee esos archivos escritos en HCL (HashiCorp Configuration Language) y calcula qué llamadas a la API del proveedor cloud hacen falta para que la realidad coincida con lo declarado.
La diferencia con un script imperativo es central. Un script en bash con AWS CLI ejecuta pasos uno por uno y no sabe qué hacer si el recurso ya existe o si alguien lo borró a mano. Terraform, en cambio, compara el estado actual contra el estado deseado en cada corrida y solo aplica la diferencia.
La analogía más simple es una receta de cocina frente a un diario de lo que cocinaste. Un script imperativo es el diario: registra los pasos que diste una vez, pero no garantiza que repetirlos hoy produzca el mismo resultado. Terraform es la receta, describe el plato final y podés prepararlo las veces que quieras porque el proceso es idempotente: correr apply dos veces sin cambios en el medio no duplica nada.
Esto trae ventajas concretas. Los cambios de infraestructura pasan por pull request igual que el código de una aplicación, cualquier persona del equipo puede revisar un terraform plan antes de aprobarlo, y revertir una infraestructura rota es tan simple como hacer git revert y volver a aplicar.
Cómo funciona Terraform por dentro
Terraform separa el núcleo (Terraform Core) de los proveedores (providers). Cada provider, como AWS, Google Cloud, Kubernetes, Cloudflare o GitHub, es un binario independiente que Terraform descarga y con el que se comunica mediante un protocolo propio sobre gRPC. El core no sabe nada de la API de AWS: le pregunta al provider de AWS qué hacer y el provider traduce esa respuesta en llamadas HTTP reales.
Cuando corrés terraform plan, el core arma un grafo dirigido acíclico (DAG) con cada recurso declarado y las referencias entre ellos. Si un aws_instance referencia el id de un aws_subnet, Terraform infiere que la subred debe crearse primero, sin que nadie escriba esa dependencia a mano. Después compara ese grafo contra el state file, el archivo JSON que guarda qué recursos existen y sus atributos reales.
flowchart TD
A["Codigo HCL (.tf)"] --> B["terraform plan"]
B --> C{"Hay cambios?"}
C -->|"si"| D["terraform apply"]
D --> E["Proveedor cloud (API)"]
D --> F[("State file")]
C -->|"no"| G["Sin cambios que aplicar"]
El resultado del plan es una lista de acciones (crear, actualizar, reemplazar o destruir) que terraform apply ejecuta en el orden que marca el DAG. Cada llamada exitosa al provider actualiza el state file, así que el próximo plan siempre parte de datos reales y no de una suposición.
Para inspeccionar ese DAG sin adivinar, terraform graph exporta la estructura en formato DOT, que herramientas como Graphviz convierten en una imagen. En proyectos grandes con cientos de recursos, ver el grafo real ayuda a detectar dependencias circulares antes de que el plan falle con un error críptico.
sequenceDiagram
participant Dev as Desarrollador
participant TF as Terraform Core
participant ST as State remoto
participant AWS as Proveedor AWS
Dev->>TF: terraform apply
TF->>ST: lee el state actual
ST-->>TF: devuelve recursos existentes
TF->>AWS: crea o modifica recursos
AWS-->>TF: confirma los cambios
TF->>ST: escribe el nuevo state
Note over Dev,AWS: el lock evita otra apply simultanea
📌 Nota: el state file no es opcional ni cosmético. Si se pierde o se corrompe, Terraform deja de saber qué recursos administra y puede intentar recrearlos desde cero.
Ejemplos prácticos: de un archivo local a infraestructura real
El ejemplo más simple no necesita cuenta cloud. Declara un provider local y crea un archivo de texto para mostrar el ciclo completo sin gastar un centavo:
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
resource "local_file" "hello" {
filename = "${path.module}/hello.txt"
content = "Hola desde Terraform"
}
Al correr terraform apply sobre este archivo, Terraform crea hello.txt en el directorio del módulo y guarda ese recurso en el state. Si volvés a correr apply sin cambiar nada, el plan sale vacío porque el archivo ya coincide con lo declarado.
El segundo ejemplo se acerca a un caso real: un bucket S3 con versionado activado, usando un sufijo aleatorio para evitar colisiones de nombre, porque los nombres de bucket en S3 son globales.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "random_id" "suffix" {
byte_length = 4
}
resource "aws_s3_bucket" "logs" {
bucket = "programacion-logs-${random_id.suffix.hex}"
}
resource "aws_s3_bucket_versioning" "logs" {
bucket = aws_s3_bucket.logs.id
versioning_configuration {
status = "Enabled"
}
}
output "bucket_name" {
value = aws_s3_bucket.logs.bucket
}
Acá aparece la inferencia de dependencias en acción. aws_s3_bucket_versioning referencia aws_s3_bucket.logs.id, así que Terraform crea primero el bucket y recién después activa el versionado. Nadie escribió ese orden, lo dedujo del grafo:
flowchart LR
A["aws_s3_bucket.logs"] --> B["aws_s3_bucket_versioning.logs"]
C["random_id.suffix"] --> A
Después de aplicar, terraform output bucket_name devuelve el nombre real del bucket sin tener que ir a la consola de AWS a buscarlo.
Un tercer ejemplo muestra por qué el mismo código sirve para staging y producción. Una variable controla el tamaño de instancia sin duplicar el archivo:
variable "environment" {
type = string
default = "staging"
}
resource "aws_instance" "app" {
ami = "ami-0c101f26f147fa7fd"
instance_type = var.environment == "production" ? "t3.large" : "t3.micro"
tags = {
Name = "app-${var.environment}"
}
}
Con tofu apply -var="environment=production" se crea una t3.large, y sin esa bandera el default deja una t3.micro para staging. Es el mismo archivo, dos ambientes distintos.
Cómo empezar paso a paso
Instalar OpenTofu (o Terraform) es el primer paso. En macOS y Linux con Homebrew:
brew install opentofu
# alternativa: la version de HashiCorp
brew install hashicorp/tap/terraform
Con el binario instalado, el flujo de trabajo básico son cuatro comandos:
tofu init # descarga providers y configura el backend
tofu plan # calcula que va a cambiar, sin aplicar nada
tofu apply # ejecuta el plan y actualiza el state
tofu destroy # elimina todo lo que Terraform administra
init lee los bloques required_providers del código y descarga los binarios correspondientes a .terraform/providers. Por defecto, el state se guarda en un archivo local terraform.tfstate, algo razonable para practicar pero riesgoso en equipo: dos personas con el mismo archivo local pueden pisarse los cambios.
Para trabajar en equipo hace falta un backend remoto con locking. Un ejemplo con S3 y DynamoDB para el bloqueo:
terraform {
backend "s3" {
bucket = "mi-empresa-tfstate"
key = "prod/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
}
}
La tabla de DynamoDB guarda un lock mientras dura un apply. Si otra persona corre apply al mismo tiempo, Terraform rechaza la operación con un error de lock en vez de dejar que dos procesos escriban el mismo state a la vez.
Para verificar qué recursos administra Terraform sin salir de la terminal:
tofu state list # lista los recursos en el state
tofu show # muestra los atributos actuales
tofu output bucket_name # imprime un output puntual
Casos de uso reales
El caso más común es separar ambientes: un directorio o un workspace por staging y producción, reutilizando el mismo módulo con variables distintas, como el tamaño de instancia o la cantidad de réplicas. Así el código que se probó en staging es literalmente el mismo que se aplica en producción, solo cambian los valores de entrada.
Los módulos publicados en el Terraform Registry encapsulan patrones enteros (una VPC con subredes públicas y privadas, un cluster de Kubernetes gestionado, una base de datos con réplicas de lectura) para no reescribir la misma configuración en cada proyecto.
En integración continua, un flujo típico corre terraform plan automáticamente en cada pull request y publica el resultado como comentario, para que el equipo revise qué va a cambiar antes de aprobar:
# .github/workflows/terraform.yml
name: terraform
on: [pull_request]
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: opentofu/setup-opentofu@v1
- run: tofu init
- run: tofu plan -out=tfplan
Este workflow corre en cada pull request, descarga OpenTofu y genera el plan como paso previo a cualquier revisión, sin aplicar nada de forma automática. El apply se dispara recién al hacer merge a la rama principal, casi siempre reutilizando el mismo plan guardado para aplicar exactamente lo revisado.
Los providers no se limitan a servidores. Hay providers para configurar repositorios de GitHub, zonas DNS de Cloudflare, dashboards de Datadog o políticas de Kubernetes. Cualquier servicio con una API puede administrarse con el mismo flujo plan/apply, lo que explica por qué el ecosistema de providers creció mucho más allá de las nubes tradicionales.
Errores comunes y buenas prácticas
El error más frecuente es el drift: alguien modifica un recurso a mano en la consola web y el siguiente plan detecta una diferencia inesperada. Terraform no sabe si ese cambio manual fue intencional, así que propone revertirlo. La solución no es prohibir la consola, a veces hace falta para debug urgente, sino correr plan seguido y usar terraform import o terraform state rm cuando el cambio manual debe quedar registrado en el código.
⚠️ Ojo: el state file puede contener secretos en texto plano, incluidas contraseñas generadas por el provider. Nunca lo subas a git, usá un backend remoto cifrado y marcá los outputs sensibles con sensitive = true.
Otro error común es no fijar la versión de los providers. Un required_providers sin restricción de versión puede traer un cambio incompatible la próxima vez que alguien corra init en una máquina nueva, y el plan cambia sin que nadie tocó el código de la aplicación. Fijar rangos como ~> 5.0 evita sorpresas.
Usar count para crear varios recursos parecidos también genera dolores de cabeza: si se borra un elemento del medio de una lista, Terraform reindexa y puede destruir y recrear recursos que no cambiaron. for_each con un mapa o un set evita ese problema porque cada recurso queda identificado por una clave estable en lugar de por su posición.
Por último, los módulos gigantes con miles de líneas en un solo main.tf son difíciles de revisar y de testear. Dividir por responsabilidad (red, cómputo, base de datos) y componer con módulos más chicos hace que cada pull request sea legible y que el plan tarde menos en calcular el grafo.
Comparativa con alternativas
| Herramienta | Cuándo usarla | Ventaja | Limitación |
|---|---|---|---|
| Terraform / OpenTofu | Infraestructura multi-nube con un flujo único de plan/apply | Ecosistema enorme de providers y módulos | El state file exige gestión y locking propios |
| Pulumi | Equipos que prefieren loops, funciones y tests en un lenguaje real | TypeScript, Python o Go en vez de HCL | Menos providers maduros que Terraform |
| AWS CloudFormation | Shops 100% AWS sin necesidad de multi-nube | Integración nativa sin instalar nada | No sirve fuera de AWS |
| AWS CDK | Equipos AWS que prefieren código a YAML pero se quedan en CloudFormation | Compila a CloudFormation desde TypeScript o Python | Hereda las limitaciones de CloudFormation por debajo |
| Ansible | Configurar el sistema operativo y paquetes de un servidor ya creado | No necesita agente, corre sobre SSH | No modela dependencias de infraestructura como un grafo |
Profundizando: el protocolo de providers y el fork OpenTofu
Cada provider es en realidad un proceso separado que Terraform Core lanza y con el que habla mediante el Terraform Plugin Protocol sobre gRPC. Esa separación es la razón por la que agregar soporte para un servicio nuevo no requiere tocar el core: alguien escribe un provider en Go, lo publica en el registro y Terraform lo descarga bajo demanda con init.
El state file guarda, además de los IDs de los recursos, un número de serial y un lock ID cuando el backend lo soporta. Ese lock es lo que impide que dos apply concurrentes corrompan el archivo: el segundo proceso espera o falla con un mensaje explícito en vez de escribir sobre el primero a mitad de camino.
El state file es JSON versionado: cada formato nuevo incluye un campo terraform_version y un número de serial que se incrementa en cada escritura. Bajar la versión de Terraform después de aplicar con una más nueva puede fallar porque el state quedó en un formato que el binario viejo no entiende, otra razón para fijar versiones con required_version.
OpenTofu, el fork que nació tras el cambio de licencia de HashiCorp en 2023, mantiene compatibilidad de HCL y de formato de state con Terraform, así que la mayoría de módulos y providers funcionan sin cambios en ambos. Con el tiempo empezó a divergir en funciones propias, como el cifrado nativo del state file, algo que Terraform no ofrece de forma integrada.
💡 Tip: guardá siempre el plan antes de aplicarlo en CI conterraform plan -out=tfplany despuésterraform apply tfplan. Así aplicás exactamente lo que se revisó, no un plan recalculado en el momento del merge que pudo cambiar por drift externo.
📖 Resumen en Telegram: Ver resumen.
Tu próximo paso: instalá OpenTofu y corré tofu init && tofu plan sobre el ejemplo del bucket S3 (o el de local_file si todavía no tenés cuenta cloud) para ver el plan completo antes de aplicar nada.
Preguntas frecuentes
¿Terraform y OpenTofu son compatibles entre sí?
Sí, en general. Ambos comparten el mismo lenguaje HCL y el mismo formato de state heredado del momento del fork, así que la mayoría de módulos y providers del registro funcionan en los dos sin modificaciones. La compatibilidad puede reducirse a futuro si cada proyecto suma funciones exclusivas.
¿Necesito una cuenta en AWS o GCP para aprender Terraform?
No. El provider local (archivos) o el provider random (valores aleatorios) alcanzan para practicar el ciclo completo de init, plan y apply sin gastar un centavo ni crear una cuenta cloud.
¿Por qué no debo editar el state file a mano?
Porque es el mapa que conecta cada bloque de tu código con el recurso real que existe en el proveedor. Editarlo a mano sin usar los comandos de Terraform puede romper esa referencia y hacer que el próximo apply intente crear un recurso duplicado o borrar uno que seguís necesitando.
¿Cómo evito que dos personas apliquen cambios al mismo tiempo?
Con un backend remoto que soporte locking, como S3 combinado con una tabla de DynamoDB, o un servicio como Terraform Cloud que lo maneja de forma nativa. El lock bloquea el segundo apply hasta que el primero termine.
¿Terraform sirve para instalar paquetes dentro de un servidor?
No directamente. Terraform crea el servidor (la máquina, el disco, la red), pero para configurar el sistema operativo y los paquetes adentro se suele usar user_data o cloud-init en el momento de creación, o herramientas de configuración como Ansible después.
¿Qué pasa si alguien borra un recurso manualmente en la consola?
El próximo terraform plan detecta que el recurso ya no existe y propone recrearlo. Si el borrado fue intencional y no debería volver, hay que quitarlo del código y del state con terraform state rm antes de aplicar.
Referencias
- Documentación oficial de Terraform: guías, referencia de HCL y el protocolo de providers.
- OpenTofu: sitio oficial del fork open source mantenido por la Linux Foundation.
- Repositorio de OpenTofu en GitHub: código fuente del fork y su historial desde 2023.
- Terraform Registry: catálogo público de providers y módulos reutilizables.
📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.
0 Comentarios