Journal API
API de journaling sobre FastAPI y PostgreSQL, desplegada de punta a punta en AWS con Kubernetes, Terraform y un pipeline con GitHub OIDC. El repositorio es público y el proyecto fue desmontado por completo en noviembre de 2026, con el procedimiento documentado.
- Rol
- Autor del proyecto — cloud engineering, infraestructura y pipeline
- Estado
- Desmontado el 06/11/2026. 99 recursos destruidos. No está corriendo en AWS.
Stack
- Servicio
- Python · FastAPI · PostgreSQL 16
- Infraestructura
- 79 recursos de Terraform · EC2 t3.small con k3s · EKS
- Entrega
- GitHub Actions con OIDC · sin claves permanentes
- Seguridad
- Secrets Manager acotado a 2 ARNs · CodeQL · gitleaks
- Observabilidad
- ADOT · OpenTelemetry · X-Ray · CloudWatch · 15 series de métricas
- Licencia
- MIT
Arquitectura
push a main
│
▼
┌──────────────────────────────────────────────────┐
│ GitHub Actions │
│ test ruff + pytest sobre Postgres de servicio │
│ gitleaks sobre el historial completo │
│ build imagen Docker etiquetada con el SHA │
│ push a ECR │
│ deploy SSM Run Command → rollout → │
│ verificación contra /version │
└───────────────┬──────────────────────────────────┘
│ OIDC: sin claves en el repositorio
▼
┌──────────────────────────────────────────────────┐
│ EC2 t3.small · k3s │
│ namespace journal-api │
│ ADOT + Fluent Bit · Postgres · Caddy con TLS │
│ Secrets Manager → init container → tmpfs │
└───────────────┬──────────────────────────────────┘
│ HTTPS
▼
/version → el SHA desplegado Problema
El capstone pedía una API desplegada en AWS con CI/CD, infraestructura como código y observabilidad. El problema real no era escribir la API: era que la entrega fuera reproducible y que nadie tuviera que buscar una credencial para hacer un deploy.
Y había una segunda mitad del trabajo que casi nadie documenta: una vez que el proyecto termina, la infraestructura tiene que volver a su estado inicial. Eso no lo hace un `terraform destroy` y ya.
Enfoque
Todo lo que puede estar en Terraform, está en Terraform. La VPC, los roles IAM, el Security Group, el topic de alarmas, el dashboard, las 13 reglas gestionadas de AWS Config, el presupuesto: 79 recursos, versionados, con los secretos detrás de un placeholder que Terraform nunca completa.
Las credenciales no viajan por el estado. Terraform crea el "cascarón" del secreto —nombre, ARN, cifrado, política— pero nunca el valor. El valor se siembra fuera de banda, desde SSM Parameter Store a Secrets Manager, y lo baja un init container con una identidad propia. La identidad de la instancia tiene un Deny explícito sobre esos dos ARNs: no puede leerlos, sólo delegar.
Y el pipeline no tiene llaves permanentes. GitHub Actions se autentica en AWS por OIDC, con un rol cuyo permiso es mínimo y cuyo alcance es el repositorio, la rama `main` y el environment `production`.
Resultado
83 commits. 79 recursos de Terraform. El deploy se verifica contra /version, que devuelve el SHA del commit: cuatro valores independientes —el commit local, el tag de la imagen en ECR, el tag que corre Kubernetes y el SHA que responde la API— tienen que coincidir exactamente, y el job falla si no.
La observabilidad no es un panel: ADOT sobre OTLP hacia X-Ray y CloudWatch, Fluent Bit para los logs de los pods, spans propios (`db.entries.*`, `llm.analyze_entry`), 15 series de métricas con p50 y p95, un dashboard, y alarmas de 5xx y de latencia hacia SNS. En AWS Config, 13 reglas gestionadas —8 de conformidad y 5 de amenaza— que evalúan la postura de la cuenta; la primera evaluación dio 3 hallazgos reales, que se cerraron.
Y el teardown se hizo completo el 06/11/2026, después de que pasara la verificación de la plataforma y de que el responsable del proyecto lo confirmara explícitamente: 99 recursos destruidos por Terraform, 66 imágenes de ECR borradas, 6107 versiones de S3 eliminadas a mano y los recursos que vivían fuera del estado —presupuestos y parámetros de SSM— limpiados también. El procedimiento completo, con lo que se encontró y lo que no, quedó en `docs/cleanup.md`.
Límites conocidos
La base de datos corre en una subnet pública. La razón fue el costo: una subnet privada con NAT Gateway cuesta más que la instancia completa en una cuenta sin tráfico real. Es una decisión de costo, no de diseño, y con tráfico real habría que revisarla.
El destroy no es atómico, porque el repositorio de ECR no tiene `force_delete`: las imágenes se borran primero y el repositorio después, en dos pasadas. Y los presupuestos de AWS y algunos parámetros de SSM vivían fuera de Terraform, así que el destroy no los tocó y se limpiaron a mano.
En k3s cualquier pod alcanza IMDS y puede obtener las credenciales del instance profile, así que el `ExternalId` protege del uso accidental, no de un atacante. El aislamiento real por pod necesita que AWS valide el token criptográficamente —IRSA o EKS Pod Identity— y eso exige un issuer OIDC público, inviable con una IP de `sslip.io` que Terraform reasigna en cada `destroy`.