Módulo 6: Rollback And Safety Nets
8. Proyecto: la red de seguridad de Andes Cargo
Descripción
Este es el proyecto que cierra el Módulo 6. Las lecciones 2 a 7 construyeron cada pieza por separado —el mecanismo de rollback, branch protection, el análisis del incidente, y el guardrail real—. Este proyecto corre el pipeline completo de Andes Cargo, con el guardrail de la lección 7 ya integrado de forma permanente en ci.yml, y lo prueba dos veces, sobre dos escenarios que ya conoces: un cambio inocuo, que pasa de punta a punta como cualquier Pull Request normal de esta guía; y un intento de destruir la tabla Shipments, que el pipeline detiene antes de que exista cualquier oportunidad de aplicar.
Conexión con el módulo
Este proyecto no introduce ningún mecanismo nuevo — es la confirmación final de que el guardrail de la lección 7 no es un experimento aislado: vive, de forma permanente, dentro del mismo ci.yml que corre en cada Pull Request de Andes Cargo desde el Módulo 3. Con esto, el Módulo 6 completo queda cerrado, y el Módulo 7 puede ubicar todo lo construido dentro del panorama más amplio de CI/CD, sin construir nada nuevo sobre andes-cargo-infra/.
El pipeline completo, de un vistazo
CASO 1 — CAMBIO INOCUO CASO 2 — INTENTO DE DESTRUCCIÓN
Pull Request #42 "PR" que elimina Shipments
(feature/add-shipment-tags → main) (fixture de state sembrado,
│ HCL sin la tabla)
▼ │
ci.yml (pull_request) ▼
fmt → init → validate → plan guardrail-demo.yml
Plan: 12 to add, 0 destroy (workflow_dispatch)
│ terraform state push (fixture)
▼ → plan -refresh=false
Guardrail — block any plan Plan: 10 to add, 1 to destroy
that destroys Shipments │
"Guardrail passed" ▼
│ Guardrail — block any plan
▼ that destroys Shipments
Publish + upload artifact "Guardrail failed"
│ exit 1
▼ │
🏁 Job succeeded ▼
🏁 Job failed
(apply.yml NUNCA se dispara:
no hay artefacto que descargar)
El detalle que hace a este diagrama la prueba final de la tesis de este módulo: en el Caso 2, el job falla antes del step Upload the plan for apply.yml to use later — el mismo mecanismo que ya viste en el Módulo 5 con needs: (un Stage que falla detiene el siguiente), ahora aplicado dentro de un mismo job: ningún step posterior al guardrail corre si el guardrail falla. apply.yml nunca tiene un artefacto que descargar, así que nunca podría aplicar nada, incluso si alguien lo disparara manualmente.
Paso 1 — Confirmar el punto de partida
cd andes-cargo-infra
git log --oneline -5
Qué esperar (representativo en los hashes, literal en la estructura — asumiendo que ya completaste las lecciones 3, 4 y 7 de este módulo):
<hash> ci.yml: add a guardrail step that blocks any plan destroying the Shipments table; add guardrail-demo.yml to test it against a seeded fixture
<hash> Add pr-event-revert.json to simulate the revert PR with act
<hash> Revert "Add Compliance tag to the shipment-docs bucket"
<hash> drift.yml: schedule-based drift detection with terraform_wrapper: false
<hash> apply.yml: needs, artifact download, and the real terraform apply attempt
git status
Qué esperar (literal):
On branch main
nothing to commit, working tree clean
Caso 1 — El cambio inocuo: ci.yml corre entero, guardrail incluido
Corre ci.yml, con el guardrail ya integrado, exactamente sobre el mismo PR #42 que atraviesa esta guía desde el Módulo 2:
export ARTIFACT_ADDR=$(ipconfig getifaddr en0) # Linux: hostname -I | awk '{print $1}'
rm -rf .artifacts && mkdir -p .artifacts
act pull_request -e .github/act-events/pr-event.json -j terraform-checks \
--artifact-server-path ./.artifacts \
--artifact-server-addr "$ARTIFACT_ADDR"
Qué esperar (salida literal, ejecutada para escribir esta lección — el job completo, once steps, todos en verde):
[ci/terraform-checks] ⭐ Run Set up job
[ci/terraform-checks] ✅ Success - Set up job
[ci/terraform-checks] ⭐ Run Main Check out andes-cargo-infra
[ci/terraform-checks] ✅ Success - Main Check out andes-cargo-infra [41.6135ms]
[ci/terraform-checks] ⭐ Run Main Set up Terraform
[ci/terraform-checks] ✅ Success - Main Set up Terraform [2.248916334s]
[ci/terraform-checks] ⭐ Run Main Terraform format check
[ci/terraform-checks] ✅ Success - Main Terraform format check [155.826625ms]
[ci/terraform-checks] ⭐ Run Main Terraform init
[ci/terraform-checks] ✅ Success - Main Terraform init [15.009926792s]
[ci/terraform-checks] ⭐ Run Main Terraform validate
[ci/terraform-checks] ✅ Success - Main Terraform validate [1.903371334s]
[ci/terraform-checks] ⭐ Run Main Install awslocal
[ci/terraform-checks] ✅ Success - Main Install awslocal [11.256066667s]
[ci/terraform-checks] ⭐ Run Main Confirm the runner can reach LocalStack on the host
[ci/terraform-checks] | Could not connect to the endpoint URL: "http://host.docker.internal:4566/"
[ci/terraform-checks] Failed but continue next step
[ci/terraform-checks] ❌ Failure - Main Confirm the runner can reach LocalStack on the host [9.37373375s]
[ci/terraform-checks] ⭐ Run Main Install tflocal
[ci/terraform-checks] ✅ Success - Main Install tflocal [2.799894084s]
[ci/terraform-checks] ⭐ Run Main Terraform plan
[ci/terraform-checks] | Plan: 12 to add, 0 to change, 0 to destroy.
[ci/terraform-checks] ✅ Success - Main Terraform plan [5.791599917s]
[ci/terraform-checks] ⭐ Run Main Guardrail — block any plan that destroys the Shipments table
[ci/terraform-checks] | Guardrail passed: no destroy action found for aws_dynamodb_table.shipments.
[ci/terraform-checks] ✅ Success - Main Guardrail — block any plan that destroys the Shipments table [2.79587475s]
[ci/terraform-checks] ⭐ Run Main Publish the plan to the job summary
[ci/terraform-checks] ✅ Success - Main Publish the plan to the job summary [100.399167ms]
[ci/terraform-checks] ⭐ Run Main Upload the plan for apply.yml to use later
[ci/terraform-checks] ✅ Success - Main Upload the plan for apply.yml to use later [1.086368541s]
[ci/terraform-checks] ⭐ Run Complete job
[ci/terraform-checks] ✅ Success - Complete job
[ci/terraform-checks] 🏁 Job succeeded
Once steps, el mismo ci.yml de siempre, con un step más que no cambió nada del resultado: Plan: 12 to add, exactamente como en cada corrida anterior de esta guía sobre este mismo cambio. El guardrail no interfiere con ningún cambio legítimo — es exactamente el comportamiento que esperarías de una red de seguridad bien diseñada: invisible cuando no hace falta, decisiva cuando sí.
Caso 2 — El intento de destrucción: el pipeline se detiene antes de aplicar
Reproduce el escenario completo de la lección 7: siembra el fixture, aplica el cambio destructivo, y corre el workflow de prueba.
cp .github/guardrail-fixtures/shipments-already-applied.state.json /tmp/seed.json
Vacía dynamodb.tf y quita el output dependiente de outputs.tf, exactamente como en la lección 7:
# dynamodb.tf — la tabla Shipments se dio de baja aquí por error
terraform validate
Qué esperar (literal):
Success! The configuration is valid.
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 [69.198542ms]
[guardrail-demo/destroy-shipments-check] ⭐ Run Main Set up Terraform
[guardrail-demo/destroy-shipments-check] ✅ Success - Main Set up Terraform [2.42054s]
[guardrail-demo/destroy-shipments-check] ⭐ Run Main Terraform init
[guardrail-demo/destroy-shipments-check] ✅ Success - Main Terraform init [15.786407458s]
[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) [776.935083ms]
[guardrail-demo/destroy-shipments-check] ⭐ Run Main Install tflocal
[guardrail-demo/destroy-shipments-check] ✅ Success - Main Install tflocal [6.340297833s]
[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) [6.640812125s]
[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] ❌ Failure - Main Guardrail — block any plan that destroys the Shipments table [2.691965959s]
[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. seguido de 🏁 Job failed — la misma secuencia exacta de la lección 7, corrida de nuevo aquí como la confirmación final del proyecto: la red de seguridad de Andes Cargo funciona bajo presión, no solo en el ejemplo aislado en el que se construyó. Fíjate en lo que no pasó, y que es tan importante como lo que sí pasó: no hubo ningún step de Upload the plan for apply.yml to use later — este plan bloqueado nunca se convirtió en un artefacto descargable, así que ni siquiera existe la posibilidad técnica de que un apply.yml disparado por error lo aplique.
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
Cierre del Módulo 6
Completaste el módulo que enseña qué hacer cuando algo sale mal. Repasa lo que te llevas:
- El rollback de infraestructura, ejecutado de punta a punta:
git revert --no-editsobre un commit real, con el historial completo preservado (el commit original y su revert, ambos visibles para siempre),ci.ymlcalculando elplandel revert, yapply.ymlintentando aplicarlo con el mismo SHA256 coincidiendo entre jobs que ya viste en el Módulo 5 (lecciones 2-3). - Branch protection, con el camino de clics exacto: la configuración de repositorio —no un workflow— que convierte "cualquiera puede tocar
main" en "nada llega sin pasar porci.yml", con la honestidad completa de por quéactno puede ejecutarla (lección 4). - El incidente de Claude Code, revisitado con profundidad: trazado a través de las cinco capas de este módulo, con la respuesta honesta en dos partes —ningún control de proceso cambia una mala decisión humana ya informada, pero un guardrail automatizado hace algunas decisiones técnicamente imposibles de ejecutar, sin depender del juicio de nadie (lección 5).
conftesty policy-as-code, nombrados con precisión: la diferencia exacta entre ungrepartesanal y una política declarativa evaluada contra una estructura de datos, con la frontera clara haciacloud-security-and-guardrails-guide(lección 6).- Un guardrail real, construido y probado dos veces: un
grepsobreterraform show -json, integrado de forma permanente enci.yml, que pasa sin interferir en un cambio normal y bloquea de verdad —con salida literal, job en rojo— un intento de destruirShipments, incluida la técnica honesta delfixturedestatepara poder probarlo sin unapplyreal completado (lecciones 7 y este proyecto).
Qué viene después
El Módulo 7 ubica todo lo que construiste en esta guía dentro de un panorama más amplio de CI/CD: herramientas alternativas (GitLab CI, CircleCI, Jenkins), el GitOps pull-based de Kubernetes (ArgoCD, Flux), las estrategias de despliegue de aplicación, y la frontera exacta entre CI/CD de infraestructura —lo que construiste— y CI/CD de código de aplicación. No agrega ninguna pieza nueva a andes-cargo-infra/ salvo un pipeline mínimo de contraste. El Módulo 8, el capstone, corre el pipeline completo —ci.yml, apply.yml, drift.yml, y el guardrail de este módulo— de punta a punta, con un cambio real que sí se aprueba y uno que el guardrail rechaza, exactamente como este proyecto, pero como parte del recorrido final completo de la guía.
Errores comunes
Correr el Caso 2 sin restaurar dynamodb.tf/outputs.tf antes de seguir al Módulo 7 (de flujo, revisita la lección 7). Qué pasa: alguien corre este proyecto completo, ve ambos casos funcionar, 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 este proyecto —y la lección 7— con el proyecto en un estado limpio, exactamente como muestra el paso de restauración de esta lección.
Pensar que este proyecto prueba algo distinto de la lección 7 (de alcance). Qué pasa: alguien espera un mecanismo nuevo en este proyecto, distinto del guardrail ya construido. Cómo corregirlo: este proyecto, a propósito, no introduce ningún mecanismo nuevo — su valor es exactamente confirmar que el guardrail funciona integrado al pipeline real, no solo en el ejemplo aislado donde se construyó por primera vez. Es la misma disciplina que ya viste en el Módulo 5 (lección 8): cada pieza construida por separado, corrida junta al final.
Ejercicios
Ejercicio 1 — Explica, a un colega que solo vio el Caso 1, por qué el guardrail "no hizo nada" en esa corrida. ¿Es correcto decir que el guardrail no funcionó en el Caso 1?
Ver solución
No es correcto decir que "no funcionó" — el guardrail corrió exactamente igual en ambos casos, evaluando el plan real contra la misma condición. En el Caso 1, la evaluación correcta —y correctamente calculada— es que no hay ninguna acción delete sobre Shipments, así que el guardrail pasa y no interfiere. Un guardrail que nunca deja pasar nada no sería útil; uno que evalúa correctamente y distingue "esto es seguro" de "esto es peligroso" es exactamente lo que este proyecto demuestra en sus dos casos.
Ejercicio 2 — Reconstruye la razón exacta por la que apply.yml nunca corrió en el Caso 2. Sin mirar esta lección, explica la cadena completa de eventos.
Ver solución
Una respuesta completa suena, más o menos, así: "El guardrail falló dentro del job terraform-checks de ci.yml (o, en este proyecto, dentro de guardrail-demo.yml, que reproduce la misma lógica), con exit 1, antes de llegar al step Upload the plan for apply.yml to use later. Sin ese step, nunca se sube ningún artefacto llamado terraform-plan. apply.yml depende, desde el Módulo 5, de descargar exactamente ese artefacto para poder aplicar algo — sin él, ni siquiera el intento de apply podría empezar, sin importar si alguien disparara apply.yml manualmente después."
Ejercicio 3 — Diseña un tercer caso de prueba, más allá de los dos de este proyecto. ¿Qué otro escenario, distinto de "cambio inocuo" y "destruye Shipments", valdría la pena probar contra este guardrail? Descríbelo y predice el resultado.
Ver solución
Un caso interesante: un plan que reemplaza la tabla Shipments —por ejemplo, cambiando hash_key a un valor distinto, lo que fuerza a Terraform a destruirla y volver a crearla en el mismo plan ("actions":["delete","create"])—. El guardrail, tal como está escrito, sí debería bloquear este caso también: el grep busca la palabra "delete" dentro del arreglo de acciones, sin importar si aparece sola o junto a "create" — un reemplazo completo de la tabla es, después de todo, exactamente el tipo de pérdida de datos que este guardrail existe para prevenir, incluso si técnicamente "recrea" la tabla vacía después.
Resumen y siguiente paso
En este proyecto corriste el pipeline completo de Andes Cargo con el guardrail de la lección 7 integrado de forma permanente en ci.yml, confirmado en dos escenarios reales: un cambio inocuo que atravesó los once steps sin ninguna interferencia (Plan: 12 to add, guardrail pasando, artefacto subido), y un intento de destruir Shipments que el pipeline detuvo antes de cualquier oportunidad de aplicar (Plan: 10 to add, 1 to destroy, guardrail fallando, job en rojo, ningún artefacto subido).
Antes de avanzar deberías poder: describir de memoria el recorrido completo de ambos casos de este proyecto; explicar por qué apply.yml nunca tuvo la oportunidad de correr en el Caso 2; y confirmar, con tus propias palabras, que el guardrail no interfiere con ningún cambio legítimo de Andes Cargo.
Con esto, el Módulo 6 queda cerrado. Tienes rollback de infraestructura probado de punta a punta, branch protection entendida con su camino de clics exacto, el incidente de Claude Code revisitado con la profundidad completa que el Módulo 1 dejó pendiente, y un guardrail real, integrado y probado, que protege el recurso más crítico de Andes Cargo.
Siguiente módulo: el panorama completo de CI/CD más allá de esta guía — GitLab CI, CircleCI, Jenkins por contraste, el GitOps pull-based de Kubernetes, las estrategias de despliegue de aplicación, y la frontera exacta entre lo que construiste (CI/CD de infraestructura) y lo que queda para las guías hermanas (CI/CD de aplicación).
Recursos
- nektosact.com — User Guide — referencia completa de
act pull_requestyact workflow_dispatch, usadas de punta a punta en este proyecto. - Terraform Internals — JSON Output Format — el esquema del
planen JSON que el guardrail evalúa en ambos casos. - Módulo 6 de esta guía (
07-hands-on-a-failing-guardrail-example.md) — el origen del guardrail y delfixtureque este proyecto reutiliza sin cambios. - Módulo 5 de esta guía (
08-project-andes-cargos-full-plan-to-apply-pipeline.md) — el proyecto hermano que corrióci.yml/apply.yml/drift.ymljuntos por primera vez, el mismo patrón que este proyecto extiende con el guardrail.