Módulo 4: Secrets Environments And Identity

7. Manos a la obra: credenciales dummy para LocalStack

Descripción

Es momento de corregir, en el ci.yml real de Andes Cargo, exactamente el antipatrón que nombró la lección 2: las credenciales dummy de LocalStack, escritas directamente en el YAML desde el Módulo 3, van a migrar a un Secret real, leído con la sintaxis secrets.AWS_ACCESS_KEY_ID que aprendiste en la lección 3. Vas a correr el pipeline completo dos veces —antes y después del cambio— y confirmar, con salida literal de act, que el comportamiento es idéntico: el cambio no rompe nada, porque nunca se trató de que el pipeline funcionara distinto, se trató de que la credencial dejara de vivir donde no debería.

Honestidad explícita, antes de escribir una sola línea: que test/test viva en un .secrets gitignorado en vez de en ci.yml es una mejora real de higiene, no una solución de seguridad completa — porque, como ya sabes desde la lección 5, sigue siendo una credencial de larga vida en el sentido técnico, aunque no tenga ningún valor real que proteger. Esto es aceptable únicamente porque el destino final es LocalStack, que no valida nada contra ninguna cuenta real de AWS. Contra una cuenta AWS real, ni siquiera esta versión mejorada sería suficiente — haría falta OIDC (lección 5), no solo mover la clave a un Secret.

Conexión con el módulo

Esta lección construye directamente sobre el ci.yml que dejaste terminado en el Módulo 3, lección 8. No agrega ningún step nuevo, no cambia el resultado del plan — cambia exclusivamente de dónde vienen las dos líneas de credenciales en el bloque env:. El proyecto de este módulo (lección 8) hereda este mismo ci.yml ya migrado, y agrega el documento de qué haría falta para el salto completo a OIDC en un despliegue real.


Paso 1 — El antes: confirma el antipatrón, corriendo

Antes de cambiar nada, confirma que el ci.yml heredado del Módulo 3 sigue corriendo exactamente como lo dejaste. Sobre andes-cargo-infra/:

act pull_request -e .github/act-events/pr-event.json -j terraform-checks --secret-file .secrets

Qué esperar (extracto literal, ejecutado para escribir esta lección — los pasos de fmt, init y validate corren con éxito, la conexión a LocalStack falla honestamente, y el plan de todas formas se completa):

