Infraestructura como código con Terraform
Terraform permite definir, versionar y ejecutar infraestructura completa mediante archivos declarativos. Aprende a convertir servidores, redes y servicios en código que se puede revisar, probar y ejecutar de forma repetible.
¿Qué es IaC?
Infraestructura como código (IaC) es el práctica de gestionar y aprovisionar recursos de infraestructura mediante archivos de configuración en lugar de procesos manuales. En vez de hacer clic en una consola web o ejecutar comandos ssh uno por uno, describes el estado deseado en archivos que se pueden versionar, auditar y ejecutar automáticamente.
Declarativo vs imperativo
El enfoque declarativo (Terraform, CloudFormation) dice "quiero que exista X con estas propiedades" y el motor se encarga de calcular los pasos necesarios. El enfoque imperativo (scripts Bash, Pulumi en modo procedural) describe paso a paso cómo llegar al resultado. Terraform es declarativo por naturaleza: defines el estado final y él decide qué crear, modificar o eliminar.
Beneficios de IaC
Reproducibilidad — la misma configuración produce el mismo entorno en desarrollo, staging y producción. Versionado — cada cambio queda registrado en Git con un diff legible. Auditabilidad — puedes revisar quién cambió qué y cuándo. Colaboración — el equipo trabaja sobre los mismos archivos en vez de sobre conocimiento tácito.
plan justamente para eso: ver qué cambiaría antes de ejecutar.Primeros pasos con Terraform
Instalación
En macOS instala con Homebrew. En Linux descarga el binario o usa el gestor de paquetes. En Windows usa winget o Chocolatey.
# macOS
brew install hashicorp/tap/terraform
# Linux (Debian/Ubuntu)
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
# Verificar instalación
terraform versionSintaxis HCL
Terraform usa HCL (HashiCorp Configuration Language). Un archivo .tf contiene bloques con atributos, comentarios con # o //, y strings entre comillas.
# Esto es un comentario en línea
terraform {
required_version = ">= 1.5"
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0"
}
}
}
# Bloque provider: configura DÓNDE ejecutar
provider "docker" {
host = "unix:///var/run/docker.sock"
}
# Bloque resource: define QUÉ crear
resource "docker_container" "web" {
name = "mi-nginx"
image = docker_image.nginx.image_id
ports {
internal = 80
external = 8080
}
}
# Bloque data: consulta recursos existentes
data "docker_image" "nginx" {
name = "nginx:alpine"
}Providers
Los providers son plugins que conectan Terraform con APIs externas: AWS, Azure, GCP, Docker, Kubernetes, GitHub, etc. Cada provider expone recursos y data sources que puedes usar en tu configuración.
Init / Plan / Apply / Destroy
El ciclo de vida de Terraform tiene cuatro comandos fundamentales:
# 1. init: descarga providers y configura el backend
terraform init
# 2. plan: muestra qué cambiaría (dry run)
terraform plan
# 3. apply: ejecuta los cambios
terraform apply
# 4. destroy: elimina todo lo gestionado
terraform destroyEl state file
Terraform guarda el estado actual de la infraestructura en un archivo terraform.tfstate. Este archivo mapea los recursos definidos en código con los recursos reales en el proveedor.Nunca lo edites a mano — usa terraform state para manipularlo.
Ejemplo práctico: provisionar un contenedor Docker con Terraform:
# main.tf
terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0"
}
}
}
provider "docker" {}
resource "docker_image" "nginx" {
name = "nginx:alpine"
}
resource "docker_container" "web" {
name = "terraform-nginx"
image = docker_image.nginx.image_id
ports {
internal = 80
external = 8080
}
}
# Ejecutar:
# terraform init
# terraform apply -auto-approve
# curl http://localhost:8080 -> funciona
# terraform destroy -auto-approveRecursos, variables y outputs
Resources
Los resources son los bloques que definen componentes de infraestructura. Cada resource tiene un tipo (aws_instance, docker_container) y un nombre lógico. Terraform compara el estado actual con el deseado y ejecuta los cambios necesarios.
Data Sources
Los data sources permiten consultar información de recursos que ya existen sin gestionarlos. Útiles para obtener AMIs 最新, IPs de VPCs existentes o IDs de subnets.
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}Variables
Las variables parametrizan tu configuración. Terraform soporta strings, números, booleanos, listas y maps. Defínelas en variables.tf y asigna valores en terraform.tfvars.
# variables.tf
variable "container_name" {
description = "Nombre del contenedor"
type = string
default = "mi-app"
}
variable "replicas" {
description = "Número de réplicas"
type = number
default = 1
}
variable "enable_logging" {
description = "Activar logging"
type = bool
default = true
}
variable "ports" {
description = "Puertos a exponer"
type = list(number)
default = [80, 443]
}
variable "environment_vars" {
description = "Variables de entorno"
type = map(string)
default = {
NODE_ENV = "production"
LOG_LEVEL = "info"
}
}
# terraform.tfvars
container_name = "web-production"
replicas = 3
enable_logging = true
ports = [80, 443, 8080]
environment_vars = {
NODE_ENV = "production"
LOG_LEVEL = "warn"
}Outputs
Los outputs exponen información útil tras el apply: IPs públicas, IDs de recursos, URLs de endpoints. Marca valores sensibles con sensitive = true para que no se muestren en la CLI.
# outputs.tf
output "container_id" {
description = "ID del contenedor creado"
value = docker_container.web.id
}
output "app_url" {
description = "URL de la aplicación"
value = "http://localhost:8080"
}
output "db_password" {
description = "Contraseña de la base de datos"
value = var.db_password
sensitive = true
}Ejemplo completo: crear un security group parametrizado con variables:
# variables.tf
variable "allowed_ports" {
type = list(number)
default = [22, 80, 443]
}
variable "allowed_cidrs" {
type = list(string)
default = ["0.0.0.0/0"]
}
# main.tf
resource "aws_security_group" "web" {
name = "web-sg"
description = "Security group para servidores web"
dynamic "ingress" {
for_each = var.allowed_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = var.allowed_cidrs
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}Módulos reutilizables
Un módulo es un paquete de configuración Terraform que agrupa recursos relacionados y expone inputs (variables) y outputs. Los módulos promueven la reutilización, el testing y la separación de responsabilidades.
Estructura de directorios
modules/
docker-app/
main.tf # recursos principales
variables.tf # inputs del módulo
outputs.tf # outputs del módulo
README.md # documentaciónMódulos del registry
Terraform tiene un registry público en registry.terraform.io con módulos oficiales para AWS, GCP, Azure y terceros. Usa módulos verificados para infraestructura de producción.
# Usar un módulo del registry
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.1.0"
name = "mi-vpc"
cidr = "10.0.0.0/16"
azs = ["eu-west-1a", "eu-west-1b"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
}
# Acceder a outputs del módulo
output "vpc_id" {
value = module.vpc.vpc_id
}Ejemplo: módulo para deploy de una app con Docker provider que acepta imagen, puerto y replicas:
# modules/docker-app/main.tf
variable "image" {
type = string
}
variable "port" {
type = number
default = 80
}
variable "replicas" {
type = number
default = 1
}
resource "docker_image" "app" {
name = var.image
}
resource "docker_container" "app" {
count = var.replicas
name = "app-${count.index + 1}"
image = docker_image.app.image_id
ports {
internal = var.port
external = var.port + count.index
}
}
# modules/docker-app/outputs.tf
output "container_ids" {
value = docker_container.app[*].id
}
output "endpoints" {
value = [for c in docker_container.app :
"http://localhost:${c.ports[0].external}"
]
}
# --- Uso del módulo ---
module "web_app" {
source = "./modules/docker-app"
image = "nginx:alpine"
port = 80
replicas = 3
}
output "urls" {
value = module.web_app.endpoints
}source = "./ruta". Los del registry usan source = "organizacion/modulo-provider". Siempre fija la versión con version = "~> X.Y" para evitar breaking changes.Buenas prácticas y CI/CD para IaC
Remote state
En producción nunca uses el state file local. Configura un backend remoto como S3 con bloqueo DynamoDB para evitar que dos personas ejecuten apply simultáneamente.
# backend.tf
terraform {
backend "s3" {
bucket = "mi-terraform-state"
key = "produccion/terraform.tfstate"
region = "eu-west-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
# Crear el bucket y tabla DynamoDB primero (one-time setup)
# aws s3api create-bucket --bucket mi-terraform-state --region eu-west-1
# aws dynamodb create-table --table-name terraform-locks \
# --attribute-definitions AttributeName=LockID,AttributeType=S \
# --key-schema AttributeName=LockID,KeyType=HASH \
# --billing-mode PAY_PER_REQUESTWorkspaces
Los workspaces permiten tener múltiples estados en el mismo directorio. Usa uno por entorno: dev, staging, prod. Cada workspace tiene su propio terraform.tfstate.
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod
terraform workspace list
terraform workspace select dev
# Ejecutar apply solo en el workspace activo
terraform applyDrift detection
El drift ocurre cuando alguien modifica la infraestructura manualmente (fuera de Terraform). Detecta drift ejecutando terraform plan en un pipeline periódico: si hay cambios no esperados, falla la build y avisa.
tfsec para seguridad
tfsec (o trivy config) escanea tu código Terraform en busca de malas prácticas de seguridad: puertos abiertos, blobs sin cifrado, IAM overly permissive, etc. Ejecútalo como paso previo al plan.
# Instalar tfsec
brew install aquasecurity/tap/tfsec
# Ejecutar sobre el directorio actual
tfsec .
# Output en formato JSON para CI
tfsec . --format json --out tfsec-report.jsonPipeline CI/CD con GitHub Actions
El flujo estándar: PR ejecuta terraform plan y comenta el resultado. Tras el merge a main, ejecuta terraform apply con aprobación manual.
# .github/workflows/terraform.yml
name: Terraform CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
pull-requests: write
id-token: write
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.7.0"
- name: Terraform fmt
run: terraform fmt -check
- name: Terraform Init
run: terraform init
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
id: plan
run: terraform plan -no-color
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Comentar plan en PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: \`### Terraform Plan
\`\`\`
\${{ steps.plan.outputs.stdout }}
\`\`\`
\`
body: \`### Terraform Plan
\`\`\`
\${{ steps.plan.outputs.stdout }}
\`\`\`
\`
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}terraform apply sin terraform plan primero en producción. El plan es tu seguro: muestra exactamente qué se va a crear, modificar o destruir antes de tocar infraestructura real.environment: production en el job para que requiera aprobación de un maintainer antes de ejecutar.Con Terraform tienes el ciclo completo de infraestructura como código: definir declarativamente, planificar cambios, ejecutar de forma repetible y gestionar estado remoto. El siguiente paso natural es integrar módulos con CI/CD, agregar testing con terratest y explorar herramientas como OpenTofu como alternativa open-source.