DEV Community

Cover image for Como fazer deploy automatizado de uma aplicação React para o Cloudfront usando S3 como armazenamento
Gledson Afonso
Gledson Afonso

Posted on

Como fazer deploy automatizado de uma aplicação React para o Cloudfront usando S3 como armazenamento

Índice

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 
Enter fullscreen mode Exit fullscreen mode

Na raiz do projeto, rode o comando para instalar as dependências:

npm install 
Enter fullscreen mode Exit fullscreen mode

E depois o comando para rodar o projeto:

npm run start 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

e

npm install @aws-sdk/client-s3 
Enter fullscreen mode Exit fullscreen mode

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(); })(); 
Enter fullscreen mode Exit fullscreen mode

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á vazio

  • Logo 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 S3

  • Note que dentro das chamadas do método _push uma outra chamada é feita para o método _getContentType que serve exclusivamente para determinar o ContentType correto do arquivo no envio. Isso é necessário pois, com o ContentType 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" }, // ... } 
Enter fullscreen mode Exit fullscreen mode

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)