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.
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:
| Pieza | Para qué sirve |
|---|---|
| Astro | Genera HTML en build |
| S3 | Almacena los archivos estáticos |
| CloudFront | CDN + proxy inverso |
| (Route 53) | Lo veremos en el siguiente post |
Con esto puedes servir millones de requests sin tocar nada más.

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: 200A 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:
{
"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:
"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
-
aws s3 sync- Sube los archivos de
dist/ - Borra lo que ya no exista
- Aplica cache agresiva (perfecta para assets)
- Sube los archivos de
-
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
$ npm run build
$ BLOG_BUCKET_S3=YOUR_BUCKET_NAME BLOG_DISTRIBUTION_ID=YOUR_DISTRIBUTION_ID npm run deployAl 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:
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:
function handler(event) {
var response = event.response;
var headers = response.headers;
delete headers['server'];
return response;
}Resultado
// Antes
server: Amazon S3
// Ahora
server: CloudFrontAsí 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↗