Í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)