Otimizando o deploy de aplicações Web
Em uma segunda-feira rotineira…
Segunda-feira, 8h da manhã e você é interrompido antes da máquina liberar seu café: estamos com 32 tickets de clientes reclamando de um bug que subiu na última release, e já é suficiente para que você volte para sua mesa correndo, sem nem sequer pegar seu café.
Duas horas depois, bug identificado, deploy inicializado, e agora sim você pode tomar seu café, e novamente te interrompem:
- O problema continua.
- Acabei de corrigir. Na minha máquina funciona. Limpou o cache?
- Sim e continua.
E aí você lembra que precisa limpar o cache do CDN para que a solução do bug seja propagada para os clientes, e daí vai ser esperar a “bondade” de cada browser diferente para que expire a versão anterior do seu código.
Porque isso acontece?
Em nosso site hipotético, estamos utilizando o AWS S3 para armazenar os arquivos, e o AWS Cloudfront como CDN.
O Cloudfront irá servir seus arquivos por servidores espalhados pelo mundo com o intuito de disponibiliza-los para seus clientes do servidor que estiver geograficamente mais próximo. Isso faz com que diminua o tempo de resposta do site (diferença quase imperceptível pelo cliente final), além disso nos possibilita utilizar um domínio (URL do site) personalizado, certificado ssl gratuito e automaticamente renovado pela própria AWS e, principalmente, devido ao fato de ter muitos servidores distribuídos pelo globo, mantém um cache de nossos arquivos em cada um destes muitos servidores, poupando nosso bucket da S3 ou se for o caso, nossos servidores de receberem muitas requisições destes arquivos.
Descobrimos que os arquivos do site estão saindo do S3 sem o cabeçalho Cache-Control, que é o responsável por definir por quanto tempo os serviços de CDN e os navegadores irão reutilizar estes arquivos, e quando este valor não é informado, as ferramentas fazem uma inferência, cada uma escolhe com base em critérios próprios um tempo de duração.
Talvez você esteja pensando: Se não digo por quanto tempo deve-se guardar estes arquivos em cache, então eles não devem ser guardados em cache.
Existe uma corrida armada ( armada com cache) entre os navegadores e serviços de distribuição para conseguir performance, e nesta disputa vale quase tudo, inclusive deixar seus clientes com arquivos obsoletos.
O que o RFC nos diz sobre isso ?
De acordo com a especificação do HTTP 1.1, existe um cálculo “heurístico” para definir uma data de duração de cache para estes arquivos, mas cada implementação tem liberdade para criar suas próprias regras. Em outras palavras: Você nunca vai saber quanto tempo seus arquivos serão armazenados em cache em cada navegador. Diante disto, não informar um valor para o Cache-Control
é jogar roleta-russa com este tempo de duração.
Como resolver este problema ?
Para este nosso site estamos utilizando ReactJS e o Create React App, que é uma ferramenta para nos ajudar a criar, testar e gerar os arquivos finais para nosso site.
Nosso fluxo de desenvolvimento e deploy esta definido da seguinte forma:
Esta é uma proposta para um site pronto para receber um volume ilimitado ( limitado à disponibilidade financeira ), mas o mesmo resultado pode ser obtido utilizando apenas o Github/Gitlab + DroneCI/GithubActions/GitlabCICD + AWS S3.
Esta arquitetura completa, para novas contas na AWS, custaria apenas $ 0.50 ( custo para gerenciar um domínio no AWS Route 53 ), considerando que o armazenamento no S3 e o tráfego de rede ficariam dentro da cota de gratuidade de um ano para uso apenas pelo time de desenvolvimento.
De acordo com a documentação do Create React App, ao executar o comando react-scripts build
será criada uma pasta, build
, e dentro desta pasta teremos a pasta build/static
. Segundo esta documentação, tudo o que está dentro da pasta build/static
, pode ser armazenado em cache por tempo indeterminado.
Os arquivos criados na pasta build/static
seguem o seguinte padrão de nome:
[number].[hash].chunk.js
Existem outros padrões dentro do mesmo build, mas sempre com o “hash”
Onde a sessão hash
é um identificador único para o build atual, e estes arquivos serão carregados pelo service-worker.js
através do arquivo build/precache-manifest.[hash].js
, por tanto, é muito importante que estes dois arquivos, além dos demais fora de build/static
não sejam armazenados em cache.
Com base nestas informações, o que faremos é modificar nosso processo de deploy no DroneCI para informar que todos os arquivos fora da pasta build/static
não devem ser amarzenados em nenhum mecanismo de cache, e todos os arquivos dentro de build/static
devem ser armazenados em cache.
Após “compilar” os arquivos, nosso processo de deploy consistia em apenas sincronizar os arquivos criados no bucket da AWS S3, informando apenas que estes arquivos deveriam estar disponíveis para que qualquer pessoa possa ler ( public-read
).
aws s3 sync --acl 'public-read' ./ s3://$AWS_S3_BUCKET/
Para resolvermos o problema dos clientes não receberem a versão mais nova do site e continuarmos tendo os benefícios de cache tanto do browser quando do Cloudfront, removemos o comando anterior e inicluímos estes dois novos:
aws s3 sync --acl 'public-read' --cache-control "public, max-age=604800, immutable" ./static/ s3://$AWS_S3_BUCKET/static/
aws s3 sync --acl 'public-read' --cache-control 'no-store' ./ s3://$AWS_S3_BUCKET/
O primeiro comando sincroniza os novos arquivos na pasta build/static
com os seguintes parâmetros:
--acl 'public-read'
informa ao AWS S3 que estes arquivos podem ser lidos por qualquer pessoa.
--cache-control "public, max-age=604800, immutable"
informa ao AWS S3 que estes arquivos devem ser servidos com o cabeçalho Cache-Control
com o valor especificado.
O segundo comando sincroniza os demais arquivos ( note que como é um processo de sincronia, os arquivos estáticos que foram copiados anteriormente não serão modificados ).
Neste segundo comando é onde resolvemos nosso problema de atualização do site. Os arquivos index.html
e service-worker.js
, são, normalmente, os arquivos que fazem referência aos arquivos da pasta build/static
e por isso estes arquivos nunca devem ser salvos em cache, que é o que o cabeçalho Cache-Control
com o valor 'no-store'
irá informar para o Cloudfront e para os browsers.
Nosso aquivo .drone.yml
final ficou da seguinte forma:
---
kind: pipeline
type: docker
name: defaultsteps:########################################################
- name: Check
image: node:14.10.0
commands:
- npm install
- npm run lint
- npm run test:coverage########################################################
- name: build-prod
image: node:14.10.0
environment:
AWS_ACCESS_KEY_ID:
from_secret: AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY:
from_secret: AWS_SECRET_ACCESS_KEY
volumes:
- name: build_files
path: /build_files
commands:
- npm install
- npm run build
- cp -r build/* /build_files/
when:
branch:
- master
event:
- push########################################################
- name: deploy-prod
image: amazon/aws-cli
environment:
AWS_ACCESS_KEY_ID:
from_secret: AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY:
from_secret: AWS_SECRET_ACCESS_KEY
AWS_S3_BUCKET:
from_secret: AWS_S3_BUCKET_PROD
volumes:
- name: build_files
path: /build_files
commands:
- cd /build_files
- aws s3 sync --acl 'public-read' --cache-control "public, max-age=604800, immutable" ./static/ s3://$AWS_S3_BUCKET/static/
- aws s3 sync --acl 'public-read' --cache-control 'no-store' ./ s3://$AWS_S3_BUCKET/
when:
branch:
- master
event:
- pushvolumes:
- name: build_files
temp: {}
Conclusão
Os browsers e serviços de CDN estão sempre trabalhando para entregar a melhor performance e precisamos conhecer bem as estratégias utilizadas para conseguirmos tirar o máximo de proveito de cada ferramenta.
Referências
Que tal usar tecnologia para impactar a saúde e ajudar médicos a fazer a medicina melhor? Vem pra Sanar!