Módulo 5: Apply On Merge The Cd Half
8. Proyecto: el pipeline completo de plan a apply
Descripción
Este es el proyecto que cierra el Módulo 5. Las lecciones 2 a 7 construyeron cada pieza por separado —el disparador de apply.yml, el encadenamiento de sus jobs, el apply real, la protección de concurrencia, y drift.yml completo—. Este proyecto las corre todas juntas, en secuencia, sobre el mismo cambio real que el Módulo 3 introdujo: el tag Compliance del bucket andes-cargo-shipment-docs. Vas a seguir ese cambio desde el Pull Request hasta la detección de drift posterior, con tres workflows distintos, tres invocaciones separadas de act, y la misma honestidad de siempre sobre qué corrió de verdad y qué queda representativo sin un token de LocalStack.
Conexión con el módulo
Este proyecto no introduce ningún YAML nuevo — es la integración de ci.yml (extendido en la lección 3 con upload-artifact), apply.yml (lecciones 2 a 5), y drift.yml (lecciones 6 y 7), corridos en el orden exacto en que correrían en un repositorio real: Pull Request abierto → fusión → vigilancia posterior. Con esto, el Módulo 5 completa la mitad de CD del pipeline; el Módulo 6 construye las redes de seguridad que faltan.
El hilo completo, de un vistazo
① PULL REQUEST #42 (feature/add-shipment-tags → main)
│
▼
② ci.yml (pull_request) EJECUTADO — Módulo 3 + lección 3 de este módulo
fmt → init → validate → plan (-out=tfplan) → resumen → upload-artifact
│
│ Plan: 12 to add, 0 to change, 0 to destroy
│ artefacto "terraform-plan" subido, SHA256 registrado
▼
③ (una persona revisa el plan, aprueba, fusiona el PR)
│
▼
④ apply.yml (push a main) EJECUTADO — lecciones 2, 3, 4, 5 de este módulo
fetch-reviewed-plan (descarga el MISMO artefacto, SHA256 verificado)
│
▼
terraform-apply (needs: fetch-reviewed-plan)
checkout → setup-terraform → download → init → apply
│
│ intento real contra host.docker.internal:4566
│ REPRESENTATIVO sin token: connection refused, 9 reintentos
▼
⑤ drift.yml (workflow_dispatch, o schedule diario) EJECUTADO — lecciones 6 y 7
checkout → setup-terraform (terraform_wrapper: false) → init → plan -detailed-exitcode
│
│ sin apply real completado: Plan: 12 to add → exitcode=2 → "DRIFT DETECTED"
│ REPRESENTATIVO con apply exitoso: Plan: 0 to add → exitcode=0 → "no drift"
▼
Andes Cargo, con el pipeline completo probado de punta a punta
Paso 1 — Confirmar el punto de partida
Sigues exactamente donde el Módulo 3 (lección 8) te dejó: s3.tf con el tag Compliance ya agregado, ci.yml con nueve steps, y el historial de commits del Módulo 3 completo. Esta lección agrega la extensión de la lección 3 de este módulo (-out=tfplan + upload-artifact) si todavía no la commiteaste:
cd andes-cargo-infra
git log --oneline -3
Qué esperar (representativo en los hashes, literal en los mensajes — asumiendo que ya completaste las lecciones 3, 4 y 6 de este módulo):
<hash> drift.yml: schedule-based drift detection with terraform_wrapper: false
<hash> apply.yml: needs, artifact download, and concurrency control
<hash> ci.yml: save the plan as tfplan and upload it as an artifact
export ARTIFACT_ADDR=$(ipconfig getifaddr en0) # Linux: hostname -I | awk '{print $1}'
rm -rf .artifacts && mkdir -p .artifacts
Empezar con .artifacts/ vacía asegura que el artefacto que vas a ver subir en el Paso 2 es, de verdad, el de esta corrida — no uno viejo de una sesión anterior.
Paso 2 — ci.yml: el Pull Request #42, revisado
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 — resumen de los diez steps, en orden):
[ci/terraform-checks] ⭐ Run Main Check out andes-cargo-infra
[ci/terraform-checks] ✅ Success - Main Check out andes-cargo-infra [42.181834ms]
[ci/terraform-checks] ⭐ Run Main Set up Terraform
[ci/terraform-checks] ✅ Success - Main Set up Terraform [3.112824208s]
[ci/terraform-checks] ⭐ Run Main Terraform format check
[ci/terraform-checks] ✅ Success - Main Terraform format check [823.899875ms]
[ci/terraform-checks] ⭐ Run Main Terraform init
[ci/terraform-checks] | Terraform has been successfully initialized!
[ci/terraform-checks] ✅ Success - Main Terraform init [33.162088417s]
[ci/terraform-checks] ⭐ Run Main Terraform validate
[ci/terraform-checks] | Success! The configuration is valid.
[ci/terraform-checks] ✅ Success - Main Terraform validate [12.453844292s]
[ci/terraform-checks] ⭐ Run Main Install awslocal
[ci/terraform-checks] ✅ Success - Main Install awslocal [13.181793916s]
[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.505391208s]
[ci/terraform-checks] ⭐ Run Main Install tflocal
[ci/terraform-checks] ✅ Success - Main Install tflocal [3.144388042s]
[ci/terraform-checks] ⭐ Run Main Terraform plan
[ci/terraform-checks] | Plan: 12 to add, 0 to change, 0 to destroy.
[ci/terraform-checks] | Saved the plan to: tfplan
[ci/terraform-checks] ✅ Success - Main Terraform plan [18.483023375s]
[ci/terraform-checks] ⭐ Run Main Publish the plan to the job summary
[ci/terraform-checks] ✅ Success - Main Publish the plan to the job summary [112.681333ms]
[ci/terraform-checks] ⚙ Summary - ## Terraform plan — andes-cargo-infra
[ci/terraform-checks] ⭐ Run Main Upload the plan for apply.yml to use later
[ci/terraform-checks] | Artifact terraform-plan has been successfully uploaded! Final size is 12484 bytes.
[ci/terraform-checks] | Artifact download URL: https://github.com/nektos/act/actions/runs/1/artifacts/2119430229
[ci/terraform-checks] ✅ Success - Main Upload the plan for apply.yml to use later [1.280550167s]
[ci/terraform-checks] 🏁 Job succeeded
Confirma el tag que motivó todo este hilo, exactamente como en el Módulo 3:
grep -A1 "Compliance" plan-output.txt
Qué esperar (literal):
+ "Compliance" = "manifest-retention-required"
+ "Environment" = "dev"
Este es el momento exacto donde, en un repositorio real, una persona entraría a revisar el resumen del job, vería el tag nuevo en el bucket correcto, sin nada destruido, y aprobaría la fusión del PR #42. Nada de lo que sigue puede pasar sin este paso — es la mitad de CI que le da sentido a la mitad de CD que sigue.
Paso 3 — La fusión (simulada) y apply.yml
En un repositorio real, fusionar el PR #42 dispara un push a main. Simúlalo corriendo apply.yml directamente —el mismo comando de la lección 4—:
act push -W .github/workflows/apply.yml \
--artifact-server-path ./.artifacts \
--artifact-server-addr "$ARTIFACT_ADDR"
Qué esperar (salida literal, ejecutada para escribir esta lección):
[apply/fetch-reviewed-plan] ⭐ Run Main Download the plan reviewed in the pull request
[apply/fetch-reviewed-plan] | SHA256 digest of downloaded artifact is 17f7ebe9b774bb3520631bf3e252311300139416f0082e95103304af6e033900
[apply/fetch-reviewed-plan] ✅ Success - Main Download the plan reviewed in the pull request [972.244958ms]
[apply/fetch-reviewed-plan] ⭐ Run Main Confirm the plan file arrived intact
[apply/fetch-reviewed-plan] | tfplan is present: 15826 bytes
[apply/fetch-reviewed-plan] ✅ Success - Main Confirm the plan file arrived intact [119.824ms]
[apply/fetch-reviewed-plan] 🏁 Job succeeded
[apply/terraform-apply ] ⭐ Run Main Download the plan reviewed in the pull request
[apply/terraform-apply ] | SHA256 digest of downloaded artifact is 17f7ebe9b774bb3520631bf3e252311300139416f0082e95103304af6e033900
[apply/terraform-apply ] ✅ Success - Main Download the plan reviewed in the pull request [970.303916ms]
[apply/terraform-apply ] ⭐ Run Main Terraform init
[apply/terraform-apply ] | Terraform has been successfully initialized!
[apply/terraform-apply ] ✅ Success - Main Terraform init [35.464288875s]
[apply/terraform-apply ] ⭐ Run Main Install tflocal
[apply/terraform-apply ] ✅ Success - Main Install tflocal [8.011445042s]
[apply/terraform-apply ] ⭐ Run Main Terraform apply
[apply/terraform-apply ] | module.shipment_docs_bucket.aws_s3_bucket.this: Creating...
[apply/terraform-apply ] | aws_dynamodb_table.shipments: Creating...
[apply/terraform-apply ] | module.app_server_role.aws_iam_role.this: Creating...
[apply/terraform-apply ] | module.lambda_manifest_processor_role.aws_iam_role.this: Creating...
[apply/terraform-apply ] | module.shipment_docs_bucket.aws_s3_bucket.this: Still creating... [00m40s elapsed]
[apply/terraform-apply ] | ╷
[apply/terraform-apply ] | │ Error: creating S3 Bucket (andes-cargo-shipment-docs): operation error S3: CreateBucket, exceeded maximum number of attempts, 9, ... connect: connection refused
[apply/terraform-apply ] | ╵
[apply/terraform-apply ] ❗ ::error::Terraform exited with code 1.
[apply/terraform-apply ] ❌ Failure - Main Terraform apply [1m3.705523875s]
[apply/terraform-apply ] 🏁 Job failed
Error: Job 'terraform-apply' failed
El SHA256 vuelve a coincidir, en las tres apariciones —el subido en ci.yml, el descargado en fetch-reviewed-plan, el descargado en terraform-apply—: 17f7ebe9b774... en las tres. Es la prueba, repetida por tercera vez en este módulo, de que el archivo que casi se aplicó es exactamente el que se revisó en el PR #42 — ninguna reimpresión, ningún recálculo. El fallo final es el mismo de siempre: intento real, reintentos reales del proveedor de AWS, connection refused porque LocalStack no está corriendo en esta máquina.
Paso 4 — drift.yml, inmediatamente después
Aunque el apply de este momento específico no llegó a completarse de verdad, el pipeline de Andes Cargo incluye drift.yml corriendo después de cualquier cambio a la infraestructura —programado a diario, o disparado a mano tras un despliegue importante—. Confírmalo, corriendo el mismo job de las lecciones 6 y 7:
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 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] ⚙ 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
Lee esto con la misma precisión que la lección 7 ya enseñó. drift.yml reporta "DRIFT DETECTED" — mecánicamente correcto, porque el state sigue sin tener ningún recurso registrado (el apply del Paso 3 nunca llegó a completarse). No es la señal de que alguien tocó algo a mano por fuera del pipeline —el escenario que drift.yml está diseñado para atrapar en producción real—; es la consecuencia honesta de correr el pipeline completo en una máquina sin LOCALSTACK_AUTH_TOKEN. El mecanismo —-detailed-exitcode, terraform_wrapper: false, el reporte en $GITHUB_STEP_SUMMARY— es exactamente el mismo que correría en producción; lo único que cambia es la causa detrás del resultado.
El recorrido completo, con un token válido (representativo)
Para que el hilo quede completo, esto es lo que verías si cada paso de este proyecto corriera contra un LocalStack real, con LOCALSTACK_AUTH_TOKEN exportado desde el Módulo 1 (lección 8, Paso 5):
Qué esperar (representativo — la secuencia completa, sin una ejecución en vivo contra un token válido en este momento):
① ci.yml (pull_request)
Plan: 12 to add, 0 to change, 0 to destroy.
Artifact terraform-plan uploaded successfully.
② apply.yml (push, tras fusión)
Apply complete! Resources: 12 added, 0 changed, 0 destroyed.
Outputs: shipments_table_name = "Shipments", ...
③ drift.yml (workflow_dispatch, corrido justo después del apply)
Plan: 0 to add, 0 to change, 0 to destroy.
exitcode=0
Result: no drift detected. Infrastructure matches the state.
La diferencia entre este bloque y lo que corriste de verdad en los Pasos 2 a 4 no está en el mecanismo de ningún workflow —los tres archivos son idénticos en ambos casos— sino, exclusivamente, en si hay un LocalStack real del otro lado de host.docker.internal:4566. El ③ de este bloque es la confirmación final de que el ciclo completo funciona: un apply exitoso deja el state reflejando la realidad, y el próximo drift.yml lo confirma con exitcode=0, no 2.
Commiteando el cierre del módulo
git add -A
git commit -m "Wire ci.yml, apply.yml, and drift.yml into a single plan-to-apply pipeline"
git log --oneline
Qué esperar (representativo en los hashes, literal en la estructura):
<hash> Wire ci.yml, apply.yml, and drift.yml into a single plan-to-apply pipeline
<hash> drift.yml: schedule-based drift detection with terraform_wrapper: false
<hash> apply.yml: concurrency control to prevent a double apply
<hash> apply.yml: needs, artifact download, and the real terraform apply attempt
<hash> ci.yml: save the plan as tfplan and upload it as an artifact
a10c9b7 Add Compliance tag to the shipment-docs bucket
Cierre del Módulo 5
Completaste la mitad de CD del pipeline de Andes Cargo. Repasa lo que te llevas:
- El disparador correcto, con su límite conocido:
pushamain, confirmado corriendo con un evento sintético de rama de feature queactno bloqueó —el mismo hallazgo del Módulo 2, ahora con consecuencias directas sobre el archivo que aplica infraestructura real (lección 2). - El plan exacto, no uno nuevo:
needs:dentro de un mismo archivo,upload-artifact/download-artifactentre corridas distintas, verificado tres veces por SHA256 idéntico —y dos hallazgos reales de red: el--artifact-server-addrque necesita tu IP real bajo Docker Desktop, y por quérun_idfijo en1hace quedownload-artifactfuncione sinrun-id:bajoact, algo que un repositorio real sí necesitaría (lección 3). - El
applyreal, intentado de verdad:tflocal apply -auto-approve tfplan, con un fallo honesto deconnection refusedtras 9 reintentos del proveedor de AWS —más lento y más detallado que los fallos deawslocalde módulos anteriores, por la misma razón de siempre: LocalStack no está corriendo (lección 4). - La protección contra el doble apply:
concurrency: { group, cancel-in-progress: false }, con la razón exacta detrás de elegir esperar en vez de cancelar para infraestructura, y la honestidad de queactno puede demostrar el bloqueo entre corridas —un mecanismo de coordinación centralizada de GitHub, comoenvironment:(lección 5). - La vigilancia programada:
drift.yml, con-detailed-exitcodey un hallazgo real y documentado sobreterraform_wrapper: falseenhashicorp/setup-terraform@v3—sin él, el job reportaría "sin drift" incorrectamente, incluso cuando sí lo hay (lecciones 6 y 7). - Todo junto, sobre el mismo cambio real: el tag
Compliance, siguiendo el hilo desde el PR #42 (Módulo 2) hasta el Módulo 3, y ahora hasta un intento real de aplicación y su vigilancia posterior (este proyecto).
Qué viene después
El Módulo 6 enseña qué hacer cuando algo sale mal: el patrón de rollback específico de infraestructura (git revert, no "volver a una versión de imagen"), branch protection como el control que impide que main reciba cambios sin pasar por ci.yml, y un guardrail real —ejecutado, no solo nombrado— que falla el job si un plan intenta destruir la tabla Shipments. Con eso, el pipeline completo de Andes Cargo queda con las tres capas que un sistema de CI/CD de infraestructura necesita: revisión (Módulo 3), aplicación automática (este módulo), y redes de seguridad (Módulo 6).
Errores comunes
Esperar que el Paso 4 muestre "no drift" (de expectativa, revisita la lección 7). Qué pasa: alguien, después de ver el apply.yml del Paso 3, espera que drift.yml confirme "todo en orden" inmediatamente después. Cómo corregirlo: el apply del Paso 3 no llegó a completarse —falló por falta de conexión a LocalStack—, así que no hay ningún recurso nuevo que el state pueda reflejar. drift.yml reportando "DRIFT DETECTED" en el Paso 4 es la consecuencia correcta y esperada de esa cadena de eventos, no un error nuevo.
Correr los tres workflows sin limpiar .artifacts/ primero (de flujo). Qué pasa: alguien corre este proyecto varias veces seguidas, sin borrar la carpeta de artefactos entre corridas, y termina con SHA256 de una corrida anterior mezclados con los de la actual. Cómo corregirlo: el Paso 1 de esta lección incluye rm -rf .artifacts && mkdir -p .artifacts exactamente para esto — empezar limpio en cada recorrido completo del pipeline evita confusión sobre qué artefacto corresponde a qué corrida.
Pensar que este proyecto reemplaza los proyectos de los Módulos 2 y 3 (de alcance). Qué pasa: alguien asume que, con este proyecto, hello-andes-cargo.yml o el ci.yml original ya no importan. Cómo corregirlo: cada archivo de .github/workflows/ sigue existiendo y cumpliendo su rol — ci.yml corre en cada Pull Request nuevo, para siempre; este proyecto solo demuestra cómo esos archivos, junto a los dos nuevos de este módulo, trabajan juntos sobre un cambio específico.
Ejercicios
Ejercicio 1 — Reconstruye la cadena de SHA256 de memoria. Sin mirar esta lección, explica en cuántos puntos distintos de este proyecto se verificó el mismo SHA256, y qué garantía concreta da cada verificación.
Ver solución
Tres puntos: (1) cuando ci.yml sube el artefacto (el SHA256 se calcula y se registra por primera vez); (2) cuando fetch-reviewed-plan, en apply.yml, lo descarga (confirma que lo que llegó es exactamente lo que se subió); (3) cuando terraform-apply, en el mismo apply.yml, lo vuelve a descargar en su propio contenedor (confirma lo mismo, una segunda vez, de forma independiente). Las tres coincidencias, juntas, son la prueba de que el archivo que casi se aplicó nunca se modificó entre el momento en que alguien lo revisó (Paso 2) y el momento en que se intentó aplicar (Paso 3).
Ejercicio 2 — Explica por qué el Paso 4 sigue siendo útil, aunque su resultado en este momento no sea "drift real". Un colega te dice: "si el Paso 4 no detectó drift real, ¿para qué correrlo en este proyecto?". Respóndele.
Ver solución
Una respuesta completa suena, más o menos, así: "El propósito de este paso no es solo el resultado que muestra hoy —confirma que el mecanismo completo funciona: terraform_wrapper: false propaga el código de salida correcto, -detailed-exitcode distingue 'sin cambios' de 'con cambios', y el resumen se publica con el mensaje correcto según ese código. Ya vimos, en las lecciones 6 y 7, que sin ese flag el job reportaría 'sin drift' incorrectamente incluso con cambios reales presentes —correrlo aquí, sobre este proyecto real, es la confirmación final de que ese hallazgo sigue aplicando, no solo en un ejemplo aislado."
Ejercicio 3 — Diseña el siguiente paso de Andes Cargo. Si tuvieras un LOCALSTACK_AUTH_TOKEN válido ahora mismo, ¿en qué orden exacto correrías los comandos de este proyecto para ver el recorrido completo con éxito, de principio a fin?
Ver solución
El mismo orden exacto de esta lección, sin cambiar ningún comando: (1) arrancar LocalStack con el token (Módulo 1, lección 8, Paso 5); (2) act pull_request -e pr-event.json -j terraform-checks con los flags de artefactos (Paso 2); (3) act push -W apply.yml con los mismos flags (Paso 3) — esta vez terminaría en Apply complete! Resources: 12 added; (4) act workflow_dispatch -j check-drift (Paso 4) — esta vez terminaría en exitcode=0, Result: no drift detected. La secuencia de comandos no cambia nunca; lo único que cambia es si hay un LocalStack real del otro lado.
Resumen y siguiente paso
En este proyecto corriste ci.yml, apply.yml y drift.yml en secuencia, sobre el mismo cambio real que atraviesa esta guía desde el Módulo 2: el tag Compliance del PR #42. Verificaste, con SHA256 idéntico en tres puntos distintos, que el plan aplicado es exactamente el plan revisado. Viste el intento real de terraform apply fallar de forma honesta sin un token, y drift.yml reportar correctamente sobre un state que todavía no tiene nada aplicado. Cerraste con el recorrido representativo completo —plan → apply exitoso → drift limpio— para que el ciclo completo quede claro, incluso sin poder ejecutarlo de punta a punta en esta máquina.
Antes de avanzar deberías poder: describir de memoria el recorrido completo de un cambio de Andes Cargo, desde el Pull Request hasta la vigilancia posterior; explicar qué garantiza, exactamente, que el SHA256 coincida en los tres puntos de verificación; y decir con precisión qué cambiaría en cada uno de los tres workflows si corrieras este mismo proyecto con un LOCALSTACK_AUTH_TOKEN válido.
Con esto, el Módulo 5 queda cerrado. Tienes la mitad de CD del pipeline completa: aplicación automática, protegida contra concurrencia, con vigilancia de drift programada.
Siguiente módulo: rollback y redes de seguridad — qué hacer cuando un apply sale mal, branch protection como control real de repositorio, y un guardrail ejecutado que protege la tabla Shipments de una destrucción accidental.
Recursos
- HashiCorp Developer — Automate Terraform with GitHub Actions — el patrón completo que este módulo termina de implementar, citado desde el Módulo 3.
- nektosact.com — User Guide — referencia completa de
act pull_request,act push -Wyact workflow_dispatch, usadas de punta a punta en este proyecto. - GitHub Docs — Using concurrency — referencia oficial de
concurrency:, integrada enapply.ymldesde la lección 5. - Módulo 3 de esta guía (
08-project-andes-cargos-ci-workflow.md) — el origen del cambio de HCL (Compliancetag) que este proyecto sigue hasta su intento de aplicación.