Volver a artículos
9 min de lecturaastro-ssg-aws-s3-cloudfront

Escrito por Rodrigo de Miguel

S3 + CloudFront para servir un blog estático rápido y barato (3/5)

Cómo servir un blog estático con S3 y CloudFront usando Astro, con URLs limpias, buen cacheo y un coste casi cero, sin sobreingeniería ni plataformas mágicas.

AWS
Cloudfront
S3
Astro
Static Site Generation SSG
Web Performance Optimization WPO

No necesitamos mucho para servir HTML

A estas alturas de la serie ya hay varias cosas claras:

  • Un blog montado en Astro
  • HTML estático generado
  • Cero partes dinámicas
  • Nada que “calcular” en cada request

Así que la pregunta sale sola: ¿cómo sirvo estos archivos de la forma más simple posible, sin meter infraestructura de más?

Para mi portfolio (y para este ejemplo) la respuesta fue directa: S3 + CloudFront.

No es “enterprise”. Es sencillo, barato y predecible.
Y para un blog, eso suele ser justo lo que apetece.

La realidad es que no se cuanto me va a durar el gusto por escribir en este formato, pero mientras tanto, me vale.

¿Cuántos blogs se han empezado y luego abandonado?

Qué vamos a montar (y qué no)

Antes de entrar al lío, mejor dejar claro el alcance.

Lo que sí vamos a hacer

  • Subir HTML estático a S3
  • Servirlo a través de CloudFront (CDN)
  • Tener URLs limpias sin /index.html
  • 'Automatizar' el despliegue con un comando

Lo que NO vamos a hacer

  • Kubernetes
  • Load balancers
  • Backend
  • Nada que aquí no aporte valor real

Esto sigue siendo un blog sencillo, sin florituras.

Visión general de la arquitectura

Muy resumido:

PiezaPara qué sirve
AstroGenera HTML en build
S3Almacena los archivos estáticos
CloudFrontCDN + proxy inverso
(Route 53)Lo veremos en el siguiente post

Con esto puedes servir millones de requests sin tocar nada más.

Sencillo pero matón
Sencillo pero matón

Paso 1 — Crear el bucket de S3

Asumo que ya tienes una cuenta de AWS.

En S3:

  • Crea un bucket con el nombre que quieras (luego irá a variables)
  • Región: la que prefieras (yo uso la más cercana al país de producción, pero no es determinante)

Configuración clave:

  • ACL deshabilitadas
  • Bloquear todo el acceso público

La idea es sencilla:

👉 nadie accede a S3 directamente, todo pasa por CloudFront.

Paso 2 — Crear la distribución de CloudFront

Aquí montamos el “frontal” del blog.

En CloudFront:

  • Distribution type: Single website or app
  • Domain: déjalo vacío (el dominio propio viene luego)

Origen

  • Origin type: Amazon S3
  • Bucket: el que acabas de crear
  • Marca: Allow private S3 bucket access to CloudFront
  • CloudFront te propondrá crear un Origin Access Control (OAC)
    → acéptalo, es lo más limpio

Esto garantiza que:

  • CloudFront puede leer del bucket
  • Nadie más tiene acceso

Paso 3 — Ajustes básicos de la distribución

Con la distribución creada, revisa:

Security

  • Activa Core protections
  • WAF en modo monitor (más que suficiente aquí)

Behavior (por defecto está bien)

  • Cache policy: CachingOptimized
  • Origin request policy: CORS-S3Origin
  • Response headers: CORS-and-SecurityHeadersPolicy

Error Pages

  • Añade una página de error 404 personalizada:
    • Código 404/404.html → Código de respuesta HTTP: 200

    • Código 403/404.html → Código de respuesta HTTP: 200

      A veces S3 devuelve 403 en vez de 404 para rutas no existentes

No hay mucho más que tocar en este punto.

Paso 4 — Permitir acceso desde CloudFront al bucket

CloudFront necesita permiso explícito para leer desde S3.

En el bucket:

  • Permissions → Bucket policy

Ejemplo sencillo:

JSON
{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowCloudFrontServicePrincipal", "Effect": "Allow", "Principal": { "Service": "cloudfront.amazonaws.com" }, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*", "Condition": { "StringEquals": { "AWS:SourceArn": "YOUR_DISTRIBUTION_ARN" } } } ] }

