¿Realmente necesitás WordPress? Migrá tu blog a S3 + CloudFront con Claude en menos de una hora
WordPress, Drupal, Joomla — son plataformas maduras y resuelven muchos casos. Y sí, para un sitio con usuarios, formularios, e-commerce o un equipo editorial, siguen teniendo sentido. Pero si lo único que hacés es publicar notas, estás pagando un costo alto por algo que no usás: PHP, una base de datos, plugins que hay que parchear y un panel de admin expuesto a internet.
La alternativa es vieja y aburrida: HTML estático en un bucket de S3, servido por CloudFront. Lo que cambió es el costo de la migración. Con Claude Code, la migración completa — contenido, infraestructura en stage y prod, pipelines — me llevó entre 30 y 50 minutos.
Como referencia: estimo que un senior dev que conoce AWS, haciendo lo mismo a mano para un sitio de 50 posts, tarda entre 3 y 5 días de trabajo — unas 20 a 35 horas. La mayor parte no es la infra, sino convertir y limpiar el contenido post por post y replicar el theme. Si ya tiene la infra en Terraform o CDK, baja a un día y medio o dos. Sigue siendo otra escala.
Acá va el paso a paso, con los detalles técnicos y — sobre todo — el modelo de seguridad.

¿Necesitás un CMS? Los trade-offs
Antes de migrar, conviene ser honesto sobre qué perdés y qué ganás.
Lo que ganás:
- Superficie de ataque casi nula — no hay PHP, ni base de datos, ni
/wp-admin. No hay nada que ejecutar del lado del servidor, así que no hay nada que explotar. - Cero mantenimiento — no más updates de core, de plugins ni de theme. Un sitio estático de hace cinco años sigue funcionando igual.
- Costo — para un blog chico, centavos por mes. CloudFront tiene un free tier de 1 TB de transferencia mensual.
- Performance — todo sale del edge de CloudFront. No hay render por request.
- Versionado real — el sitio entero vive en Git. Cada cambio tiene historia, review y rollback.
Lo que perdés:
- Comentarios — se resuelve con un servicio externo (Giscus, por ejemplo), o se eliminan. Yo los eliminé.
- Formularios — necesitás un endpoint aparte (Lambda, Formspree). Agrega una pieza móvil.
- Búsqueda — sólo del lado del cliente (Pagefind, Lunr). Funciona bien hasta unos miles de posts.
- El editor visual — escribís en Markdown. Si eso no es lo tuyo, más abajo muestro cómo Claude resuelve esa parte.
Si la lista de lo que perdés no te duele, seguí leyendo.
La arquitectura
El diseño final es simple:
- GitHub repo — el contenido en Markdown y el generador de sitio estático (uso Astro).
- GitHub Actions — un pipeline que buildea y publica en stage con cada push a
main, y en prod después de una aprobación manual. - S3 — dos buckets privados, uno por ambiente. Sin website hosting, sin acceso público.
- CloudFront — una distribución por ambiente, con Origin Access Control (OAC) para leer del bucket, certificado de ACM y una CloudFront Function para las URLs.
- IAM — dos identidades distintas, con permisos distintos: una temporal para que Claude haga la migración, y un role por ambiente para el pipeline, vía OIDC.
El modelo de seguridad, antes de tocar nada
La idea original era darle a Claude una access key, crear el repo, crear los pipelines y listo. Funciona. Pero al revisarlo aparecen tres problemas, y los corregí antes de empezar:
- Claude no puede tener permisos de IAM. Si el user de Claude puede crear roles y policies, puede otorgarse cualquier permiso — es escalación de privilegios por diseño. El OIDC provider y los roles del pipeline los creás vos.
- El pipeline no usa access keys. Nada de
AWS_ACCESS_KEY_IDen GitHub Secrets. GitHub Actions se autentica contra AWS con OIDC y recibe credenciales temporales, y el trust policy restringe qué repo y qué ambiente puede asumir cada role. - Claude no puede modificar el pipeline. Si el agente puede editar
.github/workflows/, puede cambiar a dónde se publica y con qué permisos. Los workflows los commiteás vos, y el token de Claude no tiene permiso para tocarlos.
La regla que aplico: el agente opera dentro de los límites; nunca define los límites.
Paso a paso
1. Creá el IAM user para Claude
En la consola de IAM, creá un user sin acceso a la consola — por ejemplo claude-migration. Asignale esta policy inline. Reemplazá mi-blog por el prefijo de tus buckets:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "SiteBucketsOnly",
"Effect": "Allow",
"Action": [
"s3:CreateBucket",
"s3:PutBucketPolicy",
"s3:GetBucketPolicy",
"s3:PutBucketPublicAccessBlock",
"s3:GetBucketPublicAccessBlock",
"s3:PutBucketOwnershipControls",
"s3:PutEncryptionConfiguration",
"s3:ListBucket",
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": [
"arn:aws:s3:::mi-blog-*",
"arn:aws:s3:::mi-blog-*/*"
]
},
{
"Sid": "CloudFront",
"Effect": "Allow",
"Action": [
"cloudfront:CreateDistribution",
"cloudfront:UpdateDistribution",
"cloudfront:GetDistribution",
"cloudfront:GetDistributionConfig",
"cloudfront:ListDistributions",
"cloudfront:CreateOriginAccessControl",
"cloudfront:GetOriginAccessControl",
"cloudfront:ListOriginAccessControls",
"cloudfront:CreateFunction",
"cloudfront:UpdateFunction",
"cloudfront:DescribeFunction",
"cloudfront:TestFunction",
"cloudfront:PublishFunction",
"cloudfront:CreateResponseHeadersPolicy",
"cloudfront:ListResponseHeadersPolicies",
"cloudfront:ListCachePolicies",
"cloudfront:CreateInvalidation",
"cloudfront:GetInvalidation"
],
"Resource": "*"
},
{
"Sid": "Certificates",
"Effect": "Allow",
"Action": [
"acm:RequestCertificate",
"acm:DescribeCertificate",
"acm:ListCertificates"
],
"Resource": "*"
}
]
}
Tres cosas a notar:
- S3 está acotado por ARN — Claude sólo puede tocar buckets que empiecen con tu prefijo. No hay
s3:DeleteBucket: borrar un bucket es una decisión tuya. - CloudFront usa
"Resource": "*"— la creación de distribuciones no admite restricción por recurso. Es el punto más amplio de la policy, y es la razón por la que la key vive sólo durante la migración. - No hay ninguna acción de IAM. Deliberado.
Si durante la migración aparece un AccessDenied, Claude te va a decir qué acción falta. Agregala puntual. No pongas cloudfront:* para destrabar.
Creá la access key del user y configurala en un profile propio, no en el default:
aws configure --profile claude-migration
export AWS_PROFILE=claude-migration
aws sts get-caller-identity
Nunca pegues la key en el chat con Claude. El agente usa el profile; no necesita ver el secreto.
2. Creá el repo y acotá lo que Claude puede hacer
Creá el repo en GitHub (privado, aunque el sitio sea público — no hace falta exponer drafts). Después generá un fine-grained personal access token:
- Repository access — sólo este repo.
- Contents — read and write.
- Pull requests — read and write.
- Workflows — sin acceso. Así Claude no puede pushear cambios a
.github/workflows/. - Expiration — 7 días.
Del lado de Claude Code, agregá un .claude/settings.json en el repo con reglas de permisos:
{
"permissions": {
"deny": [
"Read(~/.aws/**)",
"Bash(aws iam:*)",
"Bash(aws s3 rb:*)"
],
"ask": [
"Bash(aws cloudfront create-distribution:*)",
"Bash(aws cloudfront update-distribution:*)",
"Bash(aws s3 sync:*)",
"Bash(git push:*)"
]
}
}
Tené en cuenta que esto es una segunda capa, no la principal. Un deny sobre Read no impide un cat desde Bash. El control real es la policy de IAM del paso anterior. Las reglas de Claude Code sirven para que las operaciones importantes pasen por tu aprobación.
3. Migrá el contenido: Claude lee WordPress vía MCP
Acá no hay export, ni XML, ni scripts de conversión. Instalo el plugin AI Engine (de Meow Apps) en el WordPress, que expone un MCP server, y conecto Claude directamente al sitio. Claude lee todos los posts, categorías y tags, baja las imágenes y arma el sitio nuevo. Todo lo hace Claude.
Para autenticar uso OAuth, no un Bearer Token estático. La diferencia importa:
- No hay secreto compartido — no hay un token que copiar, pegar en un comando y que termine en el historial del shell o en un archivo de config.
- Identidad real — cada autorización queda atada a un usuario de WordPress, con login normal y una pantalla de consentimiento que muestra qué app pide acceso y con qué rol.
- Revocación en un click — el panel Connected Apps de AI Engine lista cada autorización activa.
En WordPress, AI Engine → Settings → MCP: activá el MCP server. Con OAuth no hace falta definir Bearer Token — AI Engine publica los endpoints de discovery (/.well-known/oauth-*) y el cliente se registra solo vía Dynamic Client Registration. Desde el repo:
claude mcp add --transport http wp-origen https://mi-sitio.com/wp-json/mcp/v1/http
Después, dentro de Claude Code, corré /mcp, elegí wp-origen y Authenticate. Se abre el browser, te logueás en WordPress y aprobás. El token queda en el almacenamiento seguro de Claude Code, no en un archivo del repo.
Y acá está la distinción que más se pasa por alto: con OAuth, Claude tiene los permisos del usuario con el que te logueás. Si aprobás como admin, Claude es admin — puede borrar posts, cambiar settings, instalar plugins si el grupo está expuesto. OAuth mejora la autenticación; no achica la autorización. Por eso:
- Creá un usuario dedicado — por ejemplo
claude-migracion, con el rol más bajo que te deje leer lo que necesitás. No aprobés con tu cuenta de admin. - Exponé sólo lo necesario — en la pantalla de MCP, dejá activo sólo el grupo WordPress (posts, media, taxonomías). Plugins, Themes y Dynamic REST, apagados.
- Revisá el WAF — algunos hostings bloquean los POST a
/wp-json/mcp/v1/oauth/registery el registro falla. Si pasa, hay que permitir/.well-known/oauth-*y/wp-json/mcp/v1/*.
Si tu versión de AI Engine exige un usuario admin para el MCP, el trade-off cambia: un Bearer Token con access level Read-Only se restringe del lado del servidor, y ahí puede ser la opción más segura de las dos. Revisalo antes de elegir.
Como segunda capa, bloqueá las tools de escritura del lado de Claude Code. Agregá al .claude/settings.json:
{
"permissions": {
"deny": [
"mcp__wp-origen__wp_create_post",
"mcp__wp-origen__wp_update_post",
"mcp__wp-origen__wp_delete_post",
"mcp__wp-origen__wp_upload_media",
"mcp__wp-origen__wp_update_option"
]
}
}
Los nombres exactos de las tools dependen de la versión del plugin — con /mcp ves la lista. Bloqueá todo lo que escriba. Claude sólo necesita las tools de lectura.
Con el MCP conectado, el prompt:
Conectate al MCP wp-origen y migrá el sitio a Astro en este repo.
- Leé todos los posts publicados, con sus categorías, tags y excerpt.
- Convertí cada post a Markdown en una content collection, con front
matter: title, date, slug, categories, tags, description.
- Mantené exactamente las mismas URLs que en WordPress.
- Bajá todas las imágenes referenciadas (incluida la featured image)
a public/images/ y reescribí las rutas.
- Convertí los shortcodes y bloques de Gutenberg a HTML limpio.
- Ignorá comentarios, drafts y páginas de sistema.
- No escribas nada en WordPress: sólo lectura.
- Antes de escribir código, armá un plan en PLAN.md y esperá mi OK.
Revisá el plan. Con cuidado. Lo que más se rompe en esta etapa son las URLs: si cambian, perdés el SEO acumulado y todos los links externos. En Astro, la ruta la define la estructura de src/pages/ — por ejemplo src/pages/[year]/[month]/[slug].astro si tu WordPress usa /%year%/%monthnum%/%postname%/. Si la estructura coincide, no hay redirects que mantener.
Dejá el build.format de Astro en directory (el default): genera /mi-post/index.html, que es lo que después resuelve la CloudFront Function del paso 4. Y creá src/pages/404.astro — el build lo convierte en 404.html.
Para previsualizar local:
npm run dev
Y acá hay un riesgo que se suele pasar por alto: el contenido que Claude está leyendo es untrusted (no confiable). Posts viejos, HTML pegado de otros sitios, un comentario spam que se coló. Un texto con instrucciones escondidas es un vector de prompt injection — y con un MCP que puede escribir en tu sitio en vivo, el daño posible ya no es sólo local. Por eso las tools de escritura van en deny, no en ask, y por eso los permisos de AWS son mínimos.
La alternativa sin MCP es el export nativo (Tools → Export, formato WXR) más una copia de wp-content/uploads. No toca el sitio en vivo, pero te toca a vos bajar y ordenar los archivos. Para un sitio con pocos posts, el MCP es bastante más rápido.
4. Pedile a Claude la infraestructura
Con el sitio buildeando local, pedile a Claude que cree la infra para stage y prod. Esto es lo que tiene que quedar, y lo que conviene verificar:
- Buckets privados — Block Public Access activado en los cuatro flags, sin static website hosting. CloudFront lee vía el endpoint REST del bucket, no el de website.
- Origin Access Control — no el legacy OAI. El bucket sólo acepta
s3:GetObjectdesde esa distribución específica. - Certificado ACM en
us-east-1— CloudFront sólo acepta certificados de esa región, sin importar dónde estén los buckets. La validación es por DNS. - Response headers policy — HSTS,
X-Content-Type-Options,X-Frame-Options,Referrer-Policy. La managed policy SecurityHeadersPolicy cubre lo básico. - TLS mínimo —
TLSv1.2_2021y redirect de HTTP a HTTPS.
La bucket policy que tiene que quedar en prod:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontOAC",
"Effect": "Allow",
"Principal": { "Service": "cloudfront.amazonaws.com" },
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::mi-blog-prod/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::ACCOUNT_ID:distribution/DISTRIBUTION_ID"
}
}
}
]
}
5. Conectá GitHub con AWS vía OIDC (esto lo hacés vos)
Este es el paso que no le delego a Claude, porque requiere permisos de IAM. Primero, el identity provider (una vez por cuenta):
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com
Después, un role por ambiente. El trust policy del role de prod:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:TU_USUARIO/TU_REPO:environment:production"
}
}
}
]
}
La condición sobre sub es la que resuelve “sólo este repo puede publicar”. Y va un paso más allá: sólo un job que corre en el environment production de ese repo puede asumir el role. Usá StringEquals, no StringLike con wildcards — un repo:TU_USUARIO/* abre el acceso a todos tus repos.
El role de stage es idéntico, con environment:staging y apuntando al bucket de stage.
Los permisos del role de prod — sólo lo que el deploy necesita:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::mi-blog-prod"
},
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:DeleteObject", "s3:GetObject"],
"Resource": "arn:aws:s3:::mi-blog-prod/*"
},
{
"Effect": "Allow",
"Action": "cloudfront:CreateInvalidation",
"Resource": "arn:aws:cloudfront::ACCOUNT_ID:distribution/DISTRIBUTION_ID"
}
]
}
Acá CreateInvalidation sí admite restricción por ARN. El pipeline puede publicar en un bucket e invalidar una distribución. Nada más.
6. Los pipelines de stage y prod
En el repo, Settings → Environments, creá dos environments:
- staging — deployment branch: sólo
main. Variables:AWS_ROLE_ARN,BUCKET,DISTRIBUTION_ID,SITE_URL. - production — deployment branch: sólo
main, required reviewer: vos. Las mismas variables, con los valores de prod.
Son variables, no secrets. Un role ARN no es un secreto — sin el token OIDC de tu repo no sirve para nada.
El workflow, en .github/workflows/deploy.yml, tiene tres jobs:
- check — corre en cada PR. Instala dependencias con
npm ciy buildea. No pide credenciales de AWS. - deploy-stage — corre en cada push a
main, en el environmentstaging. Buildea, asume el role de stage vía OIDC, sube el sitio conaws s3 sync --deletee invalida la cache de CloudFront. - deploy-prod — depende de stage y corre en el environment
production. Los mismos pasos, con el role y las variables de prod.
Dos detalles que hacen la diferencia: el permiso id-token: write se declara sólo en los jobs de deploy, no a nivel de workflow, y los tres jobs leen todo de las variables del environment — el mismo código publica en stage o en prod según dónde corre.
El flujo queda así: un PR sólo buildea — sin credenciales de AWS, así que un PR malicioso no puede publicar nada. Un merge a main publica en stage automáticamente. Prod queda esperando tu aprobación en la pestaña Actions. Aprobás, y publica.
Sobre la invalidación: /* cuenta como un solo path, y los primeros 1.000 paths por mes son gratis. Para un blog, invalidar todo en cada deploy es la opción más simple y no cuesta nada.
Buildear dos veces (una por ambiente) es deliberado: la URL del sitio cambia, y Astro la embebe en el HTML — canonical, sitemap, RSS. En astro.config.mjs:
export default defineConfig({
site: process.env.SITE_URL,
build: { format: 'directory' },
});
En el pipeline, SITE_URL sale de la variable del environment. El costo son unos segundos más de build.
7. Protegé el pipeline
El pipeline es ahora el único camino a prod. Eso lo convierte en el activo a proteger:
- Branch protection en
main— PR obligatorio, status checkchecken verde, sin force push. - CODEOWNERS sobre
.github/— cualquier cambio a los workflows requiere tu review. - Required reviewer en production — ya configurado en el paso anterior. Es el último control humano antes de publicar.
8. Cutover
Con prod publicado y verificado en la URL de CloudFront (dxxxx.cloudfront.net):
- Bajá el TTL del registro DNS a 300 segundos, un día antes.
- Apuntá el dominio a la distribución: un alias record en Route 53, o un CNAME en tu proveedor (para el apex, usá ALIAS/ANAME si lo soporta).
- Verificá que las URLs viejas respondan. Un script simple que recorra el sitemap de WordPress y compare códigos HTTP alcanza.
- Mantené el WordPress apagado — no borrado — un par de semanas, por si aparece algo que se escapó.
9. Cerrá todo lo que abriste para la migración
Primero, la key de AWS:
aws iam update-access-key \
--user-name claude-migration \
--access-key-id AKIAXXXXXXXXXXXXXXXX \
--status Inactive
Y sacala de ~/.aws/credentials. Una key inactiva en disco sigue siendo una key esperando que alguien la reactive.
Mi recomendación, de hecho, es un paso más: borrarla. Cuando la necesites, creás una nueva — toma diez segundos y te asegura que ninguna key vieja quede olvidada. Revocá también el token de GitHub, o dejá que expire.
Después, el MCP. Si el WordPress sigue online durante el período de transición, revocá la autorización en Connected Apps, desactivá el MCP en AI Engine y borrá el usuario claude-migracion. Si el plugin sólo estaba para la migración, desinstalalo. Y del lado de Claude Code:
claude mcp remove wp-origen
Un MCP con permisos de escritura sobre un sitio que ya no usás es exactamente el tipo de cosa que se olvida y queda abierta.
Claude es tu editor “visual”
Acá está lo que convence a la gente que no quiere saber nada de Markdown ni de Git. El panel de WordPress desaparece, y el editor pasa a ser una conversación. Abrís Claude Code en el repo y le decís:
Creá un post nuevo. Este es mi draft:
[tu texto, bla bla]
Categoría Tecnología.
Levantá npm run dev para que lo revise.
Cuando te dé el OK, creá una branch, commiteá y abrí un PR.
Revisás en localhost:4321, pedís ajustes — “achicá la intro”, “mové la imagen debajo del primer párrafo”, “cambiá el título” — y aprobás. Claude abre el PR, el check pasa, mergeás, stage se actualiza solo, aprobás prod. Listo.
Para esto no hacen falta credenciales de AWS. Claude sólo necesita el token de GitHub — el deploy lo hace el pipeline. Es la configuración que dejo de forma permanente.
Un skill con las reglas de tu blog
El prompt de arriba funciona, pero deja mucho librado a lo que Claude infiera de los posts existentes. La forma de fijarlo es un skill: un archivo con las reglas de cómo tiene que ser un post, que Claude carga cada vez que le pedís uno. Es lo que hace un editor — conocer el manual de estilo sin que se lo repitas.
Vive en el repo, en .claude/skills/nuevo-post/SKILL.md:
---
name: nuevo-post
description: Crea o edita posts del blog. Usar siempre que se pida
escribir, corregir o publicar un post.
---
# Nuevo post
## Front matter (obligatorio)
- title, date (ISO), slug en kebab-case, description (máx. 160 caracteres)
- categories: sólo Tecnología, Seguridad o AI
- tags: entre 2 y 5
## Estilo
- Español, primera persona, frases cortas.
- Términos técnicos en inglés, sin traducir.
- Apertura con el problema, no con una definición.
- Un H2 por sección. Nada de H1 en el cuerpo.
## Imágenes
- En public/images/[slug]/, formato webp, máx. 1600px de ancho.
- alt obligatorio y descriptivo.
- La principal va debajo de la intro.
## Flujo
1. Crear el archivo en src/content/blog/[slug].md como draft: true.
2. Levantar npm run dev y pasar la URL local.
3. Aplicar los cambios que pida.
4. Con el OK: draft: false, branch post/[slug], commit y PR.
Nunca pushear directo a main.
Con el skill, el prompt se reduce a “creá un post con este draft”. El resto — formato, imágenes, categorías, el flujo hasta el PR — ya está definido.
La validación de seguridad, resumida
Estos son los riesgos que revisé, y cómo queda cada uno:
- Access key de larga duración — mitigado. Vive sólo durante la migración, en un profile separado, y se borra después. Riesgo residual: la ventana de la migración, menos de una hora.
- Escalación de privilegios vía IAM — eliminado. El user de Claude no tiene ninguna acción de IAM; los roles del pipeline los creás vos.
- Policy de CloudFront con
Resource: *— aceptado. Es una limitación de la API. Durante la migración, Claude podría modificar otras distribuciones de la cuenta. Si tenés otras distribuciones en producción, hacé la migración en una cuenta separada de AWS Organizations. - Credenciales en GitHub — eliminado. OIDC con credenciales temporales, sin secrets.
- Otro repo publicando en tu bucket — eliminado. El trust policy exige repo y environment exactos.
- Agente modificando el pipeline — mitigado. El token no tiene permiso de Workflows, y CODEOWNERS exige tu review.
- Prompt injection desde el contenido importado — mitigado, no eliminado. El blast radius está acotado por los permisos, y los comandos sensibles requieren tu aprobación.
- Bucket expuesto — eliminado. Block Public Access, sin website hosting, OAC atado a una distribución.
s3 sync --deleteborrando de más — aceptado. Si un build sale vacío, borra el sitio. Activá versioning en el bucket de prod: recuperás cualquier versión anterior por centavos.- MCP con escritura sobre el WordPress en vivo — mitigado. OAuth con un usuario dedicado de rol bajo, sólo el grupo WordPress expuesto, tools de escritura en
deny, y autorización revocada y MCP apagado al terminar. Riesgo residual: el endpoint queda expuesto a internet mientras está activo, y los permisos reales son los del rol del usuario. - Supply chain — mitigado. Actions pineadas por SHA,
npm cicon lockfile commiteado.
¿Menos de una hora? Depende
Para un blog de algunas decenas de posts, sí. Esos 30–50 minutos son tiempo mío revisando y aprobando; buena parte no es trabajo de Claude, sino espera: la validación del certificado ACM, el deploy inicial de CloudFront (varios minutos) y la propagación de DNS. Si el sitio tiene cientos de posts con shortcodes custom de plugins, sumá tiempo de revisión — la conversión va a necesitar más de una iteración.
Esta es una forma de hacerlo. Los componentes son intercambiables — Astro por Hugo, AI Engine por el export nativo, GitHub Actions por otro CI, Route 53 por tu DNS actual. Lo que no cambiaría es el modelo de permisos: el agente con lo mínimo y por tiempo limitado, el pipeline sin keys, y un humano aprobando lo que llega a prod.
