Criando ambientes de teste dinamicamente com GitHub Actions
Tags: github, kubernetes, helm, aks
Published: 2021-07-27 14:18:33

---
title: Criando ambientes de teste dinamicamente com GitHub Actions
published: true
date: 2021-06-09 13:00:00 UTC
tags: github,kubernetes,helm,aks
canonical_url: https://blog.lsantos.dev/ambientes-dinamicos-helm-actions/
cover_image: https://images.unsplash.com/photo-1584187167877-c7c4bbd60878?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8c2VhcmNofDExfHxkeW5hbWljfGVufDB8fHx8MTYyMjc0NzU0MQ&ixlib=rb-1.2.1&q=80&w=2000
---

Como otimizar um pipeline de testes para que os times não tenham problemas de concorrência ao testar suas funcionalidades e seus módulos é um assunto recorrente em vários tópicos que tratei tanto no passado quanto recentemente.

Inclusive, já fiz algumas talks sobre o assunto e tenho até [um repositório de exemplo](https://github.com/khaosdoctor/helm-dynamic-envs/) usando o [Azure DevOps](https://dev.azure.com/?WT.mc_id=containers-12314-ludossan) como ferramenta de CI. Você pode ver os slides e o vídeo abaixo!

<!--kg-card-end: markdown-->

<iframe width="356" height="200" src="https://www.youtube.com/embed/tYy3HTR0f90?list=PLnOICPAPShyRZd7nnbC7h8kCQwM-6K3KW" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

<!--kg-card-begin: html--><style>
    iframe.speakerdeck-iframe {
       width: 37vw;
    }
</style><script async data-id="07925e1f31434bc2bc8279c20e9100e5" data-ratio="1.77777777777778" src="//speakerdeck.com/assets/embed.js"></script><!--kg-card-end: html-->

A questão é: Como podemos fazer com que vários times de desenvolvimento consigam testar suas funcionalidades em um ambiente completamente separado dos demais de forma simples e rápida?

A resposta está, é claro, em **containers**. Quando utilizamos o Kubernetes com Helm juntamente a uma ferramenta de CI, podemos fazer muitas coisas dinamicamente. Neste artigo, vou atualizar minha palestra anterior e mostrar a mesma aplicação, porém rodando no pipeline do GitHub Actions. Para deixar o cenário mais real, vamos utilizar a [Azure](https://azure.microsoft.com?WT.mc_id=containers-12314-ludossan) com o [Azure Kubernetes Service](https://azure.microsoft.com/services/kubernetes-service/?WT.mc_id=containers-12314-ludossan) baixando imagens de um [Azure Container Registry privado](https://azure.microsoft.com/services/container-registry/?WT.mc_id=containers-12314-ludossan) já integrado de forma privada com o cluster. Todos os dados serão armazenados em um [CosmosDB](https://azure.microsoft.com/services/cosmos-db/?WT.mc_id=containers-12314-ludossan) do tipo Mongo.

Vamos lá!

## Antes de começar

Vamos ter que criar o ambiente antes de começar a mostrar como a parte dinâmica pode ser feita. Como esse não é o intuito do post, vou deixar apenas as referências de comandos do que vamos fazer por aqui, mas todas as documentações necessárias podem ser encontradas diretamente na documentação de cada serviço.

Primeiramente você precisa ter uma conta na [Azure](https://azure.microsoft.com?WT.mc_id=containers-12314-ludossan), uma vez com esta conta, instale o [Azure CLI](https://docs.microsoft.com/cli/azure/install-azure-cli?WT.mc_id=containers-12314-ludossan), vamos utilizar somente a linha de comando.

O primeiro comando será o comando `az login` para fazer o login na sua conta e escolher qual será a subscription que será utilizada pra criar os recursos. Assim que o login estiver pronto, vamos começar criando o primeiro recurso, o resource group.

```
az group create -l eastus -n ship-manager-pipeline

```

Agora vamos criar o nosso ACR para poder armazenar nossas imagens:

```
az acr create -n shipmanager --sku Basic -g ship-manager-pipeline

```

Espere até o CR ser criado e execute o seguinte comando para habilitar o login via usuário e senha, só assim vamos poder fazer o login pelo nosso CI para poder construir as imagens:

```
az acr update -n shipmanager --admin-enabled true

```

Agora vamos partir para o CosmosDB, com um simples comando podemos criar toda a estrutura:

```
az cosmosdb create --kind MongoDB -n ship-manager-db -g ship-manager-pipeline

```

Este comando demora um pouco mais para ser executado, então seja paciente. É importante dizer que, apesar de ser incrível, o CosmosDB não é recomendado para este caso específico de criação de bancos de dados quando temos ambientes efêmeros como estes pois é mais complexo de remover futuramente. Mas, para simplificar, vamos utilizá-lo e vamos entender alternativas que podemos ter a esta abordagem mais a frente no artigo.

Por fim vamos criar o nosso AKS que vai unir todas as partes:

```
az aks create -n ship-manager -g ship-manager-pipeline \
--enable-addons http_application_routing \
--attach-acr shipmanager \
--vm-size Standard_B2s \
--generate-ssh-keys \
--node-count 2

```

Isso fará com que nosso AKS seja criado já em conjunto com o ACR, dessa forma não precisamos criar um secret para cada namespace com o nosso arquivo de login do Docker para baixar as imagens, e também não precisamos fazer um bind em uma service account.

Obtenha as credenciais do AKS com o seguinte comando:

```
az aks get-credentials -n ship-manager -g ship-manager-pipeline --admin

```

Lembrando que você precisa ter o `kubectl` instalado na máquina para este comando funcionar, caso não o possua, use o comando `az aks install-cli` para instalá-lo.

## Criando o chart

O primeiro passo para a criação da pipeline é saber como ela vai funcionar, inicialmente vamos trabalhar somente com dois ambientes, o primeiro será o ambiente de produção e o segundo será o ambiente de testes.

O ambiente de produção será publicado sempre que um push com uma tag `v*` for feito. Já o ambiente de teste será publicado em um push de qualquer outro branch que não seja o `master` ou `main` (dependendo do caso).

Para que você possa acompanhar, preparei [este repositório de exemplo](https://github.com/khaosdoctor/helm-actions-dynamic-env-example) que contém tanto o código da aplicação quanto o código das actions em si.

> Se você quiser acompanhar passo a passo, faça um _fork_ do repositório, mas não se esqueça de remover a pasta `.github` para que as actions sejam removidas.

Antes de criar os arquivos da pipeline, vamos criar os arquivos do Helm, para podermos criar nosso chart! Crie uma pasta chamada `kubernetes` na raiz do repositório, depois crie uma segunda pasta chamada `ship-manager`.

> Poderíamos criar o chart do helm de forma automática via CLI, porém ele cria vários arquivos que não precisamos, então vamos criá-lo manualmente para facilitar.

Dentro da pasta `ship-manager` crie mais duas pastas: `templates` e `charts`. Agora crie dois arquivos no mesmo nível da pasta `templates`, um deles se chamará `Chart.yaml` e o outro `values.yaml`.

Agora, vamos para dentro da pasta `charts`, nela, crie uma pasta `backend` e, dentro desta última adicione um arquivo `Chart.yaml` seguido de uma pasta `templates`.

A estrutura final deverá ser assim:

```
kubernetes
└── ship-manager
    ├── Chart.yaml
    ├── charts
    │ └── backend
    │ ├── Chart.yaml
    │ └── templates
    │
    ├── templates
    └── values.yaml

```

No arquivo `Chart.yaml` da pasta `ship-manager` vamos escrever o seguinte:

```
apiVersion: v2
name: ship-manager
description: Chart for the ship manager app
version: 0.1.0

```

E no da pasta `backend` será o seguinte:

```
apiVersion: v2
name: backend
description: Chart for the backend part of the ship manager app
version: 0.1.0

```

O que fizemos foi criar o arquivo equivalente a um `package.json` do Helm, ou seja, o arquivo que define o pacote que vamos instalar no nosso cluster.

O Helm funciona com base em uma hierarquia, o que acabamos de criar aqui é uma ordem de dependências, ou seja, acabamos de dizer que o frontend da aplicação, que está no `ship-manager`, é dependente de um backend localizado na pasta `charts`, se criássemos outra pasta `charts` dentro de `backend` iríamos dizer que o backend é dependente dela e assim sucessivamente. Desta forma, quando damos apenas um comando, o Helm já instala todas as dependências em ordem para nós.

Vamos criar nosso primeiro template, crie um arquivo `frontend.yaml` dentro da pasta `templates` localizada na pasta `ship-manager`. Este template vai ser o que vai ser de fato criado dentro do cluster. Nele vamos ter todos os recursos do Kubernetes, começando pelo Deployment:

```
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ship-manager-frontend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ship-manager-frontend
  template:
    metadata:
      labels:
        app: ship-manager-frontend
    spec:
      containers:
        - image: {{ required "Registry is required" .Values.global.registryName }}/{{ required "Image name is required" .Values.frontend.imageName }}:{{ required "Image tag is required" .Values.global.imageTag }}
          name: ship-manager-frontend
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 250m
              memory: 256Mi
          ports:
            - containerPort: 8080
              name: http
          volumeMounts:
            - name: config
              mountPath: /usr/src/app/dist/config.js
              subPath: config.js
      volumes:
        - name: config
          configMap:
            name: frontend-config

```

Veja que estamos utilizando placeholders do Helm para poder identificar partes que podem ser alteradas, e é isso que faz com que tudo seja possível. A criação de ambientes e alteração de variáveis em tempo de CLI e não depois de compilado faz com que possamos passar os valores que quisermos para essas variáveis ao criar o ambiente.

Depois temos as outras configurações:

```
apiVersion: v1
kind: Service
metadata:
  name: ship-manager-frontend
spec:
  selector:
    app: ship-manager-frontend
  ports:
    - name: http
      port: 80
      targetPort: 8080
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: ship-manager-frontend
  annotations:
    kubernetes.io/ingress.class: addon-http-application-routing
spec:
  rules:
    - host: {{ default "ship-manager-frontend" .Values.frontend.ingress.hostname }}.{{ .Values.global.dnsZone }}
      http:
        paths:
          - path: /
            backend:
              serviceName: ship-manager-frontend
              servicePort: http
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: frontend-config
data:
  config.js: |
    const config = (() => {
      return {
        'VUE_APP_BACKEND_BASE_URL': 'http://{{ default "ship-manager-backend" .Values.backend.ingress.hostname }}.{{ .Values.global.dnsZone }}',
        'VUE_APP_PROJECT_VERSION': '{{ .Values.global.imageTag }}'
      }
    })()

```

Perceba que estou buscando tudo de `.Values`, este é o arquivo `values.yaml` que vamos ver logo mais. Perceba também que a maioria das coisas que podem ser alteradas e que precisam ser alteradas, como o nome da imagem, a tag, o hostname e banco de dados, também são variáveis.

Nestes casos, utilizar configmaps e secrets ajuda muito a manter a pipeline simples.

O arquivo final será:

```
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ship-manager-frontend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ship-manager-frontend
  template:
    metadata:
      labels:
        app: ship-manager-frontend
    spec:
      containers:
        - image: {{ required "Registry is required" .Values.global.registryName }}/{{ required "Image name is required" .Values.frontend.imageName }}:{{ required "Image tag is required" .Values.global.imageTag }}
          name: ship-manager-frontend
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 250m
              memory: 256Mi
          ports:
            - containerPort: 8080
              name: http
          volumeMounts:
            - name: config
              mountPath: /usr/src/app/dist/config.js
              subPath: config.js
      volumes:
        - name: config
          configMap:
            name: frontend-config
---
apiVersion: v1
kind: Service
metadata:
  name: ship-manager-frontend
spec:
  selector:
    app: ship-manager-frontend
  ports:
    - name: http
      port: 80
      targetPort: 8080
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: ship-manager-frontend
  annotations:
    kubernetes.io/ingress.class: addon-http-application-routing
spec:
  rules:
    - host: {{ default "ship-manager-frontend" .Values.frontend.ingress.hostname }}.{{ .Values.global.dnsZone }}
      http:
        paths:
          - path: /
            backend:
              serviceName: ship-manager-frontend
              servicePort: http
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: frontend-config
data:
  config.js: |
    const config = (() => {
      return {
        'VUE_APP_BACKEND_BASE_URL': 'http://{{ default "ship-manager-backend" .Values.backend.ingress.hostname }}.{{ .Values.global.dnsZone }}',
        'VUE_APP_PROJECT_VERSION': '{{ .Values.global.imageTag }}'
      }
    })()

```

Vamos fazer o mesmo com o backend, criando um arquivo `backend.yaml` na pasta `charts/backend/templates`:

```
# backend.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ship-manager-backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ship-manager-backend
  template:
    metadata:
      labels:
        app: ship-manager-backend
    spec:
      containers:
        - image: {{ required "Registry is required" .Values.global.registryName }}/{{ required "Image name is required" .Values.imageName }}:{{ required "Image tag is required" .Values.global.imageTag }}
          name: ship-manager-backend
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 250m
              memory: 256Mi
          ports:
            - containerPort: 3000
              name: http
          env:
            - name: DATABASE_MONGODB_URI
              valueFrom:
                secretKeyRef:
                  key: database_mongodb_uri
                  name: backend-db
            - name: DATABASE_MONGODB_DBNAME
              value: {{ default "ship-manager" .Values.global.dbName }}
---
apiVersion: v1
kind: Service
metadata:
  name: ship-manager-backend
spec:
  selector:
    app: ship-manager-backend
  ports:
    - name: http
      port: 80
      targetPort: 3000
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: ship-manager-backend
  annotations:
    kubernetes.io/ingress.class: addon-http-application-routing
spec:
  rules:
    - host: {{ default "ship-manager-backend" .Values.ingress.hostname }}.{{ .Values.global.dnsZone }}
      http:
        paths:
          - path: /
            backend:
              serviceName: ship-manager-backend
              servicePort: http
---
apiVersion: v1
kind: Secret
metadata:
  name: backend-db
type: Opaque
stringData:
  database_mongodb_uri: {{ required "DB Connection is required" .Values.global.dbConn | quote }}

```

Perceba que tenho algumas funções também, como `required`, `default` e `quote`, esta são [funções nativas do Helm](https://helm.sh/docs/chart_template_guide/function_list/) e quebram um bom galho quando a gente precisa de funcionalidades mais complexas.

### O arquivo values

Assim como os charts, o arquivo `values.yaml` é baseado em uma hierarquia de escopos, ou seja, tome como exemplo a nossa estrutura:

```
# values.yaml
global:
  chave: # Acessível a todos os charts, tanto o frontend como o backend como `.Values.global.chave`

backend:
  chave: # Acessível somente ao frontend e ao backend, porém para o frontend será `.Values.backend.chave` e o backend usará como `.Values.chave`

frontend:
  chave: # Acessível pelo frontend como `.Values.frontend.chave`, mas não pelo backend

chave: # Acessível somente ao frontend como `.Values.chave`

```

Perceba que temos uma quebra de escopo dentro do arquivo values, as chaves que tem o mesmo nome dos seus charts dependentes serão acessadas somente por eles e pelos charts de mais alta ordem, então como o nosso frontend é o chart de maior ordem, ele tem acesso a todos os valores, enquanto o backend só tem acesso às chaves definidas sob `backend:`.

Perceba também que dentro de `backend` o escopo sofre um "levelling", ou seja, o escopo é removido de dentro de `backend` então você não precisa acessar o valor como `.Values.backend.chave` se você estiver dentro do chart `backend`, mas somente como `.Values.chave`.

> É possível ter mais arquivos values dentro dos charts dependentes e a regra se mantém a mesma, a diferença é que o chart de maior ordem será alterado, porém este padrão torna a manutenção bastante complexa.

Nosso arquivo values precisa ter as mesmas chaves que definimos dentro dos nossos templates, então elas vão ser as seguintes:

```
global:
  registryName:
  imageTag:
  dbName: ship-manager
  dbConn:
  dnsZone:

backend:
  imageName: ship-manager-backend
  ingress:
    hostname:

frontend:
  imageName: ship-manager-frontend
  ingress:
    hostname:

```

As chaves que estou deixando em branco são as que serão ou preenchidas pelo CLI ou pelas funções `default`.

## Criando a pipeline

Para criamos a pipeline, vamos utilizar o modo manual de criação, ou seja, vamos criar uma pasta `.github` e dentro dela uma pasta `workflows`. O primeiro workflow será o mais simples, o de produção.

Dentro da pasta `workflows` vamos criar um arquivo `deploy-production.yml` (pode ser qualquer nome, na verdade) e começar escrevendo o nome da nossa pipeline e quais são os gatilhos que vão fazer ela funcionar.

```
name: Build and push the tagged build to production

on:
  push:
    tags:
      - 'v*'

```

Aqui estamos dizendo que nossa action vai rodar em todos os pushes com uma tag `v*`, ou seja, `v1.0.0` e até mesmo `vabc`, se você quiser reduzir as possibilidades pode usar regex como `v[0-9]\.[0-9]\.[0-9]`.

Depois, vamos criar nosso primeiro job e definir uma variável em comum:

```
name: Build and push the tagged build to production

on:
  push:
    tags:
      - 'v*'

env:
  IMAGE_NAME: ship-manager

jobs:
  build_push_image:
    runs-on: ubuntu-20.04

```

Criamos um job chamado `build_push_image` que vai rodar no ubuntu 20, e uma variável compartilhada que será o nome base da imagem. Agora vamos para a ação de verdade, vamos começar a criar os passos do nosso job, começando com dois super importantes:

```
name: Build and push the tagged build to production

on:
  push:
    tags:
      - 'v*'

env:
  IMAGE_NAME: ship-manager

jobs:
  build_push_image:
    runs-on: ubuntu-20.04

    steps:
      - uses: actions/checkout@v2

      - name: Set env
        id: tags
        run: echo tag=${GITHUB_REF#refs/tags/} >> $GITHUB_ENV

```

O primeiro passo é um checkout do nosso repositório, ele está praticamente presente em todas as actions e é sempre o primeiro passo. O segundo é a definição de uma segunda variável, o nome da tag.

Por padrão `$GITHUB_REF` é ou o nome do branch ou o nome da tag como `/refs/heads/main` ou `/refs/tags/v1.0.0`, temos que remover o `/refs/*` e ficar só com o final, por isso estamos usando uma substituição via shell para adicioná-la as variáveis globais, porém esta variável só funciona **dentro deste job**.

> Não podemos definir a variável dentro de `env` porque esta chave não executa nenhum tipo de shell, portanto não podemos usar substituição de valores e nem expansões.

Agora vamos fazer o pipeline do Docker, ou seja, build e push das imagens do backend e frontend.

```
name: Build and push the tagged build to production

on:
  push:
    tags:
      - 'v*'

env:
  IMAGE_NAME: ship-manager

jobs:
  build_push_image:
    runs-on: ubuntu-20.04

    steps:
      - uses: actions/checkout@v2

      - name: Set env
        id: tags
        run: echo tag=${GITHUB_REF#refs/tags/} >> $GITHUB_ENV

      - name: Set up Buildx
        uses: docker/setup-buildx-action@v1

      - name: Login to ACR
        uses: docker/login-action@v1
        with:
          # Username used to log in to a Docker registry. If not set then no login will occur
          username: ${{secrets.ACR_LOGIN }}
          # Password or personal access token used to log in to a Docker registry. If not set then no login will occur
          password: ${{secrets.ACR_PASSWORD }}
          # Server address of Docker registry. If not set then will default to Docker Hub
          registry: ${{ secrets.ACR_NAME }}

      - name: Build and push frontend image
        uses: docker/build-push-action@v2
        with:
          # Docker repository to tag the image with
          tags: ${{secrets.ACR_NAME}}/${{ env.IMAGE_NAME }}-frontend:latest,${{secrets.ACR_NAME}}/${{ env.IMAGE_NAME }}-frontend:${{env.tag}}
          labels: |
            image.revision=${{github.sha}}
            image.release=${{github.ref}}
          file: frontend/Dockerfile
          context: frontend
          push: true

      - name: Build and push backend image
        uses: docker/build-push-action@v2
        with:
          # Docker repository to tag the image with
          tags: ${{secrets.ACR_NAME}}/${{ env.IMAGE_NAME }}-backend:latest,${{secrets.ACR_NAME}}/${{ env.IMAGE_NAME }}-backend:${{env.tag}}
          labels: |
            image.revision=${{github.sha}}
            image.release=${{github.ref}}
          file: backend/Dockerfile
          context: backend
          push: true

```

O Docker tem 3 actions que são utilizadas, a primeira delas é o setup do `buildx` o utilitário de build de imagens do Docker, a segunda é o login em um registry, no caso será nosso ACR, e aqui temos o nosso primeiro `secret` que vamos criar dentro do nosso repositório, que serão os dados de login do ACR.

Por fim, estamos criando e dando um push na imagem e criando as tags `latest` e o nome da `tag` do GitHub, desta forma sabemos quais são as imagens "de produção" e quais serão as de teste, adicionamos também duas labels para cada imagem, uma delas tem a revisão, o sha do nosso commit, e a outra o nome da tag.

Com isso finalizamos nosso primeiro job, o segundo é a parte onde vamos fazer o deploy para o cluster. O início é o mesmo então vou omitir o conteúdo até aqui para focarmos só nessa parte:

```
# inicio do arquivo
jobs:
  build_push_image:
    # job de envio da imagem

  deploy:
      runs-on: ubuntu-20.04
      needs: build_push_image
  
      steps:
        - uses: actions/checkout@v2
  
        - name: Set env
          id: tags
          run: echo tag=${GITHUB_REF#refs/tags/} >> $GITHUB_ENV
  
        - name: Install Helm
          uses: Azure/setup-helm@v1
          with:
            version: v3.3.1

```

Perceba que estamos criando uma relação entre os dois jobs com a chave `needs`, isso diz que o segundo job só será executado se o primeiro passar. Fazemos o checkout e copiamos a criação da variável, depois rodamos um step simples de instalação do Helm na máquina.

```
# inicio do arquivo
jobs:
  build_push_image:
    # job de envio da imagem

  deploy:
      runs-on: ubuntu-20.04
      needs: build_push_image
  
      steps:
        - uses: actions/checkout@v2
  
        - name: Set env
          id: tags
          run: echo tag=${GITHUB_REF#refs/tags/} >> $GITHUB_ENV
  
        - name: Install Helm
          uses: Azure/setup-helm@v1
          with:
            version: v3.3.1

      - name: Get AKS Credentials
        uses: Azure/aks-set-context@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}
          # Resource group name
          resource-group: ship-manager-pipeline
          # AKS cluster name
          cluster-name: ship-manager

      - name: Run Helm Deploy
        run: |
          helm upgrade \
            ship-manager-prd \
            ./kubernetes/ship-manager \
            --install \
            --create-namespace \
            --namespace production \
            --set global.registryName=${{ secrets.ACR_NAME }} \
            --set global.dbConn="${{ secrets.DB_CONNECTION }}" \
            --set global.dnsZone=${{ secrets.DNS_NAME }} \
            --set global.imageTag=${{env.tag}}

```

Por fim vamos ter o comando para obter as credenciais do Kubernetes e o deploy do Helm. Perceba que estamos fazendo o deploy para um namespace `production` e setando os valores do `values.yaml` através das flags `--set`, isso torna tudo mais fácil quando precisarmos remover os dados, pois só precisamos remover o namespace e tudo é deletado.

A pipeline de testes é quase igual, a diferença é que estamos setando mais variáveis e mudamos o namespace de publicação e também o trigger. O outro arquivo, que chamei de `deploy-test` ficou assim:

```
# deploy-test.yml
name: Build and push the tagged build to test

on:
  push:
    branches-ignore:
      - 'main'
      - 'master'

env:
  IMAGE_NAME: ship-manager

jobs:
  build_push_image:
    runs-on: ubuntu-20.04

    steps:
      - uses: actions/checkout@v2

      - name: Set env
        id: tags
        run: echo tag=${GITHUB_REF#refs/heads/} >> $GITHUB_ENV

      - name: Set up Buildx
        uses: docker/setup-buildx-action@v1

      - name: Login to ACR
        uses: docker/login-action@v1
        with:
          # Username used to log in to a Docker registry. If not set then no login will occur
          username: ${{secrets.ACR_LOGIN }}
          # Password or personal access token used to log in to a Docker registry. If not set then no login will occur
          password: ${{secrets.ACR_PASSWORD }}
          # Server address of Docker registry. If not set then will default to Docker Hub
          registry: ${{ secrets.ACR_NAME }}

      - name: Build and push frontend image
        uses: docker/build-push-action@v2
        with:
          # Docker repository to tag the image with
          tags: ${{ secrets.ACR_NAME }}/${{ env.IMAGE_NAME }}-frontend:${{env.tag}}
          labels: |
            image.revision=${{github.sha}}
          file: frontend/Dockerfile
          context: frontend
          push: true

      - name: Build and push backend image
        uses: docker/build-push-action@v2
        with:
          # Docker repository to tag the image with
          tags: ${{ secrets.ACR_NAME }}/${{ env.IMAGE_NAME }}-backend:${{env.tag}}
          labels: |
            image.revision=${{github.sha}}
          file: backend/Dockerfile
          context: backend
          push: true

  deploy:
    runs-on: ubuntu-20.04
    needs: build_push_image

    steps:
      - uses: actions/checkout@v2

      - name: Set env
        id: tags
        run: echo tag=${GITHUB_REF#refs/tags/} >> $GITHUB_ENV

      - name: Install Helm
        uses: Azure/setup-helm@v1
        with:
          version: v3.3.1

      - name: Get AKS Credentials
        uses: Azure/aks-set-context@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}
          # Resource group name
          resource-group: ship-manager-pipeline
          # AKS cluster name
          cluster-name: ship-manager

      - name: Run Helm Deploy
        run: |
          helm upgrade \
            ship-manager-${{env.tag}} \
            ./kubernetes/ship-manager \
            --install \
            --create-namespace \
            --namespace test-${{env.tag}} \
            --set global.registryName=${{ secrets.ACR_NAME }} \
            --set global.dbConn="${{ secrets.DB_CONNECTION }}" \
            --set global.dbName=ship-manager-test-${{env.tag}} \
            --set global.dnsZone=${{ secrets.DNS_NAME }} \
            --set backend.ingress.hostname=ship-manager-backend-${{env.tag}} \
            --set frontend.ingress.hostname=ship-manager-frontend-${{env.tag}} \
            --set global.imageTag=${{env.tag}}

```

### Secrets

Agora que já temos as pipelines criadas, vamos ao GitHub criar nossos secrets! Abra o repositório no seu browser, navegue até a aba `Settings` e depois `secrets`, então clique em `New repository secret` para criar um novo secret local.

Vamos criar um secret chamado `ACR_LOGIN` que será o nome do nosso ACR, ou seja, `shipmanager`. Outro chamado `ACR_NAME`, que não é bem um segredo, porque é o DNS do nosso CR, porém assim evitamos de ter um valor fixo na nossa action, este valor é o `shipmanager.azurecr.io`.

> Ambas as informações podem ser obtidas no portal da Azure, sabendo o nome do ACR o login é o mesmo e o DNS é sempre `<nome>.azurecr.io`

A senha do ACR pode ser obtida com um comando do AZ CLI: `az acr credential show -n shipmanager --query "passwords[0].value" -o tsv` e deve ser colocada em outro secret chamado `ACR_PASSWORD`.

Para obtermos a chave do nosso AKS, vamos precisar de um acesso via service principal na Azure, que pode ser obtido pelo comando `az ad sp create-for-rbac --sdk-auth`, este comando vai te devolver um JSON, copie **todo o JSON** e cole no secret chamado `AZURE_CREDENTIALS`.

A conexão com o banco do secret chamado `DB_CONNECTION` pode ser obtida também pelo comando `az cosmosdb keys list -n ship-manager-db -g ship-manager-pipeline --type connection-strings --query "connectionStrings[0].connectionString"`.

E o secret final pode ser obtido através de uma query na lista de addons habilitados do AKS, como ligamos o HTTP Application Routing, vamos ter uma zona de DNS liberada que podemos obter com o comando `az aks show -n ship-manager -g ship-manager-pipeline --query "addonProfiles.httpApplicationRouting.config.HTTPApplicationRoutingZoneName` e colocar no secret chamado `DNS_NAME`.

## Testando

Commitamos nossas mudanças e agora vamos criar uma tag com `git tag -a v<versão> -m'nova versão` e depois `git push --tags` para podermos fazer a trigger na nosso build. Teremos um pequeno delay e então uma saída como esta:

![](https://raw.githubusercontent.com/khaosdoctor/blog-assets/master/images/2021/06/image-2.png)

Se visualizarmos a nossa aplicação depois de alguns minutos (o DNS demora para propagar), iremos ver que temos um endereço igual ao do nosso ingress (podemos obter o endereço do frontend com `kubectl get ing -n production`), ao acessar, vamos ter a nossa aplicação rodando:

![](https://raw.githubusercontent.com/khaosdoctor/blog-assets/master/images/2021/06/image-3.png)

<!--kg-card-begin: markdown-->

Nossa aplicação está em um branch de produção e será acessível e atualizada para a versão mais nova sempre que fizermos um push com uma tag específica. O mesmo vai acontecer quando criarmos um novo branch e fizermos um push, experimente criar um branch qualquer e enviar um código!

## Conclusão e melhorias

A criação de um ambiente dinâmico não é simples, porém pode ser a solução entre um time que demora a testar suas funcionalidades para um time que pode ser muito mais eficiente. Neste exemplo chegamos aos 50% do que é necessário, a outra parte importante do pipeline é também **remover** seus recursos sempre que não estão mais utilizados.

Por este motivo, utilizar outro banco de dados ao invés da mesma instância é muito mais preferível, o ideal seria criarmos uma dependência no chart backend para um chart do MongoDB completamente vazio, desta forma podemos ter certeza que este ambiente está totalmente isolado e podemos remover todo o ambiente sem nenhum problema.

Deixem seus comentários e quem sabe podemos continuar essa série!

<!--kg-card-end: markdown-->