Github Actions + Digital Ocean + Elixir = ❤️
Hoje realizei um experimento com o novo não tão novo assim CI do Github: Github Actions,
implementando no meu blog (sim, este que você está lendo).
O projeto está aqui,
Portanto, realizei esse teste com:
- Droplet simples no DigitalOcean ($5 doleta)
- Elixir + Phoenix
- Docker + DockerHub - Github Actions
Bom, para esse experimento, decidi utilizar docker para facilitar o desenvolvimento/deploy e a facilidade de escalar no K8s. Claro, esse blog jamais terá a necessidade de utilizar algum orquestrador de containers, mas, vale a boa prática.
Antes de esmiuçar neste expertimento, vale deixar claro algumas coisas:
- Foi um teste realizado em ~3h.
- Há downtime - o tempo de matar o container e subi-lo. e.g: docker kill blog_prod.
- Há uma configuração prévia no nginx que vou deixar no final do artigo.
- Não haveria a necessidade de utilizar DockerHub, mas gosto da portabilidade que ele me trás.
First of all
Vamos criar um Dockerfile para realizar o build na pipeline.
No projeto tenho um Dockerfile para desenvolvimento e um para produção (prod.dockerfile), e nesse artigo vou mostrar o
prod.dockerfile
sem adentrarmos muito no Dockerfile, temos os seguintes comandos:
ENV PORT=4000 \
MIX_ENV=prod \
SECRET_KEY_BASE=${SECRET_KEY_BASE} \
DATABASE_URL=${DATABASE_URL}
CMD ["mix", "phx.server"]
No qual passamos uma variavel de ambiente sensível para o build (SECRET_KEY_BASE, DATABASE_URL).
Go to Action
Então, em um breve resumo precisamos que; Ao realizar um push para master façamos o build da nova imagem e enviamos para o nosso repositório DockerHub. Após isso, deveremos entrar na droplet, e subir o container na porta esperada pelo nginx (4000).
Algumas informações relevantes a considerar:
- Precisamos enviar de alguma forma segura a credencial de acesso ao banco de dados e a secret key do Phoenix
- Para realizar o push para o repositório precisamos ter realizado o login no docker.
Portanto, vamos criar nosso arquivo actions
dentro de .github/workflows/actions.yml
e adicionar o seguinte comando:
on:
push:
branches:
- master
Para especificarmos que este action deverá ser rodado quando for realizado algum push para a branch master
E então criaremos nosso primeiro job:
...
jobs:
build:
name: Build, push
runs-on: ubuntu-latest
steps:
- name: Checkout master
uses: actions/[email protected]
Definimos um job build
com o nome de Build, push que rodará em uma imagem ubuntu
.
Após isso, criamos nosso primeiro step: actions/[email protected]
que é responsável por fazer o pull da master.
Vamos seguir com os steps…
steps:
...
- name: Build container image
run: docker build -t rafaelgss/projects:blog-latest -f prod.dockerfile .
- name: Docker Login
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
- name: Push image to Docker Hub
run: docker push rafaelgss/projects
Bem… adicionamos uma sequência interessante de steps agora, sendo elas:
- Build da imagem de produção (prod.dockerfile) com a tag.
- Login no DockerHub
- Push da imagem para o DockerHub
Perceba, que no passo 2 utilizamos ${{ secrets.* }}
, essas são as secrets definidas no projeto em questão, é ali
que iremos guardar toda informação sensível de forma “segura”.
Bom, com isso conseguimos buildar e enviar a imagem para o DockerHub… Agora, vamos ao passo principal o deploy!
Primeiro, vamos criar um novo job, e chama-lo de Deploy
e adjusta-lo para que o mesmo só seja rodado após o BUILD
deploy:
needs: build
name: Deploy
runs-on: ubuntu-latest
E então, vamos adicionar nossos steps:
steps:
- name: executing remote ssh commands using key
uses: appleboy/[email protected]
env:
VIRTUAL_HOST: 'blog.rafaelgss.com.br'
SECRET_KEY_BASE: ${{ secrets.SECRET_KEY_BASE }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.key }}
port: ${{ secrets.PORT }}
envs: VIRTUAL_HOST,SECRET_KEY_BASE,DATABASE_URL
script: |
docker pull rafaelgss/projects:blog-latest
docker kill blog_prod
docker rm blog_prod
docker run -d -p 4000:4000 --name blog_prod -e VIRTUAL_HOST="$VIRTUAL_HOST" -e SECRET_KEY_BASE="$SECRET_KEY_BASE" -e DATABASE_URL="$DATABASE_URL" -t rafaelgss/projects:blog-latest
Neste único step, criamos uma conexão ssh com nosso droplet e executamos oque está em script
em sua respectiva sequência.
- Relizamos o pull da imagem feita no job anterior
- Matamos o container em execução no momento (se houver) — Por isso o downtime.
- Removemos o container pré-estabelecido
- Subimos um novo container com a nova imagem, passando as variáveis de ambiente necessárias (salvas em Secrets)
Isso é tudo! Nosso action.yml
ficou assim:
on:
push:
branches:
- master
jobs:
build:
name: Build, push
runs-on: ubuntu-latest
steps:
- name: Checkout master
uses: actions/[email protected]
- name: Build container image
run: docker build -t rafaelgss/projects:blog-latest -f prod.dockerfile .
- name: Docker Login
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
- name: Push image to Docker Hub
run: docker push rafaelgss/projects
deploy:
needs: build
name: Deploy
runs-on: ubuntu-latest
steps:
- name: executing remote ssh commands using key
uses: appleboy/[email protected]
env:
VIRTUAL_HOST: 'blog.rafaelgss.com.br'
SECRET_KEY_BASE: ${{ secrets.SECRET_KEY_BASE }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.key }}
port: ${{ secrets.PORT }}
envs: VIRTUAL_HOST,SECRET_KEY_BASE,DATABASE_URL
script: |
docker pull rafaelgss/projects:blog-latest
docker kill blog_prod
docker rm blog_prod
docker run -d -p 4000:4000 --name blog_prod -e VIRTUAL_HOST="$VIRTUAL_HOST" -e SECRET_KEY_BASE="$SECRET_KEY_BASE" -e DATABASE_URL="$DATABASE_URL" -t rafaelgss/projects:blog-latest
Nginx - Docker
Deixo aqui a config do nginx que usei:
upstream phoenix_upstream {
server 127.0.0.1:4000;
}
server {
listen [::]:80;
listen 80;
server_name blog.rafaelgss.com.br;
location ~ ^/(.*)$ {
proxy_pass http://phoenix_upstream/$1;
}
location / {
#try_files $uri $uri/ =404;
proxy_pass http://phoenix_upstream;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
}