¡Hola! Envíame un mensaje
Contacto

Ciudad de México, México
Disponible para proyectos remotos y presenciales.

Redes profesionales

LinkedIn · GitHub · Email
Ingeniería aplicada, resultados medibles.

Optimizando infraestructura AWS con Terraform: lecciones de operar 23+ sitios en producción

· 8 min de lectura · Samuel Torres

Superfórmula de 7 puntas dibujándose
r(t) = (|cos(7t/4)|^1.7 + |sin(7t/4)|^1.7)^(-1/0.2)

En resumen

  • Con 23+ sitios, lo que no está en código termina siendo distinto en cada servidor.
  • Modela la flota como un mapa y cada sitio como un módulo.
  • Etiquetas obligatorias y un presupuesto por sitio vuelven visible el costo.
  • Un control en CI rechaza el plan antes de que el gasto exista.
  • La seguridad base se declara una vez, a nivel de cuenta, y la hereda toda la flota.

La optimización de AWS con Terraform no empieza por el descuento. Empieza por poder responder dos preguntas sin abrir la consola: cuánto cuesta cada sitio y qué tan expuesto está.

Administro una flota de 23+ sitios en producción. Esa operación me enseñó algo que aplica a cualquier nube: cuando son muchos sitios, lo que no está escrito en código termina siendo distinto en cada servidor.

Una precisión honesta: esa flota vive en servidores AlmaLinux con WHM, no en AWS. Lo que sigue es el diseño que propongo para llevar esa misma disciplina a AWS con Terraform. El código es ilustrativo y no incluye cifras de ahorro, porque no voy a inventarlas.

Lo que enseña operar 23+ sitios en producción

El trabajo diario con una flota así es poco glamoroso: ajustar Apache y PHP-FPM, recuperar espacio en disco, actualizar versiones de PHP y resolver DNS repartido entre varios proveedores.

Ninguna de esas tareas es difícil una vez. El problema es hacerlas 23 veces, en momentos distintos, por personas distintas. Así nace la deriva: cada servidor acumula decisiones pequeñas que nadie documentó.

En AWS el mismo fenómeno tiene dos consecuencias medibles:

  • Costo que nadie puede atribuir. Discos sin usar, instancias sobradas y respaldos eternos aparecen en la factura sin nombre de sitio.
  • Seguridad desigual. Un sitio queda con cifrado y accesos cerrados; otro, creado con prisa, no.

Terraform no resuelve esto por existir. Lo resuelve si lo usas para que la forma correcta sea la única forma de crear un sitio.

Un módulo por sitio, un mapa para la flota

La decisión de diseño que más rinde es tratar a la flota como datos. Cada sitio es una entrada en un mapa; el módulo define cómo se construye cualquiera de ellos.

Así, agregar el sitio 24 es un cambio de cuatro líneas en un pull request, con revisión y con historial.

provider "aws" {
  region = var.region
  # Toda la flota hereda estas etiquetas: sin ellas no hay costo atribuible.
  default_tags {
    tags = { Administrado = "terraform", Flota = "sitios-web" }
  }
}

variable "sitios" {
  type = map(object({
    instance_type   = string
    disco_gb        = number
    presupuesto_usd = number
  }))
}

# Un sitio nuevo es una entrada en el mapa, no una tarde en la consola.
module "sitio" {
  source   = "./modules/sitio"
  for_each = var.sitios

  nombre        = each.key
  instance_type = each.value.instance_type
  disco_gb      = each.value.disco_gb
  etiquetas     = { Sitio = each.key }
}

Hay dos detalles que importan más de lo que parece.

El primero es for_each sobre un mapa y no count sobre una lista. Con count, borrar el sitio 3 recorre los índices y Terraform quiere recrear del 4 en adelante. Con for_each, cada sitio tiene identidad propia.

El segundo es default_tags. Las etiquetas dejan de depender de la memoria de quien escribe el recurso. Todo lo que el proveedor crea sale etiquetado.

Automatizando el ahorro de costos con Terraform

El ahorro sostenido no sale de una limpieza heroica cada trimestre. Sale de tres controles automáticos: visibilidad, alerta temprana y un freno antes de aplicar.

Un presupuesto por sitio

Como cada recurso lleva la etiqueta Sitio, se puede declarar un presupuesto por cada entrada del mismo mapa. El aviso se dispara sobre el gasto pronosticado, no sobre el gasto ya hecho.

