Artículo

Cómo construí mi portafolio: sitio estático, CMS serverless y despliegue automático

Este es el primer post de este blog, así que lo justo es empezar explicando cómo llega hasta ti. El portafolio ya tenía juegos, laboratorios y currículum. Faltaba un lugar para escribir. Quería publicar sin editar código en cada texto, pero sin renunciar a lo que el sitio ya tenía de bueno: ser estático, rápido y barato de mantener.

  • aws
  • serverless
  • arquitetura

La idea central: el sitio sigue siendo estático

Cada página de este sitio es un archivo HTML generado en el build y servido por CloudFront desde un bucket S3 privado. Cuando abres este artículo, no se llama a ninguna API ni se consulta ninguna base de datos.

El contenido del blog vive en otro lugar, en un CMS que llamé Portfolio Studio. Publicar un artículo significa volver a ejecutar el despliegue del sitio, ahora con el artículo incluido. Esa separación guio todas las demás decisiones.

El sitio

El sitio es una aplicación React Router con prerenderizado: en el build, cada ruta se convierte en un HTML listo, con título, descripción, datos estructurados y versiones en portugués, inglés y español. Una CloudFront Function convierte URLs limpias, como /es/blog, en el archivo correcto dentro del bucket. Los juegos son aplicaciones Vite independientes, publicadas en el mismo bucket.

Las imágenes de los artículos se procesan en el build: se giran según la orientación de la foto, pierden los metadatos y se convierten a WebP en cuatro anchos (480, 960, 1200 y 1600 px). Los originales nunca son públicos.

Portfolio Studio

El CMS está en /admin, dentro del propio sitio, pero su HTML no tiene contenido. Todo llega desde la API después de iniciar sesión.

  • Inicio de sesión: Amazon Cognito con el flujo authorization code + PKCE y MFA obligatorio. Solo existe un usuario, creado por mí, en el grupo admin.

  • API: API Gateway (HTTP API) delante de Lambdas en Node.js. El borde valida el token, y la Lambda vuelve a comprobar el grupo y el tipo de token. El navegador solo decide qué mostrar; quien autoriza es la API.

  • Editor: Tiptap, limitado a los elementos que el sitio sabe renderizar. El documento se guarda como JSON validado por un esquema en la API, así que nunca se almacena HTML proveniente del navegador.

  • Borradores: guardado automático tras una pausa al escribir, con control de versiones optimista. Si el mismo artículo cambia en otra pestaña, yo elijo qué versión se queda, en lugar de que una sobrescriba a la otra.

  • Datos: una tabla en DynamoDB para artículos, medios y despliegues, y un bucket S3 privado para revisiones inmutables, imágenes originales y snapshots.

Publicar es desplegar

Arquitectura del portafolio de AWS
Arquitectura del portafolio de AWS

Cuando hago clic en Publicar, ocurre lo siguiente:

  1. La API guarda una revisión inmutable del artículo, reserva los slugs de cada idioma y crea un registro de despliegue.

  2. Mediante una GitHub App, la API dispara el workflow de despliegue del repositorio del sitio.

  3. El primer paso del workflow le pide a la API el contenido a publicar. Esa petición congela un snapshot de lo que debe estar publicado en ese instante.

  4. El workflow procesa las imágenes, hace el build, sincroniza el bucket e invalida la caché de CloudFront.

  5. Al final, el workflow informa el resultado a la API, y solo entonces el artículo queda marcado como publicado.

El congelamiento resuelve algunos casos que parecen pequeños, pero no lo son. Si publico otro texto durante un build, entra en el despliegue siguiente, sin mezclar estados. Si alguien vuelve a ejecutar un workflow antiguo, la API lo rechaza, y el contenido viejo no vuelve a publicarse. Si el build falla, no se sube nada y el artículo vuelve a su estado anterior.

Seguridad sin secretos compartidos

Ninguna credencial de larga duración circula entre los servicios:

  • El workflow demuestra a la API quién es con el token OIDC del propio GitHub Actions. La API solo acepta el job de producción de la rama main del repositorio del sitio.

  • La API habla con GitHub mediante una GitHub App instalada solo en el repositorio del sitio, con un único permiso: ejecutar workflows. El token de instalación dura una hora.

  • Los despliegues asumen roles en AWS también mediante OIDC, sin claves de acceso guardadas en GitHub.

  • Las claves que sí deben existir, como la de la GitHub App y el token de LinkedIn, se guardan cifradas en SSM Parameter Store, fuera del código y fuera del state de Terraform.

LinkedIn sin atajos

Difundir un artículo en LinkedIn también pasa por el Studio, y siempre con la API oficial: los productos "Share on LinkedIn" y "Sign In with LinkedIn using OpenID Connect". Ningún post sale sin mi aprobación, y los programados usan EventBridge Scheduler. Cuando la integración no está conectada, el propio Studio prepara el texto y abre LinkedIn para que yo lo publique a mano.

Desarrollar sin AWS

Una parte que disfruté mucho: para trabajar en el Studio no necesito una cuenta de AWS ni Docker. Un npm run dev en la API levanta un servidor local que ejecuta las mismas Lambdas, con DynamoDB, S3, SSM y Scheduler simulados en memoria y guardados en disco. En el sitio, npm run dev:studio abre el admin con la sesión ya iniciada. Publicar en local ejecuta los mismos scripts que el despliegue real, y el blog se actualiza al instante. Las mismas simulaciones se usan en las pruebas automatizadas.

Lo que aprendí en el camino

  • Un bucket privado responde 403, no 404. Sin permiso para listar el bucket, S3 no revela si un archivo existe. Para que una página inexistente muestre el 404 del sitio, CloudFront tiene que convertir ese 403 en /404.html con estado 404.

  • Lo estático tiene un costo pequeño y predecible. Publicar tarda lo que tarda un build, unos minutos. A cambio, un pico de visitas en un artículo nunca toca el backend.

  • Separar "guardado" de "publicado" lo simplifica todo. El estado editorial y lo que está publicado son campos distintos, y el segundo solo cambia cuando el despliegue lo confirma. Eso hizo que los fallos fueran fáciles de manejar.

En los próximos posts quiero profundizar en algunas de estas partes, empezando por el flujo de publicación. Gracias por leer hasta aquí.