Episodio 1 · Marta, sola

Alguien lo cambió a mano

lectura · 13 mindominio · BCPS Bankinfraestructura · la VPC de la API

El puerto que nadie abrió

Un martes, una auditoría de seguridad encuentra el puerto 22 abierto a todo internet en el grupo de seguridad de la API de la app, la que atiende a Ana cuando mira su saldo en Cuentasver en el mapa. Nadie sabe quién lo abrió, ni cuándo, ni por qué. La consola no guarda el motivo, y el registro de llamadas dice que fue una cuenta compartida del equipo de la app, un viernes a las 19:40.

Toda la red de la serie de redes se montó así: a golpe de clic en la consola del proveedor. La VPC de la API, sus subredes, el túnel al CPD, el hub. Funciona, pero nadie puede decir cómo es sin abrir la consola y mirar, y nadie puede rehacerla igual si mañana hace falta otra.

Marta, del equipo de plataforma, decide que la red pasa a código. Empieza por lo más pequeño: la VPC de la API. Esta serie va de lo que viene después, porque escribir el código es lo fácil; lo que se diseña es el estado: dónde vive, quién lo toca y cuánto rompe un apply. Este episodio va del primero de esos tres, y de qué pasa cuando alguien toca la nube sin pasar por él.

La VPC de la API, en código

Los dos paneles tienen tres columnas: lo que dice el código, lo que dice el estado y lo que hay de verdad en la nube. En la nube ya está la VPC que se hizo a mano: la VPC, cuatro subredes y el grupo de seguridad de la API. A la izquierda, Marta la describe con un script: los mismos pasos que se dieron en la consola, uno detrás de otro. A la derecha, con código declarativo: el resultado que quiere, y Terraform calcula qué falta.

Empieza pulsando Plan, importa lo que ya existe y aplica un par de veces. Luego cambia cosas a mano, como haría cualquiera desde la consola. El marcador tiene las cuatro cifras de toda la serie: qué propone el plan (+ crear, ~ cambiar, - destruir, -/+ reemplazar), cuántos recursos tienen drift, el radio de impacto de un apply y cuántas manos pueden aplicar.

Qué ha pasado

Pasos o resultado

El script de la izquierda es imperativo: dice qué hacer. «Crea una VPC, crea cuatro subredes, crea un grupo.» Cada línea es una llamada a la API de la nube, y el script no sabe nada de lo que pasó la vez anterior. Ejecutarlo sobre la VPC que ya existe es dar los mismos pasos por segunda vez: crea otra VPC, otras cuatro subredes, y falla al crear el grupo porque ya hay uno con ese nombre. Lo que creó hasta ahí se queda, a medias.

El código de la derecha es declarativo: dice qué tiene que haber. Terraform compara eso con lo que hay y hace solo la diferencia. Aplicarlo dos veces es lo mismo que aplicarlo una: la segunda vez no hay diferencia y no hace nada. Es la misma propiedad que se pide a los consumidores de eventos en la serie de eventos: idempotencia. En infraestructura importa por lo mismo: los applies se repiten, se cortan a mitad y se relanzan.

El plan es un diff entre tres cosas

Para saber qué falta, Terraform necesita recordar qué creó. Eso es el estado: un fichero con cada recurso del código y el id que le dio la nube. Antes de cada plan, Terraform lee de la API de la nube cada recurso del estado por su id (son las llamadas leer de la simulación) y después compara el código con lo que ha leído. Lo que sale es un diff, con los símbolos de la columna de la izquierda:

resource "nube_grupo" "api" {
  vpc     = nube_vpc.api.id
  nombre  = "api"
  entrada = ["443 ← 10.64.0.0/26"]
}
Note: Objects have changed outside of Terraform
  ~ nube_grupo.api: entrada = ["443 ← 10.64.0.0/26", "22 ← 0.0.0.0/0"]

  # nube_grupo.api will be updated in-place
~ nube_grupo.api {
    ~ entrada = ["443 ← 10.64.0.0/26", "22 ← 0.0.0.0/0"] -> ["443 ← 10.64.0.0/26"]
  }
Plan: 0 to add, 1 to change, 0 to destroy.

El proveedor de los ejemplos, nube, es inventado: la serie explica conceptos, no los recursos de AWS, Azure o Google. Pero la forma del plan es la de Terraform tal cual, y es de lo más reconocible de la herramienta: leer el plan es la mitad del trabajo.

El estado es una réplica, y la nube, otra

Mira el primer plan, antes de importar: Plan: 6 to add. La VPC está ahí, pero el estado está vacío, y Terraform solo sabe de lo que está en su estado. Si hubieras aplicado, habría hecho lo mismo que el script: duplicar la VPC y las subredes y fallar en el grupo. Importar mete en el estado lo que ya existe, leyéndolo por su id, sin tocar la nube; desde Terraform 1.5 se escribe en el propio código, con un bloque import, y el plan enseña qué va a entrar antes de hacerlo.

A partir de ahí hay dos copias de la misma información: el estado, lo que Terraform cree que hay, y la nube, lo que hay. Como en cualquier sistema con réplicas, pueden divergir. Cuando alguien abre el SSH desde la consola, la nube cambia y el estado no se entera. Eso es el drift. El siguiente plan lo descubre al leer, lo avisa (Objects have changed outside of Terraform) y propone deshacerlo, porque el código, que manda, no tiene esa regla. Al script de la izquierda no le pasa nada de esto: ni lo ve ni lo deshace.