resource "aws_budgets_budget" "sitio" {
  for_each     = var.sitios
  name         = "sitio-${each.key}"
  budget_type  = "COST"
  limit_amount = tostring(each.value.presupuesto_usd)
  limit_unit   = "USD"
  time_unit    = "MONTHLY"

  # Filtra por etiqueta: cada sitio responde por su propio gasto.
  cost_filter {
    name   = "TagKeyValue"
    values = [format("user:Sitio$%s", each.key)]
  }

  # Avisa sobre el pronóstico: te enteras antes de gastar, no después.
  notification {
    comparison_operator       = "GREATER_THAN"
    threshold                 = 80
    threshold_type            = "PERCENTAGE"
    notification_type         = "FORECASTED"
    subscriber_sns_topic_arns = [aws_sns_topic.alertas.arn]
  }
}

Un requisito que se olvida seguido: la etiqueta debe estar activada como etiqueta de asignación de costos en la consola de facturación. Sin ese paso, el filtro no encuentra nada.

Un control de costos en el pipeline

El presupuesto avisa cuando el gasto ya va en camino. El control en CI actúa antes: lee el plan en JSON y rechaza lo que rompe las reglas de la flota.

import json
import sys

# Se genera con: terraform show -json tfplan > plan.json
plan = json.load(open("plan.json"))
PERMITIDAS = {"t3.micro", "t3.small", "t3.medium"}
errores = []

for rc in plan.get("resource_changes", []):
    despues = rc["change"].get("after") or {}
    # Sin etiqueta Sitio no hay forma de atribuir el costo a nadie.
    if "tags_all" in despues and "Sitio" not in (despues["tags_all"] or {}):
        errores.append(f"{rc['address']}: falta la etiqueta Sitio")
    # Una instancia fuera de catálogo se discute en el PR, no en la factura.
    if rc["type"] == "aws_instance" and despues.get("instance_type") not in PERMITIDAS:
        errores.append(f"{rc['address']}: tipo de instancia fuera de catálogo")

if errores:
    sys.exit("\n".join(errores))   # salida distinta de 0: el PR no se fusiona

Son veinte líneas y corren en GitHub Actions después del terraform plan. El catálogo de instancias es un ejemplo; el tuyo sale de medir lo que cada sitio realmente consume.

Lo que se limpia solo

El espacio en disco fue una lección constante con la flota. En AWS, ese problema se previene con reglas de ciclo de vida declaradas junto al recurso:

  • Respaldos en S3 con caducidad. Una regla de ciclo de vida mueve los respaldos viejos a una clase más barata y los borra al cumplir su retención.
  • Discos que mueren con su instancia. El volumen raíz se marca para borrarse al terminar la instancia, y los adicionales viven en el mismo módulo.
  • Entornos de prueba con horario. Si un entorno solo se usa en horas de oficina, su apagado también se declara en código.

Seguridad con Terraform: la línea base en cada plan

Con muchos sitios, la seguridad no puede depender de una lista de verificación por servidor. Tiene que ser una propiedad del módulo y de la cuenta.

La regla es declarar una vez, lo más arriba posible. Lo que se fija a nivel de cuenta protege también al sitio que alguien cree mañana con prisa.

# Nivel de cuenta: se declara una vez y protege a toda la flota.
resource "aws_ebs_encryption_by_default" "base" {
  enabled = true
}

