DEV Community

Igor Souza
Igor Souza

Posted on

Criando Review Apps com Github Action, Terraform e DigitalOcean!

Antes de mais nada... por que Review Apps?

Um problema bem comum seja numa arquitetura de microservices, microlitos ou monolitos é que dependendo do tamanho do seu time podem existir diversas features sendo desenvolvidas de forma simultâneas e algumas vezes até com break changes. Um caso atual nosso e a migracao de uma app para a nova versao do ruby.

No meu caso, existem 3 principais motivações para utilizar o RA(Review Apps).

  • Validar que o código terraform está 100% funcional recriando ambiente necessário para a aplicação funcionar.
  • Validar novas features junto ao PO antes de mandar pro ambiente de staging onde alguns clientes podem acessar.
  • Dar ao time um ambiente seguro para testar não só mudanças em código mas mudanças que afetem banco de dados sem medo.

Vamos por partes!

Github Action

Nesse caso a escolha do Github Action foi completamente baseada em Hype eu estava querendo a oportunidade de brincar com as actions a algum tempo e pareceu uma boa hora, mas você pode utilizar qualquer plataforma de CI/CD, basta seguir a mesma linha de raciocínio e adaptar algumas linhas de código. 🙂

Terraform

Caso você não conheça o Terraform eu escrevi um texto sobre o básico do terraform, mas a ideia dele e codificar toda infra numa linguagem própria chamada HCL para que possamos criar e recriar a infraestrutura sempre que quisermos e também manter um histórico da sua evolução. Basicamente, tentar manter infra da forma que mantemos software!

DigitalOcean

Apesar de ter um certificado do GCP e trabalhar no dia a dia com AWS, sempre que preciso fazer prova de conceitos ou sugerir uma cloud para clientes que não dependem de tantos servicos eu acabo sempre indicando a DigitalOcean não só pela facilidade em começar e manter uma infraestrutura, também pela facilidade em prever custos e pela sua API que é muito boa.


Vamos ao que interessa!

A primeira coisa a se fazer e criar um workflow, caso você não esteja vendo esse botão nos seus repositórios, pelo menos enquanto escrevo esse texto, você precisa aceitar fazer parte da versão beta.

Quando clicar em actions, você vai ver uma opção como Set up a workflow yourself, que basicamente e para não utilizarmos um template e montarmos tudo com nossos próprios dedos.

Vamos fazer isso 🙂

Voce deve cair em uma pagina parecida com essa:

Se pra você nem todos os nomes forem explicativos como não foram pra mim ou quiser saber mais detalhes sobre os parametos aceitos por cada comando segue o link para o docs, mas durante o texto voce vai entender os principais.

Como nossa ideia é criar RA baseados apenas em pull requests abertos, vamos alterar a linha 3 para que esse workflow seja executado apenas em PR.

name: Review Apps
on:
  pull_request:
    types: [assigned, opened, synchronize, reopened, closed]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
    - name: Run a one-line script
      run: echo Hello, world!
    - name: Run a multi-line script
      run: |
        echo Add other actions to build,
        echo test, and deploy your project.

Dessa forma abrangemos a maior parte dos eventos disponíveis no pull_request, ou seja sempre que alguém abrir o PR, commitar uma nova mudança ou mesmo fizer merge esse pipeline será executado.
Tudo bem que ele ainda não faz muito além de fazer checkout do código.

Como planejamos utilizar o terraform para isso, um pre requisito e ter o codigo terraform pronto para fazer deploy do ambiente.
Nesse texto temos um exemplo bem simples com um código para criar um Droplet na DigitalOcean com o nome da maquina sendo o mesmo nome do workspace do terraform.

provider "digitalocean" {}

provider "aws" {
  version = "~> 2.0"
  region  = "us-east-1"
}

terraform {
  backend "s3" {
    bucket = "icf-tf states-terraform"
    key    = "terratest/terraformt.tfstate"
    region = "us-east-1"
  }
}

resource "digitalocean_droplet" "web" {
  image  = "ubuntu-18-04-x64"
  name   = "${terraform.workspace}"
  region = "nyc3"
  size   = "s-1vcpu-1gb"
}

