Módulo 2: Anatomy Of A Github Actions Workflow

8. Proyecto: el primer workflow real de Andes Cargo

Descripción

Este es el proyecto que cierra el Módulo 2. Todo lo anterior —la anatomía completa (lección 2), los disparadores (lecciones 3 y 4), las Actions reutilizables (lección 5), eventos escritos a mano (lección 6) y secretos gitignorados (lección 7)— converge aquí en hello-andes-cargo.yml: el primer workflow de esta guía que intenta de verdad tocar algo relacionado con la infraestructura de Andes Cargo, no un ejemplo desechable. No corre terraform plan todavía —eso es el Módulo 3—, pero sí confirma algo que ese módulo va a necesitar sin excepción: que el contenedor efímero donde corre un job de act puede alcanzar el LocalStack que corre, por separado, en tu máquina host.

Conexión con el módulo

Esta lección usa, sin cambios, actions/checkout@v4 (lección 5), corre con act push (lección 3), y hereda el .actrc y la estructura de andes-cargo-infra/ que construiste en las lecciones 6 y 7. Es la primera vez en esta guía que un workflow necesita red entre dos contenedores Docker distintos —el del job de act y el de LocalStack—, el problema de arquitectura que el diseño de esta guía investigó y resolvió con host.docker.internal. El Módulo 3 retoma esta misma conexión de red, ahora para un terraform plan real.


El workflow: hello-andes-cargo.yml

.github/workflows/hello-andes-cargo.yml, dentro de andes-cargo-infra/:

name: hello-andes-cargo

on: push

jobs:
  say-hello-to-localstack:
    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: Install awslocal
        run: pip3 install --quiet --break-system-packages awscli awscli-local

      - name: Confirm the runner can reach LocalStack on the host
        run: awslocal sts get-caller-identity

Tres piezas nuevas que vale la pena nombrar antes de correrlo:

  • AWS_ENDPOINT_URL: http://host.docker.internal:4566host.docker.internal es un nombre DNS especial que Docker resuelve, desde dentro de un contenedor, hacia la máquina que lo hospeda —tu host—. Es la pieza exacta que conecta el contenedor efímero del job (donde corre este workflow) con el contenedor de LocalStack (que corriste, por separado, en tu host, siguiendo el mismo patrón del Módulo 1). Sin este endpoint, awslocal intentaría conectarse a localhost:4566 dentro del contenedor del job — y ahí no hay nada escuchando, porque LocalStack corre en un contenedor completamente distinto.
  • --container-options "--add-host=host.docker.internal:host-gateway", ya agregado a tu .actrc (segunda línea, junto a la imagen pineada del Módulo 1) — necesario para que host.docker.internal resuelva de forma confiable en cualquier sistema, incluido Linux (en Docker Desktop de macOS suele resolver sin este flag, pero esta guía lo fija explícitamente desde ya para no depender de esa diferencia de plataforma). El Módulo 3, lección 5, vuelve sobre este mecanismo con mucho más detalle — por ahora, alcanza con saber que existe y por qué.
  • Instalar awslocal en cada corrida — la imagen catthehacker/ubuntu:act-latest (Medium, la que elegiste en el Módulo 1) no trae preinstalado el cliente de AWS ni su wrapper de LocalStack; como cualquier runner real y efímero, cada corrida empieza "limpia", así que el propio workflow tiene que instalar lo que necesita, cada vez.

Ejecutándolo: la mitad que SÍ llega, con éxito

act -l

Qué esperar (salida literal, ejecutada para escribir esta lección — con los cuatro workflows que construiste en esta guía hasta ahora):

Stage  Job ID                   Job name                 Workflow name        Workflow file           Events
0      say-hello-to-localstack  say-hello-to-localstack  hello-andes-cargo    hello-andes-cargo.yml   push
0      print-payload            print-payload            print-event-payload  print-event.yml         pull_request
0      check-secrets            check-secrets            secrets-test         secrets-test.yml        workflow_dispatch
0      check-one-secret         check-one-secret         single-secret-test   single-secret-test.yml  workflow_dispatch
act push -j say-hello-to-localstack

Qué esperar (salida literal, ejecutada para escribir esta lección; los primeros dos steps corren con éxito):

