Módulo 5: Apply On Merge The Cd Half
7. Manos a la obra: corriendo el job de drift a mano
Descripción
Esta lección corre drift.yml con act workflow_dispatch —el botón manual, no el temporizador— y completa el cuadro que la lección 6 dejó abierto: qué verías si, en vez de un proyecto que nunca terminó de aplicarse, existiera infraestructura real y alguien la hubiera modificado por fuera de Terraform. La parte que corre de verdad es el job completo, de punta a punta. La parte que simula "alguien cambió algo a mano" es representativa, con la razón exacta explicada, en el mismo espíritu que cada awslocal de esta guía sin un token válido.
Conexión con el módulo
Esta lección usa el mismo drift.yml de la lección 6, sin cambiar una sola línea — solo cambia el disparador (workflow_dispatch en vez de schedule) y el contexto narrativo (qué pasaría si hubiera infraestructura real que modificar). La lección 8 —el proyecto de este módulo— integra este workflow en el pipeline completo, junto a ci.yml y apply.yml.
Analogía: la ronda del guardia, con una ventana forzada de verdad
La lección 6 mostró al guardia haciendo su ronda sobre un edificio todavía vacío —nada que revisar, salvo confirmar que la construcción sigue en pie según los planos—. Esta lección le da al guardia algo real que encontrar: una ventana forzada, un candado cambiado, algo que no coincide con lo que el plano original describe. La diferencia entre "todo en orden" y "algo cambió" no está en cómo hace la ronda el guardia —el mismo recorrido, el mismo chequeo— sino en si hay una diferencia real que encontrar.
Paso 1 — Correr el job a mano, con act workflow_dispatch
act workflow_dispatch -j check-drift -W .github/workflows/drift.yml
Qué esperar (salida literal, ejecutada para escribir esta lección):
[drift-detection/check-drift] ⭐ Run Set up job
[drift-detection/check-drift] 🚀 Start image=catthehacker/ubuntu:act-latest
[drift-detection/check-drift] ✅ Success - Set up job
[drift-detection/check-drift] ⭐ Run Main Check out andes-cargo-infra
[drift-detection/check-drift] ✅ Success - Main Check out andes-cargo-infra [42.181834ms]
[drift-detection/check-drift] ⭐ Run Main Set up Terraform
[drift-detection/check-drift] ✅ Success - Main Set up Terraform [3.112824208s]
[drift-detection/check-drift] ⭐ Run Main Terraform init
[drift-detection/check-drift] | Initializing the backend...
[drift-detection/check-drift] | Initializing modules...
[drift-detection/check-drift] | Initializing provider plugins...
[drift-detection/check-drift] | Terraform has been successfully initialized!
[drift-detection/check-drift] ✅ Success - Main Terraform init [33.162088417s]
[drift-detection/check-drift] ⭐ Run Main Install tflocal
[drift-detection/check-drift] ✅ Success - Main Install tflocal [3.144388042s]
[drift-detection/check-drift] ⭐ Run Main Terraform plan (read-only drift check)
[drift-detection/check-drift] | Plan: 12 to add, 0 to change, 0 to destroy.
[drift-detection/check-drift] ✅ Success - Main Terraform plan (read-only drift check) [17.596187875s]
[drift-detection/check-drift] ⚙ ::set-output:: exitcode=2
[drift-detection/check-drift] ⭐ Run Main Report drift status
[drift-detection/check-drift] ✅ Success - Main Report drift status [102.771083ms]
[drift-detection/check-drift] ⚙ Summary - ## Drift check — andes-cargo-infra
Triggered by: workflow_dispatch
Result: DRIFT DETECTED. Terraform found differences between the state and reality.
[drift-detection/check-drift] 🏁 Job succeeded
Fíjate en la única diferencia real frente a la lección 6: Triggered by: workflow_dispatch, no schedule — github.event_name cambia según cómo disparaste el job, exactamente como confirmó el Módulo 2 (lección 4) con su propio par act schedule/act workflow_dispatch. El resto —el plan, el exitcode=2, el resultado— es idéntico, porque ambos disparadores llevan al mismo job, con la misma lógica interna.
Paso 2 (REPRESENTATIVO) — Modificar algo por fuera de Terraform
Con un LOCALSTACK_AUTH_TOKEN válido y la infraestructura de Andes Cargo ya aplicada (Módulo 5, lección 4, con éxito), el escenario que drift.yml está diseñado para atrapar sería algo así: alguien, con acceso directo a LocalStack —o, en una cuenta real, a la consola de AWS— cambia un atributo del bucket andes-cargo-shipment-docs sin pasar por ningún Pull Request, sin que ci.yml ni apply.yml se enteren:
awslocal s3api put-bucket-tagging \
--bucket andes-cargo-shipment-docs \
--tagging 'TagSet=[{Key=Project,Value=andes-cargo},{Key=Environment,Value=dev},{Key=ManagedBy,Value=terraform}]'
Por qué este comando está etiquetado representativo, con la razón técnica exacta: este awslocal necesita un LocalStack corriendo y con licencia activa para tener algo real que modificar —y, como ya confirmaste en el Módulo 1 (lección 8) y en cada lección de esta guía desde entonces, esta máquina no tiene un LOCALSTACK_AUTH_TOKEN válido exportado—. Fíjate en el detalle específico de este comando: quita deliberadamente el tag Compliance que el Módulo 3 (lección 8) agregó al HCL —manifest-retention-required—, dejando solo los tres tags originales. Es exactamente el tipo de cambio que un guardia debería notar: alguien "arregló" algo a mano, sin darse cuenta de que estaba deshaciendo una decisión que sí vivía en el código.
Qué esperar (representativo — el formato exacto de confirmación de put-bucket-tagging, ya visto en aws-core-services-guide para operaciones similares de S3):
{
"ResponseMetadata": {
"HTTPStatusCode": 200
}
}
Paso 3 (REPRESENTATIVO) — El próximo drift.yml detectaría la diferencia
Con ese cambio hecho por fuera de Terraform, el siguiente terraform plan de solo lectura —ya sea el del cron: de las 6 AM, ya sea uno disparado a mano con workflow_dispatch— compararía el HCL (que todavía dice Compliance = "manifest-retention-required") contra la realidad (donde ese tag ya no existe), y encontraría una diferencia real de tipo actualización, no de creación:
Qué esperar (representativo — el formato exacto de un ~ update in-place de Terraform, la forma en que se ve un cambio de atributo sobre un recurso que ya existe, distinto de un + create):
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:
# module.shipment_docs_bucket.aws_s3_bucket.this will be updated in-place
~ resource "aws_s3_bucket" "this" {
id = "andes-cargo-shipment-docs"
~ tags = {
- "Compliance" = "manifest-retention-required" -> null
"Environment" = "dev"
"ManagedBy" = "terraform"
"Project" = "andes-cargo"
}
# (5 unchanged attributes hidden)
}
Plan: 0 to add, 1 to change, 0 to destroy.
~ en vez de +, y 1 to change en vez de 12 to add — la diferencia visual exacta entre "esto es una creación completa" (lo que viste en las lecciones 6 y 7 hasta ahora, sobre un proyecto sin aplicar) y "esto es una desviación real, sobre infraestructura que sí existe" (lo que este paso representa). El step Report drift status de drift.yml, corriendo sobre este plan, capturaría exitcode=2 —la misma señal que ya viste, correcta esta vez por la razón que drift.yml fue diseñado para atrapar— y publicaría "DRIFT DETECTED" en el resumen del job, exactamente como en el Paso 1, pero ahora con un motivo genuino detrás.
La diferencia honesta entre lo que corrió y lo que se representó
Vale la pena cerrar esta lección con la distinción exacta, sin dejarla implícita:
| Parte | Estado |
|---|---|
El job check-drift completo, disparado con act workflow_dispatch | Ejecutado — salida literal, Paso 1 |
El terraform plan dentro de ese job, calculando 12 recursos por crear | Ejecutado — el mismo mecanismo del Módulo 3 (skip_requesting_account_id), sin necesitar LocalStack corriendo |
terraform_wrapper: false propagando exitcode=2 correctamente | Ejecutado — el hallazgo verificado en la lección 6 |
El awslocal put-bucket-tagging que simula un cambio manual | Representativo — necesita LocalStack con token válido y una infraestructura ya aplicada |
El plan ~ update in-place que ese cambio produciría | Representativo — mismo formato ya confirmado en aws-core-services-guide/terraform-and-iac-guide para actualizaciones de atributos, sin una ejecución en vivo en este momento |
No hay ninguna parte de esta lección "simulada en prosa" sin etiquetar — cada bloque de código dice, explícitamente, si corrió de verdad o no, en el momento exacto en que aparece.
Errores comunes
Confundir "Plan: 12 to add" (lecciones 6 y 7, Paso 1) con "Plan: 0 to add, 1 to change" (Paso 3) como si fueran el mismo tipo de resultado (conceptual). Qué pasa: alguien lee ambos bloques de código de esta lección y no distingue por qué uno dice + create y el otro ~ update in-place. Cómo detectarlo: si no puedes explicar, sin volver a leer, por qué el símbolo cambia. Cómo corregirlo: + significa que el recurso no existe todavía en el state (el caso de este proyecto en esta máquina, sin ningún apply real completado); ~ significa que el recurso sí existe, pero uno o más de sus atributos no coinciden con el HCL — la firma específica de drift real, solo posible después de un apply exitoso.
Pensar que drift.yml puede prevenir el cambio manual (de expectativa). Qué pasa: alguien espera que drift.yml, de alguna forma, bloquee o revierta automáticamente un cambio hecho por fuera de Terraform. Cómo corregirlo: drift.yml, tal como está construido en este módulo, solo detecta y reporta — no revierte nada automáticamente. Corregir el drift (aplicar de nuevo el HCL para restaurar el estado deseado, o actualizar el HCL para reflejar el cambio intencional) sigue siendo una decisión humana, tomada después de leer el reporte.
Ejecutar el awslocal put-bucket-tagging de esta lección esperando que funcione sin más (de flujo). Qué pasa: alguien copia el comando del Paso 2 y lo corre, esperando ver el JSON de confirmación, sin haber seguido el Módulo 1 (lección 8, Paso 5) para arrancar LocalStack con un token válido. Cómo corregirlo: revisa "La versión que verías con un token válido" en la lección 4 de este módulo — el mismo requisito aplica aquí. Sin LocalStack corriendo, este comando fallaría con el mismo tipo de error de conexión que ya conoces, no con el JSON representativo de esta lección.
Ejercicios
Ejercicio 1 — Distingue los símbolos de un plan. Sin mirar esta lección, explica la diferencia entre +, ~ y - en la salida de un terraform plan, y qué tipo de cambio de infraestructura representa cada uno.
Ver solución
+ (create) significa que el recurso no existe todavía y Terraform lo crearía desde cero. ~ (update in-place) significa que el recurso ya existe, pero uno o más atributos necesitan cambiar para coincidir con el HCL — el caso de drift real de esta lección. - (destroy) significa que el recurso existe pero ya no está declarado en el HCL, y Terraform lo eliminaría — el caso que el guardrail del Módulo 6 va a vigilar específicamente para la tabla Shipments.
Ejercicio 2 — Explica por qué el Paso 2 de esta lección es representativo, sin decir "no hay LocalStack". Sin mirar esta lección, da la razón técnica exacta —no solo "LocalStack no está corriendo"— por la que el awslocal put-bucket-tagging de esta lección no se ejecutó de verdad.
Ver solución
Una respuesta completa suena, más o menos, así: "El comando necesita dos cosas que esta máquina no tiene en este momento: un LocalStack corriendo con un LOCALSTACK_AUTH_TOKEN válido, y —más importante todavía— un bucket andes-cargo-shipment-docs que ya exista de verdad dentro de ese LocalStack, producto de un terraform apply exitoso previo (Módulo 5, lección 4). Ninguna de esas dos condiciones se cumplió en esta guía hasta ahora, así que no hay ningún bucket real sobre el cual aplicar el cambio de tags."
Ejercicio 3 — Predice el resultado si el drift fuera sobre la tabla Shipments. Adelantándote al Módulo 6: si el cambio representativo del Paso 2 hubiera sido, en cambio, borrar por completo la tabla Shipments con awslocal dynamodb delete-table, ¿qué símbolo esperarías ver en el próximo drift.yml, y qué relación tiene esto con el guardrail que vas a construir más adelante?
Ver solución
Verías el símbolo + (create) para aws_dynamodb_table.shipments — no porque Terraform "detecte una eliminación", sino porque, desde la perspectiva del state, la tabla debería existir y ya no existe, así que el próximo plan propondría recrearla desde cero. Esto conecta directamente con el Módulo 6: el guardrail que vas a construir ahí revisa específicamente los plan que proponen destruir Shipments antes de que un apply real lo haga — drift.yml, en cambio, solo detectaría el hecho consumado después de que alguien ya la borró por fuera del pipeline, demasiado tarde para prevenirlo, solo a tiempo para notarlo.
Resumen y siguiente paso
En esta lección corriste drift.yml de punta a punta con act workflow_dispatch, con salida literal idéntica en estructura a la de la lección 6, salvo el event_name. Completaste el cuadro con dos pasos representativos, claramente etiquetados: un cambio manual vía awslocal put-bucket-tagging que quita el tag Compliance, y el plan ~ update in-place que ese cambio produciría en la próxima corrida de drift.yml —la diferencia visual exacta entre un proyecto sin aplicar (+ create, lo único que pudiste ejecutar de verdad en esta máquina) y drift real sobre infraestructura existente (~ update in-place, representativo).
Antes de avanzar deberías poder: correr drift.yml a mano con act workflow_dispatch; distinguir +, ~ y - en cualquier salida de terraform plan; y explicar, con la razón técnica exacta, por qué el escenario de drift real de esta lección no pudo ejecutarse en esta máquina.
Con ci.yml, apply.yml y drift.yml construidos y probados por separado, la lección 8 —el proyecto de este módulo— los corre en secuencia, sobre el mismo cambio real de Andes Cargo que atraviesa este módulo desde su inicio.
Recursos
- Terraform Docs — Resource actions and plan output — referencia oficial de los símbolos
+/~/-en la salida de un plan. - nektosact.com — User Guide — referencia de
act workflow_dispatch, usada de punta a punta en esta lección. - AWS CLI —
s3api put-bucket-tagging— referencia oficial del comando representativo del Paso 2. - Módulo 3 de esta guía (
06-hands-on-running-terraform-plan-in-ci.md) — el mecanismo deskip_requesting_account_idque permite que elterraform plande esta lección corra sin LocalStack, reutilizado aquí sin cambios.