Módulo 8: Capstone The Andes Cargo Pipeline

4. Recorrido end-to-end: un cambio rechazado

Descripción

La lección 3 probó que el pipeline aprueba y aplica un cambio legítimo, de punta a punta, sin que nadie tenga que intervenir a mano en el medio. Esta lección prueba la otra mitad de la tesis, la más importante para cualquier sistema de seguridad real: que el pipeline detiene un cambio peligroso, también de punta a punta, sin depender de que una persona lo note a tiempo. Vas a reproducir el escenario completo del Módulo 6 —sembrar el state con el fixture que declara Shipments como ya existente, y proponer eliminarla— pero esta vez como la prueba final de esta guía: la confirmación de que la red de seguridad sigue funcionando bajo la misma presión narrativa que ya atravesó el resto del pipeline en la lección 3, no solo en el ejemplo aislado donde se construyó por primera vez.

Conexión con el módulo

Esta lección reutiliza, sin cambiar una sola línea, el guardrail y el fixture del Módulo 6 (lección 7) y el proyecto que los integró (lección 8 de ese módulo). No construye ningún mecanismo nuevo — su valor es exactamente confirmarlo, una tercera vez, dentro del recorrido final de esta guía. La lección 5 cambia de naturaleza por completo: de aquí en adelante, el resto del módulo es honestidad de frontera, sin más ejecución.


Analogía: la prueba de fuego del sistema de rociadores

Un sistema de rociadores contra incendios se instala, se prueba en un ambiente controlado, y se certifica — pero la prueba que de verdad importa, la que un inspector exige antes de aprobar un edificio, es la simulación completa: un fuego de prueba, en condiciones lo más realistas posibles, con el sistema completo activo, no solo el rociador aislado sobre una mesa de laboratorio. Esta lección es esa prueba de fuego. El guardrail ya pasó su prueba de mesa en el Módulo 6. Aquí se prueba dentro del edificio completo, con el resto del pipeline —ci.yml entero, no solo el step del guardrail— alrededor.


Paso 1 — El escenario: alguien intenta eliminar Shipments por error

Retoma exactamente el escenario del Módulo 6, lección 7: alguien en Andes Cargo, pensando erróneamente que la tabla Shipments se va a reemplazar por un servicio nuevo (fuera del alcance de esta guía), abre un Pull Request que elimina el recurso por completo. Antes de tocar ningún archivo, respalda el estado actual del proyecto para poder restaurarlo con precisión después:

cd andes-cargo-infra
git status

Qué esperar (literal):

On branch main
nothing to commit, working tree clean

Confirmar que partes de un estado limpio es tan importante como el propio guardrail — la lección 8 de este módulo asume que andes-cargo-infra/ queda exactamente así al cerrar esta lección.


Paso 2 — Sembrando el fixture y proponiendo la destrucción

El fixture del Módulo 6 sigue exactamente donde lo dejaste, sin ningún cambio:

ls .github/guardrail-fixtures/

Qué esperar (literal):

shipments-already-applied.state.json

Vacía dynamodb.tf por completo —la tabla y la política IAM que dependía de ella, en el mismo archivo— y quita el output que dependía de ese recurso, exactamente como en el Módulo 6, lección 7:

# dynamodb.tf — la tabla Shipments se dio de baja aquí por error. Se deja el
# archivo vacío a propósito, para que el diff sea exactamente lo que describe
# esta lección: se elimina el recurso, no el archivo completo.
terraform validate

Qué esperar (literal):

Success! The configuration is valid.

Paso 3 — Corriendo el guardrail dentro del recorrido completo

Siembra el fixture y corre el mismo workflow dedicado del Módulo 6 —guardrail-demo.yml, separado a propósito de ci.yml, para que ningún Pull Request real pueda empujar un state falso al proyecto—:

rm -f terraform.tfstate
act workflow_dispatch -j destroy-shipments-check -W .github/workflows/guardrail-demo.yml

Qué esperar (salida literal, ejecutada para escribir esta lección):