[hello-andes-cargo/say-hello-to-localstack] ⭐ Run Set up job
[hello-andes-cargo/say-hello-to-localstack] 🚀  Start image=catthehacker/ubuntu:act-latest
[hello-andes-cargo/say-hello-to-localstack]   ✅  Success - Set up job
[hello-andes-cargo/say-hello-to-localstack] ⭐ Run Main Check out andes-cargo-infra
[hello-andes-cargo/say-hello-to-localstack]   🐳  docker cp src=/ruta/a/andes-cargo-infra/. dst=/ruta/a/andes-cargo-infra
[hello-andes-cargo/say-hello-to-localstack]   ✅  Success - Main Check out andes-cargo-infra [26.322666ms]
[hello-andes-cargo/say-hello-to-localstack] ⭐ Run Main Install awslocal
[hello-andes-cargo/say-hello-to-localstack]   | WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
[hello-andes-cargo/say-hello-to-localstack]   ✅  Success - Main Install awslocal [9.777224542s]
[hello-andes-cargo/say-hello-to-localstack] ⭐ Run Main Confirm the runner can reach LocalStack on the host
[hello-andes-cargo/say-hello-to-localstack]   | Could not connect to the endpoint URL: "http://host.docker.internal:4566/"
[hello-andes-cargo/say-hello-to-localstack]   ❌  Failure - Main Confirm the runner can reach LocalStack on the host [8.799492333s]
[hello-andes-cargo/say-hello-to-localstack] exitcode '255': failure
[hello-andes-cargo/say-hello-to-localstack] 🏁  Job failed
Error: Job 'say-hello-to-localstack' failed

Esto es un fallo real, y es exactamente el resultado correcto para este momento — léelo con cuidado, porque hay dos cosas distintas mezcladas en esa única línea roja: una que funcionó, y una que no, por una razón que nada tiene que ver con act o con el workflow.


Leyendo el fallo: la parte que SÍ funcionó

El comando awslocal sts get-caller-identity tardó 8.8 segundos en fallar — no falló instantáneamente. Ese detalle es la prueba de que la conexión de red sí funcionó: host.docker.internal resolvió correctamente a la IP de tu host (si el DNS hubiera fallado, el error habría sido inmediato, del tipo "no se puede resolver el nombre de host" — no lo que viste). El cliente de AWS intentó conectarse al puerto 4566 de tu host, reintentó según su política de reintentos por defecto (la razón de los ~8.8 segundos, no una casualidad), y finalmente reportó Could not connect to the endpoint URL — el mensaje exacto de "llegué hasta la puerta, pero no hay nadie del otro lado", no "no encontré el edificio".

Confirma esto tú mismo, con una prueba directa e independiente del workflow:

docker ps -a --filter name=localstack_main

Qué esperar (literal, en esta ejecución):

CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

Sin ninguna fila — LocalStack no está corriendo. La razón exacta, ya la conoces del Módulo 1, lección 8: sin LOCALSTACK_AUTH_TOKEN exportado con un valor válido, el contenedor de LocalStack arranca y se cierra en segundos con el error de licencia (exit code 55). Reproducido de nuevo, hoy, exactamente igual que en el Módulo 1:

LocalStack version: 2026.7.3
LocalStack build date: 2026-08-12
LocalStack build git hash: 8f10c66d8

Localstack returning with exit code 55. Reason:
===============================================
License activation failed! 🔑❌

Reason: No credentials were found in the environment. Please make sure to either set the LOCALSTACK_AUTH_TOKEN variable to a valid auth token.

El fallo de este workflow no es un error de esta lección, de act, ni del YAML — es la consecuencia directa y esperada de que LocalStack no está corriendo, porque no tienes un token exportado en esta sesión. El step que intenta llegar hasta LocalStack corrió de verdad, hizo un intento de red real, con un timeout real — y falló por la razón correcta y honesta: no hay nada escuchando del otro lado, no porque el camino esté roto.


La versión que verías con un token válido (representativa)

Con LOCALSTACK_AUTH_TOKEN correctamente exportado antes de arrancar el contenedor de LocalStack —siguiendo exactamente el Paso 5 del Módulo 1, lección 8—, el mismo awslocal sts get-caller-identity, corrido dentro del mismo job de act, devolvería:

Qué esperar (representativo — mismo formato ya confirmado, dos veces, en las guías anteriores de este ecosistema; sin una ejecución en vivo contra un token válido en este momento):

{
    "UserId": "AKIAIOSFODNN7EXAMPLE",
    "Account": "000000000000",
    "Arn": "arn:aws:iam::000000000000:root"
}

"Account": "000000000000" — el mismo account ID fijo de siempre. La diferencia entre esta salida y el error real de arriba no es de red, ni de configuración del workflow — es, exclusivamente, si LocalStack está corriendo o no del otro lado de host.docker.internal:4566. Si tienes un token válido, exporta LOCALSTACK_AUTH_TOKEN y arranca LocalStack (Módulo 1, lección 8, Paso 5) antes de repetir act push -j say-hello-to-localstack — deberías ver exactamente este JSON en tu terminal, en vez del error de conexión.


Commiteando el proyecto

git add -A
git commit -m "Add hello-andes-cargo.yml: first workflow reaching for LocalStack via host.docker.internal"
git log --oneline

