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.

Oldest comments (0)