[guardrail-demo/destroy-shipments-check] ⭐ Run Set up job
[guardrail-demo/destroy-shipments-check]   ✅  Success - Set up job
[guardrail-demo/destroy-shipments-check] ⭐ Run Main Check out andes-cargo-infra
[guardrail-demo/destroy-shipments-check]   ✅  Success - Main Check out andes-cargo-infra [36.931458ms]
[guardrail-demo/destroy-shipments-check] ⭐ Run Main Set up Terraform
[guardrail-demo/destroy-shipments-check]   ✅  Success - Main Set up Terraform [2.667890417s]
[guardrail-demo/destroy-shipments-check] ⭐ Run Main Terraform init
[guardrail-demo/destroy-shipments-check]   ✅  Success - Main Terraform init [14.8967295s]
[guardrail-demo/destroy-shipments-check] ⭐ Run Main Seed the state with the fixture (Shipments already exists)
[guardrail-demo/destroy-shipments-check]   ✅  Success - Main Seed the state with the fixture (Shipments already exists) [755.256083ms]
[guardrail-demo/destroy-shipments-check] ⭐ Run Main Install tflocal
[guardrail-demo/destroy-shipments-check]   ✅  Success - Main Install tflocal [5.80159425s]
[guardrail-demo/destroy-shipments-check] ⭐ Run Main Terraform plan (offline, against the seeded fixture)
[guardrail-demo/destroy-shipments-check]   | Plan: 10 to add, 0 to change, 1 to destroy.
[guardrail-demo/destroy-shipments-check]   ✅  Success - Main Terraform plan (offline, against the seeded fixture) [4.740371625s]
[guardrail-demo/destroy-shipments-check] ⭐ Run Main Guardrail — block any plan that destroys the Shipments table
[guardrail-demo/destroy-shipments-check]   ❗  ::error::Guardrail failed: this plan destroys aws_dynamodb_table.shipments (Shipments). Blocking before apply.
[guardrail-demo/destroy-shipments-check]   | "address":"aws_dynamodb_table.shipments","mode":"managed","type":"aws_dynamodb_table","name":"shipments","provider_name":"registry.terraform.io/hashicorp/aws","change":{"actions":["delete"]
[guardrail-demo/destroy-shipments-check]   ❌  Failure - Main Guardrail — block any plan that destroys the Shipments table [2.123080458s]
[guardrail-demo/destroy-shipments-check] ⭐ Run Complete job
[guardrail-demo/destroy-shipments-check]   ✅  Success - Complete job
[guardrail-demo/destroy-shipments-check] 🏁  Job failed
Error: Job 'destroy-shipments-check' failed

Plan: 10 to add, 0 to change, 1 to destroy. — el 1 to destroy es la señal exacta que el guardrail existe para atrapar: la tabla Shipments, sembrada como ya existente por el fixture, y ausente del HCL después del Paso 2. El guardrail encontró la línea exacta —"actions":["delete"]— y detuvo el job con exit 1, antes de que existiera cualquier oportunidad de que este plan llegara a convertirse en un artefacto que apply.yml pudiera descargar.


Lo que NO pasó, y por qué es la parte más importante de esta lección

Fíjate en lo que el log de arriba no muestra, con la misma atención que ya practicaste en el Módulo 5 para leer un fallo honesto:

  • Ningún step Upload the plan for apply.yml to use later corrió. Ese step vive, en ci.yml, después del guardrail — un job que falla nunca llega a los steps posteriores, ni bajo act ni en GitHub real.
  • Ningún artefacto llamado terraform-plan se subió en esta corrida. Confírmalo directamente:
ls .artifacts/ 2>/dev/null || echo "no .artifacts directory for this run"

Qué esperar (literal, si corriste esta lección sin reutilizar una carpeta .artifacts/ de la lección 3):

no .artifacts directory for this run
  • apply.yml, si alguien lo disparara a mano ahora mismo, no tendría nada que descargar. No es que apply.yml "decida" no aplicar este cambio — es que, técnicamente, no existe ningún artefacto terraform-plan de esta corrida contra el cual apuntar. La ausencia de una acción es, aquí, tan real como el exit 1 del guardrail: dos capas independientes, la explícita (el guardrail bloqueando) y la estructural (nada que aplicar), protegiendo el mismo recurso.

Esta es, con evidencia y no solo con la lógica del diagrama de la lección 2, la prueba completa de la tesis de este módulo: la red de seguridad de Andes Cargo funciona bajo la misma presión narrativa —un Pull Request con número, una rama con nombre, un cambio que parece razonable a primera vista— que atravesó el cambio aceptado de la lección 3, no solo en el ejemplo aislado y controlado donde el Módulo 6 la construyó por primera vez.


Restaurando el proyecto

git checkout -- dynamodb.tf outputs.tf
rm -f terraform.tfstate tfplan plan.json
git status

Qué esperar (literal):

On branch main
nothing to commit, working tree clean

git checkout -- <archivo> restaura cada archivo a su versión commiteada — exactamente lo que corresponde para un cambio que nunca se aprobó, nunca se fusionó, y nunca debió tocar main. El proyecto queda, otra vez, en el mismo estado limpio del Paso 1, listo para la lección 5.


Errores comunes

Esperar que apply.yml corra y falle "de otra forma" en esta lección (de expectativa). Qué pasa: alguien, después de ver que el guardrail bloquea el cambio, intenta correr act push -W apply.yml de todas formas, esperando ver algún mensaje relacionado con la tabla Shipments. Cómo corregirlo: no hay ningún artefacto terraform-plan de esta corrida — apply.yml, corrido en este punto, fallaría en su primer step (fetch-reviewed-plan) con un error de artefacto no encontrado, sin ninguna relación con el contenido del cambio rechazado. La prueba de esta lección termina exactamente donde termina: en el guardrail, dentro de ci.yml/guardrail-demo.yml, antes de que apply.yml entre en escena.

Olvidar restaurar dynamodb.tf/outputs.tf antes de seguir a la lección 5 (de flujo, revisita el Módulo 6). Qué pasa: alguien corre esta lección completa, ve el guardrail bloquear el cambio, y avanza sin correr el paso de git checkout --. Cómo detectarlo: git status mostraría cambios sin commitear, o el proyecto tendría la tabla Shipments eliminada de forma permanente. Cómo corregirlo: siempre cierra esta lección con el proyecto en un estado limpio — la lección 8 de este módulo, el proyecto final, asume andes-cargo-infra/ exactamente como quedó al cerrar esta lección.

Pensar que esta lección prueba algo distinto del Módulo 6 (de alcance). Qué pasa: alguien espera un mecanismo nuevo, distinto del guardrail ya construido. Cómo corregirlo: esta lección, a propósito, no introduce ningún mecanismo nuevo — su valor es confirmar, por tercera vez en esta guía (Módulo 6, lección 7; Módulo 6, lección 8; y ahora), que el mismo guardrail exacto sigue funcionando, integrado al recorrido completo del capstone, no solo en el ejemplo donde se construyó.


Ejercicios

Ejercicio 1 — Compara los dos recorridos de este módulo, lado a lado. Sin mirar atrás, describe las tres diferencias más importantes entre el recorrido de la lección 3 (cambio aceptado) y el de esta lección (cambio rechazado), en términos de qué steps corrieron y cuáles no.

Ver solución

(1) En la lección 3, los doce steps de ci.yml corrieron completos, incluido Upload the plan for apply.yml to use later; en esta lección, el job se detuvo en el step del guardrail, y ningún step posterior corrió. (2) En la lección 3, apply.yml corrió de verdad (fetch-reviewed-plan con éxito, terraform-apply con un intento real que falló solo por falta de LocalStack); en esta lección, apply.yml nunca se disparó, porque no existía ningún artefacto que descargar. (3) En la lección 3, el Plan: mostró 0 to destroy; en esta lección, mostró 1 to destroy — la señal que, en ambos casos, el guardrail evaluó correctamente antes de decidir si dejar pasar el cambio o bloquearlo.

Ejercicio 2 — Explica por qué el fixture sigue siendo necesario en este módulo, y no solo en el Módulo 6. Un colega pregunta por qué esta lección no puede, simplemente, intentar eliminar Shipments del HCL sobre el state real de este proyecto, sin sembrar nada. Respóndele con la razón exacta.

Ver solución

El state de este proyecto, en esta máquina, sin un LOCALSTACK_AUTH_TOKEN válido, nunca tuvo un apply real completado — sigue completamente vacío, la misma limitación estructural confirmada desde el Módulo 3 y el Módulo 5. Sin el fixture, quitar la tabla del HCL sobre un state vacío no produce ninguna acción "delete" — produce, simplemente, "una cosa menos por crear", porque nunca hubo nada aplicado que destruir. El fixture sigue siendo, en este módulo como en el Módulo 6, la única forma honesta de generar un plan con una destrucción genuina contra esta LocalStack específica, sin necesitar infraestructura real aplicada.

Ejercicio 3 — Diseña la variante donde el guardrail SÍ debería fallar en dejar pasar el cambio. Describe un cambio de HCL, distinto al de esta lección, que el guardrail actual —tal como está escrito— no detectaría, aunque también sea peligroso para la tabla Shipments.

Ver solución

El guardrail busca, específicamente, el texto "delete" dentro de las 200 caracteres siguientes a "address":"aws_dynamodb_table.shipments" en el JSON del plan (Módulo 6, lección 7). Un cambio que el guardrail actual no detectaría: renombrar el recurso Terraform (por ejemplo, de aws_dynamodb_table.shipments a aws_dynamodb_table.shipments_v2) manteniendo el mismo name = "Shipments" en sus argumentos — Terraform trataría esto como destruir el recurso viejo y crear uno nuevo con una dirección distinta (aws_dynamodb_table.shipments_v2), y el grep, que busca la dirección exacta aws_dynamodb_table.shipments, nunca encontraría la acción "delete" asociada, porque esa acción aparecería bajo una dirección distinta a la que el guardrail conoce. Es exactamente el tipo de caso límite que la lección 6 del Módulo 6 ya nombró como la razón de fondo por la que un sistema de políticas real (conftest/Rego, evaluando la estructura de datos completa) es más robusto que un grep sobre texto.


Resumen y siguiente paso

En esta lección reprodujiste el escenario del Módulo 6 —el fixture de state sembrado, la tabla Shipments eliminada del HCL— dentro del recorrido final de esta guía. El guardrail bloqueó el cambio con Plan: 10 to add, 1 to destroy seguido de Job failed, exactamente como en su ejemplo original, confirmando que ningún artefacto se subió y que apply.yml nunca tuvo la oportunidad de correr. Restauraste el proyecto a un estado limpio, listo para las tres lecciones de honestidad de frontera que siguen.

Antes de avanzar deberías poder: explicar, sin ayuda, las tres diferencias entre el recorrido aceptado (lección 3) y el rechazado (esta lección); justificar por qué el fixture sigue siendo necesario en este módulo; y describir un tipo de cambio que el guardrail actual, tal como está escrito, no detectaría.

Con las dos mitades de la prueba técnica final completas —un cambio que pasa, uno que no—, la lección 5 cierra la honestidad de ejecución de esta guía: qué parte de todo este proceso solo se puede vivir con una cuenta de GitHub real.

Recursos

  1. Terraform Docs — Command: state push — referencia oficial del comando que siembra el fixture reutilizado en esta lección.
  2. Terraform Internals — JSON Output Format — el esquema del plan en JSON que el guardrail evalúa, con el campo resource_changes[].change.actions en el centro de esta prueba.
  3. Módulo 6 de esta guía (07-hands-on-a-failing-guardrail-example.md) — el origen exacto del guardrail y del fixture que esta lección reutiliza sin cambios.
  4. Módulo 6 de esta guía (08-project-andes-cargos-safety-net.md) — el proyecto hermano que integró por primera vez el guardrail al pipeline completo, el mismo patrón que esta lección confirma una vez más.