Qué esperar (representativo en los hashes de commit, literal en la estructura — cuatro commits, uno por cada pieza que este módulo agregó a andes-cargo-infra/):

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/

Cierre del Módulo 2

Completaste el módulo que enseña a leer y escribir un workflow con criterio. Repasa lo que te llevas:

  • La anatomía completa, diseccionada sobre un archivo real y corrida, no solo leída: on como la condición de disparo, jobs/steps como la estructura de trabajo, runs-on como el destino, uses/with como reutilización con parámetros, y env en sus tres niveles (lección 2).
  • Los cuatro disparadores de un pipeline de infraestructura —push, pull_request, workflow_dispatch, schedule— con un hallazgo verificado y honesto: act no evalúa branches:/paths: antes de correr un job (lecciones 3 y 4).
  • Versionado de Actions por SHA vs. tag, con dos SHAs reales verificados contra la API de GitHub, y por qué esto es la práctica de seguridad más citada como ausente en la competencia (lección 5).
  • Simulación completa sin cuenta de GitHub: eventos escritos a mano con act -e (lección 6), y secretos pasados sin comprometerlos jamás en un commit (lección 7).
  • El primer workflow real de Andes Cargo, con un fallo honesto y explicado —no escondido— que confirma exactamente lo que necesitaba confirmar: el camino de red hasta LocalStack existe y funciona, aunque LocalStack en sí no esté corriendo en este momento.

Qué viene después

El Módulo 3 toma este mismo andes-cargo-infra/ y construye la mitad de CI del pipeline: el patrón estándar de HashiCorp/GitHub de fmt/validate/plan, corridos automáticamente en cada Pull Request. Vas a instalar Terraform dentro de un runner efímero con hashicorp/setup-terraform@v3 —la Action que ya conoces de la lección 5 de este módulo—, vas a conectar ese runner al mismo LocalStack a través del mismo host.docker.internal que acabas de usar aquí, y vas a cerrar el módulo con ci.yml, el primer workflow que corre un terraform plan real, dentro de CI, sobre la infraestructura de Andes Cargo.


Errores comunes

Pensar que el fallo de esta lección significa que algo está mal escrito en el YAML (de expectativa, el más importante de este proyecto). Qué pasa: alguien ve Job failed en rojo y asume, por reflejo, que hay un error de sintaxis o de lógica en hello-andes-cargo.yml. Por qué pasa: un job en rojo, en la mayoría de los contextos de programación, significa "algo que escribiste está mal". Cómo detectarlo: si tu primer instinto es revisar el YAML en busca de un typo, en vez de revisar si LocalStack está corriendo. Cómo corregirlo: el mensaje exacto Could not connect to the endpoint URL, después de varios segundos de intento (no instantáneo), es la firma específica de "el camino de red funciona, pero no hay nada del otro lado" — no de un error de configuración del workflow. docker ps -a --filter name=localstack_main es siempre el primer comando de diagnóstico correcto en este escenario.

Exportar LOCALSTACK_AUTH_TOKEN en la terminal, pero olvidar que act corre en un contenedor separado (de alcance de variables). Qué pasa: alguien exporta el token en su shell, arranca LocalStack correctamente, y sigue viendo el mismo error de conexión en el workflow. Por qué pasa: exportar una variable en tu terminal la hace disponible para comandos que corras desde esa misma terminal —como el propio docker run de LocalStack—, pero no la inyecta automáticamente dentro del contenedor efímero que act crea para el job, que tiene su propio entorno aislado. Cómo detectarlo: docker ps muestra LocalStack corriendo (STATUS: Up), pero el workflow sigue fallando igual. Cómo corregirlo: en este caso específico no hace falta pasarle el token de LocalStack al job —el job solo necesita AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY dummy (ya están en el env: del workflow) para hablar con LocalStack, nunca el LOCALSTACK_AUTH_TOKEN, que es exclusivo del contenedor de LocalStack mismo, arrancado por separado en tu host.

Olvidar --container-options en .actrc y ver un error de resolución de DNS distinto (de configuración). Qué pasa: en un sistema donde host.docker.internal no resuelve automáticamente (típicamente Linux sin Docker Desktop), sin la línea --container-options "--add-host=host.docker.internal:host-gateway" en .actrc, el error cambiaría de "no puedo conectar" a algo como "no puedo resolver el nombre de host" — una falla de DNS, no de conexión. Cómo detectarlo: si el error menciona explícitamente no poder resolver host.docker.internal, en vez de fallar la conexión después de varios segundos de intento. Cómo corregirlo: confirma que tu .actrc tiene ambas líneas —la imagen pineada del Módulo 1, y el --container-options de esta lección— antes de seguir; el Módulo 3, lección 5, profundiza en este mecanismo si necesitas el detalle completo.


Ejercicios

