Você realmente precisa do WordPress? Migre seu blog para S3 + CloudFront com o Claude em menos de uma hora
Traduzido do original em espanhol. Ler em espanhol
WordPress, Drupal, Joomla — são plataformas maduras e resolvem muitos casos. E sim, para um site com usuários, formulários, e-commerce ou uma equipe editorial, continuam fazendo sentido. Mas se tudo o que você faz é publicar artigos, você está pagando caro por algo que não usa: PHP, um banco de dados, plugins que precisam de patch e um painel de admin exposto na internet.
A alternativa é velha e sem graça: HTML estático em um bucket do S3, servido pelo CloudFront. O que mudou é o custo da migração. Com o Claude Code, a migração completa — conteúdo, infraestrutura em stage e prod, pipelines — me levou entre 30 e 50 minutos.
Como referência: estimo que um senior dev que conhece AWS, fazendo o mesmo à mão para um site de 50 posts, leve de 3 a 5 dias de trabalho — umas 20 a 35 horas. A maior parte não é a infra, e sim converter e limpar o conteúdo post por post e replicar o theme. Se ele já tiver a infra em Terraform ou CDK, cai para um dia e meio ou dois. Continua sendo outra escala.
Aqui vai o passo a passo, com os detalhes técnicos e — sobretudo — o modelo de segurança.