Cuándo hace daño: el drift que era la única copia

Provoca el incidente. A las 03:10, una avería en la zona a mueve el balanceador a la subred pública de la zona b, y el grupo de seguridad de la API solo deja entrar desde la de la zona a: la app deja de cargar. La guardia lo ve, abre la consola y añade la regla que falta. A las 03:25 todo funciona. Nadie toca el código: son las tres de la mañana y el incidente está resuelto.

Por la mañana, Marta aplica. El plan trae un ~ sobre el grupo, que quita la regla de la zona b, porque el código no la tiene. Si Marta esperaba «sin cambios» y no lo lee, el apply cierra la regla y el incidente vuelve a abrirse. Terraform no distingue un cambio a mano que fue un error de uno que fue una decisión: para él, lo que no está en el código sobra.

El drift no siempre es un error. A veces es la única copia de una decisión: la que tomó la guardia a las tres de la mañana y que nadie apuntó. Por eso el arreglo de un incidente no termina cuando la app vuelve, sino cuando el cambio llega al código. Y por eso hay que leer el plan, aunque se espere que esté vacío: el marcador lo cuenta en naranja precisamente para que se vea. Terraform permite ignorar un atributo (ignore_changes) para que un cambio a mano no se deshaga, pero entonces ese atributo deja de estar en el código: es drift permitido, no drift resuelto.

Corregir cuando lo ejecutas, o todo el rato

Terraform corrige el drift cuando alguien lo ejecuta: es una reconciliación puntual. Entre un apply y otro, la nube puede ser cualquier cosa. Hay otra forma: un controlador que compara lo declarado con lo real cada pocos segundos y corrige solo. Es como funcionan Kubernetes, Crossplane o las herramientas de GitOps, y tiene su propio tema en el catálogo (estado deseado y bucles de reconciliación, en el bloque 13).

Con un controlador, el SSH abierto se habría cerrado en segundos, sin esperar a nadie. Y la regla de la guardia, también: la guardia habría estado peleándose con él a las tres de la mañana, abriendo una regla que se cerraba sola, hasta dar con el código o apagar el controlador. La reconciliación continua es mejor para lo que nunca debe cambiar a mano, y peor para el día en que cambiarlo a mano es lo único que funciona. En los dos casos, la salida es la misma: que el cambio de urgencia tenga un camino rápido hacia el código.

Criterio

Un script no es un error: es la forma más directa de dar unos pasos. La pregunta es si esos pasos se van a repetir sobre algo que ya existe.

Script (imperativo)Código (declarativo)
Ejecutarlo otra vezRepite los pasos: duplica o falla, salvo que cada paso compruebe antes si ya existeSolo hace la diferencia; si no hay, nada
Antes de ejecutarNo hay forma de saber qué va a hacerUn plan con cada cambio
Un cambio a manoNo lo ve ni lo deshaceLo ve en el siguiente plan y lo deshace si se aplica
Lo que necesitaNada más que el scriptUn estado que guardar, proteger y compartir (episodio 3)
Bueno paraTareas de una vez, migraciones, pasos que no describen un resultadoLo que tiene que existir y seguir igual
  1. ¿Lo que haces describe algo que tiene que seguir existiendo?Una red, un permiso, una base de datos; no una migración de datos o un reinicio.
    código declarativo, e importar lo que ya existe
  2. ¿Se va a ejecutar más de una vez?Los scripts «de una vez» se suelen volver a ejecutar.
    script, pero con cada paso comprobando antes si ya está hecho
  3. Script. Y en cualquier caso: lo que se cambie a mano en un incidente, al código en cuanto pase el incidente.

En el mundo real

Terraform, de HashiCorp, es la herramienta más extendida, y OpenTofu es su bifurcación abierta desde que HashiCorp cambió la licencia en 2023; se usan igual y la serie vale para las dos. Cada nube tiene la suya (CloudFormation en AWS, Bicep en Azure), con el estado guardado por el propio proveedor. Y hay herramientas que cambian el lenguaje sin cambiar el modelo: Pulumi y el CDK de AWS permiten escribir la infraestructura en TypeScript, Python o Go, pero por debajo siguen calculando un diff contra un estado.

El drift se vigila de dos formas. Con un terraform plan programado cada noche que avisa si no sale vacío (Terraform Cloud y otras plataformas lo traen hecho) o con controladores que reconcilian solos: Crossplane gestiona recursos de nube desde Kubernetes, y Argo CD y Flux hacen lo mismo con lo que se despliega dentro del clúster. Para lo que ya existe, además de import, hay herramientas que recorren una cuenta y generan el código de lo que encuentran, como Terraformer o terraform plan -generate-config-out, que escribe el bloque de cada recurso importado.

Equivalencias

En la serieAWSAzureGoogle Cloud
nube_vpcaws_vpcazurerm_virtual_networkgoogle_compute_network
nube_grupoaws_security_groupazurerm_network_security_groupgoogle_compute_firewall
Registro de llamadas a la APICloudTrailActivity LogCloud Audit Logs

Siguiente episodio

La VPC ya está en código y el plan sale vacío. Marta añade lo que falta: la base de datos de la API y lo que depende de ella. Y un día la mueve a otra subred, que parece un cambio de una línea. El plan dice -/+.

Episodio 2 El orden importa Grafo de dependencias, oleadas, reemplazo y create_before_destroy.

← Todos los episodios · La red de la serie de redes · El mapa de BCPS Bank