Módulo 8: Capstone The Andes Cargo Security Gate
3. Manos a la obra: encadenando el gate en `ci.yml`
Descripción
Esta lección convierte el diagrama de la lección 2 en YAML real, y lo corre de verdad, dos veces, con act pull_request contra un runner Docker real. La primera corrida confirma que los tres jobs existen y están correctamente encadenados; el resto del módulo (lecciones 4 y 5) va a reutilizar exactamente este mismo ci.yml contra dos cambios distintos —uno que pasa, uno que no—. Todo lo que ves aquí corrió para escribir esta lección, con la misma imagen de runner (catthehacker/ubuntu:act-latest) que cicd-and-gitops-on-aws-guide y los Módulos 5 y 6 de esta guía ya usaron.
Nota de aislamiento. Como en el Módulo 5, lección 7, y el Módulo 6, lección 8, este ci.yml extendido corre sobre un laboratorio nuevo y desechable (git init propio), no sobre el andes-cargo-infra/ que has ido acumulando desde el Módulo 1 — act corriendo contra Docker toca .git/ de formas que no quieres mezclar con tu proyecto principal.
Conexión con el módulo
Las lecciones 4 a 6 de esta guía ya probaron conftest, Trivy y cosign en aislamiento, cada uno a mano, en tu terminal. Esta lección no repite ninguna de esas pruebas — toma los tres comandos ya verificados y los pone, cada uno, dentro de su propio job de ci.yml, con needs: declarando la cadena exacta que la lección 2 dibujó.
Analogía: instalando los controles del aeropuerto, uno por uno, y probando la cadena completa
La lección 2 fue el plano. Esta es la construcción: instalar el primer control, probar que funciona solo; instalar el segundo, probar que el primero lo alimenta correctamente; instalar el tercero, probar la cadena completa de punta a punta. Ningún aeropuerto real prueba sus tres controles por primera vez el día que abre al público — los prueba, en conjunto, antes de que el primer pasajero llegue. Esta lección es esa prueba de aceptación.
Paso 1 — El laboratorio, con las piezas heredadas de M1-M7
mkdir andes-cargo-infra && cd andes-cargo-infra
git init -b main
Copia el HCL completo, endurecido de punta a punta por los Módulos 1 a 7 (los archivos de la raíz, modules/, lambda/, policy/, .trivyignore), y los tres artefactos de cadena de suministro del Módulo 6 (sbom.cyclonedx.json, cosign.pub, manifest.sig — cosign.key se queda fuera del repositorio, gitignorado desde el Módulo 6, lección 5):
cat > .gitignore <<'EOF'
cosign.key
tfplan
tfplan.json
.terraform/
EOF
cat > .actrc <<'EOF'
-P ubuntu-latest=catthehacker/ubuntu:act-latest
EOF
git add -A && git commit -m "Bootstrap the extended security gate: policy-check + iac-scan + verify-artifact chained in ci.yml"
Paso 2 — ci.yml: tres jobs nuevos, cada uno con su needs:
Este es el ci.yml heredado de cicd-and-gitops-on-aws-guide (el job terraform-checks original, sin ningún cambio) con tres jobs nuevos agregados al lado. Fíjate en el step de Trivy: se mueve desde dentro de terraform-checks (donde el Módulo 5, lección 7, lo había puesto) hacia su propio job, iac-scan — exactamente el cambio que la lección 2 justificó.
name: ci
on:
pull_request:
branches: [main]
jobs:
policy-check:
runs-on: ubuntu-latest
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 init
run: terraform init -input=false
- name: Terraform plan
run: terraform plan -out=tfplan -input=false
- name: Convert plan to JSON
run: terraform show -json tfplan > tfplan.json
- name: Install conftest
run: |
curl -sL -o conftest.tar.gz \
https://github.com/open-policy-agent/conftest/releases/download/v0.69.0/conftest_0.69.0_Linux_x86_64.tar.gz
tar -xzf conftest.tar.gz conftest
sudo mv conftest /usr/local/bin/
- name: Evaluate the policy library against the plan
run: conftest test tfplan.json -p policy/
iac-scan:
needs: policy-check
runs-on: ubuntu-latest
steps:
- name: Check out andes-cargo-infra
uses: actions/checkout@v4
- name: Install Trivy
run: |
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.74.0
- name: IaC security scan (Trivy)
run: trivy config --exit-code 1 --severity CRITICAL,HIGH .
verify-artifact:
needs: iac-scan
runs-on: ubuntu-latest
steps:
- name: Check out andes-cargo-infra
uses: actions/checkout@v4
- name: Install envsubst (required by the cosign installer, missing on act's runner image)
run: apt-get update -qq && apt-get install -y -qq gettext-base
- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6
- name: Verify function.zip against manifest.sig
run: |
cosign verify-blob \
--key cosign.pub \
--bundle manifest.sig \
--insecure-ignore-tlog=true \
lambda/function.zip
Tres decisiones, ninguna arbitraria:
policy-checkcorreterraform init/plande cero, dentro del mismo job. A diferencia deterraform-checks(que también los corre, para su propio propósito),policy-checknecesita su propia copia delplanporqueconftestno puede evaluar nada sin el JSON queterraform show -jsonproduce. Es trabajo duplicado en apariencia —dos jobs corriendoterraform init— pero es el precio correcto de que cada job sea independiente y pueda correr en un runner distinto, sin compartir estado entre sí (la alternativa, pasar eltfplande un job a otro conactions/upload-artifact/download-artifact, es exactamente el patrón queci.yml/apply.ymlya usan entre workflows distintos — dentro de un mismo workflow, cada job de este gate se mantiene deliberadamente autosuficiente).sudo mv conftest /usr/local/bin/, un detalle nuevo de este módulo. El Módulo 4, lección 3, instalóconftesten el directorio de trabajo del alumno, donde ya estaba en elPATHefectivo de esa sesión de terminal. Dentro de un runner de GitHub Actions, elPATHno incluye el directorio de trabajo por defecto — moverlo a/usr/local/bin/(que sí está en elPATH) es el paso que hace que el siguiente step,conftest test, encuentre el binario sin necesitar una ruta relativa.- El step
Install envsubstes idéntico, línea por línea, al que el Módulo 6, lección 8, ya documentó paraapply.yml. Es la misma incompatibilidad real entre la imagenmediumdeactysigstore/cosign-installer— se repite aquí porqueverify-artifactes, literalmente, el mismo job que ese módulo escribió, ahora también enci.yml.
Paso 3 — Primera corrida: los tres jobs, en cadena, todos en verde
act pull_request -e .github/act-events/pr-event.json
Qué esperar (literal — ejecutado para escribir esta lección, con Docker real y la imagen catthehacker/ubuntu:act-latest; recortado a las líneas que muestran progreso y resultado, filtrando el ruido repetido de docker pull/docker exec que act imprime en cada step):
[ci/policy-check] ⭐ Run Main Terraform init
[ci/policy-check] ✅ Success - Main Terraform init [16.926710125s]
[ci/policy-check] ⭐ Run Main Terraform plan
[ci/policy-check] ✅ Success - Main Terraform plan [5.83613625s]
[ci/policy-check] ⭐ Run Main Convert plan to JSON
[ci/policy-check] ✅ Success - Main Convert plan to JSON [2.367656084s]
[ci/policy-check] ⭐ Run Main Install conftest
[ci/policy-check] ✅ Success - Main Install conftest [2.035634042s]
[ci/policy-check] ⭐ Run Main Evaluate the policy library against the plan
[ci/policy-check] |
[ci/policy-check] | 4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions
[ci/policy-check] ✅ Success - Main Evaluate the policy library against the plan [267.45375ms]
[ci/policy-check] 🏁 Job succeeded
[ci/iac-scan ] ⭐ Run Main Install Trivy
[ci/iac-scan ] ✅ Success - Main Install Trivy [4.488681208s]
[ci/iac-scan ] ⭐ Run Main IaC security scan (Trivy)
[ci/iac-scan ] | Report Summary
[ci/iac-scan ] | ┌───────────────────────────┬───────────┬───────────────────┐
[ci/iac-scan ] | │ Target │ Type │ Misconfigurations │
[ci/iac-scan ] | ├───────────────────────────┼───────────┼───────────────────┤
[ci/iac-scan ] | │ . │ terraform │ 0 │
[ci/iac-scan ] | ├───────────────────────────┼───────────┼───────────────────┤
[ci/iac-scan ] | │ dynamodb.tf │ terraform │ 0 │
[ci/iac-scan ] | ├───────────────────────────┼───────────┼───────────────────┤
[ci/iac-scan ] | │ lambda.tf │ terraform │ 0 │
[ci/iac-scan ] | ├───────────────────────────┼───────────┼───────────────────┤
[ci/iac-scan ] | │ modules/s3-bucket/main.tf │ terraform │ 0 │
[ci/iac-scan ] | ├───────────────────────────┼───────────┼───────────────────┤
[ci/iac-scan ] | │ secrets.tf │ terraform │ 0 │
[ci/iac-scan ] | └───────────────────────────┴───────────┴───────────────────┘
[ci/iac-scan ] ✅ Success - Main IaC security scan (Trivy) [1.837377541s]
[ci/iac-scan ] 🏁 Job succeeded
[ci/verify-artifact] ⭐ Run Main Install cosign
[ci/verify-artifact] ✅ Success - Main Install cosign [4.038418125s]
[ci/verify-artifact] ⭐ Run Main Verify function.zip against manifest.sig
[ci/verify-artifact] | WARNING: Skipping tlog verification is an insecure practice that lacks transparency and auditability verification for the blob.
[ci/verify-artifact] | Verified OK
[ci/verify-artifact] ✅ Success - Main Verify function.zip against manifest.sig [87.011458ms]
[ci/verify-artifact] 🏁 Job succeeded
Tres 🏁 Job succeeded, en el orden exacto de la cadena. act respetó needs: sin que hiciera falta pedírselo explícitamente: arrancó policy-check primero (sin ninguna dependencia declarada), esperó su éxito, arrancó iac-scan, esperó el suyo, y solo entonces arrancó verify-artifact. El mismo 4 tests, 4 passed que ya viste corriendo conftest a mano en el Módulo 4, el mismo Misconfigurations: 0 en los cinco archivos que ya viste con Trivy en el Módulo 5 (después de que el .trivyignore de esa lección se calibrara), el mismo Verified OK del Módulo 6 — los tres, ahora, corriendo dentro de un pipeline real, no en tu terminal.
Un detalle real que vale la pena documentar: tfplan.json confunde a Trivy si queda en el directorio
Al preparar esta lección, correr trivy config . fuera de un job de CI, directamente sobre andes-cargo-infra/, con tfplan.json todavía presente en el directorio de un terraform plan anterior, produjo esto:
ERROR [terraform parser] Error parsing file module="root" file_path="main.tf" cause="<nil>" err="main.tf:137,7-8: Invalid expression..."
Un error confuso —menciona main.tf, un archivo que ni siquiera existe en este proyecto— porque Trivy, al escanear un directorio, intenta detectar automáticamente un snapshot de plan de Terraform junto a los archivos .tf, y tfplan.json calza con ese patrón lo suficiente como para que Trivy intente parsearlo como si fuera HCL. El resultado final (Misconfigurations: 0 en los archivos reales) no cambia, pero el mensaje de error es ruido puro, sin ninguna relación con un hallazgo real. Dentro de iac-scan, este problema nunca aparece —el job hace checkout limpio en cada corrida, sin ningún tfplan.json residual de un terraform plan anterior—, pero vale la pena saberlo si alguna vez corres trivy config . a mano, en el mismo directorio donde ya corriste terraform plan -out=tfplan.
Errores comunes
Instalar conftest/Trivy/cosign en el directorio de trabajo del runner, sin moverlos a un directorio del PATH. Qué pasa: alguien copia el comando de instalación de la lección correspondiente (Módulo 4, 5 o 6) tal cual, sin el sudo mv .../usr/local/bin/ adicional que un runner de CI necesita. Cómo detectarlo: el step de instalación termina en éxito, pero el siguiente step (conftest test, por ejemplo) falla con conftest: command not found. Cómo corregirlo: en tu terminal local, el directorio de trabajo suele estar en el PATH efectivo de la sesión; dentro de un runner de Actions, no lo está por defecto — siempre mueve el binario descargado a /usr/local/bin/ (o agrega el directorio al PATH con echo "$dir" >> $GITHUB_PATH) antes de usarlo en un step posterior.
Confundir needs: policy-check (un solo nombre) con needs: [policy-check] (una lista de un elemento). Qué pasa: alguien, revisando la sintaxis YAML de este ci.yml, se pregunta si needs: iac-scan (sin corchetes) funciona igual que needs: [iac-scan]. Cómo detectarlo: ambas formas son válidas y equivalentes en la especificación de GitHub Actions —no es un error, pero puede generar dudas innecesarias al leer el archivo. Cómo corregirlo: usa la forma sin corchetes cuando dependes de un solo job (como en este ci.yml), y la forma de lista solo cuando dependes de más de uno —es una convención de legibilidad, no una diferencia funcional, pero mantenerla consistente ayuda a que cualquiera que lea el archivo distinga, de un vistazo, una dependencia simple de un punto de sincronización real.
Correr act pull_request sin el flag -e y sorprenderse de un evento vacío. Qué pasa: alguien corre act pull_request sin especificar .github/act-events/pr-event.json, y act sintetiza un evento de Pull Request mínimo, sin los campos que un workflow real podría esperar (número de PR, rama base, etc.). Cómo detectarlo: si tu workflow no usa ninguno de esos campos —como el ci.yml de esta lección, que no los necesita—, el resultado no cambia visiblemente; pero si algún job del futuro sí los necesitara, fallaría de forma confusa. Cómo corregirlo: pasa siempre -e con un evento explícito, aunque el ci.yml actual no lo necesite — es la misma disciplina que cicd-and-gitops-on-aws-guide ya estableció, y evita sorpresas cuando el workflow crezca.
Ejercicios
Ejercicio 1 — Corre act pull_request -j iac-scan en aislamiento, y explica por qué funciona sin que policy-check haya corrido antes. Usando el flag -j que ya viste en la lección 2, corre solo el job iac-scan. ¿Por qué no falla, a pesar de que needs: policy-check está declarado?
Ver solución
act -j <job> corre el job pedido de forma aislada, ignorando cualquier needs: declarado — es un modo de depuración, no una simulación fiel del pipeline completo (el mismo comportamiento que el Ejercicio 3 de la lección 2 ya predijo). iac-scan no depende, en tiempo de ejecución, de ningún archivo o estado que policy-check produzca —a diferencia de verify-artifact dentro de apply.yml original, que si necesitaba el tfplan descargado de un job anterior—, así que corre sin ningún problema en aislamiento. Esto es útil para depurar un job específico rápido, pero no confirma que el needs: esté correctamente encadenado — para eso, la única prueba válida es correr el workflow completo, sin -j, como el Paso 3 de esta lección.
Ejercicio 2 — Calcula el tiempo total aproximado del gate completo, sumando los tres tiempos de "evaluación" reales de esta lección (sin contar instalación). Usando los tiempos entre corchetes de la salida del Paso 3 (Evaluate the policy library..., IaC security scan (Trivy), Verify function.zip...), ¿cuál es el tiempo combinado de los tres controles, sin contar el tiempo de instalar cada herramienta?
Ver solución
267.45375ms (policy-check) + 1.837377541s (iac-scan) + 87.011458ms (verify-artifact) ≈ 2.19 segundos de evaluación real, de los tres controles combinados — una fracción minúscula frente a los 16.9 segundos que solo terraform init tomó en el mismo job. El punto del ejercicio es notar que el costo real de este security gate, una vez que las herramientas están instaladas, es casi despreciable — la mayor parte del tiempo de un pipeline real se va en instalar dependencias e inicializar Terraform, no en evaluar las políticas de seguridad en sí. Es un argumento fuerte, con números reales, contra la objeción común de "un gate de seguridad hace más lento el pipeline".
Ejercicio 3 — Diseña un cuarto job hipotético, sbom-check, que valide que sbom.cyclonedx.json sigue existiendo y no está vacío, y decide dónde encadenarlo. Sin escribir el YAML completo, describe en prosa: ¿de qué job dependería (needs:), y por qué ese lugar específico en la cadena tiene sentido según el criterio de costo creciente de la lección 2?
Ver solución
Una respuesta razonable: sbom-check no depende de ningún plan de Terraform ni de ninguna herramienta externa costosa —solo necesita confirmar que un archivo existe y tiene contenido (test -s sbom.cyclonedx.json, por ejemplo)—, así que, siguiendo el criterio de costo creciente, debería correr primero, incluso antes de policy-check, o en paralelo con él (sin needs: en absoluto, ya que no depende de ningún resultado de los otros tres). Encadenarlo al final, después de verify-artifact, sería el error de diseño opuesto al que la lección 1 ya advirtió con Trivy y terraform init: gastar el tiempo de los controles más caros antes de un chequeo casi instantáneo que podría haber fallado rápido, primero.
Resumen y siguiente paso
En esta lección construiste ci.yml con tres jobs nuevos —policy-check, iac-scan, verify-artifact—, cada uno con needs: apuntando al anterior, y los corriste de punta a punta con act pull_request contra Docker real: los tres terminaron en 🏁 Job succeeded, en el orden exacto que la cadena declara. Confirmaste que act respeta needs: sin necesidad de ningún flag adicional, y documentaste un detalle real —tfplan.json confundiendo al detector automático de Trivy— que no afecta el resultado del gate pero vale la pena conocer.
La lección 4 reutiliza este mismo ci.yml, sin ningún cambio, contra un cambio de negocio real y pequeño — la prueba de que el gate deja pasar lo que debería pasar, no solo detiene lo que debería detener.
Recursos
- GitHub Docs —
jobs.<job_id>.needs— referencia oficial, ya citada en la lección 2. - nektosact.com — User Guide — referencia completa de
act, incluido el flag-jusado en el Ejercicio 1. - Este curso, Módulo 4, lección 3; Módulo 5, lección 3; Módulo 6, lección 5 — el origen de cada comando de instalación que esta lección adapta a un runner de CI.
cicd-and-gitops-on-aws-guide, Módulo 3, lección 8 — elci.ymloriginal de nueve steps, el punto de partida de este archivo.