resource "aws_s3_account_public_access_block" "base" {
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# Dentro del módulo del sitio. IMDSv2 obligatorio corta el robo de
# credenciales por SSRF, un riesgo real en sitios con plugins de terceros.
resource "aws_instance" "web" {
  # ...
  metadata_options {
    http_endpoint = "enabled"
    http_tokens   = "required"
  }
}

Lo demás de la línea base sigue la misma lógica:

  • Sin SSH abierto a internet. El acceso administrativo pasa por Session Manager o por una red privada, y el grupo de seguridad solo expone los puertos web.
  • Roles de IAM con permisos mínimos. Cada sitio lee su propio bucket y sus propios secretos, nada más.
  • Secretos fuera del código. Las contraseñas viven en Secrets Manager o Parameter Store; Terraform solo guarda la referencia.

¿Y la seguridad «en tiempo real»? Terraform no vigila, pero sí detecta. Un plan programado compara el código contra la realidad y avisa cuando alguien cambió algo a mano.

# ⚠️ Producción: el estado es la fuente de verdad. Respáldalo antes de operar.
terraform state pull > "respaldo-$(date +%F).tfstate"

# Código de salida 2 = la realidad ya no coincide con el código (deriva).
terraform plan -detailed-exitcode -lock-timeout=60s -out=tfplan

# El plan en JSON alimenta el control de costos y queda como evidencia.
terraform show -json tfplan > plan.json

Programado cada noche en GitHub Actions, ese plan convierte un cambio manual en una alerta a la mañana siguiente, en lugar de una sorpresa en la próxima auditoría.

Mejores prácticas para la gestión de infraestructura

Estas son las prácticas que separan un repositorio de Terraform que escala de uno que da miedo tocar:

  • Estado remoto, cifrado y con bloqueo. Un bucket de S3 con versionado y bloqueo de estado. Nunca un archivo local en la laptop de alguien.
  • Estados separados por capa. La base de cuenta y red en un estado; los sitios en otros. Un error en un sitio no debe poder tocar la red de todos.
  • Plan en el pull request, aplicación solo desde CI. El plan se revisa como se revisa el código. Nadie aplica desde su máquina.
  • prevent_destroy en lo que guarda datos. Bases de datos y buckets de respaldo no se destruyen por un cambio de nombre.
  • Versiones fijas. El proveedor y los módulos llevan versión exacta. Las actualizaciones entran por pull request, no por accidente.
  • Importa antes de recrear. Lo que ya existe se trae al estado con un bloque import. Recrear un servidor en producción para «dejarlo limpio» es un riesgo innecesario.

La migración de una flota existente se hace por partes. Primero la base de cuenta, después un sitio de bajo riesgo, después el resto en grupos. Cada paso con su respaldo y su plan revisado.

Lo que NO debes hacer

  • Un solo estado para todo. Planes lentos, bloqueos entre equipos y un radio de daño que abarca a la flota completa. Es el error más común y el más caro de corregir después.
  • Mezclar consola y código. Un «arreglo rápido» a mano que nunca se lleva al repositorio. El siguiente apply lo revierte, o peor, nadie se atreve a aplicar.
  • Secretos en archivos .tfvars dentro del repositorio. El estado y el historial de Git los conservan para siempre. Usa un gestor de secretos y variables del pipeline.
  • Copiar y pegar en lugar de modularizar. Veintitrés carpetas casi iguales no son infraestructura como código. Son veintitrés oportunidades de que una quede distinta.

El siguiente paso

Optimizar AWS con Terraform es, sobre todo, un ejercicio de orden: un módulo, un mapa, etiquetas que no se pueden omitir y controles que actúan antes de que el costo o el riesgo existan.

Si tu equipo enfrenta cuellos de botella en infraestructura, migraciones complejas o necesita auditar el costo y la seguridad de su nube, en Conqr Digital Agency diseñamos arquitecturas que escalan. Revisemos tu caso.

Si quieres ver cómo trabajo la parte de nube y despliegue, revisa el servicio de Cloud y DevOps y el de servidores Linux y correo.

Preguntas frecuentes

¿Conviene un solo estado de Terraform para todos los sitios?
No. Un estado único hace que cada plan sea lento y que un error alcance a toda la flota. Separa por capas: una base de cuenta y de red, y estados por grupo de sitios o por entorno.
¿Cómo controlo costos de AWS con Terraform?
Con tres piezas: etiquetas por omisión en el proveedor, un presupuesto de AWS Budgets por sitio filtrado por etiqueta, y un control en el pipeline que lee el plan en JSON y rechaza recursos sin etiqueta o fuera del catálogo permitido.
¿Terraform detecta cambios hechos a mano en la consola?
Sí. Un plan programado con la opción -detailed-exitcode devuelve el código 2 cuando la infraestructura real ya no coincide con el código. Eso permite avisar de la deriva sin aplicar nada.
Samuel Torres Pichardo

Samuel Torres Pichardo
Ingeniero de software y cloud en Ciudad de México, con unos 3 años de experiencia profesional.
LinkedIn · GitHub · Contacto

Contacto profesional:

Ciudad de México, México

Disponible para proyectos remotos y presenciales.
Ingeniero en Tecnologías de Computación y Telecomunicaciones

© 2026 Ing. Samuel Torres. Ingeniería aplicada.