Pipeline de Deploy Automatizado com GitHub Actions e Docker para EC2
Fábio Maciel
Posted on May 1, 2023
O github actions é uma ferramenta que vem substituindo várias outras do mercado, como Jenkins e Circle CI, pela sua facilidade de utilização, já que não existe a necessidade de toda uma infraestrutura direcionada para a execução de pipelines, sem contar o fato de que muitas empresas ja utilizam o github como repositorio oficial de seus projetos.
Existem diversos propositos na criação de um pipeline, dentre eles o deploy automatizado, que ao inserir um novo commit no repositório ele executa, tornando assim o continuos deploy uma realidade.
Então chega de papo, e vamos entender como fazer isso na prática.
Irei usar uma aplicação escrita em nodejs com apenas um Hello world na rota [GET /].
Primeiramente precisamos de uma instancia EC2 linux com o docker instalado. Além disso, é necessário que tenha sido guardado o key-pair usado para proteger a instância.
Agora precisamos de um repositório com nosso código da aplicação no github. usarei o seguinte repo como base do nosso guia (https://github.com/fabiomaciel/actions-deploy).
Como dito anteriormente, usaremos uma aplicação rest simples em nodejs.
const express = require('express');
const app = express();
const PORT = 80
app.get('/', (req, res) => {
res.send('Hello!');
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
O build da nossa aplicação será feito utilizando Docker, portanto criaremos um Dockerfile simples e pratico, que basicamente realizará a instalação das dependencias do nosso projeto ( que no nosso caso é somente o express) e iniciará o servidor http na porta 80.
FROM node:18-alpine
WORKDIR ~/hello-world
COPY . .
RUN npm ci
EXPOSE 80
CMD ["npm", "start"]
Com isso ja podemos fazer o build da nossa aplicação e fazer o deploy manualmente utilizando ssh por exemplo, mas queremos aqui automatizar esse processo, e o github actions vai nos ajudar com isso.
Existem alguns cuidados que devemos tomar ao configurar builds e deploys em repositórios, principalmente quando estes são públicos (como no nosso caso). Todo tipo de dado sensivel, como passwords, e dados de acesso a ambientes devem ser guardados fora do alcance público, onde apenas pessoas autoriazadas tem acesso. Precisaremos aqui de todos os dados de acesso ao servidor EC2 para podermos fazer o deploy da aplicação na nossa máquina. E para guardarmos com segurança nossos dados, utilizaremos o secrets do proprio github.
Na aba settings temos a seção "Secrets and variables", que possui uma lista de paginas, e usaremos a "Actions".
Nessa seção usaremos 4 secrets:
PUBLIC_KEY: necessário para adicionar ao known hosts as informações do servidor.
DEPLOY_KEY: chave de acesso ao servidor, é uma chave privada gerada na hora da criação do ec2 para o acesso ssh.
HOST: ip público da máquina hospedada no aws ec2
USER: usuário usado na máquina ec2.
Agora vamos setar cada um dos secrets:
Para conseguir o PUBLIC_KEY precisamos acessar a máquina ec2 e rodar o seguinte comando:
ssh-keyscan -t ecdsa [ip-públic]
onde [ip-público] deverá ser substituído pelo ip da maquina
Ao rodar esse commando, copie a saída e cole no secret do github.
Para conseguir o DEPLOY_KEY basta apenas copiar o contúdo de dentro do arquivo key-pair gerado na hora da criação da maquina ec2 no console da aws.
Esse arquivo segue um padrão parecido com o seguinte:
-----BEGIN RSA PRIVATE KEY-----
Sed ut perspiciatis unde omnis iste natus error sit voluptatem
accusantium doloremque laudantium totam rem aperiam eaque
ipsa quae ab illo inventore veritatis et quasi architecto
beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem
quia voluptas sit aspernatur aut odit aut fugit sed quia
consequuntur magni dolores eos qui ratione voluptatem sequi
nesciunt. Neque porro quisquam est qui dolorem ipsum quia
dolor sit amet consectetur adipisci velit sed quia non numquam
eius modi tempora incidunt ut labore et dolore magnam aliquam
quaerat voluptatem. Ut enim ad minima veniam quis nostrum
exercitationem ullam corporis suscipit laboriosam nisi ut
aliquid ex ea commodi consequatur Quis autem vel eum iure
reprehenderit qui in ea voluptate velit esse quam nihil
molestiae consequatur vel illum qui dolorem eum fugiat quo
voluptas nulla pari
-----END RSA PRIVATE KEY-----
HOST e USER podemos pegar tanto pelo console da aws, por padrão o usuário de acesso ao ec2 é o "ec2-user", mas isso é configurável, e o host é o ip publico da sua máquina.
Agora que temos todas as informações que precisamos, vamos a acão.
Criaremos um arquivo com todas as informações do deploy no repositório .github/workflows/deploy.yml, e o conteúdo do arquivo é o seguinte:
name: Docker EC2 Deploy
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Add EC2 instance to known_hosts
env:
PUBLIC_KEY: $\{{ secrets.PUBLIC_KEY }}
run: |
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo $PUBLIC_KEY >> ~/.ssh/known_hosts
- name: Setting private key
env:
PRIVATE_KEY: $\{{ secrets.DEPLOY_KEY }}
run: |
echo "$PRIVATE_KEY" > /tmp/private_key.pem
chmod 600 /tmp/private_key.pem
- name: Copy repository files to EC2
env:
HOST: $\{{ secrets.HOST }}
USER: $\{{ secrets.USER }}
run: |
ssh -i /tmp/private_key.pem $USER@$HOST "mkdir -p ~/app"
scp -i /tmp/private_key.pem -r ./* $USER@$HOST:~/app
- name: Build Docker image and run container on EC2
env:
HOST: $\{{ secrets.HOST }}
USER: $\{{ secrets.USER }}
run: |
ssh -i /tmp/private_key.pem $USER@$HOST "\
cd ~/app \
&& docker build -t hello . \
&& docker run -d -p 80:80 --name hello-container hello"
- name: Cleanup
env:
HOST: $\{{ secrets.HOST }}
USER: $\{{ secrets.USER }}
run: |
ssh -i /tmp/private_key.pem $USER@$HOST "rm -rf ~/app"
rm -f /tmp/private_key.pem
A estrutura do arquivo segue alguns padrões que iremos analizar.
A estrtutura principal esta dividida em 3 partes: name, on, jobs
name: é simplesmente um identificador da pipeline
jobs: possui os eventos nos quais essa pipeline vai ser conectada, como no nosso exemplo um push na branch main, mas podemos ter variados eventos, como abertura de pr,abertura de uma issue, dentre outras coias.
(para mais informações acesse a documentação oficial https://docs.github.com/en/actions/using-workflows/triggering-a-workflow)
jobs: aqui é onde a execução da lógica da nossa pipeline vai rodar de fato.
A seção jobs é onde fazemos a magia acontecer, aqui nós separamos nossa execução da pipeline em steps, onde podemos nomear os steps, e ter uma melhor vizualização na tela de acompanhamento da pipeline, ajudando a encontrar possíveis erros com mais facilidade. Além disso precisamos definir qual será o ambiente onde nossa pipeline será executada, nesse exemplo usaremos um ambiente linux com a distro ubunutu.
Agora vamos para os steps. Como dito anteriormente, é sempre bom separar de forma a facilitar um possivel troubleshooting, tendo isso em mente vamos criar alguns steps, e usaremos nomes que referenciam exatamente oque está sendo feito.
- Add EC2 instance to known_hosts
- Setting private key
- Copy repository files to EC2
- Build Docker image and run container on EC2
- Cleanup
Antes de mergulharmos dentro de cada step, vale notar que aqui é onde usamos nossos secrets que configuramos anteriormente, em cada step que iremos usar os secrets, o configuramos como uma variavel de ambiente dentro da estrutura env.
Aqui dizemos que a variavel de ambiente PUBLIC_KEY tem o valor configurado no secrete PUBLIC_KEY, e faremos a mesma coisa com os outros secrets em cada step quando necessário.
Add EC2 instance to known_hosts:
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo $PUBLIC_KEY >> ~/.ssh/known_hosts
Aqui configuramos o nosso known_hosts com as informacões da nossa maquina ec2, para que possamos acessá-la usando ssh.
Setting private key:
echo "$PRIVATE_KEY" > /tmp/private_key.pem
chmod 600 /tmp/private_key.pem
Aqui criaremos um arquivo .pem e no conteúdo desse arquivo colocamos o conteúdo que pegamos do arquivo key-pair gerado na criação da instancia do ec2, e configuramos as permissões READ e WRITE para o usuário dono do arquivo, e nenhuma permissão para o grupo e outros usuários. (https://www.computerhope.com/unix/uchmod.htm)
Copy repository files to EC2:
ssh -i /tmp/private_key.pem $USER@$HOST "mkdir -p ~/app"
scp -i /tmp/private_key.pem -r ./* $USER@$HOST:~/app
Aqui criamos um diretório app na home do usuário, e copiamos todos os arquivos do repositório para o ec2.
Build Docker image and run container on EC2:
ssh -i /tmp/private_key.pem $USER@$HOST "\
cd ~/app \
&& docker build -t hello . \
&& docker run -d --rm -p 80:80 --name hello-container hello"
Aqui conectamos via ssh na máquina ec2, criamos uma imagem docker da aplicação e em seguida executamos o docker run para subir o container fazendo bind da porta 80 do container com a porta 80 do host.
Cleanup:
ssh -i /tmp/private_key.pem $USER@$HOST "rm -rf ~/app"
rm -f /tmp/private_key.pem
Por fim fazemos uma limpeza nos arquivos do repo dentro do host ec2, e limpamos o nosso arquivo com as credenciais de acesso ssh.
Pronto, nosso pipeline de deploy automatizado está funcionando, mas ainda temos alguns problemas para resolver.
estamos apenas criando novas imagens docker, e nunca deletando as antigas, hora ou outra iremos acabar estourando o armazenamento do nosso host ec2, apesar de usarmos uma imagem bem enxuta "node:18-alpine", é sempre bom limparmos o "lixo", para evitar problemas no armazenamento ou até mesmo custos indevidos na nosa infra. Além disso, na segunda vez que rodarmos a nossa pipeline, a nossa aplicação falhará, pois ja existe um container rodando com o mesmo nome, e fazendo bind na mesma porta que gostariamos, então é necessário fazermos mais 2 coisas no nosso step de Build and Run:
- Parar o container
- Deletar as imagens antigas
Para isso vamos adicionar 2 comandos bash
#deleta todas as imagens docker salvas na máquina
docker images -q | xargs -r docker rmi -f
# deleta todos os containers do host
docker ps -q | xargs -r docker container rm -f
Vamos adicionar esses comandos no nosso arquivo deploy.yml
name: Docker EC2 Deploy
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Add EC2 instance to known_hosts
env:
PUBLIC_KEY: $\{{ secrets.PUBLIC_KEY }}
run: |
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo $PUBLIC_KEY >> ~/.ssh/known_hosts
- name: Setting private key
env:
PRIVATE_KEY: $\{{ secrets.DEPLOY_KEY }}
run: |
echo "$PRIVATE_KEY" > /tmp/private_key.pem
chmod 600 /tmp/private_key.pem
- name: Copy repository files to EC2
env:
HOST: $\{{ secrets.HOST }}
USER: $\{{ secrets.USER }}
run: |
ssh -i /tmp/private_key.pem $USER@$HOST "mkdir -p ~/app"
scp -i /tmp/private_key.pem -r ./* $USER@$HOST:~/app
- name: Build Docker image and run container on EC2
env:
HOST: $\{{ secrets.HOST }}
USER: $\{{ secrets.USER }}
run: |
ssh -i /tmp/private_key.pem $USER@$HOST "\
cd ~/app \
&& (docker ps -q | xargs -r docker container rm -f ) \
&& (docker images -q | xargs -r docker rmi -f) \
&& docker build -t hello . \
&& docker run -d -p 80:80 --name hello-container hello"
- name: Cleanup
env:
HOST: $\{{ secrets.HOST }}
USER: $\{{ secrets.USER }}
run: |
ssh -i /tmp/private_key.pem $USER@$HOST "rm -rf ~/app"
rm -f /tmp/private_key.pem
Pronto, agora a nossa pipeline de deploy está 100% funcional, e não teremos problemas de armazenamento e nem de custos indevidos no nosso servidor.
Em resumo, utilizamos o GitHub Actions para automatizar o processo de deploy de uma aplicação Node.js simples em um ambiente EC2 da AWS, utilizando Docker. Através da criação de um pipeline, conseguimos automatizar etapas como a conexão SSH, transferência de arquivos, construção e execução de imagens Docker. Além disso, foram abordadas práticas de segurança, como o uso de secrets para armazenar informações sensíveis, e estratégias para evitar problemas de armazenamento, como a remoção de contêineres e imagens antigas. Com essas ferramentas em mãos, desenvolvedores podem se concentrar no desenvolvimento de suas aplicações, sabendo que o processo de deploy está devidamente automatizado e otimizado..
Posted on May 1, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024