Como fazer deploy automatizado de uma aplicação React para o Cloudfront usando S3 como armazenamento
Tags: react, aws, cloud, tutorial
Published: 2023-02-22 22:57:12

## Índice

- [Introdução](#introduction)
- [Pré-requisitos](#pre-requirements)
- [Criando um hello-world em React](#creating-hello-world)
- [Escrevendo um script para automatizar o deploy](#writing-script)
- [Conclusão](#conclusion)

## Introdução <a name="introduction"></a>

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 <a name="pre-requirements"></a>

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 <a name="creating-hello-world"></a>

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`:

```bash
npx create-react-app test
cd test
```

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

```bash
npm install
```

E depois o comando para rodar o projeto:

```bash
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:

```bash
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 <a name="writing-script"></a>

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:

```bash
npm install @aws-sdk/client-cloudfront
```

e

```bash
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): 

```js
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á 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:

```json
{
  // ...
  "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 <a name="conclusion"></a>

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](https://github.com/GledsonAfonso). Críticas construtivas e sugestões são sempre bem-vindas.

Agradecimento especial à [Jonathan](https://github.com/jonathan-sh) pelos snippets de busca recursiva dos arquivos no build e do envio à S3.