Você precisa de um CMS? Os trade-offs
Antes de migrar, vale ser honesto sobre o que você perde e o que ganha.
O que você ganha:
- Superfície de ataque quase nula — não há PHP, nem banco de dados, nem
/wp-admin. Nada é executado do lado do servidor, então não há nada para explorar. - Zero manutenção — chega de updates de core, de plugins e de theme. Um site estático de cinco anos atrás continua funcionando igual.
- Custo — para um blog pequeno, centavos por mês. O CloudFront tem um free tier de 1 TB de transferência mensal.
- Performance — tudo sai do edge do CloudFront. Não há render por request.
- Versionamento de verdade — o site inteiro vive no Git. Cada mudança tem histórico, review e rollback.
O que você perde:
- Comentários — resolve-se com um serviço externo (Giscus, por exemplo) ou eles são removidos. Eu os removi.
- Formulários — você precisa de um endpoint à parte (Lambda, Formspree). Adiciona uma peça móvel.
- Busca — só do lado do cliente (Pagefind, Lunr). Funciona bem até alguns milhares de posts.
- O editor visual — você escreve em Markdown. Se isso não é para você, mais abaixo mostro como o Claude resolve essa parte.
Se a lista do que você perde não dói, continue lendo.
A arquitetura
O design final é simples:
- GitHub repo — o conteúdo em Markdown e o gerador de site estático (uso Astro).
- GitHub Actions — um pipeline que faz o build e publica em stage a cada push para
main, e em prod depois de uma aprovação manual. - S3 — dois buckets privados, um por ambiente. Sem website hosting, sem acesso público.
- CloudFront — uma distribuição por ambiente, com Origin Access Control (OAC) para ler do bucket, certificado do ACM e uma CloudFront Function para as URLs.
- IAM — duas identidades diferentes, com permissões diferentes: uma temporária para o Claude fazer a migração e um role por ambiente para o pipeline, via OIDC.
O modelo de segurança, antes de mexer em qualquer coisa
A ideia original era dar ao Claude uma access key, criar o repo, criar os pipelines e pronto. Funciona. Mas ao revisar aparecem três problemas, e eu os corrigi antes de começar:
- O Claude não pode ter permissões de IAM. Se o user do Claude pode criar roles e policies, pode se conceder qualquer permissão — é escalação de privilégios por design. O OIDC provider e os roles do pipeline são criados por você.
- O pipeline não usa access keys. Nada de
AWS_ACCESS_KEY_IDnos GitHub Secrets. O GitHub Actions se autentica na AWS com OIDC e recebe credenciais temporárias, e a trust policy restringe qual repo e qual ambiente pode assumir cada role. - O Claude não pode modificar o pipeline. Se o agente pode editar
.github/workflows/, pode mudar para onde se publica e com quais permissões. Os workflows são commitados por você, e o token do Claude não tem permissão para mexer neles.
A regra que aplico: o agente opera dentro dos limites; nunca define os limites.
Passo a passo
1. Crie o IAM user para o Claude
No console do IAM, crie um user sem acesso ao console — por exemplo claude-migration. Atribua a ele esta policy inline. Substitua mi-blog pelo prefixo dos seus 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": "*"
}
]
}
Três coisas a notar:
- O S3 está restrito por ARN — o Claude só pode mexer em buckets que comecem com o seu prefixo. Não há
s3:DeleteBucket: apagar um bucket é uma decisão sua. - O CloudFront usa
"Resource": "*"— a criação de distribuições não admite restrição por recurso. É o ponto mais amplo da policy, e é o motivo pelo qual a key só existe durante a migração. - Não há nenhuma ação de IAM. De propósito.
Se durante a migração aparecer um AccessDenied, o Claude vai dizer qual ação está faltando. Adicione só ela. Não coloque cloudfront:* para destravar.
Crie a access key do user e configure-a em um profile próprio, não no default:
aws configure --profile claude-migration
export AWS_PROFILE=claude-migration
aws sts get-caller-identity
Nunca cole a key no chat com o Claude. O agente usa o profile; não precisa ver o segredo.
2. Crie o repo e limite o que o Claude pode fazer
Crie o repo no GitHub (privado, mesmo que o site seja público — não é preciso expor drafts). Depois gere um fine-grained personal access token:
- Repository access — só este repo.
- Contents — read and write.
- Pull requests — read and write.
- Workflows — sem acesso. Assim o Claude não consegue fazer push de mudanças em
.github/workflows/. - Expiration — 7 dias.
Do lado do Claude Code, adicione um .claude/settings.json no repo com regras de permissões:
{
"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:*)"
]
}
}
Lembre-se de que esta é uma segunda camada, não a principal. Um deny sobre Read não impede um cat pelo Bash. O controle real é a policy de IAM do passo anterior. As regras do Claude Code servem para que as operações importantes passem pela sua aprovação.
3. Migre o conteúdo: o Claude lê o WordPress via MCP
Aqui não há export, nem XML, nem scripts de conversão. Instalo o plugin AI Engine (da Meow Apps) no WordPress, que expõe um MCP server, e conecto o Claude diretamente ao site. O Claude lê todos os posts, categorias e tags, baixa as imagens e monta o site novo. Tudo é feito pelo Claude.
Para autenticar uso OAuth, não um Bearer Token estático. A diferença importa:
- Não há segredo compartilhado — não há um token para copiar, colar em um comando e que acabe no histórico do shell ou em um arquivo de config.
- Identidade real — cada autorização fica vinculada a um usuário do WordPress, com login normal e uma tela de consentimento que mostra qual app pede acesso e com qual papel.
- Revogação em um clique — o painel Connected Apps do AI Engine lista cada autorização ativa.
No WordPress, AI Engine → Settings → MCP: ative o MCP server. Com OAuth não é preciso definir Bearer Token — o AI Engine publica os endpoints de discovery (/.well-known/oauth-*) e o cliente se registra sozinho via Dynamic Client Registration. A partir do repo:
claude mcp add --transport http wp-origen https://mi-sitio.com/wp-json/mcp/v1/http
Depois, dentro do Claude Code, rode /mcp, escolha wp-origen e Authenticate. O browser abre, você faz login no WordPress e aprova. O token fica no armazenamento seguro do Claude Code, não em um arquivo do repo.
E aqui está a distinção mais ignorada: com OAuth, o Claude tem as permissões do usuário com que você faz login. Se você aprova como admin, o Claude é admin — pode apagar posts, mudar settings, instalar plugins se o grupo estiver exposto. OAuth melhora a autenticação; não reduz a autorização. Por isso:
- Crie um usuário dedicado — por exemplo
claude-migracion, com o papel mais baixo que permita ler o que você precisa. Não aprove com sua conta de admin. - Exponha só o necessário — na tela do MCP, deixe ativo só o grupo WordPress (posts, media, taxonomias). Plugins, Themes e Dynamic REST, desligados.
- Revise o WAF — algumas hospedagens bloqueiam os POST para
/wp-json/mcp/v1/oauth/registere o registro falha. Se isso acontecer, é preciso liberar/.well-known/oauth-*e/wp-json/mcp/v1/*.
Se a sua versão do AI Engine exige um usuário admin para o MCP, o trade-off muda: um Bearer Token com access level Read-Only é restringido do lado do servidor, e aí pode ser a opção mais segura das duas. Verifique antes de escolher.
Como segunda camada, bloqueie as tools de escrita do lado do Claude Code. Adicione ao .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"
]
}
}
Os nomes exatos das tools dependem da versão do plugin — com /mcp você vê a lista. Bloqueie tudo o que escreve. O Claude só precisa das tools de leitura.
Com o MCP conectado, o 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.
Revise o plano. Com cuidado. O que mais quebra nesta etapa são as URLs: se mudarem, você perde o SEO acumulado e todos os links externos. No Astro, a rota é definida pela estrutura de src/pages/ — por exemplo src/pages/[year]/[month]/[slug].astro se o seu WordPress usa /%year%/%monthnum%/%postname%/. Se a estrutura coincidir, não há redirects para manter.
Deixe o build.format do Astro em directory (o padrão): ele gera /mi-post/index.html, que é o que depois a CloudFront Function do passo 4 resolve. E crie src/pages/404.astro — o build o transforma em 404.html.
Para visualizar localmente:
npm run dev
E aqui há um risco que costuma passar despercebido: o conteúdo que o Claude está lendo é untrusted (não confiável). Posts antigos, HTML colado de outros sites, um comentário de spam que passou. Um texto com instruções escondidas é um vetor de prompt injection — e com um MCP que pode escrever no seu site ao vivo, o dano possível já não é só local. Por isso as tools de escrita vão em deny, não em ask, e por isso as permissões da AWS são mínimas.
A alternativa sem MCP é o export nativo (Tools → Export, formato WXR) mais uma cópia de wp-content/uploads. Não mexe no site ao vivo, mas cabe a você baixar e organizar os arquivos. Para um site com poucos posts, o MCP é bem mais rápido.
4. Peça ao Claude a infraestrutura
Com o site fazendo build localmente, peça ao Claude que crie a infra para stage e prod. Isto é o que precisa ficar pronto, e o que vale verificar:
- Buckets privados — Block Public Access ativado nas quatro flags, sem static website hosting. O CloudFront lê pelo endpoint REST do bucket, não pelo de website.
- Origin Access Control — não o legacy OAI. O bucket só aceita
s3:GetObjecta partir daquela distribuição específica. - Certificado ACM em
us-east-1— o CloudFront só aceita certificados dessa região, não importa onde estejam os buckets. A validação é por DNS. - Response headers policy — HSTS,
X-Content-Type-Options,X-Frame-Options,Referrer-Policy. A managed policy SecurityHeadersPolicy cobre o básico. - TLS mínimo —
TLSv1.2_2021e redirect de HTTP para HTTPS.
A bucket policy que deve ficar em 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. Conecte o GitHub à AWS via OIDC (isto é com você)
Este é o passo que não delego ao Claude, porque exige permissões de IAM. Primeiro, o identity provider (uma vez por conta):
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com
Depois, um role por ambiente. A trust policy do 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"
}
}
}
]
}
A condição sobre sub é a que garante “só este repo pode publicar”. E vai um passo além: só um job rodando no environment production desse repo pode assumir o role. Use StringEquals, não StringLike com wildcards — um repo:TU_USUARIO/* abre o acesso a todos os seus repos.
O role de stage é idêntico, com environment:staging e apontando para o bucket de stage.
As permissões do role de prod — só o que o deploy precisa:
{
"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"
}
]
}
Aqui CreateInvalidation admite restrição por ARN. O pipeline pode publicar em um bucket e invalidar uma distribuição. Nada mais.
6. Os pipelines de stage e prod
No repo, Settings → Environments, crie dois environments:
- staging — deployment branch: só
main. Variáveis:AWS_ROLE_ARN,BUCKET,DISTRIBUTION_ID,SITE_URL. - production — deployment branch: só
main, required reviewer: você. As mesmas variáveis, com os valores de prod.
São variáveis, não secrets. Um role ARN não é um segredo — sem o token OIDC do seu repo, ele não serve para nada.
O workflow, em .github/workflows/deploy.yml, tem três jobs:
- check — roda em cada PR. Instala as dependências com
npm cie faz o build. Não pede credenciais da AWS. - deploy-stage — roda em cada push para
main, no environmentstaging. Faz o build, assume o role de stage via OIDC, sobe o site comaws s3 sync --deletee invalida o cache do CloudFront. - deploy-prod — depende de stage e roda no environment
production. Os mesmos passos, com o role e as variáveis de prod.
Dois detalhes que fazem a diferença: a permissão id-token: write é declarada só nos jobs de deploy, não no nível do workflow, e os três jobs leem tudo das variáveis do environment — o mesmo código publica em stage ou em prod conforme onde roda.
O fluxo fica assim: um PR só faz o build — sem credenciais da AWS, então um PR malicioso não consegue publicar nada. Um merge em main publica em stage automaticamente. Prod fica esperando a sua aprovação na aba Actions. Você aprova, e publica.
Sobre a invalidação: /* conta como um único path, e os primeiros 1.000 paths por mês são gratuitos. Para um blog, invalidar tudo a cada deploy é a opção mais simples e não custa nada.
Fazer o build duas vezes (uma por ambiente) é proposital: a URL do site muda, e o Astro a embute no HTML — canonical, sitemap, RSS. Em astro.config.mjs:
export default defineConfig({
site: process.env.SITE_URL,
build: { format: 'directory' },
});
No pipeline, SITE_URL vem da variável do environment. O custo são alguns segundos a mais de build.
7. Proteja o pipeline
O pipeline agora é o único caminho para prod. Isso o torna o ativo a proteger:
- Branch protection em
main— PR obrigatório, status checkcheckverde, sem force push. - CODEOWNERS sobre
.github/— qualquer mudança nos workflows exige o seu review. - Required reviewer em production — já configurado no passo anterior. É o último controle humano antes de publicar.
8. Cutover
Com prod publicado e verificado na URL do CloudFront (dxxxx.cloudfront.net):
- Baixe o TTL do registro DNS para 300 segundos, um dia antes.
- Aponte o domínio para a distribuição: um alias record no Route 53, ou um CNAME no seu provedor (para o apex, use ALIAS/ANAME se ele suportar).
- Verifique se as URLs antigas respondem. Um script simples que percorra o sitemap do WordPress e compare os códigos HTTP é suficiente.
- Mantenha o WordPress desligado — não apagado — por algumas semanas, caso apareça algo que tenha escapado.
9. Feche tudo o que você abriu para a migração
Primeiro, a key da AWS:
aws iam update-access-key \
--user-name claude-migration \
--access-key-id AKIAXXXXXXXXXXXXXXXX \
--status Inactive
E remova-a de ~/.aws/credentials. Uma key inativa em disco continua sendo uma key esperando que alguém a reative.
Minha recomendação, na verdade, é ir um passo além: apagá-la. Quando precisar, você cria uma nova — leva dez segundos e garante que nenhuma key antiga fique esquecida. Revogue também o token do GitHub, ou deixe-o expirar.
Depois, o MCP. Se o WordPress continuar online durante o período de transição, revogue a autorização em Connected Apps, desative o MCP no AI Engine e apague o usuário claude-migracion. Se o plugin estava só para a migração, desinstale-o. E do lado do Claude Code:
claude mcp remove wp-origen
Um MCP com permissões de escrita sobre um site que você não usa mais é exatamente o tipo de coisa que é esquecida e fica aberta.
O Claude é o seu editor “visual”
Aqui está o que convence quem não quer saber de Markdown nem de Git. O painel do WordPress desaparece, e o editor passa a ser uma conversa. Você abre o Claude Code no repo e diz:
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.
Você revisa em localhost:4321, pede ajustes — “encurte a intro”, “mova a imagem para baixo do primeiro parágrafo”, “mude o título” — e aprova. O Claude abre o PR, o check passa, você faz o merge, stage se atualiza sozinho, você aprova prod. Pronto.
Para isso não são necessárias credenciais da AWS. O Claude só precisa do token do GitHub — o deploy é feito pelo pipeline. É a configuração que deixo de forma permanente.
Um skill com as regras do seu blog
O prompt acima funciona, mas deixa muito por conta do que o Claude inferir dos posts existentes. A forma de fixar isso é um skill: um arquivo com as regras de como um post deve ser, que o Claude carrega toda vez que você pede um. É o que um editor faz — conhecer o manual de estilo sem que você precise repeti-lo.
Ele vive no repo, em .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.
Com o skill, o prompt se reduz a “crie um post com este draft”. O resto — formato, imagens, categorias, o fluxo até o PR — já está definido.
A validação de segurança, resumida
Estes são os riscos que revisei, e como fica cada um:
- Access key de longa duração — mitigado. Só existe durante a migração, em um profile separado, e é apagada depois. Risco residual: a janela da migração, menos de uma hora.
- Escalação de privilégios via IAM — eliminado. O user do Claude não tem nenhuma ação de IAM; os roles do pipeline são criados por você.
- Policy do CloudFront com
Resource: *— aceito. É uma limitação da API. Durante a migração, o Claude poderia modificar outras distribuições da conta. Se você tem outras distribuições em produção, faça a migração em uma conta separada do AWS Organizations. - Credenciais no GitHub — eliminado. OIDC com credenciais temporárias, sem secrets.
- Outro repo publicando no seu bucket — eliminado. A trust policy exige repo e environment exatos.
- Agente modificando o pipeline — mitigado. O token não tem permissão de Workflows, e o CODEOWNERS exige o seu review.
- Prompt injection a partir do conteúdo importado — mitigado, não eliminado. O blast radius é limitado pelas permissões, e os comandos sensíveis exigem a sua aprovação.
- Bucket exposto — eliminado. Block Public Access, sem website hosting, OAC vinculado a uma distribuição.
s3 sync --deleteapagando demais — aceito. Se um build sair vazio, ele apaga o site. Ative o versioning no bucket de prod: você recupera qualquer versão anterior por centavos.- MCP com escrita sobre o WordPress ao vivo — mitigado. OAuth com um usuário dedicado de papel baixo, só o grupo WordPress exposto, tools de escrita em
deny, e autorização revogada e MCP desligado ao terminar. Risco residual: o endpoint fica exposto na internet enquanto está ativo, e as permissões reais são as do papel do usuário. - Supply chain — mitigado. Actions fixadas por SHA,
npm cicom lockfile commitado.
Menos de uma hora? Depende
Para um blog de algumas dezenas de posts, sim. Esses 30–50 minutos são tempo meu revisando e aprovando; boa parte não é trabalho do Claude, e sim espera: a validação do certificado ACM, o deploy inicial do CloudFront (vários minutos) e a propagação do DNS. Se o site tem centenas de posts com shortcodes custom de plugins, some tempo de revisão — a conversão vai precisar de mais de uma iteração.
Esta é uma forma de fazer. Os componentes são intercambiáveis — Astro por Hugo, AI Engine pelo export nativo, GitHub Actions por outro CI, Route 53 pelo seu DNS atual. O que eu não mudaria é o modelo de permissões: o agente com o mínimo e por tempo limitado, o pipeline sem keys e um humano aprovando o que chega a prod.
