Criando Review Apps com Github Action, Terraform e DigitalOcean!
Tags: terraform, digitalocean, githubaction, reviewapp
Published: 2019-11-15 14:38:09

---
title: Criando Review Apps com Github Action, Terraform e DigitalOcean!
published: true
description: 
tags: terraform, digitalocean, githubaction, review app
---


Antes de mais nada... por que [Review Apps](https://docs.gitlab.com/ee/ci/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](https://github.com/features/actions) 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](https://medium.com/@igordcsouza/terraform-uma-pequena-introdu%C3%A7%C3%A3o-eae86f22db55), 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](https://github.com/features/actions).

![](https://i.ibb.co/NCf0Thw/post-01.png)

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:

![](https://i.ibb.co/QYFVfb3/post-02.png)


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](https://help.github.com/en/actions/automating-your-workflow-with-github-actions), 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](https://medium.com/@igordcsouza/terraform-uma-pequena-introdu%C3%A7%C3%A3o-eae86f22db55) então corre lá... eu espero pra continuar.

![](https://media.giphy.com/media/o5oLImoQgGsKY/giphy.gif)

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](https://github.com/hashicorp/terraform-github-actions) que se encontra no repositório criado e mantido pela própria [Hashicorp](https://www.hashicorp.com/).

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

![](https://i.ibb.co/KLTM4hZ/post-03.png)

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.

![](https://i.ibb.co/N19Bmy1/post-04.png)



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.

![](https://i.ibb.co/KF1gVsX/Screenshot-from-2019-11-15-09-26-48.png)

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:

![](https://i.ibb.co/xHBq2S6/Screenshot-from-2019-11-15-09-44-29.png)

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:

![](https://i.ibb.co/MctsHjz/Screenshot-from-2019-11-15-09-27-07.png)


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 🙂