Alguns pontos bem importantes aqui:

O primeiro são as variaveis necessarias, no caso estamos usando 2 providers diferentes DigitalOcean e AWS e cada um precisa de variáveis diferentes para funcionar.
Segundo e o fato de precisarmos utilizar um arquivo de estado remoto caso contrário perderíamos o nosso arquivo de estado após a primeira execução e se vc nao sabe o que e arquivo de estado e a sua importante e pq nao leu esse post então corre lá... eu espero pra continuar.

Por último mas não menos importante e que o nome da máquina está utilizando uma variável do terraform que é o nome do workspace. O workspace padrao tem nome de default mas vamos alterar esse nome durante a execução então segura aí!

A primeira coisa a se fazer com o código terraform e executar um terraform init então vamos adicionar esse passo no nosso workflow utilizando a action do terraform que se encontra no repositório criado e mantido pela própria Hashicorp.

name: Terraform Workflow
on:
  pull_request:
    types: [assigned, opened, synchronize, reopened, closed]
jobs:
  greeting:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
    - name: 'Terraform Init'
      uses: hashicorp/terraform-github-actions@master
      with:
        tf_actions_version: 0.12.13
        tf_actions_subcommand: 'init'
        tf_actions_working_dir: '.'
        tf_actions_comment: false

Se você tentou executar provavelmente tomou um erro, e pro bem dos seus conhecimentos em Terraform te recomendo entender o erro antes de continuar. Mas caso você queira seguir adiante, o problema e a falta de variáveis para comunicação com a AWS.
Lembra que estamos utilizando arquivo remoto hospedado no S3? Então, pra que possamos ler esse arquivo precisamos de credenciais da AWS.

name: Terraform Workflow
on:
  pull_request:
    types: [assigned, opened, synchronize, reopened, closed]
jobs:
  greeting:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
    - name: 'Terraform Init'
      uses: hashicorp/terraform-github-actions@master
      with:
        tf_actions_version: 0.12.13
        tf_actions_subcommand: 'init'
        tf_actions_working_dir: '.'
        tf_actions_comment: false
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

Agora para o nosso pipeline funcionar precisamos cadastrar de alguma forma essas variáveis. Para isso você precisa ter acesso ao settings do seu repositório.

Cadastrar os valores dessas variáveis e simples, talvez você tenha um pouco mais dificuldade em pegar os valores delas caso nunca tenha precisado fazer isso antes.

Configurando Secrets no repositório

Infelizmente eu não encontrei uma forma de criar essas variáveis para toda a conta, então vamos seguir criando direto no repositório. Mas se você souber como fazer para todos os meus repos seria de grande ajuda.

Vamos aproveitar que estamos com a página aberta para cadastrar outras variáveis que vamos precisar:

AWS_ACCESS_KEY_ID     = Chave fornecida pela AWS para acessar o nosso arquivo de estado 
AWS_SECRET_ACCESS_KEY = Chave fornecida pela AWS para acessar o nosso arquivo de estado
DIGITALOCEAN_TOKEN    = Vamos precisar dessa chave para que o terraform consiga fazer chamadas a API da DigitalOcean para criar recursos.
TOKEN                 = Vamos utilizar para setar uma variável de ambiente chamada BRANCH_NAME

Vamos commitar essas alterações numa branch qualquer e abrir um PR para master.
Agora abra a aba de Actions e o pipeline deve começar a qualquer segundo.

Tudo certinho? Se nao, da uma olhada nas suas chaves e tente entender o log do erro.

DICA IMPORTANTE!

Leiam o log de erro sempre, quase sempre a solução está na mensagem de erro!

Uma coisa que você pode estar se perguntando e de onde saiu aquele 2 step Build hashicorp/terraform-github-actions@master se não colocamos nada disso no código.
Acontece que o Github Action faz build de todas as actions cadastradas no pipeline. Esse é um dos motivos para seu workflow ser extremamente específico e deve evitar ao máximo utilizar as condicionais dentro dos steps mas isso é papo pra um outro texto.

