Terraform: Guía Completa de IaC desde Cero hasta Equipos
Domina Terraform desde cero: providers, estado, módulos, workspaces, backends remotos, pipelines CI/CD y cómo IaC encaja en un sprint ágil de verdad.
Piensa en la diferencia entre un restaurante que escribe sus recetas y uno donde el chef guarda todo en su cabeza. El segundo está secuestrado por una sola persona. En el momento en que esa persona se enferma, se va de vacaciones o simplemente renuncia, la cocina se detiene. Nadie más puede reproducir lo que hace. El conocimiento no está en el sistema — está en la memoria de una persona.
La mayoría de los equipos de infraestructura empiezan como ese segundo restaurante. Servidores provisionados a través de consolas web, configuraciones aplicadas a mano, los pasos exactos conocidos solo por quien estaba en el teclado ese día. Funciona hasta que algo se rompe, hasta que alguien se va, hasta que necesitas una segunda copia del entorno, hasta que una auditoría te pide demostrar qué cambió y cuándo.
Terraform es la disciplina de escribir la receta. No como documentación que se vuelve obsoleta, sino como código ejecutable que provisiona exactamente la infraestructura que describes, cada vez, en cualquier nube, con un registro de cada cambio.
Qué Hace Terraform Realmente
Antes de tocar código, el modelo mental importa. Terraform hace tres cosas:
Declara el estado deseado. Describes la infraestructura que quieres en HCL (HashiCorp Configuration Language). Un servidor con 2 CPUs, un firewall con reglas específicas, un registro DNS apuntando a la IP de ese servidor. Terraform no le dice cómo crear estas cosas — le dice cómo deben verse cuando terminen.
Calcula un plan. Terraform compara tu estado declarado contra el estado real actual de tu infraestructura y calcula el conjunto mínimo de cambios necesarios para alcanzar el estado deseado. Crea esto, modifica aquello, destruye lo otro. Revisas el plan antes de que se aplique cualquier cosa.
Aplica cambios y los registra. Cuando apruebas el plan, Terraform ejecuta las llamadas API para crear, modificar o destruir recursos. Después de cada ejecución, registra el estado resultante en un archivo de estado. Ese archivo es lo que Terraform usa para comparar en la siguiente ejecución.
El flujo de trabajo es siempre el mismo: escribir → terraform plan → revisar → terraform apply. Sin excepciones en producción.
Quién Usa Terraform y Cómo
Terraform sirve a diferentes roles dependiendo de tu posición en el equipo. Entender esto previene el error común de tratarlo como una herramienta exclusiva de ingenieros senior.
Desarrolladores junior usan Terraform en modo lectura primero. Ejecutan terraform plan para entender qué cambios hará un PR. Leen módulos existentes para entender cómo está estructurado el entorno. Agregan variables de entorno, actualizan registros DNS, redimensionan recursos dentro de módulos existentes. El objetivo es comodidad con el flujo de trabajo antes de escribir nueva infraestructura.
Ingenieros de nivel medio escriben nuevos recursos dentro de una estructura existente, crean llamadas a módulos, actualizan versiones de providers, y administran configuraciones específicas por workspace. Revisan planes antes de aplicaciones en producción y participan en la respuesta a incidentes cuando cambios de infraestructura causan problemas.
Ingenieros DevOps senior diseñan la jerarquía de módulos, configuran el backend remoto, definen el flujo de trabajo del equipo (políticas de branches, integración CI/CD, requisitos de revisión), administran upgrades de providers y son dueños del estado.
Tech leads y arquitectos definen qué nubes y qué servicios, toman decisiones de comprar vs construir en módulos (escribir los propios vs usar el Terraform Registry), establecen los estándares para convenciones de nombres, tagging y políticas de ciclo de vida de recursos.
Nivel 1: Tu Primer Proyecto de Terraform
Empieza con la cosa más pequeña posible que sea real: un servidor con un firewall.
Instalación
# en macOS
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
# en Linux (Debian/Ubuntu)
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) 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
terraform version
Estructura del Proyecto
mi-primera-infra/
├── main.tf # recursos
├── variables.tf # declaraciones de variables de entrada
├── outputs.tf # declaraciones de valores de salida
└── terraform.tfvars # valores reales de variables (nunca commitees secretos aquí)
Configuración del Provider
Un provider es un plugin que sabe cómo hablar con una API específica — AWS, GCP, Hetzner, Cloudflare, GitHub, etc. Declaras qué providers necesitas y Terraform los descarga.
# main.tf
terraform {
required_version = ">= 1.7.0"
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "~> 1.45" # ~> significa >= 1.45.0, < 2.0.0
}
}
}
provider "hcloud" {
token = var.hcloud_token
}
Tu Primer Recurso
# main.tf — continuación
resource "hcloud_server" "web" {
name = "web-${var.environment}"
server_type = "cx22" # 2 vCPU, 4GB RAM
image = "ubuntu-24.04"
location = "nbg1" # Nuremberg
ssh_keys = [hcloud_ssh_key.default.id]
labels = {
environment = var.environment
managed-by = "terraform"
}
}
resource "hcloud_ssh_key" "default" {
name = "deploy-key"
public_key = file("~/.ssh/id_ed25519.pub")
}
resource "hcloud_firewall" "web" {
name = "web-${var.environment}"
rule {
direction = "in"
protocol = "tcp"
port = "22"
source_ips = ["0.0.0.0/0", "::/0"]
}
rule {
direction = "in"
protocol = "tcp"
port = "443"
source_ips = ["0.0.0.0/0", "::/0"]
}
}
resource "hcloud_firewall_attachment" "web" {
firewall_id = hcloud_firewall.web.id
server_ids = [hcloud_server.web.id]
}
Variables
# variables.tf
variable "hcloud_token" {
type = string
sensitive = true
description = "Token de API de Hetzner Cloud"
}
variable "environment" {
type = string
description = "staging o production"
validation {
condition = contains(["staging", "production"], var.environment)
error_message = "environment debe ser staging o production"
}
}
# terraform.tfvars — valores para uso local
# NO commitees este archivo. Agrégalo a .gitignore.
hcloud_token = "tu-api-token-aqui"
environment = "staging"
Outputs
# outputs.tf
output "server_ip" {
value = hcloud_server.web.ipv4_address
description = "Dirección IPv4 pública del servidor web"
}
output "server_id" {
value = hcloud_server.web.id
}
La Primera Ejecución
# inicializar — descarga providers, configura el backend
terraform init
# previsualizar qué se creará
terraform plan
# aplicar el plan
terraform apply
# después de aplicar, inspeccionar el estado
terraform show
# ver outputs específicos
terraform output server_ip
# cuando termines con el recurso
terraform destroy
La primera salida de terraform plan te enseña a leer Terraform. Los recursos con prefijo + se crearán. Los con ~ se modificarán en su lugar. Los con -/+ se destruirán y recrearán (porque el cambio no puede hacerse en su lugar). Los con - se destruirán. Revisar esta salida con cuidado es el hábito más importante en Terraform.
Nivel 2: Gestión del Estado
El estado es lo que hace a Terraform diferente de un script. También es lo que lo hace peligroso cuando se malentiende.
El archivo de estado (terraform.tfstate) registra el mapeo entre tus declaraciones HCL y los IDs de recursos reales en el provider. Sin él, Terraform no puede saber que hcloud_server.web corresponde al servidor ID 12345678 en Hetzner. Intentaría crear uno nuevo en cada ejecución.
El Problema con el Estado Local
El estado local (terraform.tfstate en tu disco) falla inmediatamente cuando:
- Dos ingenieros ejecutan
terraform applysimultáneamente — conflicto de estado, uno sobreescribe al otro - Tu laptop se rompe — el estado desaparece, Terraform no puede reconciliar con la infraestructura real
- Necesitas ejecutar Terraform desde CI/CD — sin acceso a tu archivo local
La solución es un backend remoto con bloqueo de estado.
Backend Remoto: S3 + DynamoDB
terraform {
backend "s3" {
bucket = "tu-org-terraform-state"
key = "projects/mi-app/staging/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-locks" # para bloqueo
}
}
Crea el bucket S3 y la tabla DynamoDB una vez (manualmente o con una config Terraform de bootstrap):
# bootstrap/main.tf — ejecuta esto una vez para crear el backend de estado
resource "aws_s3_bucket" "terraform_state" {
bucket = "tu-org-terraform-state"
}
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled" # el versionado da historial de estado y rollback
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-state-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
Con este backend, cuando alguien ejecuta terraform apply:
- Terraform adquiere un lock en DynamoDB — ningún otro proceso puede aplicar simultáneamente
- Lee el estado más reciente de S3
- Aplica el plan
- Escribe el nuevo estado en S3
- Libera el lock
Terraform Cloud (Alternativa)
El backend administrado de HashiCorp maneja estado remoto, bloqueo y ejecución de planes con UI web y API. El tier gratuito cubre la mayoría de equipos pequeños:
terraform {
cloud {
organization = "tu-org"
workspaces {
name = "mi-app-staging"
}
}
}
Nivel 3: Módulos
Un módulo es una unidad reutilizable de infraestructura. Es la diferencia entre copiar y pegar la misma configuración de servidor + firewall + DNS cuatro veces para cuatro entornos, versus definirla una vez y llamarla cuatro veces con diferentes parámetros.
Cuándo Escribir un Módulo
Escribe un módulo cuando tienes dos o más grupos de recursos similares que difieren solo en sus entradas. Un solo recurso no es un módulo. Tres configuraciones de servidor idénticas con nombres diferentes sí lo son.
Estructura del Módulo
modules/
server/
main.tf # los recursos
variables.tf # entradas
outputs.tf # lo que los llamadores pueden referenciar
versions.tf # versiones requeridas de providers
database/
main.tf
variables.tf
outputs.tf
versions.tf
environments/
staging/
main.tf # llama a módulos
variables.tf
outputs.tf
backend.tf
production/
main.tf
variables.tf
outputs.tf
backend.tf
Escribiendo un Módulo
# modules/server/variables.tf
variable "name" {
type = string
description = "Nombre del servidor"
}
variable "server_type" {
type = string
default = "cx22"
description = "Tipo de servidor Hetzner"
}
variable "environment" {
type = string
}
variable "allowed_ports" {
type = list(number)
default = [80, 443]
description = "Puertos TCP de entrada permitidos"
}
# modules/server/main.tf
resource "hcloud_server" "this" {
name = var.name
server_type = var.server_type
image = "ubuntu-24.04"
location = "nbg1"
labels = {
environment = var.environment
managed-by = "terraform"
}
}
resource "hcloud_firewall" "this" {
name = "${var.name}-firewall"
dynamic "rule" {
for_each = var.allowed_ports
content {
direction = "in"
protocol = "tcp"
port = tostring(rule.value)
source_ips = ["0.0.0.0/0", "::/0"]
}
}
}
resource "hcloud_firewall_attachment" "this" {
firewall_id = hcloud_firewall.this.id
server_ids = [hcloud_server.this.id]
}
# modules/server/outputs.tf
output "ipv4_address" {
value = hcloud_server.this.ipv4_address
}
output "server_id" {
value = hcloud_server.this.id
}
Llamando al Módulo
# environments/staging/main.tf
module "web" {
source = "../../modules/server"
name = "web-staging"
environment = "staging"
server_type = "cx22"
allowed_ports = [80, 443]
}
module "api" {
source = "../../modules/server"
name = "api-staging"
environment = "staging"
server_type = "cx32"
allowed_ports = [8080]
}
output "web_ip" {
value = module.web.ipv4_address
}
Usando Módulos del Registry
# usar el módulo oficial de AWS VPC
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.5.1" # siempre fija la versión
name = "main-vpc"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
enable_nat_gateway = true
single_nat_gateway = true # optimización de costos
tags = {
Environment = var.environment
ManagedBy = "terraform"
}
}
Nivel 4: Flujos Multi-Entorno
Directorios Separados por Entorno
environments/
staging/
backend.tf # backend "s3" { key = "staging/terraform.tfstate" }
main.tf
variables.tf
terraform.tfvars
production/
backend.tf # backend "s3" { key = "production/terraform.tfstate" }
main.tf
variables.tf
terraform.tfvars
Esto significa archivos de estado separados, ciclos de plan/apply separados, y sin riesgo de destruir accidentalmente producción mientras se trabaja en staging.
Workspaces: Cuándo Usarlos y Cuándo No
Los workspaces de Terraform permiten múltiples archivos de estado dentro de una misma configuración de backend:
terraform workspace new staging
terraform workspace new production
terraform workspace select staging
terraform apply
resource "hcloud_server" "web" {
server_type = terraform.workspace == "production" ? "cx32" : "cx22"
}
Los workspaces funcionan bien para entornos verdaderamente idénticos que difieren solo en escala — entornos efímeros de feature branches, por ejemplo. Funcionan mal para staging vs producción porque la lógica condicional se acumula en configuración ilegible. Usa directorios separados para entornos de larga duración y arquitecturalmente diferentes. Usa workspaces para entornos de corta duración y estructuralmente idénticos.
Archivos de Variables por Entorno
terraform apply -var-file="staging.tfvars"
terraform apply -var-file="production.tfvars"
# staging.tfvars
environment = "staging"
server_type = "cx22"
replica_count = 1
enable_backups = false
# production.tfvars
environment = "production"
server_type = "cx32"
replica_count = 3
enable_backups = true
Nivel 5: Patrones Avanzados de HCL
for_each y count
count crea múltiples copias idénticas de un recurso. for_each crea un recurso por ítem en un map o set, con cada recurso independientemente direccionable.
# count — crea 3 servidores: server[0], server[1], server[2]
resource "hcloud_server" "worker" {
count = var.worker_count
name = "worker-${count.index}"
server_type = "cx22"
image = "ubuntu-24.04"
}
# for_each — crea servidores con direcciones significativas
resource "hcloud_server" "services" {
for_each = {
web = { type = "cx22", location = "nbg1" }
api = { type = "cx32", location = "fsn1" }
}
name = each.key
server_type = each.value.type
location = each.value.location
image = "ubuntu-24.04"
}
# referencia: hcloud_server.services["web"].ipv4_address
Prefiere for_each sobre count para cualquier cosa que no sea una cantidad simple. Con count, eliminar un elemento del medio renumera todos los elementos siguientes, lo que causa que Terraform los destruya y recree. Con for_each, cada elemento tiene una clave estable.
Bloques Dinámicos
variable "security_group_rules" {
type = list(object({
port = number
protocol = string
cidr = string
}))
}
resource "hcloud_firewall" "main" {
name = "main"
dynamic "rule" {
for_each = var.security_group_rules
content {
direction = "in"
protocol = rule.value.protocol
port = tostring(rule.value.port)
source_ips = [rule.value.cidr]
}
}
}
Valores Locales
Los locals reducen la repetición y hacen las expresiones legibles:
locals {
common_tags = {
environment = var.environment
managed-by = "terraform"
project = "mi-app"
team = "platform"
}
server_name = "${var.project}-${var.environment}-${var.region}"
is_production = var.environment == "production"
}
resource "hcloud_server" "web" {
name = local.server_name
labels = local.common_tags
}
Data Sources
Los data sources leen infraestructura existente que Terraform no creó. Permiten referenciar recursos gestionados en otro lugar — por otro equipo, otra configuración de Terraform, o manualmente.
# leer una clave SSH existente por nombre
data "hcloud_ssh_key" "ops_team" {
name = "ops-team-2025"
}
resource "hcloud_server" "web" {
ssh_keys = [data.hcloud_ssh_key.ops_team.id]
}
# leer el ID de imagen más reciente de Ubuntu dinámicamente
data "hcloud_image" "ubuntu_24" {
name = "ubuntu-24.04"
type = "system"
}
resource "hcloud_server" "web" {
image = data.hcloud_image.ubuntu_24.id
}
Nivel 6: Terraform en Equipos Ágiles
El Ciclo de Sprint con Terraform
En un equipo basado en sprints, los cambios de infraestructura siguen un ritmo que debe integrarse con el ciclo de desarrollo de features sin convertirse en un cuello de botella.
Los dos modos de falla son: cambios de infraestructura bloqueando el trabajo de features (el equipo de infra es lento, las revisiones de PR tardan días) y trabajo de features rompiendo la infraestructura (los desarrolladores mergean cambios de infra sin revisión).
La solución es tratar la infraestructura como código con los mismos estándares de revisión que el código de aplicación, pero con un paso adicional: la salida del plan es parte de la revisión del PR.
Planificación del sprint → tareas de infraestructura creadas junto a tareas de features
↓
Desarrollador crea branch de infra → abre PR → CI ejecuta terraform plan
↓
El PR incluye la salida del plan en comentarios → el revisor lee el plan, no solo el código
↓
Merge a main → CI aplica a staging automáticamente
↓
Sprint review → staging es el entorno de demo
↓
Release → CI aplica a producción con gate de aprobación manual
GitHub Actions: Plan Automatizado en PR
# .github/workflows/terraform.yaml
name: Terraform
on:
pull_request:
paths:
- "infrastructure/**"
push:
branches: [main]
paths:
- "infrastructure/**"
env:
TF_VERSION: "1.7.3"
AWS_REGION: "us-east-1"
jobs:
plan:
name: Plan
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
permissions:
contents: read
pull-requests: write
id-token: write # para autenticación OIDC con AWS
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Configurar credenciales AWS (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/TerraformCIReadOnly
aws-region: ${{ env.AWS_REGION }}
- name: Terraform Init
working-directory: infrastructure/environments/staging
run: terraform init
- name: Terraform Plan
id: plan
working-directory: infrastructure/environments/staging
run: |
terraform plan -no-color -out=tfplan 2>&1 | tee plan.txt
echo "exitcode=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT
- name: Publicar Plan en PR
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const plan = fs.readFileSync('infrastructure/environments/staging/plan.txt', 'utf8');
const truncated = plan.length > 60000 ? plan.slice(0, 60000) + '\n\n... truncado ...' : plan;
const body = `## Plan de Terraform — staging\n\`\`\`\n${truncated}\n\`\`\``;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body
});
apply-staging:
name: Aplicar a Staging
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
environment: staging
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Configurar credenciales AWS
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/TerraformCIApply
aws-region: ${{ env.AWS_REGION }}
- name: Terraform Init
working-directory: infrastructure/environments/staging
run: terraform init
- name: Terraform Apply
working-directory: infrastructure/environments/staging
run: terraform apply -auto-approve
apply-production:
name: Aplicar a Producción
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: [apply-staging]
environment: production # requiere aprobación manual en GitHub Environments
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Configurar credenciales AWS
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/TerraformCIApply
aws-region: ${{ env.AWS_REGION }}
- name: Terraform Init
working-directory: infrastructure/environments/production
run: terraform init
- name: Terraform Apply
working-directory: infrastructure/environments/production
run: terraform apply -auto-approve
Autenticación OIDC (Sin Credenciales de Larga Duración)
El pipeline de CI usa OIDC para asumir un rol de AWS IAM en lugar de almacenar AWS_ACCESS_KEY_ID y AWS_SECRET_ACCESS_KEY como secretos. Este es el enfoque correcto. Los tokens OIDC son de corta duración y tienen alcance al repositorio y branch específico de GitHub.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:tu-org/tu-repo:pull_request"
}
}
}
]
}
Dos roles, dos políticas de confianza, dos conjuntos de permisos:
TerraformCIReadOnly— activado en pull_request, solo puede leer estado y ejecutar planes (sin permisos de escritura)TerraformCIApply— activado en push a main, tiene permisos de escritura, solo confiable para el branch main
Convenciones de Revisión del Equipo
El checklist de PR para cambios de infraestructura:
## Checklist de PR de Infraestructura
- [ ] Se ejecutó `terraform fmt` — el código está formateado
- [ ] `terraform validate` pasa
- [ ] La salida del plan está adjunta y revisada
- [ ] El plan no muestra destrucciones inesperadas
- [ ] Los cambios de variables están documentados en la descripción del PR
- [ ] Los secretos no están en la salida del plan
- [ ] El cambio fue verificado primero en un entorno local o de branch
- [ ] El plan de rollback está documentado si el cambio es destructivo
Manejando Cambios Destructivos
Terraform a veces propone destruir y recrear un recurso cuando una actualización en su lugar no es posible. Protege los recursos críticos:
resource "hcloud_server" "web" {
lifecycle {
prevent_destroy = true # terraform apply fallará si se planea una destrucción
create_before_destroy = true # crea el reemplazo antes de destruir el antiguo
ignore_changes = [
labels, # ignorar cambios de labels gestionados externamente
]
}
}
create_before_destroy es esencial para servidores detrás de un load balancer — garantiza que el nuevo servidor esté sano antes de que el antiguo sea eliminado, previniendo tiempo de inactividad.
Nivel 7: Seguridad y Cumplimiento
Menor Privilegio para CI
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:Describe*",
"ec2:CreateInstance",
"ec2:TerminateInstances",
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket",
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem"
],
"Resource": [
"arn:aws:ec2:*:*:instance/*",
"arn:aws:s3:::tu-org-terraform-state/*",
"arn:aws:dynamodb:*:*:table/terraform-state-locks"
]
}
]
}
Análisis Estático con tfsec y checkov
# tfsec — escanea configuraciones de seguridad
brew install tfsec
tfsec .
# checkov — políticas como código más amplias
pip install checkov
checkov -d .
Agrega ambos al job de plan en CI:
- name: Ejecutar tfsec
uses: aquasecurity/tfsec-action@v1.0.0
with:
working_directory: infrastructure/
- name: Ejecutar checkov
uses: bridgecrewio/checkov-action@v12
with:
directory: infrastructure/
framework: terraform
Detección de Drift
El drift ocurre cuando alguien modifica infraestructura fuera de Terraform — a través de una consola web, un comando CLI manual, o un proceso automatizado. El archivo de estado ya no coincide con la realidad.
# refrescar estado y mostrar diferencias
terraform refresh
terraform plan # si el plan muestra cambios cuando el HCL no cambió, hay drift
En CI, ejecuta un plan programado en producción para detectar drift:
# .github/workflows/drift-detection.yaml
on:
schedule:
- cron: "0 9 * * 1-5" # lunes a viernes a las 9 AM
jobs:
detect-drift:
runs-on: ubuntu-latest
steps:
- name: Detectar Drift
run: |
terraform plan -detailed-exitcode
# código de salida 0 = sin cambios (sin drift)
# código de salida 1 = error
# código de salida 2 = cambios detectados (drift)
Nivel 8: Escalar a Múltiples Equipos
Dividir el Estado por Dominio
archivos de estado:
networking/staging/terraform.tfstate ← VPCs, subnets, zonas DNS
networking/production/terraform.tfstate
platform/staging/terraform.tfstate ← clústeres K3s, bases de datos
platform/production/terraform.tfstate
equipo-a/staging/terraform.tfstate ← recursos del equipo A
equipo-a/production/terraform.tfstate
equipo-b/staging/terraform.tfstate
equipo-b/production/terraform.tfstate
Los equipos aplican su propio estado independientemente. El equipo de networking posee las VPCs. El equipo de plataforma posee los clústeres. Los equipos de app poseen sus despliegues dentro del clúster.
Referencias de Estado Remoto
# el equipo de plataforma lee IDs de VPC del estado del equipo de networking
data "terraform_remote_state" "networking" {
backend = "s3"
config = {
bucket = "tu-org-terraform-state"
key = "networking/staging/terraform.tfstate"
region = "us-east-1"
}
}
resource "aws_eks_cluster" "main" {
vpc_config {
subnet_ids = data.terraform_remote_state.networking.outputs.private_subnet_ids
}
}
Terragrunt para Configuraciones Multi-Entorno DRY
Cuando los directorios separados generan demasiada configuración repetida de backend y provider, terragrunt agrega una capa delgada que elimina el boilerplate:
# terragrunt.hcl — config raíz
remote_state {
backend = "s3"
config = {
bucket = "tu-org-terraform-state"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-state-locks"
encrypt = true
}
}
generate "provider" {
path = "provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOF
provider "aws" {
region = "us-east-1"
}
EOF
}
# environments/staging/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
inputs = {
environment = "staging"
instance_type = "t3.small"
}
Con Terragrunt, terragrunt run-all plan ejecuta planes en todos los entornos en orden de dependencia.
El Contrato de Infraestructura
Terraform no es solo una herramienta de despliegue. Es un contrato entre tu equipo y tu infraestructura. Todo en ese contrato es explícito, versionado y revisable. Todo fuera de él es un pasivo.
La disciplina que importa no es conocer cada comando de Terraform — es nunca hacer un cambio en la infraestructura de producción que no esté representado en el código. Ni una vez. En el momento en que inicias sesión en una consola y haces clic en algo, has roto el contrato. El estado diverge. El próximo plan es incorrecto. El próximo apply es impredecible.
El hábito de abrir un pull request en lugar de usar una consola web es lo que separa la infraestructura mantenible de la infraestructura secuestrada por quien la provisionó por última vez.
No eres dueño de tu infraestructura si no puedes recrearla desde código. El control de versiones no es solo para aplicaciones — es para todo lo que puede romperse en producción.