Módulo 3: The Iac Pipeline Fmt Validate Plan
5. Conectando el runner a LocalStack a través del host
Descripción
fmt y validate —lección 4— nunca hablan con ningún proveedor real: trabajan exclusivamente sobre el texto del HCL. terraform plan —lección 6— es distinto: necesita, para calcular con precisión qué cambiaría, poder alcanzar el mismo LocalStack que corre en tu host. Esta lección construye y verifica esa conexión de red, antes de confiar en ella para un plan real: extiendes .actrc con el flag que ya usaste en el Módulo 2, agregas un step que intenta hablar con LocalStack (awslocal s3 ls) dentro del mismo job de ci.yml, y lees, con honestidad completa, qué pasa cuando ese intento se topa con un LocalStack que no está corriendo — el mismo patrón exacto que ya viviste en el Módulo 2, lección 8.
Conexión con el módulo
La lección 4 dejó ci.yml con dos steps que nunca necesitan red. Esta lección agrega el tercero, que sí la necesita, y verifica el camino antes de que la lección 6 dependa de él para algo más importante que un s3 ls. La lección 6 va a mostrarte, además, una segunda cara de esta historia: por qué el terraform plan específico de Andes Cargo no necesita, en este caso particular, que esta conexión esté funcionando — un matiz real que solo tiene sentido después de ver, aquí, por qué normalmente sí haría falta.
Analogía: probar el cable antes de confiar en el electrodoméstico
Antes de enchufar algo importante —una heladera cargada de mercadería, no una lámpara de escritorio— a una toma de corriente nueva, cualquier electricista razonable prueba primero con algo simple y barato: un tester, una lámpara de mano. Si el tester no prende, mejor descubrirlo ahí, no después de haber cargado la heladera. awslocal s3 ls es ese tester: un comando de solo lectura, sin ningún costo si falla, que confirma si el cable —el camino de red entre el contenedor del job y LocalStack— está vivo, antes de que confíes en él para algo con más peso, como el terraform plan de la lección 6.
Paso 1 — .actrc, con la pieza de red
Tu .actrc, en la raíz de andes-cargo-infra/, ya tiene esta segunda línea desde el Módulo 2 (lección 8) — confírmalo:
cat .actrc
Qué esperar (literal, heredado, sin cambios en esta lección):
-P ubuntu-latest=catthehacker/ubuntu:act-latest
--container-options "--add-host=host.docker.internal:host-gateway"
La segunda línea es la que hace posible todo lo que sigue: le dice a act que, al crear el contenedor de cada job, agregue una entrada de DNS que resuelve host.docker.internal hacia la puerta de enlace del host —necesario en Linux; en Docker Desktop de macOS/Windows suele resolver sin este flag, pero esta guía lo fija explícitamente para no depender de esa diferencia de plataforma—. Sin esta línea, el nombre host.docker.internal simplemente no resolvería dentro del contenedor del job, y el error que verías más abajo sería de DNS ("no puedo resolver el nombre"), no de conexión ("resolví el nombre, pero nadie contesta") — una distinción que ya viste en el Módulo 2, lección 8, y que vas a confirmar de nuevo en esta lección.
Paso 2 — Un step que confirma el camino, sin bloquear el resto del job
Agrega dos steps a ci.yml, después de 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
Fíjate en continue-on-error: true — un campo de step que no viste hasta ahora. Le dice a act (y a GitHub Actions real): "si este step falla, marca la falla, pero sigue corriendo el resto del job, no lo detengas". Es una decisión deliberada de esta lección, y vale la pena explicar por qué antes de correrlo: este step depende de que LocalStack esté corriendo y con licencia válida —algo fuera del control de Terraform o de act—, así que un fallo aquí no debería bloquear fmt/validate (que ya pasaron) ni, como vas a confirmar en la lección 6, el plan que sigue. Es un diagnóstico, no un guardián.
Paso 3 — Corriendo el job completo, con LocalStack apagado
Confirma primero que no tienes LocalStack corriendo en este momento —el estado por defecto de esta lección, igual que en el Módulo 2, lección 8—:
docker ps -a --filter name=localstack_main
Qué esperar (literal, sin ninguna fila — ningún contenedor de LocalStack corriendo):
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
Ahora corre el job completo:
act pull_request -e .github/act-events/pr-event.json -j terraform-checks
Qué esperar (salida literal, ejecutada para escribir esta lección — fíjate en el tiempo exacto del step que falla, y en que el job completo termina en verde igual):
[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] 🐳 docker exec cmd=[bash -e /var/run/act/workflow/5] user= workdir=
[ci/terraform-checks] | 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
[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] 🐳 docker exec cmd=[bash -e /var/run/act/workflow/6] user= workdir=
[ci/terraform-checks] |
[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 [11.315293s]
[ci/terraform-checks] ⭐ Run Complete job
[ci/terraform-checks] ✅ Success - Complete job
[ci/terraform-checks] 🏁 Job succeeded
Esto es un fallo real, honesto, y exactamente lo que esperarías sin LocalStack corriendo — léelo con el mismo cuidado que en el Módulo 2, lección 8, porque la lectura es idéntica.
Leyendo el fallo: qué SÍ funcionó, y qué no
El step tardó 11.3 segundos en fallar — no falló al instante. Ese tiempo es, otra vez, la prueba de que la pieza que esta lección construye —.actrc con --container-options, host.docker.internal en el env del job— sí funciona: el nombre resolvió correctamente hacia tu host, y awslocal intentó de verdad conectarse al puerto 4566, reintentando según su política por defecto (la razón de los ~11 segundos, no una casualidad), antes de reportar Could not connect to the endpoint URL — el mensaje específico de "llegué hasta la puerta, pero no hay nadie del otro lado", no "no encontré la calle". Si --container-options faltara en .actrc, o si el env del job apuntara mal, verías un error completamente distinto: uno de resolución de DNS, típicamente instantáneo, no después de varios segundos de reintento.
Confírmalo de forma independiente, igual que en el Módulo 2:
docker ps -a --filter name=localstack_main
Qué esperar (literal): sin ninguna fila — LocalStack no está corriendo, la razón exacta de este fallo, sin ninguna relación con act, con .actrc, ni con el YAML de ci.yml.
Y fíjate en la última línea del bloque de arriba: 🏁 Job succeeded. A pesar de que Confirm the runner can reach LocalStack on the host falló, el job completo terminó en verde — exactamente lo que continue-on-error: true promete: la falla queda registrada (en GitHub real, aparecería como una marca de advertencia en ese step específico, sin poner el job entero en rojo), pero no detiene nada más.
La versión que verías con un token válido (representativo)
Con LOCALSTACK_AUTH_TOKEN correctamente exportado y LocalStack arrancado en tu host —siguiendo el Paso 5 del Módulo 1, lección 8—, el mismo awslocal s3 ls, corrido dentro del mismo job de act, devolvería:
Qué esperar (representativo — mismo formato ya confirmado, dos veces, en el Módulo 1 y el Módulo 2 de esta guía; sin una ejecución en vivo contra un token válido en este momento):
2026-08-10 12:00:00 andes-cargo-shipment-docs
Una única fila, el nombre del bucket que terraform-and-iac-guide ya declaró y aplicó en su capstone —si tu LocalStack conserva ese estado de una sesión anterior con el plan adecuado, o una lista vacía si es una sesión nueva sin ningún apply corrido todavía—. La diferencia entre esto y el error de arriba no es de configuración del workflow ni de act — es, exclusivamente, si LocalStack está corriendo del otro lado de host.docker.internal:4566.
Profundización: por qué este step no bloquea, pero sigue siendo valioso
Vale la pena ser preciso sobre el rol de este step dentro del pipeline completo. No es una verificación que garantice que el plan de la lección 6 va a funcionar —de hecho, vas a descubrir en esa lección un matiz interesante: el plan específico de Andes Cargo, sobre un estado completamente vacío, no necesita esta conexión para calcular correctamente qué crear—. Entonces, ¿para qué sirve?
Sirve como diagnóstico temprano y barato de un problema de infraestructura del propio pipeline —no del proyecto de Andes Cargo—. El día que este proyecto tenga un data source que sí necesite leer algo real de AWS/LocalStack (por ejemplo, si en el futuro alguien agrega data "aws_s3_bucket" "existing" para referenciar un bucket creado por fuera de Terraform), ese plan sí va a depender de que este camino de red funcione — y si ese día llega y la conexión está rota, prefieres enterarte en un step de 11 segundos marcado como advertencia, no en un plan completo que falla sin pistas claras de por qué. continue-on-error: true es exactamente el balance correcto: información visible, sin bloquear un pipeline que, hoy, no depende de ella para tener éxito.
Errores comunes
Quitar continue-on-error: true y que todo el job falle sin necesidad (de configuración). Qué pasa: alguien copia este step sin el campo continue-on-error, y descubre que ci.yml completo se pone en rojo cada vez que corre sin LocalStack activo —incluso cuando el plan de la lección 6 no necesitaría esa conexión para tener éxito—. Cómo detectarlo: el job falla exactamente en el step de awslocal, y ningún step posterior corre. Cómo corregirlo: confirma que continue-on-error: true está presente, con la misma indentación que run:, dentro del mismo step.
Interpretar Failed but continue next step como un mensaje de error real (de lectura). Qué pasa: alguien ve esa línea en la salida de act y la lee como un problema adicional, distinto del Could not connect to the endpoint URL de arriba. Cómo detectarlo: la línea aparece justo después del error real, en un tono neutral, no con el símbolo ❌. Cómo corregirlo: es exactamente lo que dice — act confirmando que, por el continue-on-error: true del step, va a seguir con el step siguiente en vez de detener el job. Es información sobre el comportamiento del pipeline, no un segundo error.
Confundir AWS_ENDPOINT_URL a nivel de env de job con una variable que solo awslocal necesita (de alcance, adelanto de la lección 6). Qué pasa: alguien piensa que esta variable es exclusiva del step de awslocal, y la mueve al env de ese step específico en vez de dejarla a nivel de job. Cómo corregirlo: déjala a nivel de job — la lección 6 la reutiliza para tflocal, que también la lee, y la jerarquía de tres niveles de env del Módulo 2 (lección 2) existe exactamente para este caso: una variable que más de un step necesita.
Ejercicios
Ejercicio 1 — Diagnostica sin docker ps. Sin correr docker ps -a --filter name=localstack_main, ¿qué otro dato de la salida de act de esta lección te permite distinguir "LocalStack apagado" de "un typo en AWS_ENDPOINT_URL"?
Ver solución
El tiempo que tardó en fallar el step ([11.315293s]). Un typo en AWS_ENDPOINT_URL que resultara en un nombre de host que no resuelve en absoluto (por ejemplo, hostdocker.internal, sin el punto) fallaría casi instantáneamente, con un error de resolución de DNS — no después de varios segundos de reintento. El tiempo de espera prolongado es la firma específica de "el nombre resolvió, el intento de conexión fue real, pero nadie contestó del otro lado" — exactamente lo que aprendiste a leer en el Módulo 2, lección 8, y confirmaste de nuevo aquí.
Ejercicio 2 — Explica continue-on-error a un colega que nunca lo vio. En dos frases, sin copiar la definición de esta lección, explica qué hace continue-on-error: true y por qué esta lección lo usa específicamente en el step de awslocal.
Ver solución
Una respuesta completa suena, más o menos, así: "continue-on-error: true le dice al motor que, si este step en particular falla, lo marque como fallido pero siga corriendo el resto del job igual, en vez de detenerlo ahí. Esta lección lo usa porque el step de awslocal depende de algo fuera del control del pipeline —si LocalStack está corriendo o no—, y no tiene sentido que esa dependencia externa bloquee verificaciones que sí están completamente bajo control del propio HCL, como fmt, validate, o el plan que no necesita esta conexión."
Ejercicio 3 — Predice el comportamiento de un segundo awslocal distinto. Si agregaras un segundo step, awslocal dynamodb list-tables, sin continue-on-error: true, inmediatamente después del step de esta lección, y LocalStack sigue apagado, ¿qué esperas que pase con el job completo?
Ver solución
Ese segundo step fallaría con el mismo tipo de error (Could not connect to the endpoint URL, después de varios segundos), pero esta vez, sin continue-on-error: true, el job se detendría ahí — cualquier step posterior a ese (como el terraform plan de la lección 6, si viniera después en el archivo) no llegaría a correr, y el job completo terminaría en rojo, no en verde. Es la diferencia exacta entre los dos comportamientos que viste en esta lección y en la lección 4: un step bloqueante detiene todo lo que sigue; uno con continue-on-error: true no.
Resumen y siguiente paso
En esta lección extendiste ci.yml con un step que prueba, de forma barata y no bloqueante, el camino de red hacia LocalStack a través de host.docker.internal. Confirmaste, con salida literal y honesta, que ese camino funciona —la conexión se intenta de verdad, tarda varios segundos, y falla porque LocalStack no está corriendo, no porque la red esté mal configurada—, y viste continue-on-error: true en acción: el job completo terminó en verde a pesar del fallo de ese step específico.
Antes de avanzar deberías poder: explicar qué hace la segunda línea de .actrc y por qué Linux la necesita mientras Docker Desktop a veces no; leer el tiempo de espera de un fallo de conexión como información de diagnóstico; y decidir cuándo un step debería llevar continue-on-error: true y cuándo no.
La lección 6 usa exactamente esta misma conexión —AWS_ENDPOINT_URL: http://host.docker.internal:4566— para el primer terraform plan real de esta guía, y te muestra un matiz que solo tiene sentido después de esta lección: por qué, en este caso particular, ese plan no termina necesitando que LocalStack esté corriendo.
Recursos
- Docker Docs — Networking: connect from a container to a service on the host — documentación oficial de
host.docker.internal, la pieza central de esta lección. - nektos/act — issue #1835 y issue #2412 — la discusión donde se confirma el flag
--container-options "--add-host=host.docker.internal:host-gateway"usado en.actrc. - GitHub Docs — Workflow syntax:
jobs.<job_id>.steps[*].continue-on-error— referencia oficial del campo usado en esta lección. - LocalStack Docs — Auth Token — la fuente del error de licencia, ya visto en el Módulo 1 y el Módulo 2, relevante para entender por qué LocalStack no está corriendo en esta lección.