Salve, salve! Bora subir um site na AWS pagando praticamente nada e ainda com pipeline de deploy automático no GitHub? É isso que esse post vai te mostrar, sem enrolação.
A ideia aqui é simples: pegar um repositório pronto (open source, MIT, link no final), entender o que tá rolando por baixo dos panos e adaptar pro seu projeto. Pode ser um portfólio, landing page, doc estática, SPA em React, Vue, Vite, Astro. Se gera HTML/CSS/JS no final, serve.
Antes de cair no código, vamos alinhar três conceitos rapidinho. Quem já manja, pula pra parte de mão na massa.
AWS, o que é isso aí?
AWS é o serviço de nuvem da Amazon. Em vez de você comprar um servidor, plugar na tomada e rezar pra não cair, você aluga "pedaços" de infraestrutura por uso. Vai do simples (guardar um arquivo no S3) até o complexo (cluster Kubernetes gerenciado, banco multi-região, IA generativa, etc).
Pra hospedar site estático, a gente vai usar quatro coisinhas básicas:
- S3: storage de objetos. Pensa numa pasta na nuvem onde você joga seus arquivos.
- CloudFront: CDN global. Distribui o site nos servidores de borda da AWS pelo mundo, então o cara em Tóquio carrega rápido igual ao cara em São Paulo.
- ACM: gerenciador de certificados SSL. HTTPS de graça, renovação automática.
- IAM: controle de quem pode fazer o quê. Permissões, roles, etc.
E a Cloudflare, fora da AWS, entra como provedora de DNS. Plano free dela já resolve, sem precisar comprar Route 53.
Infra como código (IaC), pra que serve?
Resumo da ópera: em vez de clicar no console da AWS, você descreve a infraestrutura em arquivos de código. Vantagens:
- Versiona no Git.
- Sobe em qualquer ambiente com um comando.
- Se quebrou, dá pra rever no diff.
- Code review na infra, mano.
As ferramentas mais conhecidas: Terraform, Pulumi, CloudFormation (da AWS) e AWS CDK, que é o que a gente vai usar.
AWS CDK, qual a sacada?
CDK = Cloud Development Kit. É a forma "moderna" de escrever infra na AWS. Em vez de escrever YAML/JSON gigante (CloudFormation puro), você escreve TypeScript, Python, Java, Go ou C#. O CDK compila aquilo em CloudFormation e a AWS aplica.
Vantagem: você ganha autocomplete, tipagem, abstrações de alto nível (chamadas de "constructs"). Em vez de configurar 20 recursos pra um CloudFront com OAC, S3 privado e bucket policy, você instancia uma classe e pronto.
Exemplo bobo do que é uma stack CDK:
export class MinhaStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const bucket = new s3.Bucket(this, 'MeuBucket', {
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
encryption: s3.BucketEncryption.S3_MANAGED,
});
}
}
Pronto, isso aí já cria um S3 privado com criptografia ativada. Sem clicar em lugar nenhum.
A arquitetura que a gente vai montar
O fluxo do site fica assim:
Traduzindo:
- Usuário acessa
seusite.comno navegador. - Cloudflare resolve o DNS e aponta pro CloudFront.
- CloudFront entrega via HTTPS (cert ACM) usando OAC pra puxar o arquivo do S3 privado.
- S3 nunca fica público. Quem fala com ele é só o CloudFront.
E no lado do deploy:
- Você dá push na branch
main. - GitHub Actions autentica na AWS via OIDC (sem access key fixa, mais seguro).
- Roda
cdk deploy, fazs3 synce invalida o cache do CloudFront.
Bonito né? Bora colocar pra rodar.
O repositório
O projeto tá aqui, MIT, pode clonar à vontade:
github.com/markgerald/aws-cdk-static-site-starter
Tem versão da doc em inglês e português. Aqui no post vou resumir o caminho feliz.
Pré-requisitos
Antes de qualquer coisa, você precisa de:
- Conta AWS (tem free tier, dá pra brincar sem pagar).
- Node.js 22+ instalado.
- AWS CLI configurado (
aws configure). - Domínio na Cloudflare como DNS autoritativo.
Se você nunca instalou Node/AWS CLI, tem um tutorial passo a passo aqui: Instalação local.
Pra configurar a Cloudflare, segue esse: Domínio, DNS e SSL/TLS na Cloudflare.
Clone e configura o .env
git clone https://github.com/markgerald/aws-cdk-static-site-starter.git
cd aws-cdk-static-site-starter
cp .env.example .env
Abre o .env no editor e ajusta. Esse é o coração da configuração:
PROJECT_NAME=meu-site
DOMAIN_NAME=meusite.com
WWW_DOMAIN_NAME=www.meusite.com
AWS_ACCOUNT_ID=123456789012
AWS_REGION=eu-west-1
CERTIFICATE_REGION=us-east-1
ENABLE_SPA_FALLBACK=true
CREATE_GITHUB_OIDC_ROLE=false
GITHUB_OWNER=seuuser
GITHUB_REPO=seu-repo
GITHUB_BRANCH=main
GITHUB_OIDC_PROVIDER_ARN=
Atenção em dois pontos:
-
CERTIFICATE_REGIONtem que serus-east-1. CloudFront só aceita cert ACM dessa região, é regra da AWS. -
AWS_REGIONpode ser onde você quiser. Eu usoeu-west-1por padrão, massa-east-1(São Paulo) funciona igual.
Instala e faz bootstrap
npm install
# Bootstrap nas duas regiões (cert e stack principal)
npx cdk bootstrap aws://123456789012/us-east-1
npx cdk bootstrap aws://123456789012/eu-west-1
Bootstrap é tipo um "setup inicial" que o CDK faz na conta. Cria um bucket de assets, role pra deploy, essas coisas. Roda uma vez por região e esquece.
Primeiro deploy
npm run build
npm run build:site
npm test
npx cdk deploy --all --require-approval never
Na primeira vez, o ACM vai ficar pendente esperando validação DNS. Isso é o seguinte: a AWS precisa confirmar que o domínio é seu. Ela mostra uns CNAME bizarros pra você copiar pra Cloudflare.
Vai no console AWS > ACM > região us-east-1 > seu certificado. Ele mostra algo tipo:
Name: _abc123.meusite.com
Value: _xyz789.acm-validations.aws
Type: CNAME
Copia isso pra Cloudflare, DNS only (nuvem cinza, sem proxy). Aguarda uns minutos, ACM marca Issued e o deploy segue.
Detalhes finos da validação: Tutorial Cloudflare ACM no repo.
Aponta o domínio pro CloudFront
Depois que a stack subiu, o CDK te devolve um output tipo d111111abcdef8.cloudfront.net. Vai na Cloudflare e cria:
Type Name Target
CNAME @ d111111abcdef8.cloudfront.net
CNAME www d111111abcdef8.cloudfront.net
Começa com DNS only. Se depois quiser ativar o proxy laranja da Cloudflare, usa modo Full (strict) no SSL/TLS. Nunca Flexible (gera loop de redirect).
Abre o navegador, acessa https://meusite.com, deve carregar a página de exemplo do repo.
Como adaptar pro seu site
Agora vem a parte que interessa: como botar seu site no lugar do exemplo.
A pasta com os arquivos do site é website_src/. Por padrão vem um React + Vite mínimo. Estrutura:
website_src/
├── favicon.svg
├── index.html
├── src/
│ └── (componentes React)
├── tsconfig.json
└── vite.config.ts
Você tem duas opções:
Opção 1: jogar seu projeto inteiro no website_src/
Mais simples. Apaga o conteúdo de website_src/ e cola o seu projeto (Next exportado, Astro, Vue, HTML puro, qualquer coisa). Ajusta o script build:site no package.json pra rodar o build do seu framework. Hoje tá assim:
"build:site": "vite build --config website_src/vite.config.ts"
Se você usa Astro, vira algo tipo:
"build:site": "cd website_src && npm install && npm run build"
O importante é que o build final tem que sair em website_dist/, porque é essa pasta que o deploy faz upload pro S3. Configura o output do seu framework pra apontar pra lá:
// vite.config.ts
export default defineConfig({
build: {
outDir: '../website_dist',
},
});
Opção 2: usar como monorepo
Você mantém seu site num repositório separado, gera os arquivos, joga no website_dist/ e faz só o deploy. Útil se o time de front é separado do time de infra.
HTML puro? Beleza.
Se seu site é só HTML/CSS/JS estático, joga tudo direto em website_dist/:
mkdir -p website_dist
cp -r meu-site-pronto/* website_dist/
E desativa o SPA fallback (não precisa de fallback pra index.html quando não tem rotas client-side):
ENABLE_SPA_FALLBACK=false
Faz o deploy do site
Depois de configurar e buildar:
npm run build:site
BUCKET_NAME=$(aws cloudformation describe-stacks \
--stack-name aws-cdk-static-site-starter-static-site \
--region eu-west-1 \
--query "Stacks[0].Outputs[?OutputKey=='WebsiteBucketName'].OutputValue" \
--output text)
DISTRIBUTION_ID=$(aws cloudformation describe-stacks \
--stack-name aws-cdk-static-site-starter-static-site \
--region eu-west-1 \
--query "Stacks[0].Outputs[?OutputKey=='CloudFrontDistributionId'].OutputValue" \
--output text)
aws s3 sync website_dist/ "s3://${BUCKET_NAME}" --delete --cache-control "public,max-age=300"
aws cloudfront create-invalidation --distribution-id "$DISTRIBUTION_ID" --paths "/*"
Esse create-invalidation é pra forçar o CloudFront a buscar a versão nova. Sem isso, ele pode servir cache velho por minutos/horas.
Dica: dá pra colocar isso num script
deploy-site.she rodar de uma vez.
Automatiza tudo no GitHub Actions
Manualzão é bom pra aprender. Em produção, você quer push na main e ver o site atualizar sozinho.
O repo já vem com dois workflows:
-
.github/workflows/ci.yml: roda em PR e push, faz build/test/synth, sem credencial AWS. -
.github/workflows/deploy.yml: roda em push na main, autentica via OIDC, fazcdk deploy, sync e invalidation.
OIDC é uma sacada legal: em vez de você guardar AWS_ACCESS_KEY_ID e AWS_SECRET_ACCESS_KEY como segredos no GitHub (que vazam em log), o GitHub Actions pede uma credencial temporária pra AWS, que confia no provedor OIDC do GitHub. Sem chave fixa, sem rotação manual.
Pra criar a role IAM com confiança OIDC, ativa no .env:
CREATE_GITHUB_OIDC_ROLE=true
GITHUB_OWNER=seuuser
GITHUB_REPO=seu-repo
GITHUB_BRANCH=main
Roda npx cdk deploy de novo, copia o output GithubActionsRoleArn e adiciona como Repository Variable no GitHub:
AWS_ROLE_ARN=arn:aws:iam::123456789012:role/...github-actions-deploy
Junto com as outras variables (mesmas chaves do .env):
AWS_ACCOUNT_ID=123456789012
AWS_REGION=eu-west-1
DOMAIN_NAME=meusite.com
WWW_DOMAIN_NAME=www.meusite.com
PROJECT_NAME=meu-site
CERTIFICATE_REGION=us-east-1
ENABLE_SPA_FALLBACK=true
CREATE_GITHUB_OIDC_ROLE=false
GITHUB_OWNER=seuuser
GITHUB_REPO=seu-repo
GITHUB_BRANCH=main
Aí dá push na main e fica esperto no Actions: build, deploy, sync, invalidation. Tudo automático.
Quanto custa essa brincadeira?
Pra um site pequeno (até alguns milhares de hits por mês), o custo fica centavos por mês ou zerado, dependendo do volume:
- ACM: certificado público pra CloudFront é gratuito.
- S3: cobra storage e requests. Site pequeno fica em centavos.
- CloudFront: tem free tier mensal generoso (1 TB transferência + 10M requests). Acima disso, paga.
- Cloudflare DNS: plano free resolve.
O que pode encarecer:
- Tráfego alto no CloudFront (acima do free tier).
- Invalidations frequentes e amplas (
/*direto, várias vezes por dia). - Habilitar WAF, logs, Lambda@Edge, CloudFront Functions sem necessidade.
Por isso o projeto propositalmente não liga essas coisas. Se você precisar, liga depois, com consciência do custo.
Segurança, o feijão com arroz
O setup já vem com:
- S3 com
BlockPublicAccess. Nunca exposto. - CloudFront acessa S3 via OAC (a forma moderna), não OAI legado.
- HTTP redireciona pra HTTPS automaticamente.
- Workflow usa OIDC, sem access key long-lived.
Em produção, troca o AdministratorAccess da role de deploy por uma política de menor privilégio. O readme explica como.
E a parte de "deletar tudo"?
Quer derrubar a brincadeira? Esvazia o bucket primeiro (o autoDeleteObjects tá false de propósito, pra evitar custom resources):
BUCKET_NAME=$(aws cloudformation describe-stacks \
--stack-name aws-cdk-static-site-starter-static-site \
--region eu-west-1 \
--query "Stacks[0].Outputs[?OutputKey=='WebsiteBucketName'].OutputValue" \
--output text)
aws s3 rm "s3://${BUCKET_NAME}" --recursive
npx cdk destroy --all
Não esquece de apagar os CNAMEs na Cloudflare também.
Pra fechar
Resumindo o que rolou:
- Você aprendeu (ou revisou) AWS, IaC e CDK.
- Subiu um site estático na AWS com HTTPS, CDN global e DNS na Cloudflare.
- Configurou deploy automático via GitHub Actions com OIDC.
- Pagou praticamente nada por isso.
O projeto é open source, tá no github.com/markgerald/aws-cdk-static-site-starter. Se curtir, deixa uma star. Se achar bug ou tiver sugestão, manda PR ou abre issue.
Dúvida? Comenta aí embaixo que eu respondo.
Valeu, falou! 👋

Top comments (0)