[ci/terraform-checks]   ✅  Success - Main Terraform format check [131.9725ms]
[ci/terraform-checks] ⭐ Run Main Terraform init
[ci/terraform-checks]   ✅  Success - Main Terraform init [15.252881667s]
[ci/terraform-checks] ⭐ Run Main Terraform validate
[ci/terraform-checks]   ✅  Success - Main Terraform validate [1.748320208s]
[ci/terraform-checks] ⭐ Run Main Install awslocal
[ci/terraform-checks]   ✅  Success - Main Install awslocal [12.325428291s]
[ci/terraform-checks] ⭐ Run Main Confirm the runner can reach LocalStack on the host
[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 [4.3284415s]
[ci/terraform-checks] ⭐ Run Main Install tflocal
[ci/terraform-checks]   ✅  Success - Main Install tflocal [2.5084035s]
[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.300556083s]
[ci/terraform-checks] ⭐ Run Main Publish the plan to the job summary
[ci/terraform-checks]   ✅  Success - Main Publish the plan to the job summary [60.103291ms]
[ci/terraform-checks] 🏁  Job succeeded

Dos cosas honestas para leer con cuidado antes de seguir. Primera: el step "Confirm the runner can reach LocalStack" falla con Could not connect to the endpoint URL — el mismo error, por la misma razón, que ya viste en el Módulo 2, lección 8: no hay ningún LocalStack corriendo en el host en este momento (sin LOCALSTACK_AUTH_TOKEN exportado, el contenedor no arranca). El continue-on-error: true de ese step, ya presente en ci.yml desde el Módulo 3, es justamente lo que permite que el job siga adelante en vez de detenerse ahí. Segunda, la más importante para esta lección: terraform plan (el step siguiente) sí tiene éxito, con Plan: 12 to add, 0 to change, 0 to destroy — porque, sobre un state vacío, generar un plan de creación no necesita ninguna llamada de red real a AWS ni a LocalStack; el provider ya tiene todo lo que necesita localmente (recuerda skip_requesting_account_id = true en providers.tf, heredado de terraform-and-iac-guide). El plan es real y correcto; la verificación de conectividad, por separado, refleja honestamente que LocalStack no está arriba ahora mismo.


Paso 2 — Migrar el bloque env: de ci.yml

Abre .github/workflows/ci.yml. El bloque que vas a cambiar es exactamente el que la lección 1 de este módulo señaló:

    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

Reemplaza las dos primeras líneas por la sintaxis secrets.<NOMBRE> de la lección 3:

    env:
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      AWS_DEFAULT_REGION: us-east-1
      AWS_ENDPOINT_URL: http://host.docker.internal:4566

Nota lo que no cambió: AWS_DEFAULT_REGION y AWS_ENDPOINT_URL siguen escritos directamente — no son credenciales, son configuración de red y de región, valores que no representan ningún riesgo de exposición aunque cualquiera los lea. Migrar a secrets.<NOMBRE> es específico de las dos líneas que sí son credenciales; convertir configuración no sensible en un Secret innecesariamente solo agregaría fricción sin ningún beneficio de seguridad real.

El .secrets que ya existe en la raíz de andes-cargo-infra/ desde el Módulo 2, lección 7, ya tiene exactamente los nombres correctos:

AWS_ACCESS_KEY_ID=test
AWS_SECRET_ACCESS_KEY=test

No hace falta crear ni modificar .secrets — el nombre a la izquierda de cada = ya coincide, letra por letra, con el nombre dentro de secrets.<NOMBRE> del YAML que acabas de escribir. Este es el mismo requisito de coincidencia exacta que advirtieron el Módulo 2, lección 7, y la lección 3 de este módulo.


Paso 3 — El después: corre de nuevo, y confirma que el comportamiento no cambió

act pull_request -e .github/act-events/pr-event.json -j terraform-checks --secret-file .secrets

Qué esperar (extracto literal, ejecutado para escribir esta lección):

[ci/terraform-checks]   ✅  Success - Main Terraform format check [125.128ms]
[ci/terraform-checks] ⭐ Run Main Terraform init
[ci/terraform-checks]   ✅  Success - Main Terraform init [15.666698709s]
[ci/terraform-checks] ⭐ Run Main Terraform validate
[ci/terraform-checks]   ✅  Success - Main Terraform validate [1.754935459s]
[ci/terraform-checks] ⭐ Run Main Install awslocal
[ci/terraform-checks]   ✅  Success - Main Install awslocal [9.942404542s]
[ci/terraform-checks] ⭐ Run Main Confirm the runner can reach LocalStack on the host
[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 [8.658047584s]
[ci/terraform-checks] ⭐ Run Main Install tflocal
[ci/terraform-checks]   ✅  Success - Main Install tflocal [2.438441625s]
[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.307120666s]
[ci/terraform-checks] ⭐ Run Main Publish the plan to the job summary
[ci/terraform-checks]   ✅  Success - Main Publish the plan to the job summary [66.274334ms]
[ci/terraform-checks] 🏁  Job succeeded

Idéntico en cada paso que importa — mismos steps, mismo Plan: 12 to add, 0 to change, 0 to destroy, mismo Job succeeded final. Los tiempos de cada step varían unos milisegundos entre corridas (es normal, cada ejecución de un contenedor tiene variación menor de rendimiento), pero la estructura, los resultados, y el contenido del plan son exactamente los mismos. Esto es, con salida real, la prueba de la lección 3: act --secret-file .secrets entrega el valor de secrets.AWS_ACCESS_KEY_ID de forma indistinguible de tenerlo escrito directamente — la única diferencia es dónde vive ese valor, nunca escrito en el archivo que Git rastrea.


La versión que verías con LOCALSTACK_AUTH_TOKEN real (representativa)

Con LocalStack corriendo de verdad en el host (Módulo 1, lección 8, Paso 5), el step "Confirm the runner can reach LocalStack on the host" pasaría de ❌ Failure a ✅ Success, devolviendo el mismo JSON de identidad que ya confirmaron las guías anteriores de este ecosistema:

Qué esperar (representativo — mismo formato ya confirmado 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):

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

Ni una sola línea del ci.yml migrado en esta lección necesitaría cambiar para producir esta salida — la diferencia entre el fallo real que viste arriba y este resultado representativo es exclusivamente si LocalStack está arriba del otro lado de host.docker.internal:4566, exactamente la misma distinción que ya explicó el Módulo 2, lección 8.


Paso 4 — Commitear la migración

git add -A
git commit -m "Migrate ci.yml credentials from hardcoded values to secrets.AWS_ACCESS_KEY_ID / secrets.AWS_SECRET_ACCESS_KEY"
git log --oneline -3

Qué esperar (representativo en los hashes, literal en la estructura):

a1b2c3d Migrate ci.yml credentials from hardcoded values to secrets.AWS_ACCESS_KEY_ID / secrets.AWS_SECRET_ACCESS_KEY
7b6421e ci.yml: publish the plan to the job summary
dfcbfce ci.yml: install tflocal and run terraform plan

Confirma, una última vez, que .secrets sigue sin aparecer en ningún lado del historial:

git log --all --full-history -- .secrets

Qué esperar (literal) — ninguna salida, ningún commit encontrado, porque .secrets nunca dejó de estar gitignorado desde el momento exacto en que se creó en el Módulo 2:

(sin salida)

Errores comunes

Migrar AWS_DEFAULT_REGION o AWS_ENDPOINT_URL a Secrets también, "para ser consistente" (de alcance). Qué pasa: alguien, con el hábito recién instalado, convierte también la región y el endpoint en Secrets, aunque no son credenciales. Por qué pasa: después de aprender la regla "las credenciales van en Secrets", es fácil sobregeneralizar a "todo lo que está en env: debería ser un Secret". Cómo detectarlo: si tu .secrets tiene líneas como AWS_DEFAULT_REGION=us-east-1, que no son ningún secreto en absoluto. Cómo corregirlo: un Secret existe para proteger un valor cuya exposición representa un riesgo real. us-east-1 o http://host.docker.internal:4566 no representan ningún riesgo si alguien los lee — convertirlos en Secrets solo agrega una capa de indirección sin ningún beneficio, y hace más difícil, no más fácil, leer el ci.yml para entender qué región o endpoint usa.

Esperar que la migración cambie el resultado del plan (de expectativa). Qué pasa: alguien, después de correr el "antes" y el "después", nota que los números del plan son idénticos y se pregunta si algo salió mal, porque esperaba ver una diferencia que confirmara que "el cambio hizo algo". Por qué pasa: es intuitivo esperar que un cambio de código produzca un resultado visible distinto. Cómo detectarlo: si buscas una diferencia en Plan: 12 to add, 0 to change, 0 to destroy entre las dos corridas de esta lección. Cómo corregirlo: la identidad del resultado es la prueba de que el cambio funcionó correctamente — mover una credencial de un lugar a otro, sin cambiar su valor, nunca debería cambiar lo que Terraform planea hacer. Si el plan hubiera cambiado, eso habría sido la señal de que algo salió mal en la migración, no de que salió bien.


Ejercicios

Ejercicio 1 — Rompe la migración a propósito, y diagnostica el error. Cambia intencionalmente el nombre de una de las claves en .secrets (por ejemplo, de AWS_ACCESS_KEY_ID a AWS_ACCES_KEY_ID, con un typo) y vuelve a correr el comando del Paso 3. ¿Qué cambia en la salida?

Ver solución

El job sigue corriendo hasta el final —terraform plan con Plan: 12 to add, 0 to change, 0 to destroy— porque, como viste en el Paso 1, la generación de un plan sobre un state vacío no necesita ninguna credencial real para tener éxito. Lo que sí cambiaría, si tuvieras un LOCALSTACK_AUTH_TOKEN real y LocalStack corriendo, es el step de conectividad: secrets.AWS_ACCESS_KEY_ID se resolvería a una cadena vacía (porque .secrets ya no tiene ninguna línea con ese nombre exacto), y awslocal sts get-caller-identity fallaría con un error de credenciales faltantes en vez de —o además de— cualquier error de conexión. Este es exactamente el error de sintaxis que advirtió la lección 3: el nombre a la izquierda del = en .secrets tiene que coincidir letra por letra con el nombre dentro de secrets.<NOMBRE>.

Ejercicio 2 — Explica por qué esta lección no resuelve el antipatrón por completo. Un colega, después de ver el ci.yml migrado, dice: "genial, ya está resuelto el problema de seguridad que mencionaba VALIDACION". ¿Estás de acuerdo? Responde en dos o tres frases.

Ver solución

Una respuesta completa suena, más o menos, así: "Parcialmente — esta migración resuelve el problema de que la credencial esté escrita en un archivo que Git rastrea, que es un paso real y correcto. Pero secrets.AWS_ACCESS_KEY_ID sigue siendo, técnicamente, una credencial de larga vida guardada en algún lugar (el .secrets local, o un Secret real de GitHub en un repositorio real) — solo que mejor protegida. La resolución completa del antipatrón que cita VALIDACION es OIDC (lección 5), donde no existe ninguna credencial de larga vida que guardar en absoluto. Esta lección practica el paso intermedio correcto, no el destino final."

Ejercicio 3 — Predice el comportamiento sin --secret-file explícito. Basándote en lo que aprendiste en el Módulo 2, lección 7, sobre el comportamiento por defecto de act, predice qué pasaría si corrieras el comando del Paso 3 sin el flag --secret-file .secrets, y por qué.

Ver solución

El resultado sería idéntico al que muestra esta lección — act busca, por defecto, un archivo llamado exactamente .secrets en el directorio de trabajo actual, aunque no se pase el flag explícitamente (confirmado en el Módulo 2, lección 7, contra act --help). El comportamiento correcto de esta lección —seguir escribiendo --secret-file .secrets de forma explícita— no cambia el resultado, pero sí documenta la intención directamente en el comando, en vez de depender de que quien lo corre sepa de memoria una convención implícita de act.


Resumen y siguiente paso

En esta lección migraste el ci.yml real de Andes Cargo, con salida literal de act antes y después del cambio, confirmando que mover las credenciales de valores escritos directamente a secrets.AWS_ACCESS_KEY_ID/secrets.AWS_SECRET_ACCESS_KEY —leídos desde .secrets con act --secret-file— no cambia el resultado del pipeline, solo dónde vive la credencial. Viste, de nuevo, el fallo honesto de conectividad a LocalStack (por no tener un token exportado en esta sesión) y el plan real completándose de todas formas, sin necesitar esa conexión. Y reconociste el límite de esta mejora: sigue siendo una credencial de larga vida, aceptable únicamente porque el destino es LocalStack.

Antes de avanzar deberías poder: migrar cualquier bloque env: de credenciales a la sintaxis secrets.<NOMBRE>; explicar por qué el plan del Paso 1 tiene éxito a pesar del fallo de conectividad; y explicar, sin dudar, por qué esta migración no es lo mismo que resolver el antipatrón por completo.

El proyecto de este módulo (lección 8) hereda este ci.yml ya migrado, y agrega el documento —sin construirlo— de qué secreto, qué ambiente, y qué rol OIDC usaría Andes Cargo en un despliegue real contra una cuenta AWS de verdad.

Recursos

  1. nektosact.com — User Guide — referencia completa de act --secret-file, usada en esta lección.
  2. GitHub Docs — Using secrets in GitHub Actions — el modelo real de Secrets, ya citado en la lección 3.
  3. Módulo 3, lección 8 de esta guía (08-project-andes-cargos-ci-workflow.md) — el ci.yml original que esta lección migra, con los pasos fmt/validate/plan que no cambian.