Agora que já conseguimos inicializar nosso código, vamos executar um terraform plan para ver se o terraform vai criar o Droplet da forma que esperamos.

name: Review Apps

on:
  pull_request:
    types: [assigned, opened, synchronize, reopened, closed]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
    - name: 'Terraform Init'
      uses: hashicorp/terraform-github-actions@master
      with:
        tf_actions_version: 0.12.13
        tf_actions_subcommand: 'init'
        tf_actions_working_dir: '.'
        tf_actions_comment: false
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    - name: 'Terraform Plan'
      uses: hashicorp/terraform-github-actions@master
      with:
        tf_actions_version: 0.12.13
        tf_actions_subcommand: 'plan'
        tf_actions_working_dir: '.'
        tf_actions_comment: true
      env:
        DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        GITHUB_TOKEN: ${{ secrets.TOKEN }}

Vamos olhar a saída do nosso workflow.

Quero chamar atenção pro nome do droplet default isso se dá porque falamos pro terraform utilizar o nome do workspace como nome do droplet e o nome do workspace padrão é default.

Para criar nossos review apps precisamos que o workspace utilizado tenha o mesmo nome da nossa branch, e é exatamente aqui que o GithubAction deixou um pouco a desejar e o terraform me surpreendeu 🙂

O Github tem uma variável GITHUB _REF que na teoria te retorna a referência do commit, o problema é que quando estamos lidando com pull request essa variavel nao retorna o nome da branch e sim uma referência ao número do pull request aberto.
Pra resolver isso eu criei uma action (https://github.com/igordcsouza/github-action-get-branch-name) baseada nessa aqui (https://github.com/jessfraz/branch-cleanup-action) para setar uma variavel chamada BRANCH_NAME.
Vamos adicionar ela no nosso código.

name: Terraform Workflow

on:
  pull_request:
    types: [assigned, opened, synchronize, reopened, closed]

jobs:
  greeting:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
    - name: 'Terraform Init'
      uses: hashicorp/terraform-github-actions@master
      with:
        tf_actions_version: 0.12.13
        tf_actions_subcommand: 'init'
        tf_actions_working_dir: '.'
        tf_actions_comment: false
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    - uses: igordcsouza/github-action-get-branch-name@master
      env:
        GITHUB_TOKEN: ${{ secrets.TOKEN }}
    - name: Set branch name as workspace
      run: echo ::set-env name=TF_WORKSPACE::${BRANCH_NAME}
    - name: 'Terraform Plan'
      uses: hashicorp/terraform-github-actions@master
      with:
        tf_actions_version: 0.12.13
        tf_actions_subcommand: 'plan'
        tf_actions_working_dir: '.'
        tf_actions_comment: true
      env:
        DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        GITHUB_TOKEN: ${{ secrets.TOKEN }}

Bom, agora temos uma variável com o nome da branch, mas ainda assim o terraform nao esta pegando esse valor pois a variavel nao esta ligada ao workspace de forma nenhuma.
Aqui entra uma feature do terraform que até começar a fazer esse post eu não fazia ideia da existência dela.
Essa variável e a TF_WORKSPACE , que caso ela esteja setada com algum valor o terraform tenta utilizar o workspace com o mesmo nome e se ele não existir ele cria o workspace.
Isso é uma MÃO NA RODA caso você esteja precisando criar workspace em tempo de execução de algum pipeline como nesse caso ou mesmo para substituir o terraform workspace select name no seu Jenkins.

Vamos setar essa variável com o nome da nossa branch.

name: Terraform Workflow

on:
  pull_request:
    types: [assigned, opened, synchronize, reopened, closed]

jobs:
  greeting:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
    - name: 'Terraform Init'
      uses: hashicorp/terraform-github-actions@master
      with:
        tf_actions_version: 0.12.13
        tf_actions_subcommand: 'init'
        tf_actions_working_dir: '.'
        tf_actions_comment: false
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    - uses: igordcsouza/github-action-get-branch-name@master
      env:
        GITHUB_TOKEN: ${{ secrets.TOKEN }}
    - name: Set branch name as workspace
      run: echo ::set-env name=TF_WORKSPACE::${BRANCH_NAME}
    - name: 'Terraform Plan'
      uses: hashicorp/terraform-github-actions@master
      with:
        tf_actions_version: 0.12.13
        tf_actions_subcommand: 'plan'
        tf_actions_working_dir: '.'
        tf_actions_comment: true
      env:
        DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        GITHUB_TOKEN: ${{ secrets.TOKEN }}

Agora estamos prontos e os passos são idênticos aos que você rodaria manualmente.

name: Terraform Workflow
on:
  pull_request:
    types: [assigned, opened, synchronize, reopened, closed]
jobs:
  greeting:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
    - name: 'Terraform Init'
      uses: hashicorp/terraform-github-actions@master
      with:
        tf_actions_version: 0.12.13
        tf_actions_subcommand: 'init'
        tf_actions_working_dir: '.'
        tf_actions_comment: false
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    - uses: igordcsouza/github-action-get-branch-name@master
      env:
        GITHUB_TOKEN: ${{ secrets.TOKEN }}
    - name: Set branch name as workspace
      run: echo ::set-env name=TF_WORKSPACE::${BRANCH_NAME}
    - name: 'Terraform Plan'
      uses: hashicorp/terraform-github-actions@master
      with:
        tf_actions_version: 0.12.13
        tf_actions_subcommand: 'plan'
        tf_actions_working_dir: '.'
        tf_actions_comment: true
      env:
        DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        GITHUB_TOKEN: ${{ secrets.TOKEN }}
    - name: 'Terraform Apply'
      uses: hashicorp/terraform-github-actions@master
      with:
        tf_actions_version: 0.12.13
        tf_actions_subcommand: 'apply'
        tf_actions_working_dir: '.'
        tf_actions_comment: true
      if: github.event.action != 'closed'
      env:
        DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 
        GITHUB_TOKEN: ${{ secrets.TOKEN }}
    - name: Terraform Destoy
      uses: igordcsouza/terraform-github-actions/destroy@master
      if: github.event.action == 'closed'
      env:
        DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

Um ponto muito importante aqui e o fato de pro destroy nao estarmos utilizando a action da Hashicorp, isso por que eles nao implementaram essa opcao. Eu cheguei a conversar com um maintainer mas ele falou que ainda nao viram necessidade de implementar o destroy. A ideia e que esse post seja traduzido pro ingles nos proximos dias e utilizado pra mostrar a real necessidade de ter um destroy tambem!

Outra diferença e que precisamos adicionar um condicional no comando de destroy, caso contrário todo o fluxo seria executado a cada commit e você não seria capaz de acessar seu ambiente.

if: github.event.action == 'closed'

Caso você esteja utilizando esse ambiente "apenas" para rodar alguns testes de integração ou coisas do tipo, poderia deixar sem essa condicional e executar alguma action especifica para rodar os testes entre o passo de criar e destruir.

No nosso caso o ambiente só será destruído após fecharmos o PR, seja dando merge ou simplesmente fechando o mesmo.

Abrindo nosso PR ou commitando em um ja aberto poderemos ver o log explicando que esta sendo criada uma máquina na digitalocean.

Após finalizarmos o trabalho no PR e darmos o merge para outra branch ou mesmo desistirmos de avançar com o PR e o fecharmos o github action se encarrega de destruir o ambiente executando novamente o workflow da seguinte maneira:

Poderiamos deixar o apply sendo executado durante o workflow de destroy, mas isso não seria muito proveitoso. Já que normalmente ele não teria nada para criar e mesmo que tivesse seria destruído em seguida. Entao nao faz muito sentido.

EXTRA

Um ponto bem legal e que essa nova versao de actions do terraform possuem a capacidade de comentar o output no proprio PR, entao ficaria algo parecido com essa imagem:

Se você leu até aqui deixa um joinha, se inscreve no canal e compartilha com os amiguinhos que isso ajuda bastante no crescimento do canal e... ops mídia errada! 🙂

É isso galera, e se o mundo não acabar em breve a gente se vê por ai 🙂

Latest comments (0)