Terraform para humanos: de "click-ops" descontrolado a IaC (sin perder la dignidad por el camino)
Publicado el 21 de mayo de 2026 • 24 minutos • 4919 palabras
Table of contents
- Qué es IaC (versión para gente que ha sufrido consolas gráficas)
- Cómo funciona Terraform
- Requisitos previos: CLIs de proveedores (porque la magia necesita credenciales)
- Instalación de Terraform (Linux, macOS, Windows)
- El fichero de estado: el diario secreto (y frágil) de Terraform
- Ficheros típicos: que tu proyecto no sea un
main.tfde 800 líneas - Comandos clave: qué hace cada uno en la vida real
- Repositorio de ejemplo
- Ejemplo 1 - Instancia pequeña en AWS (y cómo jugar con ella)
- Ejemplo 2 - VM pequeña en Azure
- Ejemplo 3 - VM pequeña en GCP
- Variables,
.tfvarsy entornos múltiples - Módulos: dejar de copiar/pegar infraestructura
- Buenas prácticas para no acabar peor que sin IaC
- Antes de cerrar el portátil
- Glosario
- Fuentes y referencias
Solo es un cambio pequeñito, lo hago a mano y ya.
Y tú, que llevas toda la semana apagando fuegos, te ves un viernes a las 18:30 en la consola de AWS, Azure o GCP, abriendo pestañas como si te pagaran por click.
Un viernes de esos, un equipo decidió “duplicar” producción a mano para una campaña. Dos personas, cada una en su consola, creando máquinas, tocando grupos de seguridad, ampliando discos “un poquito, que total”… Nadie apuntó nada porque “ya lo documentaremos luego”.
El lunes siguiente nadie sabía cuántas instancias había, cuál era la original, qué puertos se habían abierto “para probar” ni en qué región vivía cada cosa. La factura de la nube llegó un mes después y la auditoría de seguridad todavía está llorando.
La Infraestructura como Código, y Terraform en particular, existen para que todo eso no dependa de la memoria de quien estaba medio despierto un viernes por la tarde, sino de código revisable, repetible y automatizable. Vamos a verlo con teoría, práctica, y ejemplos reales en AWS, Azure y GCP.
Qué es IaC (versión para gente que ha sufrido consolas gráficas)
La Infraestructura como Código (IaC) consiste en describir tu infraestructura (máquinas, redes, discos, reglas de firewall, balanceadores…) en ficheros de texto que puedes meter en Git, revisar con pull requests y usar para recrear entornos sin sufrir demasiado.
Dejas de hacer clicks en consolas gráficas y pasas a describir cómo quieres que sea el sistema, y que una herramienta haga el trabajo sucio.
¿Y qué ganas con eso? Pues que cuando alguien rompe algo, revisas el commit en lugar de rastrear un log de actividad críptico rezando por encontrar la pista. Que puedes levantar dev, pre y prod de forma coherente, en vez de mantener tres Frankensteins que solo se parecen en el nombre.
Y que puedes destruir y recrear un entorno entero sin tener que preguntarle a nadie “oye, tú tocaste algo aquí hace meses?”.
Terraform es uno de los motores más populares para hacer todo esto: habla con las APIs de AWS, Azure y GCP, y traduce tu código en recursos reales.
Cómo funciona Terraform
Terraform es más simple de lo que parece; la complejidad la ponemos nosotros encima.
A grandes rasgos, lee tus ficheros .tf (lo que tú quieres que exista), los compara con lo que cree que ya hay gracias a su fichero de estado, calcula la diferencia y llama a las APIs del proveedor para hacer realidad tu deseo. Como un genio de la lámpara, pero con más HCL y menos humo.
Todo gira en torno a tres piezas.
La configuración (tus ficheros *.tf) es el mapa mental de cómo debería ser la infraestructura.
El estado (terraform.tfstate) es la radiografía de lo que Terraform ha creado y cómo lo rastrea . Piensa en él como la memoria a largo plazo de alguien que, a diferencia de tu equipo un viernes por la tarde, sí apunta las cosas.
Y el plan (terraform plan) es ese momento en el que Terraform te enseña el “antes/después” de la carnicería antes de hacerla, para que puedas salir corriendo a tiempo.
La consecuencia práctica es que si toqueteas la infraestructura por fuera de Terraform “porque es un cambio pequeñito”, estás creando una divergencia entre realidad y estado.
Es como contarle una versión distinta de los hechos a alguien que tiene mejor memoria que tú. No va a acabar bien.
Requisitos previos: CLIs de proveedores (porque la magia necesita credenciales)
Antes de ponerte a lanzar cosas con Terraform, necesitas poder hablar con cada nube. Y para hablar con las nubes (al menos con las de AWS, Azure y GCP), lo más práctico es tener instaladas y configuradas sus CLIs.
Técnicamente no siempre es obligatorio, pero intentar trabajar sin ellas es como intentar cortar cebollas con una cuchara: se puede, pero vas a sufrir.
AWS CLI
Instala AWS CLI v2 siguiendo la guía oficial:
Una vez instalada, configura credenciales y región por defecto:
aws configure
# AWS Access Key ID, Secret Access Key, región (ej. eu-west-1), output (json)
Azure CLI
Instala Azure CLI desde la documentación oficial (Windows, Linux, macOS):
Después:
az login
# Se abre el navegador, inicias sesión y listo
Google Cloud CLI (gcloud)
Para GCP vas a usar gcloud, la CLI oficial de Google Cloud:
Ejemplo típico en Debian/Ubuntu:
sudo apt-get update && sudo apt-get install google-cloud-cli
En Windows puedes usar el instalador gráfico o el zip del SDK, siguiendo la misma guía y para MacOs te recomiendo usar Homebrew
Después de instalar, inicializa y autentica:
gcloud init
# Elige proyecto, región/zona por defecto y autenticación
Terraform puede aprovecharse de estas configuraciones (o de cuentas de servicio específicas) para acceder a cada nube.
Instalación de Terraform (Linux, macOS, Windows)
Ahora sí, la herramienta protagonista.
Instalar Terraform es, sorprendentemente, una de las cosas más fáciles de todo este artículo. Casi sospechosamente fácil.
Linux
Desde el binario oficial:
curl -LO https://releases.hashicorp.com/terraform/1.9.8/terraform_1.9.8_linux_amd64.zip
unzip terraform_1.9.8_linux_amd64.zip
sudo mv terraform /usr/local/bin/
terraform version
macOS
Con Homebrew:
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
terraform version
O descargando el zip desde la página de HashiCorp, descomprimiendo y moviendo el binario a un directorio del PATH.
Windows
Con Chocolatey:
choco install terraform -y
terraform version
También puedes usar el instalador/zip oficial y añadir la ruta al PATH.
El fichero de estado: el diario secreto (y frágil) de Terraform
Terraform guarda en terraform.tfstate información detallada de todo lo que ha creado: IDs reales, atributos, dependencias…
Piensa en el fichero de estado como ese post-it donde apuntas todas tus contraseñas y que guardas debajo del teclado, solo que peor, porque si lo pierdes Terraform se queda con amnesia completa y tú con un fin de semana arruinado.
Estado local
Por defecto, el estado vive en un fichero local (./terraform.tfstate) en tu directorio de trabajo. Para demos y laboratorios en los que juegas tú solo, es cómodo y rápido. Para cualquier cosa en equipo, es una bomba de relojería: conflictos, sobrescrituras y ese clásico de “¿quién narices se ha llevado mi estado?”.
Hay dos reglas de supervivencia que no son negociables: no lo subas a Git (en serio, no, nunca, bajo ninguna circunstancia) y trátalo como si contuviera secretos, porque a veces los contiene.
Tu yo del futuro te lo agradecerá.
Estado remoto: AWS, Azure y GCP
Para cualquier cosa mínimamente seria (es decir, cualquier cosa donde haya más de una persona o donde alguien pueda cabrearse si se rompe), quieres estado remoto con bloqueo.
La buena noticia es que “los tres grandes” lo soportan y configurarlo no requiere un doctorado.
Backend remoto en AWS - S3 + DynamoDB
Primero necesitas crear el bucket de S3 y la tabla de DynamoDB que Terraform usará. Esto solo se hace una vez:
# Crear el bucket de S3 para el estado
aws s3api create-bucket \
--bucket mi-bucket-terraform-state \
--region eu-west-1 \
--create-bucket-configuration LocationConstraint=eu-west-1
# Activar versionado (para poder recuperar estados anteriores si algo sale mal)
aws s3api put-bucket-versioning \
--bucket mi-bucket-terraform-state \
--versioning-configuration Status=Enabled
# Crear la tabla de DynamoDB para el bloqueo
aws dynamodb create-table \
--table-name terraform-locks \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--region eu-west-1
Una vez creados, configura el backend en tu fichero .tf:
terraform {
backend "s3" {
bucket = "mi-bucket-terraform-state"
key = "demo/terraform.tfstate"
region = "eu-west-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
La idea es sencilla: S3 guarda el estado y DynamoDB se encarga del bloqueo, para que dos personas no hagan apply a la vez y se líe la de San Quintín.
Recientemente se ha activado la posibilidad de bloqueo en S3 para no depender de DynamoDB, pero eso te lo dejo a ti para que lo investigues.
Backend remoto en Azure - Azure Storage (Blob)
Antes de configurar el backend, necesitas crear el grupo de recursos, la cuenta de almacenamiento y el contenedor de blobs:
# Crear el grupo de recursos
az group create \
--name rg-terraform-state \
--location westeurope
# Crear la cuenta de almacenamiento
az storage account create \
--name stterraformstate123 \
--resource-group rg-terraform-state \
--location westeurope \
--sku Standard_LRS \
--encryption-services blob
# Crear el contenedor de blobs para el estado
az storage container create \
--name tfstate \
--account-name stterraformstate123
Con eso listo, configura el backend en tu fichero .tf:
terraform {
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "stterraformstate123"
container_name = "tfstate"
key = "demo/terraform.tfstate"
}
}
El estado se guarda como blob en Azure Storage, con versionado y controles de acceso incluidos. Microsoft no escatima en burocracia de nombres, pero al menos funciona bien.
Backend remoto en GCP - Cloud Storage
Crea un bucket de Cloud Storage para el estado:
gcloud storage buckets create gs://mi-terraform-state-bucket \
--location=europe-west1
En tu proyecto Terraform (por ejemplo, el de GCP) define:
terraform {
backend "gcs" {
bucket = "mi-terraform-state-bucket"
prefix = "gcp-vm-demo/state"
}
required_version = ">= 1.5.0"
required_providers {
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
}
}
La primera vez que hagas terraform init, te preguntará si quieres migrar el estado local al remoto. A partir de ahí, el tfstate vivirá en GCS con las ventajas de versionado e IAM.
Moraleja: local para jugar tú solo; remoto para cualquier cosa que no quieras explicar luego en un postmortem.
Ficheros típicos: que tu proyecto no sea un main.tf de 800 líneas
Organizar tu código Terraform en varios ficheros ayuda a que el proyecto no se convierta en un scroll infinito donde nadie encuentra nada y todo el mundo toca todo.
La estructura mínima que va a mantener tu cordura es más o menos esta:
- Un
providers.tfpara definir con qué nubes hablas. - Un
variables.tfpara las variables de entrada. - Un
main.tfcon los recursos principales. - Un
outputs.tfcon las salidas útiles. - Opcionalmente, ficheros
*.tfvarscon los valores concretos de cada entorno.
Cuando el proyecto crezca, puedes separar por dominios (network, compute, database…), pero con esto ya tienes bastante orden y bastante menos caos.
Comandos clave: qué hace cada uno en la vida real
Terraform tiene pocos comandos, pero los vas a usar tanto que acabarás soñando con ellos.
Empiezas con terraform init, que inicializa el directorio, descarga los proveedores y configura el backend. Es el “plug-and-pray” de cada proyecto.
Luego puedes usar terraform fmt, que te formatea los ficheros para que no parezcan escritos por alguien con prisa (que probablemente eras tú).
Con terraform validate revisas que la sintaxis no tenga barbaridades evidentes antes de ir más lejos.
El momento de la verdad llega con terraform plan, que calcula qué va a hacer sin tocar nada. Es el “mira antes de saltar al vacío”.
Si lo que ves en el plan no te provoca un ataque de ansiedad, lanzas terraform apply para que lo haga de verdad.
Y luego está terraform destroy, que borra todo lo que Terraform gestiona. Úsalo con el mismo cariño con el que tratarías un rm -rf: mucho respeto y, a ser posible, con filtros o proyectos separados.
La secuencia de trabajo que no (suele) acabar en desastre es esta:
terraform fmt
terraform validate
terraform plan -out=plan.out
terraform apply plan.out
Repositorio de ejemplo
Todos los ejemplos de este tutorial están disponibles en un repositorio listo para usar:
https://github.com/granite-stack/terraform-first-steps
Clónalo antes de seguir. Así te ahorras teclear los bloques de código a mano y puedes centrarte en entender lo que hace cada cosa:
git clone https://github.com/granite-stack/terraform-first-steps.git
cd terraform-first-steps
Cada ejemplo de los que vienen a continuación corresponde a un directorio dentro del repositorio.
Ejemplo 1 - Instancia pequeña en AWS (y cómo jugar con ella)
Necesitas: AWS CLI instalada y configurada con
aws configure, y unas credenciales con permisos para EC2.
Estructura de ficheros
El directorio aws-ec2-demo/ va a tener cuatro ficheros: providers.tf (dónde y cómo te conectas), variables.tf (qué puedes parametrizar), main.tf (los recursos en sí) y outputs.tf (lo que quieres ver al final). Nada revolucionario, pero funciona.
providers.tf:
terraform {
required_version = ">= 1.5.0"
backend "s3" {
bucket = "mi-bucket-terraform-state"
key = "aws-ec2-demo/terraform.tfstate"
region = "eu-west-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}
variables.tf:
variable "aws_region" {
type = string
description = "Región de AWS"
default = "eu-west-1"
}
variable "instance_name" {
type = string
description = "Nombre de la instancia EC2"
default = "demo-terraform-ec2"
}
variable "instance_type" {
type = string
description = "Tipo de instancia EC2"
default = "t3.micro"
}
variable "root_volume_size" {
type = number
description = "Tamaño del disco raíz en GB"
default = 8
}
main.tf:
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
owners = ["099720109477"] # Canonical
}
resource "aws_instance" "demo" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
tags = {
Name = var.instance_name
}
root_block_device {
volume_size = var.root_volume_size
volume_type = "gp3"
}
}
outputs.tf:
output "instance_id" {
description = "ID de la instancia EC2"
value = aws_instance.demo.id
}
output "public_ip" {
description = "IP pública de la instancia"
value = aws_instance.demo.public_ip
}
Crear la instancia
Ahora viene lo satisfactorio. Entra en el directorio y lanza la secuencia:
cd aws-ec2-demo
terraform init
terraform plan
terraform apply
Terraform te enseñará su plan de batalla, te pedirá confirmación (escribe yes con convicción o con miedo, según el día) y al final verás la IP pública de tu instancia recién nacida.
Modificar tipo, disco y nombre
Aquí es donde Terraform empieza a lucirse de verdad: cambias un valor en el código y él se encarga de lo demás.
Bueno … casi.
Cambiar tipo de instancia. Ve a variables.tf y cambia el tipo:
default = "t3.small"
Luego:
terraform plan
terraform apply
Verás que Terraform necesita reemplazar la instancia (destroy + create), porque AWS no deja cambiar el tipo en caliente sin pararla. No te asustes, es normal.
Ampliar disco raíz. Cambia root_volume_size de 8 a 16:
terraform plan
Normalmente será un cambio in-place sobre el volumen, así que tranquilidad.
Cambiar el nombre (tag Name). Cambia instance_name:
default = "demo-terraform-ec2-renombrada"
El plan mostrará solo un cambio de tag, sin recrear la instancia.
De los cambios más tranquilos que puedes hacer, pero no te acostumbres.
Ejemplo 2 - VM pequeña en Azure
Necesitas: Azure CLI instalada y
az loginejecutado, con una suscripción activa.
Estructura de ficheros
El directorio azure-vm-demo/ sigue la misma estructura de cuatro ficheros. Azure necesita más recursos para montar una VM (red virtual, subred, IP pública, tarjeta de red…), así que el main.tf será más largo, pero la idea es la misma.
providers.tf:
terraform {
required_version = ">= 1.5.0"
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "stterraformstate123"
container_name = "tfstate"
key = "azure-vm-demo/terraform.tfstate"
}
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
provider "azurerm" {
features {}
}
variables.tf:
variable "location" {
type = string
description = "Región de Azure"
default = "westeurope"
}
variable "vm_name" {
type = string
description = "Nombre de la máquina virtual"
default = "demo-terraform-vm"
}
variable "vm_size" {
type = string
description = "Tamaño de la VM"
default = "Standard_B1s"
}
variable "os_disk_size_gb" {
type = number
description = "Tamaño del disco OS en GB"
default = 30
}
main.tf:
resource "azurerm_resource_group" "rg" {
name = "rg-terraform-demo"
location = var.location
}
resource "azurerm_virtual_network" "vnet" {
name = "vnet-demo"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
}
resource "azurerm_subnet" "subnet" {
name = "subnet-demo"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = ["10.0.1.0/24"]
}
resource "azurerm_public_ip" "public_ip" {
name = "pip-demo"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
allocation_method = "Static"
sku = "Standard"
}
resource "azurerm_network_interface" "nic" {
name = "nic-demo"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
ip_configuration {
name = "ipconfig1"
subnet_id = azurerm_subnet.subnet.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.public_ip.id
}
}
resource "azurerm_linux_virtual_machine" "vm" {
name = var.vm_name
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
size = var.vm_size
admin_username = "azureuser"
network_interface_ids = [
azurerm_network_interface.nic.id
]
admin_ssh_key {
username = "azureuser"
public_key = file("~/.ssh/id_rsa.pub")
}
os_disk {
name = "${var.vm_name}-osdisk"
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
disk_size_gb = var.os_disk_size_gb
}
source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts-gen2"
version = "latest"
}
}
outputs.tf:
output "vm_id" {
description = "ID de la VM"
value = azurerm_linux_virtual_machine.vm.id
}
output "public_ip" {
description = "IP pública de la VM"
value = azurerm_public_ip.public_ip.ip_address
}
Crear la VM
El ritual es el mismo:
cd azure-vm-demo
terraform init
terraform plan
terraform apply
Azure es más verboso (o pesadito) que AWS. Un simple “quiero una VM” implica crear un grupo de recursos, una red virtual, una subred, una IP pública y una tarjeta de red.
La buena noticia es que Terraform se encarga de todo eso sin que tengas que abrir siete pestañas del portal.
Cambiar tamaño, disco y nombre
El mismo juego que en AWS, pero con los nombres de Azure.
Si cambias vm_size a "Standard_B2s", lanzas terraform plan y luego apply; Azure parará la VM, le cambiará el tamaño y la volverá a arrancar.
Si amplías el disco con os_disk_size_gb = 64, comprueba con plan si es un cambio in-place o necesita recreación.
Y si cambias vm_name, prepárate: en muchos casos Azure necesita destruir y recrear la VM entera, porque el nombre es parte de la identidad del recurso.
Terraform te lo advertirá en el plan, así que no hay sorpresas.
Ejemplo 3 - VM pequeña en GCP
Necesitas: Google Cloud CLI instalada,
gcloud initejecutado, proyecto con facturación activa y permisos adecuados. Sí, en GCP siempre hay un paso más.
Estructura de ficheros
El directorio gcp-vm-demo/ sigue el mismo patrón de cuatro ficheros.
GCP tiene sus propias peculiaridades (proyecto, zona, redes con auto-creación de subredes…), pero la filosofía es idéntica.
providers.tf:
terraform {
required_version = ">= 1.5.0"
backend "gcs" {
bucket = "mi-terraform-state-bucket"
prefix = "gcp-vm-demo/state"
}
required_providers {
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
}
}
provider "google" {
project = var.project_id
region = var.region
zone = var.zone
}
variables.tf:
variable "project_id" {
type = string
description = "ID del proyecto de GCP"
}
variable "region" {
type = string
description = "Región de GCP"
default = "europe-west1"
}
variable "zone" {
type = string
description = "Zona de GCP"
default = "europe-west1-b"
}
variable "instance_name" {
type = string
description = "Nombre de la instancia de Compute Engine"
default = "demo-terraform-gce"
}
variable "machine_type" {
type = string
description = "Tipo de máquina"
default = "e2-micro"
}
variable "boot_disk_size_gb" {
type = number
description = "Tamaño del disco de arranque en GB"
default = 10
}
main.tf:
resource "google_compute_network" "vpc" {
name = "vpc-demo-terraform"
auto_create_subnetworks = true
}
resource "google_compute_firewall" "allow_ssh" {
name = "fw-allow-ssh"
network = google_compute_network.vpc.name
allow {
protocol = "tcp"
ports = ["22"]
}
source_ranges = ["0.0.0.0/0"]
}
resource "google_compute_instance" "vm" {
name = var.instance_name
machine_type = var.machine_type
zone = var.zone
boot_disk {
initialize_params {
image = "projects/ubuntu-os-cloud/global/images/family/ubuntu-2204-lts"
size = var.boot_disk_size_gb
}
}
network_interface {
network = google_compute_network.vpc.name
access_config {
# IP externa
}
}
metadata = {
ssh-keys = "ubuntu:${file("~/.ssh/id_rsa.pub")}"
}
tags = ["ssh"]
}
outputs.tf:
output "instance_id" {
description = "ID de la instancia en GCE"
value = google_compute_instance.vm.id
}
output "public_ip" {
description = "IP pública de la instancia"
value = google_compute_instance.vm.network_interface[0].access_config[0].nat_ip
}
Crear la VM
En GCP necesitas pasar el project_id como variable (porque no tiene default, cada uno tiene el suyo):
cd gcp-vm-demo
gcloud init # si no lo has hecho ya
terraform init
terraform plan -var="project_id=tu-project-id"
terraform apply -var="project_id=tu-project-id"
Al finalizar verás la IP pública y podrás comprobar en la consola de GCP que la VM existe.
Si llegas hasta aquí sin errores, cuélgate una medalla (al valor, o por heridas en combate, no lo tengo claro).
Cambiar tipo, disco y nombre
La misma mecánica que en AWS y Azure.
Cambias machine_type a "e2-small" y lanzas terraform plan: según la configuración, puede implicar una parada y reinicio o directamente una recreación; el plan te lo dice sin rodeos.
Ampliar el disco de arranque con boot_disk_size_gb = 20 suele ser más tranquilo: plan y luego apply.
Y cambiar instance_name es, como en los otros proveedores, sinónimo de recreación. GCP no deja renombrar instancias, así que Terraform destruye y crea una nueva.
Al menos te avisa antes.
Variables, .tfvars y entornos múltiples
Llegará un momento en el que querrás tener dev, pre y prod sin mantener tres copias del mismo código que solo se diferencian en el tamaño de la máquina y el nombre.
Para eso están las variables y los ficheros .tfvars: la misma configuración, valores distintos según el entorno.
Por ejemplo, con AWS, te he dejado dos ficheros de ejemplo que has de renombrar:
dev.tfvars:
instance_type = "t3.micro"
root_volume_size = 8
instance_name = "demo-ec2-dev"
prod.tfvars:
instance_type = "t3.small"
root_volume_size = 30
instance_name = "demo-ec2-prod"
Ejecutas:
terraform plan -var-file="dev.tfvars"
terraform apply -var-file="dev.tfvars"
La misma idea funciona en GCP (con otro project_id, una zona más barata para desarrollo…) y en Azure (con tamaños de VM distintos).
Escribes una vez, parametrizas, y te ahorras el copiar/pegar que siempre acaba en un desastre creativo.
Módulos: dejar de copiar/pegar infraestructura
Cuando empieces a copiar bloques enteros para crear “otra VM igual pero un poco distinta”, ha llegado la hora de los módulos.
Todos hemos pasado por eso: copias, cambias dos valores, y te dices “esto lo limpio luego”.
Spoiler: no lo limpias nunca.
Cuando te das cuenta, tienes cuatro copias que se parecen entre sí como un huevo a una castaña.
Los módulos encapsulan un patrón (por ejemplo, “una VM con su red y su firewall”), lo parametrizan, y te dejan reutilizarlo sin perder la cordura.
La estructura típica tiene esta pinta:
modules/
aws_vm/
main.tf
variables.tf
outputs.tf
main.tf
variables.tf
modules/aws_vm/variables.tf:
variable "instance_name" { type = string }
variable "instance_type" { type = string }
variable "root_volume_size" { type = number }
variable "aws_region" { type = string }
modules/aws_vm/main.tf:
provider "aws" {
region = var.aws_region
}
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
owners = ["099720109477"]
}
resource "aws_instance" "vm" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
tags = {
Name = var.instance_name
}
root_block_device {
volume_size = var.root_volume_size
volume_type = "gp3"
}
}
modules/aws_vm/outputs.tf:
output "instance_id" {
value = aws_instance.vm.id
}
output "public_ip" {
value = aws_instance.vm.public_ip
}
Y en tu main.tf raíz:
module "web_dev" {
source = "./modules/aws_vm"
aws_region = "eu-west-1"
instance_name = "web-dev"
instance_type = "t3.micro"
root_volume_size = 8
}
module "web_prod" {
source = "./modules/aws_vm"
aws_region = "eu-west-1"
instance_name = "web-prod"
instance_type = "t3.small"
root_volume_size = 30
}
Puedes hacer exactamente lo mismo con módulos para Azure y GCP.
Encapsulas el patrón “VM + red + firewall”, parametrizas lo que varía, y de repente levantar diez entornos es cuestión de diez bloques module en lugar de diez archivos que nadie se atreve a tocar.
Buenas prácticas para no acabar peor que sin IaC
Después de todo lo que hemos visto, aquí te dejo unos pequeños consejos de sentido común que te ahorrarán disgustos.
Para cualquier cosa seria, usa estado remoto con bloqueo (S3 + DynamoDB, Azure Blob o GCS). El estado local es para jugar; en equipo es pedir problemas.
Prohíbe los cambios manuales en producción fuera de Terraform, salvo emergencias documentadas. Y “documentadas” no significa “se lo conté a Pepe por Slack”.
Revisa los plan como si fueran pull requests: alguien debería mirar qué se va a borrar o crear antes de darle al botón, porque un destroy inesperadom de un recurso arruina el día a cualquiera.
No metas secretos en claro en los .tf. Ni contraseñas, ni tokens, ni “es solo para probar”. Usa variables sensibles, vaults o lo que tu proveedor te ofrezca, pero no los dejes ahí en texto plano esperando a que alguien los encuentre en un repo público.
Y cuando empieces a copiar y pegar bloques de infraestructura, para y usa módulos: tu yo del futuro te invitará a una caña.
Antes de cerrar el portátil
Con todo esto tienes una base bastante sólida para pasar del “click-ops descontrolado” a una infraestructura razonablemente civilizada.
A lo largo de este tutorial has cubierto lo básico e indispensable para ponerte a treabajar en serio con IAC.
El siguiente paso natural es añadir tests de infraestructura (Terratest, tflint, checkov) y pipelines CI/CD que ejecuten plan en cada pull request y apply solo cuando el código haya pasado revisión. Porque el apply que se lanza a mano a las 17:45 de un jueves tiene una tasa de incidencias sospechosamente alta.
Si trabajas en equipo, considera añadir políticas as code: Sentinel si usas Terraform Cloud/Enterprise, OPA si prefieres algo más abierto. Son la forma de decirle a Terraform “nunca abras el puerto 22 al mundo” sin depender de que alguien lo recuerde en cada revisión. Que no lo recuerda. Nunca.
Y si algún día tu main.tf supera las 200 líneas sin ser un módulo, para. Respira. Crea un módulo. Tu yo del futuro, el que hereda ese fichero a las 23:00 con una alarma activa, te lo agradecerá en silencio.
La buena noticia es que llegar hasta aquí ya es más de lo que hace la mayoría.
La mala es que ahora eres la persona a quien van a preguntar qué está pasando con el estado de Terraform.
Pero eso ya es problema de tu yo del futuro. El de ahora merece un café.
Glosario
Porque no todo el mundo ha sufrido suficientes incidentes de producción como para conocer el idioma.
Backend (Terraform): el lugar donde Terraform guarda el fichero de estado. Puede ser local (en tu máquina, para llorar tú solo) o remoto (S3, Azure Blob, GCS), donde el estado vive centralizado y accesible para todo el equipo. El backend remoto también habilita el bloqueo de estado.
checkov: herramienta de análisis estático para ficheros Terraform (y otros formatos IaC). Detecta malas configuraciones de seguridad antes de que lleguen a producción. El típico “¿acabas de abrir el puerto 22 al mundo sin querer?”.
CI/CD (Integración y despliegue continuos): pipeline automatizado que recoge el código de un repositorio, lo valida, lo testea y lo despliega sin intervención manual. En el contexto de Terraform: el sistema que ejecuta
plancuando llega un pull request yapplysolo cuando alguien con criterio da el visto bueno.click-ops: el arte de gestionar infraestructura a golpe de ratón desde la consola web del proveedor. Funciona a pequeña escala, pero no es repetible, no es auditable y es la causa directa de ese tipo de reuniones de lunes donde nadie sabe exactamente qué hay desplegado ni quién lo tocó.
drift (divergencia de estado): diferencia entre lo que dice el estado de Terraform y lo que hay realmente en la nube. Aparece cuando alguien hace “un pequeño cambio a mano, que total”. Cuanto más tiempo pasa sin detectarlo, más creativa se vuelve la explicación en el postmortem.
HCL (HashiCorp Configuration Language): el lenguaje con el que escribes los ficheros
.tf. Está diseñado para ser más legible que JSON y menos ambicioso que un lenguaje de programación completo. En la práctica: bloques con llaves, parámetros enclave = valory alguna interpolación para atar variables.OPA (Open Policy Agent): motor de políticas de código abierto con su propio lenguaje (Rego). Permite definir reglas como “ningún bucket S3 puede ser público” y evaluarlas antes de que Terraform aplique nada. La alternativa abierta a Sentinel.
Policy as Code (Políticas como Código): definir las reglas de seguridad, cumplimiento y convenciones de infraestructura como código versionable en Git, en lugar de un documento de Word que nadie lee y que alguien actualizó por última vez en 2019. Sentinel y OPA son los dos motores más usados con Terraform.
Provider: plugin que Terraform usa para hablar con un servicio concreto (AWS, Azure, GCP, Cloudflare, GitHub…). Se declara en
providers.tf, Terraform lo descarga al hacerinity, a partir de ahí, sabe qué recursos puede crear y cómo llamar a la API correspondiente.Sentinel: lenguaje y framework de políticas as code de HashiCorp, disponible en Terraform Cloud y Enterprise. Permite escribir reglas que se evalúan antes de que
applyhaga nada. Es como tener un revisor de pull requests que nunca se distrae, nunca tiene prisa y lo recuerda absolutamente todo.State lock (bloqueo de estado): mecanismo que impide que dos personas ejecuten
applya la vez sobre el mismo estado. En AWS lo gestiona una tabla DynamoDB; en Azure y GCP viene incluido en el backend. Sin él, dosapplyconcurrentes pueden dejarte el estado corrupto y el día arruinado.Terratest: librería en Go para escribir tests de infraestructura real. Despliega recursos, verifica que funcionan como se espera (pings, endpoints, outputs…) y los destruye al terminar. Para equipos que prefieren descubrir que algo no funciona en un test y no a las 02:00 con una alerta activa.
tflint: linter para Terraform. Detecta errores de configuración, variables sin usar, tipos incorrectos y problemas específicos del provider antes de ejecutar nada. Lo que hace
terraform validatepero con criterio.
Fuentes y referencias
La documentación oficial que deberías haber leído antes de tocar nada, pero que probablemente leerás después del primer susto.
- Documentación oficial de AWS CLI - Amazon Web Services. Guía de instalación de AWS CLI v2.
- Documentación oficial de Azure CLI - Microsoft. Instrucciones de instalación de Azure CLI para todos los sistemas operativos.
- Guía de instalación de Google Cloud CLI - Google Cloud. Instalación del SDK de Google Cloud (gcloud).
- Descargas de Terraform - HashiCorp. Binarios oficiales de Terraform.
