El pipeline de despliegue
Al final de este módulo cada push a main despliega a desarrollo, sin ninguna clave de
AWS guardada en GitHub, y con las cabeceras de caché correctas.
Trabajamos en el repositorio de la aplicación.
Paso 8.1 · Registrar las variables del entorno dev
Sección titulada «Paso 8.1 · Registrar las variables del entorno dev»Objetivo: que el workflow sepa qué rol asumir y a dónde subir.
Junta los datos desde el repositorio de infraestructura:
cd taskflow-p01-inframake bootstrap-output # deploy_role_dev_arnmake output ENV=dev # bucket_name, distribution_id, site_urlY regístralos en el repositorio de la aplicación:
cd ../taskflow-p01-appmake shellgh variable set AWS_REGION --env dev --body "us-east-1"gh variable set AWS_DEPLOY_ROLE --env dev --body "arn:aws:iam::123456789012:role/taskflow-p01-deploy-dev"gh variable set AWS_S3_BUCKET --env dev --body "taskflow-p01-dev-123456789012"gh variable set AWS_CLOUDFRONT_ID --env dev --body "E1234567890ABC"gh variable set SITE_URL --env dev --body "https://d111111abcdef8.cloudfront.net"
gh variable list --env devexitQué deberías ver: cinco variables listadas.
Paso 8.2 · Tu primer despliegue automático
Sección titulada «Paso 8.2 · Tu primer despliegue automático»Objetivo: ver el pipeline funcionar de punta a punta.
Cambia algo visible, por ejemplo el título del tablero:
seccion.append( h("h1", {}, "Tablero"), resumen, formulario(scope), filtros(scope),);seccion.append( h("h1", {}, "Mis tareas"), resumen, formulario(scope), filtros(scope),);Y súbelo por un pull request:
git switch -c cambio-titulogit add -Agit commit -m "feat(tablero): cambiar el título a Mis tareas"git push -u origin cambio-titulogh pr create --fillCuando pase la validación, fusiónalo y observa:
gh run watchQué deberías ver: el workflow corre, se autentica contra AWS sin ninguna clave, sube los archivos, y termina. Abre el sitio de desarrollo: el título nuevo está ahí.
Ver el workflow completo y cómo funciona la autenticación
name: Desplegar a dev
# Cada push a main va a desarrollo. Producción es otra historia: solo se llega# por tag, y eso vive en release.yml.on: push: branches: [main] workflow_dispatch:
# Sin esto no hay OIDC. `id-token: write` es lo que permite al job pedirle a# GitHub un token firmado para presentárselo a AWS.permissions: id-token: write contents: read
# Si llegan dos pushes seguidos, cancela el despliegue anterior: no tiene# sentido publicar una versión que ya quedó obsoleta.concurrency: group: deploy-dev cancel-in-progress: true
jobs: desplegar: runs-on: ubuntu-latest
# El nombre del entorno es parte del contrato de seguridad: determina el # claim `sub` del token OIDC, y el rol de AWS solo confía en este valor. environment: name: dev url: ${{ vars.SITE_URL }}
steps: - uses: actions/checkout@v7
- uses: actions/setup-node@v7 with: node-version: 24 cache: npm
- name: Instalar dependencias run: npm ci
- name: Verificar tipos run: npm run typecheck
- name: Compilar env: APP_VERSION: dev-${{ github.run_number }} APP_COMMIT: ${{ github.sha }} run: npm run build
- name: Autenticar contra AWS uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: ${{ vars.AWS_DEPLOY_ROLE }} aws-region: ${{ vars.AWS_REGION }} # Nombre visible en CloudTrail: agradecerás poder rastrear quién # desplegó qué cuando algo salga mal. role-session-name: gha-deploy-dev-${{ github.run_id }}
# ═══════════════════════════════════════════════════════════════════ # PASO 8.5 — Las cabeceras de caché # # El sync de abajo sube los archivos pero no dice nada sobre caché, así # que CloudFront aplica su TTL por defecto de un día. Ese es el síntoma # que vas a diagnosticar en el módulo 8. # # Ojo con --delete, que aquí NO se usa a propósito. Borrar los assets # viejos rompería a quien tenga la página abierta con el index.html # anterior, y además impediría revertir un release sin recompilar. # ═══════════════════════════════════════════════════════════════════ - name: Subir assets env: BUCKET: ${{ vars.AWS_S3_BUCKET }} run: | aws s3 sync dist/ "s3://${BUCKET}/" \ --exclude "index.html" \ --only-show-errors # PASO 8.5: agregar # --cache-control "public,max-age=31536000,immutable"
- name: Subir index.html env: BUCKET: ${{ vars.AWS_S3_BUCKET }} run: | aws s3 cp dist/index.html "s3://${BUCKET}/index.html" \ --content-type "text/html; charset=utf-8" \ --only-show-errors # PASO 8.5: agregar # --cache-control "public,max-age=0,must-revalidate"
# ═══════════════════════════════════════════════════════════════════ # PASO 8.4 — Invalidar la caché de CloudFront # # Falta a propósito. Primero vas a desplegar un cambio y comprobar que NO # lo ves en el sitio. Ahí entiendes para qué sirve invalidar. # # Descomenta cuando la guía te lo indique. # ═══════════════════════════════════════════════════════════════════
# - name: Invalidar la caché de CloudFront # env: # DISTRIBUTION: ${{ vars.AWS_CLOUDFRONT_ID }} # run: | # # Solo hace falta invalidar el index.html y la raíz. Los assets son # # inmutables y llevan hash: invalidar /* costaría dinero sin motivo. # aws cloudfront create-invalidation \ # --distribution-id "${DISTRIBUTION}" \ # --paths "/" "/index.html" \ # --query 'Invalidation.Id' \ # --output text
- name: Resumen env: SITE_URL: ${{ vars.SITE_URL }} run: | { echo "### Desplegado a dev" echo "" echo "- Versión: \`dev-${{ github.run_number }}\`" echo "- Commit: \`${GITHUB_SHA:0:7}\`" echo "- Sitio: ${SITE_URL}" } >> "$GITHUB_STEP_SUMMARY"El permiso id-token: write es lo que permite al job pedirle a GitHub un token
OIDC. Sin esa línea la autenticación falla, y es el error número uno al montar esto por
primera vez porque el mensaje no lo sugiere. Fíjate también en que contents es read:
declarar los permisos al mínimo es lo correcto.
El nombre del entorno es parte de la seguridad. GitHub construye el claim sub del
token a partir del repositorio y del entorno:
repo:TU/taskflow-p01-app:environment:dev. Esa cadena exacta es la que el rol de
AWS exige. Cambia el nombre del entorno y el despliegue deja de funcionar.
El intercambio, en cuatro pasos: el job pide a GitHub un token firmado que dice qué
repositorio, qué entorno y qué referencia de git; configure-aws-credentials lo presenta
a STS con AssumeRoleWithWebIdentity; STS lo verifica contra el proveedor OIDC y
comprueba las condiciones del rol; si todo coincide, devuelve credenciales temporales de
una hora.
No hay ninguna clave de larga duración. No hay nada que rotar.
role-session-name no es cosmético. Ese nombre aparece en CloudTrail. Cuando dentro
de tres meses alguien pregunte quién sobrescribió el sitio el martes a las once, la
respuesta está ahí con el número de ejecución exacto.
Paso 8.3 · Cambiar algo y no verlo
Sección titulada «Paso 8.3 · Cambiar algo y no verlo»Objetivo: provocar el problema de caché para entender qué lo causa.
Haz otro cambio visible y despliégalo igual que antes:
seccion.append( h("h1", {}, "Mis tareas"), resumen, formulario(scope), filtros(scope),);seccion.append( h("h1", {}, "Tablero de tareas"), resumen, formulario(scope), filtros(scope),);Espera a que el workflow termine y recarga el sitio.
Qué deberías ver: el cambio no aparece. Y no es un error del pipeline: el workflow terminó bien y el archivo está en S3.
Diagnosticarlo
Sección titulada «Diagnosticarlo»curl -sI https://d111111abcdef8.cloudfront.net/ | grep -iE 'cache-control|age|x-cache'Qué deberías ver: algo como x-cache: Hit from cloudfront y un age alto, sin
ninguna cabecera cache-control útil.
Ahí está la causa. El sync sube los archivos sin decir nada sobre caché, así que S3 no
envía Cache-Control. Y cuando el origen no dice nada, la política
Managed-CachingOptimized de CloudFront aplica su TTL por defecto, que es un día.
Tu index.html está cacheado veinticuatro horas en cada punto de presencia.
Paso 8.4 · Agregar la invalidación
Sección titulada «Paso 8.4 · Agregar la invalidación»Objetivo: forzar a CloudFront a olvidar lo que tiene guardado.
Abre el workflow y busca el marcador PASO 8.4:
# - name: Invalidar la caché de CloudFront# env:# DISTRIBUTION: ${{ vars.AWS_CLOUDFRONT_ID }}# run: |# aws cloudfront create-invalidation \# --distribution-id "${DISTRIBUTION}" \# --paths "/" "/index.html" \# --query 'Invalidation.Id' \# --output text
- name: Invalidar la caché de CloudFront env: DISTRIBUTION: ${{ vars.AWS_CLOUDFRONT_ID }} run: | aws cloudfront create-invalidation \ --distribution-id "${DISTRIBUTION}" \ --paths "/" "/index.html" \ --query 'Invalidation.Id' \ --output textDescoméntalo, súbelo por pull request, fusiona, y espera al despliegue.
Qué deberías ver: ahora sí, el cambio aparece.
Paso 8.5 · Arreglar la causa, no el síntoma
Sección titulada «Paso 8.5 · Arreglar la causa, no el síntoma»Objetivo: que la caché funcione a tu favor en lugar de tener que combatirla.
La invalidación resolvió el síntoma, pero seguimos con un problema: los assets tampoco se están cacheando bien. Con el TTL por defecto de un día, cada usuario vuelve a descargar el JavaScript entero cada mañana, aunque no haya cambiado nada.
Busca el marcador PASO 8.5 y agrega las dos cabeceras:
- name: Subir assets run: | aws s3 sync dist/ "s3://${BUCKET}/" \ --exclude "index.html" \ --cache-control "public,max-age=31536000,immutable" \ --only-show-errors
- name: Subir index.html run: | aws s3 cp dist/index.html "s3://${BUCKET}/index.html" \ --content-type "text/html; charset=utf-8" \ --cache-control "public,max-age=0,must-revalidate" \ --only-show-errorsDespliega y vuelve a mirar las cabeceras:
curl -sI https://d111111abcdef8.cloudfront.net/ | grep -i cache-controlcurl -sI https://d111111abcdef8.cloudfront.net/assets/index.eXtURQbc.js | grep -i cache-controlQué deberías ver: el index.html con max-age=0, must-revalidate, y el asset con
max-age=31536000, immutable.
Las tres decisiones que acabas de tomar
Sección titulada «Las tres decisiones que acabas de tomar»Un año para los assets. Llevan un hash del contenido en el nombre, así que su
contenido nunca cambia. immutable le dice al navegador que no se moleste ni en
preguntar.
Cero para el HTML. Es el único archivo sin hash, y el que apunta a la versión vigente de los assets. Si lo cachearas, desplegarías y no verías nada, que es exactamente lo que te pasó en el paso 8.3.
El orden importa. Los assets primero, el index.html al final. Si se subiera antes,
durante unos segundos el HTML referenciaría archivos que todavía no existen, y quien
cargara la página en ese momento vería una pantalla en blanco. Subirlo al final lo
convierte en el interruptor que activa la nueva versión.
Paso 8.6 · Comprobar el mínimo privilegio
Sección titulada «Paso 8.6 · Comprobar el mínimo privilegio»Objetivo: verificar que este rol no puede hacer daño más allá de dev.
make shellaws iam get-role-policy \ --role-name taskflow-p01-deploy-dev \ --policy-name taskflow-p01-dev-deployQué deberías ver: los ARN del bucket y la distribución de desarrollo, únicamente. Sin comodines.
Ese rol no puede crear infraestructura, no puede leer otros buckets, y no puede tocar producción. Si el token de un workflow se filtrara, el daño posible queda acotado a sobrescribir el sitio de desarrollo.
Resumen de comandos
Sección titulada «Resumen de comandos»# variables del entorno devmake shellgh variable set AWS_REGION --env dev --body "us-east-1"# ... las otras cuatrogh variable list --env dev
# ciclo de trabajogit switch -c mi-cambiogit add -A && git commit -m "feat: ..."git push -u origin mi-cambiogh pr create --fill# fusionar, y luegogh run watch
# diagnosticar cachécurl -sI <site_url>/ | grep -iE 'cache-control|age|x-cache'
# comprobar los permisos del rolaws iam get-role-policy \ --role-name taskflow-p01-deploy-dev \ --policy-name taskflow-p01-dev-deploy