Artigo

Como construí meu portfólio: um site estático com um CMS serverless

Por dentro do andrepatrinicola.com.br: páginas pré-renderizadas em S3 e CloudFront, um CMS próprio em Lambda e DynamoDB e um deploy no GitHub Actions que publica cada artigo sem nenhum segredo compartilhado entre os serviços. Texto alternativo da imagem: Diagrama da arquitetura: visitantes acessam o site pelo CloudFront e S3; o administrador faz login no Cognito e usa a API Gateway com três Lambda

  • aws
  • serverless
  • arquitetura

Este é o primeiro post deste blog, e nada mais justo do que começar explicando como ele chega até você. O portfólio já tinha jogos, laboratórios e currículo. Faltava um lugar para escrever. Eu queria publicar sem editar código a cada texto, mas sem abrir mão do que o site já tinha de bom: ser estático, rápido e barato de manter.

A ideia central: o site continua estático

Cada página deste site é um arquivo HTML gerado no build e servido pelo CloudFront a partir de um bucket S3 privado. Quando você abre este artigo, nenhuma API é chamada e nenhum banco de dados é consultado.

O conteúdo do blog mora em outro lugar, num CMS que eu chamei de Portfolio Studio. Publicar um artigo significa rodar o deploy do site de novo, agora com o artigo incluído. Essa separação guiou todas as outras decisões.

O site

O site é uma aplicação React Router com pré-renderização: no build, cada rota vira um HTML pronto, com título, descrição, dados estruturados e versões em português, inglês e espanhol. Uma CloudFront Function transforma URLs limpas, como /pt/blog, no arquivo certo dentro do bucket. Os jogos são aplicações Vite independentes, publicadas no mesmo bucket.

As imagens dos artigos passam por um processamento no build: são giradas pela orientação da foto, perdem os metadados e viram WebP em quatro larguras (480, 960, 1200 e 1600 px). Os originais nunca ficam públicos.

O Portfolio Studio

O CMS fica em /admin, dentro do próprio site, mas não tem conteúdo nenhum no HTML. Tudo vem da API depois do login.

  • Login: Amazon Cognito com o fluxo authorization code + PKCE e MFA obrigatório. Só existe um usuário, criado por mim, no grupo admin.

  • API: API Gateway (HTTP API) na frente de Lambdas em Node.js. A borda valida o token, e a Lambda confere de novo o grupo e o tipo do token. O navegador decide só o que mostrar; quem autoriza é a API.

  • Editor: Tiptap, limitado aos elementos que o site sabe renderizar. O documento é salvo como JSON validado por um schema na API, então nenhum HTML vindo do navegador é armazenado.

  • Rascunhos: autosave depois de uma pausa na digitação, com controle de versão otimista. Se o mesmo artigo mudar em outra aba, eu escolho qual versão fica, em vez de uma sobrescrever a outra.

  • Dados: uma tabela no DynamoDB para artigos, mídia e deploys, e um bucket S3 privado para revisões imutáveis, imagens originais e snapshots.

Publicar é fazer deploy

Arquitetura do portfolio da AWS
Arquitetura do portfolio da AWS

Quando eu clico em Publicar, acontece o seguinte:

  1. A API grava uma revisão imutável do artigo, reserva os slugs de cada idioma e cria um registro de deployment.

  2. Por meio de um GitHub App, a API dispara o workflow de deploy do repositório do site.

  3. O primeiro passo do workflow pede à API o conteúdo a publicar. Esse pedido congela um snapshot do que deve estar no ar naquele instante.

  4. O workflow processa as imagens, faz o build, sincroniza o bucket e invalida o cache do CloudFront.

  5. No fim, o workflow informa o resultado à API, e só então o artigo passa a constar como publicado.

O congelamento resolve alguns casos que pareciam pequenos, mas não são. Se eu publicar outro texto durante um build, ele entra no deploy seguinte, sem misturar estados. Se alguém executar de novo um workflow antigo, a API recusa, e conteúdo velho não volta ao ar. Se o build falhar, nada é enviado e o artigo volta ao estado anterior.

Segurança sem segredos compartilhados

Nenhuma credencial de longa duração circula entre os serviços:

  • O workflow prova para a API quem ele é com o token OIDC do próprio GitHub Actions. A API aceita apenas o job de produção do branch main do repositório do site.

  • A API fala com o GitHub por um GitHub App instalado só no repositório do site, com uma única permissão: rodar workflows. O token de instalação dura uma hora.

  • Os deploys assumem roles na AWS também por OIDC, sem chaves de acesso guardadas no GitHub.

  • As chaves que precisam existir, como a do GitHub App e o token do LinkedIn, ficam no SSM Parameter Store, criptografadas, fora do código e fora do state do Terraform.

LinkedIn sem atalhos

Divulgar um artigo no LinkedIn também passa pelo Studio, e sempre com a API oficial: os produtos “Share on LinkedIn” e “Sign In with LinkedIn using OpenID Connect”. Nenhum post sai sem a minha aprovação, e os agendados usam o EventBridge Scheduler. Quando a integração não está conectada, o próprio Studio prepara o texto e abre o LinkedIn para eu publicar à mão.

Desenvolver sem AWS

Uma parte de que gostei bastante: para trabalhar no Studio, não preciso de conta na AWS nem de Docker. Um npm run dev na API sobe um servidor local que executa as mesmas Lambdas, com DynamoDB, S3, SSM e Scheduler simulados em memória e salvos em disco. No site, npm run dev:studio abre o admin já logado. Publicar localmente roda os mesmos scripts do deploy de verdade, e o blog atualiza na hora. As mesmas simulações rodam nos testes automatizados.

O que aprendi no caminho

  • Um bucket privado responde 403, não 404. Sem permissão de listar o bucket, o S3 não revela se um arquivo existe. Para uma página inexistente mostrar o 404 do site, o CloudFront precisa transformar esse 403 em /404.html com status 404.

  • Estático tem um preço pequeno e previsível. Publicar leva o tempo de um build, alguns minutos. Em troca, o pico de acessos de um artigo não encosta no backend.

  • Separar “salvo” de “no ar” simplifica tudo. O estado editorial e o que está publicado são campos diferentes, e o segundo só muda quando o deploy confirma. Isso tornou as falhas fáceis de tratar.

Nos próximos posts, quero detalhar algumas dessas partes, começando pelo fluxo de publicação. Obrigado por ler até aqui.