Con esto queda claro:

  • El bucket sigue siendo privado
  • Solo tu distribución puede leerlo

Check Point: qué tenemos ya

Llegados aquí:

  • S3 listo para recibir HTML
  • CloudFront delante
  • Infra protegida
  • Nada público sin pasar por el CDN

Ahora toca subir los archivos.

Paso 5 — Despliegue sencillo con un comando

Para empezar, nada de CI/CD.

Un script directo en package.json:

JSON
"deploy": "aws s3 sync dist/ s3://$BLOG_BUCKET_S3 --delete --region YOUR_BUCKET_REGION --cache-control max-age=31536000 && aws cloudfront create-invalidation --distribution-id $BLOG_DISTRIBUTION_ID --paths '/*'"

Qué hace esto

  1. aws s3 sync

    • Sube los archivos de dist/
    • Borra lo que ya no exista
    • Aplica cache agresiva (perfecta para assets)
  2. Invalidación de CloudFront

    • Te asegura ver los cambios al momento
    • Podría ser más selectivo pero por ahora nos vale así
    • Si tienes tráfico alto, se puede invalidar solo los /index.html.

Ejecutar

BASH
$ npm run build $ BLOG_BUCKET_S3=YOUR_BUCKET_NAME BLOG_DISTRIBUTION_ID=YOUR_DISTRIBUTION_ID npm run deploy

Al terminar:

  • Los archivos ya están en S3
  • CloudFront los sirve al mundo

Paso 6 — El detalle feo: /index.html

Si pruebas la URL de la distribución: https://xxxxx.cloudfront.net/index.html

Funciona.

Pero: https://xxxxx.cloudfront.net/blog/

No.

Y tener que escribir /index.html es… bastante regulero.

Vamos a dejarlo bonito.

Paso 7 — URLs limpias con CloudFront Functions

Aquí entra una CloudFront Function muy pequeña.

En CloudFront → Functions:

  • Name: append-index-html-to-request-astro-app
  • Runtime: cloudfront-js-2.0

Código:

JS
function handler(event) { var request = event.request; var uri = request.uri; // Assets: js, css, imágenes… if (uri.includes('.')) return request; if (uri === '/') { request.uri = '/index.html'; return request; } if (uri.endsWith('/')) { request.uri = uri + 'index.html'; return request; } request.uri = uri + '/index.html'; return request; }

Publica la función y asígnala:

  • Associated distributions > Add Association
  • Selecciona tu distribucion
  • Event: Viewer Request
  • Behavior: Default (*)

Con esto consigues:

  • URLs limpias
  • HTML servido correctamente
  • Sin tocar el build

Detalle opcional: ocultar la cabecera server

No es crítico, pero a mí me gusta.

Otra CloudFront Function en Viewer Response:

JS
function handler(event) { var response = event.response; var headers = response.headers; delete headers['server']; return response; }

Resultado

HTTP
// Antes server: Amazon S3 // Ahora server: CloudFront

Así no se muestra qué hay detrás del CDN. Es algo de la infra que no le tiene que interesar a nadie.

Resultado final

Ahora sí:

  • Blog servido desde S3
  • CloudFront como CDN global
  • URLs limpias
  • Coste prácticamente cero
  • Infra simple y fácil de explicar

Nada mágico. Nada que no se entienda.

Checklist final

  • Bucket privado
  • CloudFront delante
  • HTML subido
  • URLs sin /index.html
  • Despliegue en un comando

Qué nos queda por hacer

En el siguiente post:

  • Dominio propio
  • Route 53
  • Google Search Console

El blog ya está online.

Tu mamá se pondrá contenta.

Ahora toca ponerle dominio propio y enseñárselo a Google.

Posts de la serie

1️⃣ Parte 1: Un blog no debería ser un SaaS
2️⃣ Parte 2: Preparar un blog en Astro con buen criterio
👉 Parte 3: S3 + CloudFront para servir un blog estático rápido y barato
5️⃣ Parte 4: Dominio con Route 53 y CloudFront para un blog Astro SSG
5️⃣ Parte 5: Automatizar el despliegue de un blog Astro SSG con GitHub Actions y AWS
🐙 GitHub Repo: demo-astro-ssg-s3-cloudfront

¿Hablamos?

¿Buscas a alguien que entienda el producto tanto como el código?

Abrir conversación

© 2026 Rodrigo de Miguel. Todos los derechos reservados.