Ejercicio 1 — Diagnostica el fallo como lo haría alguien que recién llega a esta lección. Un colega, sin haber leído esta lección, te muestra el mismo error Could not connect to the endpoint URL: "http://host.docker.internal:4566/" y te pregunta si su workflow está roto. Respóndele con el diagnóstico correcto, en tres pasos.

Ver solución

Paso 1: revisa cuánto tardó en fallar — si fueron varios segundos (no instantáneo), la red probablemente funciona; si fue instantáneo, sospecha de un problema de DNS (--container-options faltante). Paso 2: corre docker ps -a --filter name=localstack_main — si no aparece ninguna fila, o si aparece con STATUS: Exited, LocalStack no está corriendo, que es la causa más común de este error exacto. Paso 3: si LocalStack aparece como Up, revisa que el AWS_ENDPOINT_URL del workflow apunte exactamente a http://host.docker.internal:4566 — un typo ahí (por ejemplo, localhost:4566, que no resuelve dentro del contenedor del job) produciría el mismo tipo de error.

Ejercicio 2 — Explica por qué los 8.8 segundos importan. Sin mirar esta lección, explica en una frase por qué el tiempo que tardó en fallar el step (~8.8s, no instantáneo) es información de diagnóstico real, no un detalle sin importancia.

Ver solución

El tiempo de espera revela en qué capa falló la conexión: un fallo instantáneo suele significar que el nombre de host ni siquiera resolvió (fallo de DNS, capa de resolución de nombres); un fallo después de varios segundos —el tiempo que el cliente de AWS pasó reintentando antes de rendirse— significa que el nombre resolvió y el cliente intentó conectarse a un puerto real, solo que no había ningún servicio escuchando ahí. Son dos problemas distintos, con dos soluciones distintas, y el tiempo de espera es la pista que los distingue sin necesitar ningún comando adicional.

Ejercicio 3 — Recrea el éxito, si tienes un token de LocalStack. Si tienes acceso a un token válido de LocalStack, exporta LOCALSTACK_AUTH_TOKEN, arranca el contenedor siguiendo el Paso 5 del Módulo 1/lección 8, y vuelve a correr act push -j say-hello-to-localstack. ¿Qué cambia exactamente en la salida, y qué se mantiene idéntico?

Ver solución

Lo que cambia: el último step pasa de ❌ Failure a ✅ Success, y en vez del mensaje Could not connect to the endpoint URL, deberías ver el JSON de identidad (UserId, Account: "000000000000", Arn) — el mismo formato que ya viste, representativo, en esta lección y en las dos guías anteriores. Lo que se mantiene idéntico: el YAML del workflow no cambia una sola línea, el tiempo de "Install awslocal" sigue siendo similar, y el AWS_ENDPOINT_URL sigue apuntando exactamente al mismo host.docker.internal:4566 — la única diferencia entre el fallo y el éxito es si hay un LocalStack real escuchando del otro lado, no nada del lado del workflow ni de act.


Resumen y siguiente paso

En este proyecto construiste hello-andes-cargo.yml, el primer workflow de esta guía que intenta tocar algo relacionado con Andes Cargo — y viste, con salida honesta y explicada en detalle, exactamente dónde y por qué falla sin un token de LocalStack: el camino de red (host.docker.internal) funciona, LocalStack simplemente no está corriendo. Confirmaste, con docker ps, que la causa es exactamente la esperada —no un error del workflow—, y viste el resultado representativo que obtendrías con un token válido.

Antes de avanzar deberías poder: explicar qué es host.docker.internal y por qué el workflow lo necesita; diagnosticar, en menos de tres comandos, si un fallo de awslocal dentro de un job de act es un problema de red o de LocalStack apagado; y decir de memoria las cuatro piezas que este módulo agregó a andes-cargo-infra/ (.actrc extendido, .github/act-events/pr-event.json, .secrets gitignorado, hello-andes-cargo.yml).

Con esto, el Módulo 2 queda cerrado. Tienes la anatomía completa de un workflow, la mecánica de simulación de eventos y secretos, y el primer intento real de conexión hacia Andes Cargo.

Siguiente módulo: la mitad de CI del pipeline — fmt, validate y plan corriendo automáticamente en cada Pull Request, con Terraform instalado dentro de un runner efímero, y el mismo host.docker.internal de esta lección, ahora llevando un terraform plan real hasta LocalStack.

Recursos

  1. nektosact.com — User Guide — referencia completa de act -l y act push, usadas en este proyecto.
  2. Docker Docs — Networking: use cases for host.docker.internal — documentación oficial del mecanismo de red que conecta el job de act con LocalStack.
  3. LocalStack Docs — Auth Token — la fuente del error de licencia reproducido en esta lección.
  4. terraform-and-iac-guide, Módulo 1, lección 5 (NIEVA) — el arranque limpio de LocalStack, asumido y no repetido aquí.