Una SPA sin framework
Reactividad con signals, router propio sobre la History API, y un puente al DOM de cien líneas que escribiste tú. Doce kilobytes de JavaScript.
Objetivo de este módulo: cerrar bien. Entender el costo, saber qué mirar cuando algo falle, y no dejar nada corriendo.
Recapitulando, en un día:
Una SPA sin framework
Reactividad con signals, router propio sobre la History API, y un puente al DOM de cien líneas que escribiste tú. Doce kilobytes de JavaScript.
Infraestructura como código
Bucket privado con OAC, CloudFront con TLS, certificado que se renueva solo, y dos entornos con estado separado. Todo reproducible desde cero.
Despliegue sin credenciales
OIDC en lugar de claves de acceso. Roles de mínimo privilegio, acotados a un bucket y una distribución exactos.
Releases auditables
Versión inmutable por tag, aprobación humana para producción, y reversión en un minuto sin recompilar.
Y sin una sola instancia que administrar.
Con tráfico de taller, el costo mensual de este montaje está en el orden de centavos. Vale la pena entender de dónde sale cada partida, porque la intuición suele estar mal calibrada.
| Partida | Cómo se cobra | En este taller |
|---|---|---|
| Almacenamiento S3 | por GB al mes | despreciable, el sitio pesa kilobytes |
| Peticiones S3 | por millón | despreciable, CloudFront absorbe casi todo |
| Transferencia de CloudFront | por GB servido | la partida principal |
| Peticiones de CloudFront | por millón | despreciable con tráfico bajo |
| Certificado de ACM | gratis para CloudFront | cero |
| Invalidaciones | primeras 1000 rutas al mes gratis | cero, invalidamos dos por despliegue |
| Estado en S3 | por GB y peticiones | céntimos |
Compáralo con lo que costaría una instancia pequeña corriendo veinticuatro horas al día para servir los mismos archivos estáticos, más su volumen, más el balanceador si quieres alta disponibilidad.
No montamos observabilidad en este taller, pero conviene saber qué existe y dónde mirar cuando algo falle.
Los logs de acceso de CloudFront se pueden enviar a S3 o a CloudWatch. Es lo que responde «cuántos usuarios vieron el error» y «desde qué países».
Las métricas de CloudFront en CloudWatch dan tasa de error 4xx y 5xx, y la tasa de
acierto de caché. Esa última es el indicador de si tu estrategia de Cache-Control
está funcionando: si es baja, algo se está pidiendo al origen que no debería.
CloudTrail registra quién hizo qué en la cuenta. Es donde aparecen los
role-session-name de tus despliegues, y por eso vale la pena que sean descriptivos.
El resumen de cada ejecución en GitHub Actions, que los workflows escriben con
GITHUB_STEP_SUMMARY. Versión, commit y URL, sin abrir los logs.
| Síntoma | Causa habitual |
|---|---|
| 403 con XML al recargar una ruta | falta el custom_error_response en CloudFront |
| El despliegue pasa pero no ves los cambios | el index.html se está cacheando |
Not authorized to perform sts:AssumeRoleWithWebIdentity |
falta id-token: write, o el nombre del entorno no coincide con el claim sub |
| El release por tag falla al autenticar | el tag no coincide con refs/tags/v*, o se disparó desde una rama |
| Bucles de redirección con dominio propio | hay un proxy/CDN intermedio delante de CloudFront; con Route 53 usa alias directo |
Cannot find native binding |
se mezclaron imágenes de Docker con libc distintas |
| El bucket responde 200 directo | falta el bloqueo de acceso público, revísalo de inmediato |
Hazlo en este orden. Los entornos primero, el bootstrap al final, porque contiene el bucket de estado del que dependen los demás.
Vacía los buckets del sitio. Terraform no puede destruir un bucket con objetos:
cd taskflow-p01-inframake shellaws s3 rm "s3://taskflow-p01-prod-123456789012" --recursiveaws s3 rm "s3://taskflow-p01-dev-123456789012" --recursiveexitDestruye producción y desarrollo:
make destroy ENV=prodmake destroy ENV=devCloudFront tarda en deshabilitar y eliminar una distribución. Es normal que este paso se lleve varios minutos.
Con dns_mode = "route53", Terraform creó los registros DNS (validación del
certificado y alias del subdominio), así que el destroy de prod ya los eliminó por
ti. No hay que borrar nada a mano en un panel externo.
Destruye el bootstrap. Antes hay que vaciar el bucket de estado, que tiene
force_destroy = false a propósito:
make shellaws s3 rm "s3://taskflow-p01-tfstate-123456789012" --recursiveexitcd bootstrapterraform destroymake shellaws s3 ls | grep taskflowaws cloudfront list-distributions --query "DistributionList.Items[].Comment" --output textaws iam list-roles --query "Roles[?starts_with(RoleName, 'taskflow')].RoleName" --output textaws acm list-certificates --region us-east-1 --query "CertificateSummaryList[].DomainName" --output textLos cuatro deberían salir vacíos de tus recursos. Revisa además la consola de facturación en un par de días: es la comprobación definitiva.
Tres ideas que sirven más allá de este taller.
Una SPA no necesita un servidor. Si tu aplicación compila a archivos estáticos, ponerla en una instancia te da trabajo de operación sin darte nada a cambio. La API es otra discusión.
La política de seguridad va en la infraestructura, no en el pipeline. Un archivo YAML lo cambia cualquiera con permiso de escritura. Una condición en la política de confianza de un rol de IAM, no.
Los artefactos inmutables son lo que hace posible revertir. Assets con hash en el
nombre, un index.html como interruptor, y nada de --delete. Esa combinación
convierte una reversión en un minuto en lugar de en una reconstrucción.
El workshop 02 le da backend a esta misma SPA: API Gateway con HTTP API, Lambda en
TypeScript y DynamoDB. El estado deja de vivir en localStorage y se reemplaza el
efecto de persistencia por llamadas a la API, sin tocar el resto de la aplicación.
Después vienen autenticación con Cognito, observabilidad, y Terraform desde el pipeline con los permisos acotados que ese tema requiere.
cd taskflow-p01-inframake shell
# vaciar los buckets del sitioaws s3 rm "s3://taskflow-p01-prod-123456789012" --recursiveaws s3 rm "s3://taskflow-p01-dev-123456789012" --recursiveexit
make destroy ENV=prodmake destroy ENV=dev
# el destroy de prod ya eliminó los registros de Route 53; luegomake shellaws s3 rm "s3://taskflow-p01-tfstate-123456789012" --recursiveexit
cd bootstrap && terraform destroy
# comprobar que no quedó nadamake shellaws s3 ls | grep taskflowaws cloudfront list-distributions --query "DistributionList.Items[].Comment" --output textaws iam list-roles --query "Roles[?starts_with(RoleName, 'taskflow')].RoleName" --output textaws acm list-certificates --region us-east-1 --query "CertificateSummaryList[].DomainName" --output textGracias por llegar hasta aquí. Si algo de la guía te hizo perder tiempo, dilo: es un error nuestro y se corrige.