Índice
- Introdução
- Pré-requisitos
- Criando um hello-world em React
- Escrevendo um script para automatizar o deploy
- Conclusão
Introdução
Existem situações onde se é relativamente fácil de se fazer um deploy dentro do ecossistema da AWS. No Amplify, por exemplo, basta fazer algumas configurações que seu deploy passa a acontecer assim que um commit é enviado para um branch remoto no próprio projeto, muito parecido com algunas esteiras de automatização. Porém, dependendo do projeto, ou até mesmo da empresa, pode-se ter situações onde esse não é o caso para determinado projeto.
E é visando um desses casos que esse tutorial veio a existir. Especificamente para a hospedagem de um projeto React usando AWS S3 e CloudFront.
Pré-requisitos
Para realizar o tutorial, você precisará de:
- Conta na AWS (com usuário que tenha acesso aos serviços S3 e CloudFront)
- Access key e secret de acesso AWS
- Bucket na S3 com
Static website hosting
habilitado - Distribuição no CloudFront que aponte para o seu bucket na S3
Criando um hello-world em React
Começamos com a aplicação React. Para fins de simplificação, a aplicação envolverá só um hello-world simples, portanto podemos utilizar o create-react-app
:
npx create-react-app test cd test
Na raiz do projeto, rode o comando para instalar as dependências:
npm install
E depois o comando para rodar o projeto:
npm run start
Note que outros comandos já vêm inclusos no projeto pelo próprio create-react-app
, como o que executa os testes do projeto e o de build. Execute o de build com o seguinte comando para gerarmos os dados que serão enviados para a S3:
npm run build
Isso gerará uma pasta build
com o projeto compilado. Essa será a pasta que usaremos no processo de deploy.
Escrevendo um script para automatizar o deploy
Com o projeto compilado, podemos agora começar a escrever o script de deploy. Porém, antes disso, precisaremos de algumas dependências. Portanto execute:
npm install @aws-sdk/client-cloudfront
e
npm install @aws-sdk/client-s3
Com as dependências instaladas, já podemos começar a escrever o script. Crie um arquivo com nome deploy.js
na raiz do projeto e adicione o seguinte snippet (esse seria já o script completo, mais abaixo irei explicar ele com mais detalhes):
const { CloudFrontClient, CreateInvalidationCommand } = require('@aws-sdk/client-cloudfront'); const { DeleteObjectsCommand, ListObjectsV2Command, PutObjectCommand, S3Client } = require('@aws-sdk/client-s3'); const fs = require('fs'); // constantes para autenticação com a AWS const bucketName = 'nome-do-bucket'; const region = 'região-do-seu-bucket-na-aws'; const accessKeyId = 'access-key-id-da-sua-conta-na-aws'; const secretAccessKey = 'secret-access-key-da-sua-conta-na-aws'; const distribution = 'id-da-sua-distribuição-na-cloudfront'; const s3 = new S3Client({ region }); const _getContentType = (extension) => { const contentType = { json: 'application/json', ico: 'image/x-icon', png: 'image/png', html: 'text/html', txt: 'text/plain', css: 'text/css', js: 'text/javascript', woff: 'font/woff', woff2: 'font/woff2' } return contentType[extension] ? contentType[extension] : 'text/plain'; }; const _push = async (path) => { const extension = path.split('.').reverse()[0]; const contentType = _getContentType(extension); const options = new PutObjectCommand({ Bucket: bucketName, ContentType: contentType, Body: fs.createReadStream(path), Key: path.replace('build/', '') // precisa ter o nome da pasta de build como prefixo do caminho }); try { const data = await s3.send(options); return data.Location; } catch (error) { console.log(`Não foi possível fazer o upload do ${options.Key} para o storage da S3. Erro: ${error}`); } }; const walk = async (path = 'build') => { fs.readdirSync(path, { withFileTypes: true }).forEach(item => { const dir = `${path}/${item.name}`; if (item.isDirectory()) { walk(dir); } if (item.isFile()) { _push(dir); } }); }; const forceApplicationUpdate = async () => { try { const paths = ['/*']; const createInvalidationCommand = new CreateInvalidationCommand({ DistributionId: distribution, InvalidationBatch: { CallerReference: new Date().toString(), Paths: { Quantity: paths.length, Items: paths } } }); const cloudFrontClient = new CloudFrontClient({ region, credentials: { accessKeyId, secretAccessKey } }); await cloudFrontClient.send(createInvalidationCommand); console.log('Site atualizado!'); } catch (error) { console.error('Erro ao tentar atualizar o site: ', error); } }; const cleanBucket = async () => { try { const listCommand = new ListObjectsV2Command({ Bucket: bucketName }); const listResponse = await s3.send(listCommand); if (listResponse.Contents) { const deleteObjects = listResponse.Contents.map((content) => ({ Key: content.Key })); const deleteCommand = new DeleteObjectsCommand({ Bucket: bucketName, Delete: { Objects: deleteObjects, }, }); await s3.send(deleteCommand); console.log(`Bucket limpo! Total de ${deleteObjects.length} objetos deletados!`); } else { console.log(`Bucket está vazio.`); } } catch (error) { console.log('Erro: ', error); } }; // parte do script que chama as funções na ordem correta (async () => { await cleanBucket(); await walk(); await forceApplicationUpdate(); })();
Bastante coisa, né? Bom, vamos começar pelo nosso IIFE, que é onde as coisas são executadas. Se você observar, a seguinte ordem de execução acontece:
- Primeiro chama-se a função
cleanBucket
- Depois a
walk
- Depois a
forceApplicationUpdate
Pelos nomes, já dá para ter uma noção do que está sendo feito, mas, para explicar melhor o fluxo, isso é o que acontece:
A função
cleanBucket
, que é responsável pela limpeza do bucket, ou seja, é ela quem deleta todos os arquivos contidos no bucket de deploy, é chamada para limpar os arquivos compilados da versão anterior. Se não existir nada lá, a função simplesmente te avisa de que o bucket está vazioLogo depois, a função
walk
é chamada. Essa contém alguns passos adicionais, que envolvem chamadas internas para a função_push
, mas, basicamente, o que ela faz nada mais é do que percorrer o seu diretório de build de forma recursiva chamando o método_push
para cada arquivo encontrado, sendo que esse segundo método é responsável unicamente por enviar o dito arquivo para a S3Note que dentro das chamadas do método
_push
uma outra chamada é feita para o método_getContentType
que serve exclusivamente para determinar oContentType
correto do arquivo no envio. Isso é necessário pois, com oContentType
incorreto, você pode ter comportamentos inconsistentes entre os navegadores (ex.: CSS funcionar em um navegador, mas não em outro)Terminado esse processo de envio dos dados novos de deploy para o storage da S3, basta a atualização da página no CloudFront. Para isso é que a função
forceApplicationUpdate
é chamada. Em termos técnicos, ela é quem é a responsável por invalidar todos os subdiretórios e arquivos da distribuição, forçando assim uma geração de um novo cache e, portanto, atualizando o site
E é isso! Nada muito complicado, né?
Tendo o script pronto, tudo o que falta é adicionar um novo comando no scripts
do seu package.json
da seguinte forma:
{ // ... "scripts": { // ... "deploy": "node deploy.js" }, // ... }
E daí é só rodar npm run deploy
na raiz do projeto que o seu script de deploy será executado!
Conclusão
Neste tutorial aprendemos a:
- Fazer um hello-world em React para usarmos de exemplo
- Escrever um script de deploy que automatiza o processo de limpeza do bucket na AWS S3, assim com o upload dos novos arquivos do build e a atualização do cache na AWS CloudFront
Com isso a parte do deploy deve ficar menos trabalhosa para o seu projeto!
Obrigado por ler!
Se quiser entrar em contato para alguma discussão, aqui está o meu perfil do Github. Críticas construtivas e sugestões são sempre bem-vindas.
Agradecimento especial à Jonathan pelos snippets de busca recursiva dos arquivos no build e do envio à S3.
Top comments (0)