Módulo 3: The Iac Pipeline Fmt Validate Plan
8. Proyecto: el `ci.yml` de Andes Cargo
Descripción
Este es el proyecto que cierra el Módulo 3. Las lecciones 4 a 7 construyeron ci.yml pieza por pieza —fmt, validate, la conexión a LocalStack, plan, la publicación como evidencia—, probando cada una por separado. Este proyecto las corre todas juntas, en un único archivo, de punta a punta, sobre un cambio real al HCL de Andes Cargo: exactamente el cambio que el pr-event.json del Módulo 2 anticipó desde el principio —"Add tags to the shipment documents bucket"—, ahora hecho realidad. Vas a agregar un tag nuevo al bucket andes-cargo-shipment-docs, correr act pull_request -e pr-event.json una última vez, y leer el ci.yml completo funcionando como lo haría en un Pull Request real: fmt → init → validate → conexión a LocalStack → plan → resumen publicado.
Conexión con el módulo
Este proyecto no introduce ningún step nuevo — es la integración de todo lo que ya construiste. El ci.yml que corres aquí es exactamente el mismo archivo que construiste, línea por línea, en las lecciones 4 a 7. Lo único genuinamente nuevo es el cambio de negocio que lo dispara: la primera y única línea de HCL que este módulo agrega, mínima y con un propósito claro, coherente con la rama feature/add-shipment-tags que escribiste a mano en el Módulo 2, lección 6.
El ci.yml completo
.github/workflows/ci.yml, tal como queda al cerrar este módulo:
name: ci
on:
pull_request:
branches: [main]
jobs:
terraform-checks:
runs-on: ubuntu-latest
env:
AWS_ACCESS_KEY_ID: test
AWS_SECRET_ACCESS_KEY: test
AWS_DEFAULT_REGION: us-east-1
AWS_ENDPOINT_URL: http://host.docker.internal:4566
steps:
- name: Check out andes-cargo-infra
uses: actions/checkout@v4
- name: Set up Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.15.8"
- name: Terraform format check
run: terraform fmt -check -recursive
- name: Terraform init
run: terraform init -input=false
- name: Terraform validate
run: terraform validate
- name: Install awslocal
run: pip3 install --quiet --break-system-packages awscli awscli-local
- name: Confirm the runner can reach LocalStack on the host
continue-on-error: true
run: awslocal s3 ls
- name: Install tflocal
run: pip3 install --quiet --break-system-packages terraform-local
- name: Terraform plan
run: tflocal plan -input=false -no-color | tee plan-output.txt
- name: Publish the plan to the job summary
run: |
{
echo "## Terraform plan — andes-cargo-infra"
echo '```'
cat plan-output.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
Nueve steps, cada uno con un rol específico que ya conoces: verificar estilo, instalar Terraform, verificar sintaxis, confirmar el camino de red, calcular qué cambiaría, y publicar ese cálculo donde alguien lo pueda leer. Ninguno de los nueve, en ningún momento, escribe infraestructura real — el apply que sí lo haría llega recién en el Módulo 5.
El cambio real: agregar el tag Compliance al bucket de manifiestos
Recuerda el pr-event.json que escribiste en el Módulo 2, lección 6: PR #42, rama feature/add-shipment-tags → main, título "Add tags to the shipment documents bucket". Hasta ahora, ese título describía una intención — este proyecto la hace real. Andes Cargo necesita marcar el bucket andes-cargo-shipment-docs con un tag de cumplimiento, para que herramientas de auditoría externas (fuera del alcance de esta guía) puedan identificar qué buckets están sujetos a una política de retención de manifiestos.
Abre s3.tf y modifica el bloque module "shipment_docs_bucket":
module "shipment_docs_bucket" {
source = "./modules/s3-bucket"
bucket_name = var.bucket_name
enable_versioning = true
bucket_policy_json = data.aws_iam_policy_document.require_https.json
tags = merge(local.common_tags, {
Compliance = "manifest-retention-required"
})
}
El único cambio: tags = local.common_tags pasa a tags = merge(local.common_tags, { Compliance = "manifest-retention-required" }). merge() es una función nativa de Terraform que combina dos o más mapas en uno solo —aquí, toma los tres tags que ya venían de local.common_tags (Project, Environment, ManagedBy) y agrega un cuarto (Compliance) sin tocar los demás—. Es, literalmente, la única línea de HCL de negocio que este módulo entero agrega — coherente con lo que DISEÑO.md prometió desde el principio: automatizar el pipeline, no reescribir el proyecto.
Corriendo el ci.yml completo, de punta a punta
terraform fmt s3.tf
git diff --stat
Qué esperar (literal):
s3.tf | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
Ahora corre el pipeline completo, exactamente como correría sobre un Pull Request real:
act pull_request -e .github/act-events/pr-event.json -j terraform-checks
Qué esperar (salida literal, ejecutada para escribir esta lección — resumen de los nueve steps, en orden, con los tiempos reales de esta corrida):
[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 [25.3615ms]
[ci/terraform-checks] ⭐ Run Main Set up Terraform
[ci/terraform-checks] ✅ Success - Main Set up Terraform [2.306658833s]
[ci/terraform-checks] ⭐ Run Main Terraform format check
[ci/terraform-checks] ✅ Success - Main Terraform format check [138.0015ms]
[ci/terraform-checks] ⭐ Run Main Terraform init
[ci/terraform-checks] ✅ Success - Main Terraform init [10.937006333s]
[ci/terraform-checks] ⭐ Run Main Terraform validate
[ci/terraform-checks] | Success! The configuration is valid.
[ci/terraform-checks] ✅ Success - Main Terraform validate [1.800934209s]
[ci/terraform-checks] ⭐ Run Main Install awslocal
[ci/terraform-checks] ✅ Success - Main Install awslocal [11.648304875s]
[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 [8.406601667s]
[ci/terraform-checks] ⭐ Run Main Install tflocal
[ci/terraform-checks] ✅ Success - Main Install tflocal [2.2965225s]
[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 [4.3687605s]
[ci/terraform-checks] ⭐ Run Main Publish the plan to the job summary
[ci/terraform-checks] ✅ Success - Main Publish the plan to the job summary [70.591959ms]
[ci/terraform-checks] ⚙ Summary - ## Terraform plan — andes-cargo-infra
[ci/terraform-checks] ⭐ Run Complete job
[ci/terraform-checks] ✅ Success - Complete job
[ci/terraform-checks] 🏁 Job succeeded
Plan: 12 to add, 0 to change, 0 to destroy. — el mismo conteo de recursos que la lección 6, porque el nuevo tag no crea ni destruye ningún recurso, solo cambia un atributo dentro de recursos que de todas formas se estaban creando por primera vez. Confírmalo buscando el tag dentro del plan completo:
grep -A1 "Compliance" plan-output.txt
Qué esperar (literal, dos apariciones — una en tags, otra en tags_all, del bloque aws_s3_bucket dentro de module.shipment_docs_bucket):
+ "Compliance" = "manifest-retention-required"
+ "Environment" = "dev"
--
+ "Compliance" = "manifest-retention-required"
+ "Environment" = "dev"
Esta es, literalmente, la evidencia que alguien revisando el Pull Request #42 vería en el resumen del job: un plan de 12 recursos, sin nada destruido, con el tag nuevo apareciendo exactamente donde el título del PR prometía que aparecería —en el bucket de manifiestos, no en ningún otro recurso—.
Commiteando el cambio
git add s3.tf
git commit -m "Add Compliance tag to the shipment-docs bucket"
git log --oneline
Qué esperar (representativo en los hashes de commit, literal en la estructura y en los mensajes — seis commits nuevos desde el cierre del Módulo 2, uno por cada pieza que este módulo agregó):
a10c9b7 Add Compliance tag to the shipment-docs bucket
7b6421e ci.yml: publish the plan to the job summary
dfcbfce ci.yml: install tflocal and run terraform plan
70472a2 ci.yml: confirm the runner can reach LocalStack via host.docker.internal
ba47438 Add ci.yml: terraform fmt and validate as CI steps
5094200 Add hello-andes-cargo.yml: first workflow reaching for LocalStack via host.docker.internal
4e4f5c5 Add .secrets (gitignored) and secrets-test workflows for act --secret-file / -s
b42e54d Add pr-event.json and print-event.yml to practice act -e
ce6efa5 Bootstrap CI/CD layer: initialize git repository, .actrc, and .github/workflows/
git status
Qué esperar (literal):
On branch main
nothing to commit, working tree clean
Cierre del Módulo 3
Completaste el módulo que construye la mitad de CI del pipeline de Andes Cargo. Repasa lo que te llevas:
- El patrón, con fuente exacta:
planen Pull Request,applyen merge, en dos workflows separados —no una condición dentro de un archivo compartido—, citado del tutorial oficial de HashiCorp, con la razón de seguridad concreta (evitar la familia de vulnerabilidades "pwn request") explicada a fondo (lección 2). - Terraform, instalado en cada corrida:
hashicorp/setup-terraform@v3, pineado a1.15.8, confirmado con un comando que busca el binario antes y después de instalarlo — la prueba directa de que un runner no recuerda nada entre corridas, a diferencia de tu laptop (lección 3). fmtyvalidate, vistos fallar y corregirse dos veces: un error de formato (código de salida3, sin más detalle que el nombre del archivo) y un error de referencia (mensaje completo, con sugerencia de corrección) — cada uno atrapado por la herramienta correcta, en el orden correcto (lección 4).- La conexión a LocalStack, probada antes de confiar en ella:
host.docker.internal:4566, con un fallo honesto (LocalStack apagado, 11 segundos de reintento real) que no bloqueó el resto del job gracias acontinue-on-error: true(lección 5). - El primer
terraform planreal de esta guía: 12 recursos, calculados sin necesitar LocalStack corriendo —un hallazgo verificado sobreskip_requesting_account_id, con la advertencia honesta de que es una propiedad de este momento específico del proyecto, no una garantía general (lección 6). - El
plancomo evidencia, dos formas:$GITHUB_STEP_SUMMARY, confirmado funcionando bajoact; y el comentario directo en el Pull Request víaactions/github-script, mostrado en YAML completo, etiquetado honestamente como no-ejecutable sin un Pull Request real (lección 7). - Todo junto, sobre un cambio real: el tag
Complianceen el bucket de manifiestos, exactamente lo que el título del Pull Request #42 prometía desde el Módulo 2, corrido de punta a punta conact pull_request -e pr-event.json(este proyecto).
Qué viene después
El Módulo 4 construye la mitad de seguridad que hace posible que un apply real exista sin comprometer una credencial de larga vida: GitHub Secrets, por qué una credencial nunca vive en el repositorio, qué es la federación OIDC —mostrada en YAML completo, con la razón técnica exacta de por qué no se ejecuta bajo act—, y ambientes de GitHub (dev/prod) como control de aprobación. El Módulo 5 toma ese manejo de secretos y el ci.yml que acabas de terminar, y construye apply.yml: el workflow que, disparado únicamente por push a main —nunca por un Pull Request sin revisar—, aplica exactamente el plan que una persona ya aprobó aquí.
Errores comunes
Olvidar terraform fmt s3.tf después de editar el bucket a mano (de flujo, el más fácil de cometer en este proyecto específico). Qué pasa: alguien escribe el bloque merge(...) con una indentación ligeramente distinta a la de esta lección, y el step Terraform format check falla al correr el ci.yml completo, interrumpiendo el resto del job antes de llegar al plan. Cómo detectarlo: el mismo patrón de la lección 4 —iam.tf (o, en este caso, s3.tf) impreso, seguido de Terraform exited with code 3—. Cómo corregirlo: corre terraform fmt s3.tf (sin -check) antes de commitear cualquier cambio de HCL, un hábito que vale la pena mantener para el resto de esta guía.
Esperar que el número de recursos cambie porque se agregó un tag (conceptual). Qué pasa: alguien ve Plan: 12 to add —el mismo número que en la lección 6, sin el tag nuevo— y se pregunta si el cambio realmente se aplicó al HCL. Cómo detectarlo: comparar el conteo con lecciones anteriores y no ver ninguna diferencia. Cómo corregirlo: un tag nuevo modifica un atributo dentro de un recurso que ya se estaba creando —no agrega ni quita ningún recurso completo—. El conteo de Plan: cuenta recursos, no atributos; para confirmar que el cambio está ahí, revisa el contenido del plan (como el grep de esta lección), no el número de la última línea.
Pensar que este proyecto ya aplicó el tag a LocalStack (de expectativa, el error más importante de cerrar este módulo). Qué pasa: alguien, después de ver Plan: 12 to add en verde, corre awslocal s3api get-bucket-tagging --bucket andes-cargo-shipment-docs esperando ver el tag Compliance ya presente. Cómo detectarlo: ese comando fallaría (LocalStack ni siquiera está corriendo en este módulo) o, con LocalStack corriendo pero sin ningún apply ejecutado, no mostraría el tag. Cómo corregirlo: recuerda la tesis completa de este módulo — construiste la mitad de CI, la que calcula y muestra. Ningún comando de este módulo, ni de este proyecto, aplicó nada real. El tag existe en el plan, listo para ser revisado; aplicarlo de verdad es, explícitamente, el trabajo del Módulo 5.
Ejercicios
Ejercicio 1 — Explica la coherencia narrativa del cambio. Sin mirar esta lección, explica a un colega por qué el cambio de HCL de este proyecto (Compliance tag en el bucket) no es arbitrario, sino que estaba anticipado desde el Módulo 2.
Ver solución
El pr-event.json que se escribió a mano en el Módulo 2, lección 6, ya incluía el título "Add tags to the shipment documents bucket" para el PR #42, en la rama feature/add-shipment-tags — un evento simulado que describía una intención de cambio antes de que ese cambio existiera de verdad en el HCL. Este proyecto cierra ese círculo: hace real, en s3.tf, exactamente lo que el título del Pull Request simulado prometía, para que cada pieza de esta guía —desde el nombre de la rama hasta el contenido del plan— cuente la misma historia coherente, sin números ni nombres inventados sobre la marcha.
Ejercicio 2 — Reconstruye el ci.yml completo de memoria. Sin mirar esta lección, escribe (en papel o en un editor) los nueve nombres de step de ci.yml, en el orden correcto. Después compara contra el archivo real.
Ver solución
En orden: 1. Check out andes-cargo-infra. 2. Set up Terraform. 3. Terraform format check. 4. Terraform init. 5. Terraform validate. 6. Install awslocal. 7. Confirm the runner can reach LocalStack on the host. 8. Install tflocal. 9. Terraform plan. Más un décimo, la publicación del resumen: 10. Publish the plan to the job summary. Si reconstruiste este orden sin mirar, tienes internalizada la progresión completa de las lecciones 4 a 7: barato antes de caro, verificación antes de red, red antes de plan, plan antes de publicación.
Ejercicio 3 — Decide qué pasaría si el guardrail del Módulo 6 ya existiera. Adelantándote al Módulo 6 (que vas a construir más adelante en esta guía): ese módulo agrega un guardrail que falla el job si el plan intenta destruir la tabla Shipments. Si ese guardrail ya estuviera activo en este ci.yml, ¿esperarías que el cambio de esta lección (agregar un tag al bucket) lo disparara? Justifica.
Ver solución
No, no lo dispararía. El guardrail del Módulo 6 revisa específicamente si el plan en JSON contiene una acción de destrucción sobre la tabla Shipments — este cambio no toca la tabla Shipments en absoluto (toca el bucket andes-cargo-shipment-docs, un recurso completamente distinto), y de cualquier forma, ninguna acción de este proyecto es una destrucción: son 12 creaciones, cero cambios, cero destrucciones, sobre un estado que todavía no tiene nada aplicado. El guardrail está diseñado para detectar un patrón muy específico y peligroso, no para bloquear cualquier cambio de HCL en general.
Resumen y siguiente paso
En este proyecto corriste ci.yml completo, de punta a punta, sobre un cambio real: un tag de cumplimiento agregado al bucket de manifiestos de Andes Cargo, exactamente la intención que el Pull Request #42 simulado anunció desde el Módulo 2. Los nueve steps corrieron en orden —fmt → init → validate → conexión a LocalStack (con un fallo honesto, no bloqueante) → plan (12 recursos, sin necesitar LocalStack corriendo) → resumen publicado—, confirmando que la mitad de CI del pipeline funciona de punta a punta, no solo pieza por pieza.
Antes de avanzar deberías poder: reconstruir ci.yml completo de memoria, con los nueve steps en el orden correcto; explicar por qué este módulo, en ningún momento, tocó infraestructura real; y decir con precisión qué evidencia de revisión existiría, hoy, para que alguien apruebe la fusión del PR #42.
Con esto, el Módulo 3 queda cerrado. Tienes la mitad de CI del pipeline completa, probada de punta a punta, con un plan real listo para ser revisado.
Siguiente módulo: el manejo de secretos que hace posible construir la otra mitad —GitHub Secrets, por qué una credencial de larga vida en un repositorio es el antipatrón que la propia auditoría de mercado de esta guía señala como el hueco más citado de la competencia, y OIDC federado, mostrado en YAML real, como la alternativa moderna.
Recursos
- nektosact.com — User Guide — referencia completa de
act pull_request -e, usada de punta a punta en este proyecto. - HashiCorp Developer — Automate Terraform with GitHub Actions — el patrón completo que este módulo implementa, citado en la lección 2.
- Terraform Docs — the
mergefunction — la función usada en el cambio de HCL de este proyecto. terraform-and-iac-guide(NIEVA) — el proyectoandes-cargo-infra/completo, automatizado sin reescribirse, salvo esta única línea de tags.