I am Lino
21 de mayo de 2026

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

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:

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 login ejecutado, 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 init ejecutado, 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.


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.

Sígueme

Escribo y opino sobre tecnología, desarrollo de software y lo que se me pase por la cabeza.