GitLab CI – Integração Contínua sem sair do repositório

GitLab CI – Integração Contínua sem sair do repositório

Ferramentas de CI/CD hoje em dia andam de mãos dadas com os nossos projetos – dificilmente vemos um repositório no GitHub, GitLab, Bitbucket, Gitea… sem alguma configuração de integração contínua. Todos estes têm a opção de configurar webhooks, que são basicamente ações eventuais: se repositório recebeu push, então fazer uma requisição HTTP em uma URL configurada com os dados deste push, do repositório, etc. Este é o caminho padrão para quem usa ferramentas de CI standalone como o Jenkins🇺🇸. Além disso, serviços diferentes tomam decisões diferentes: o Github, por exemplo, não tem uma ferramenta própria de CI/CD. É muito comum ver arquivos de configuração das ferramentas Travis🇺🇸 e CircleCI🇺🇸 em seus repositórios.

E então temos Bitbucket e GitLab. Estes dois decidiram seguir um outro caminho: eles têm a sua própria ferramenta de CI/CD, que é chamada internamente de Pipelines. A configuração não chega a ser nada de outro mundo: é só mais um punhado de valores YAML que temos que decorar e usar.

A 4Linux internamente usa o GitLab, então faremos os nossos exemplos em um repositório na instância oficial: https://gitlab.com🇺🇸. O autor presume que você possui um conhecimento pelo menos básico de git e repositórios.

O Projeto

Faremos um projeto simples – até de mais – para demonstrar como usar o Pipelines do GitLab. O projeto será basicamente 4 arquivos de código-fonte Python: 2 de aplicação e 2 de testes. Mandaremos o GitLab executar os testes para nós.

Nosso primeiro arquivo é um em que implementamos duas operações: soma e subtração de dois números, ops.py:

def add(x: int, y: int) -> int:
    return x + y

def sub(x: int, y: int) -> int:
    return x - y

Nada de outro mundo. A seguir temos o arquivo onde testamos as nossas operações, test_ops.py:

import unittest
import ops

class TestOps(unittest.TestCase):
    def test_add(self):
        for x in range(1, 6):
            y = x + 1
            with self.subTest(x=x, y=y, **{'x+y': x+y}):
                self.assertEqual(ops.add(x, y), x + y)

    def test_sub(self):
        for x in range(14, 9, -1):
            y = x - 5
            with self.subTest(x=x, y=y, **{'x-y': x-y}):
                self.assertEqual(ops.sub(x, y), x - y)

Ao executar os testes, vemos a saída esperada:

$ python -m unittest -v test_ops
test_add (test_ops.TestOps) ... ok
test_sub (test_ops.TestOps) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

Agora que estamos com os nossos testes funcionando, já podemos começar a escrever o nosso arquivo de pipeline. Criamos então um novo arquivo chamado .gitlab-ci.yml na raíz do projeto e escrevemos o seguinte nele:

stages:
  - test

test-ops:
  stage: test
  image: python:slim-bullseye
  script:
    - python -m unittest

Seguido de um commit e um push.

A primeira execução do Pipeline

Assim que o GitLab identificar que há um arquivo .gitlab-ci.yml na raíz do projeto, ele deve automaticamente habilitar o fluxo de pipelines do repositório. É possível ver o processo ao navegar até a seção de pipelines do projeto (imagem 1) ou acessar diretamente pela mensagem do último commit na página inicial, onde deve haver um 🕒 relógio azul caso ainda esteja sendo executado, um ✅ checkmark verde caso tenha finalizado com sucesso, ou um ❌ X vermelho caso tenha havido algum problema (imagem 2).

imagem 1:
navegação até a página de pipelines pela barra lateral

imagem 2:
navegação até o pipeline do commit em questão

Na página seguir vemos todos os pipelines executados; deve haver apenas um neste instante. Perto da mensagem do commit, há um número referente ao pipeline, o seu identificador (imagem 3). Podemos clicar neste identificador para ver os jobs do pipeline que, agora, consiste de apenas um: test-ops (imagem 4). Precisamos apenas clicar neste job para ver os detalhes de sua execução em uma janela parecida com um terminal (imagem 5).

imagem 3:
id do pipeline

imagem 4:
jobs do pipeline selecionado

imagem 5:
saída do terminal de um job

Explicando o nosso .gitlab-ci.yml

Vamos numerar cada linha do nosso arquivo:

stages: # --------------------- 1
  - test # -------------------- 2

test-ops: # ------------------- 3
  stage: test # --------------- 4
  image: python:slim-bullseye # 5
  script: # ------------------- 6
    - python -m unittest # ---- 7
  1. Aqui definimos o objeto stages, que é responsável por listar todos os estágios do nosso pipeline. Podemos colocar quantos stages forem necessários, e os seus nomes podem ser arbitrários. test é um nome comum de estágio de pipeline. Os estágios são executados em ordem de definição, e jobs que pertencem a um mesmo stage são executados em paralelo.

  2. Aqui definimos o nosso primeiro (e único, a princípio) estágio: test. Usamos este nome no momento de definição de um job para indicar quando que este deve ser executado.

  3. Este é o nosso job. O job é o que define quais instruções queremos executar neste instante do pipeline. Seu nome também é arbitrário; eu escolhi test-ops, mas poderia ser qualquer outro nome. Na imagem 4 podemos ver um objeto circulado com o nome test-ops em baixo do nome de seu stage, Test.

  4. Aqui é onde definido a qual stage este job pertence. No caso, test. A única restrição que temos que seguir é que o nome do stage precisa ser um stage válido listado no nosso objeto stages do item 1.

  5. Neste ponto definimos qual imagem do docker usaremos para executar este job. O GitLab cuida de baixar a imagem, que deve existir no Docker Hub ou seja qual for o repositório de imagens utilizado.

  6. No ponto 6 iniciamos o nosso objeto script, que consiste de uma lista de steps (passos). Cada step deve ser um comando shell válido.

  7. Finalmente, aqui definimos o nosso único step: python -m unittest. Este é o comando que usamos para executar os testes automatizados, que pode ser visto na imagem 5 acima.

Incrementando o nosso projeto

Vamos adicionar um app simples com Flask para poder testar nossa rota. O app consistirá de uma única rota que somará dois números. Criaremos, então, nosso app.py e escreveremos o seguinte:

import flask

app = flask.Flask(__name__)

@app.route("/add/<int:x>/<int:y>")
def add(x: int, y: int):
    return str(x - y)

Seguido do nosso test_app.py:

import os
import unittest
from flask.testing import FlaskClient
from app import app

class TestApp(unittest.TestCase):
    @unittest.skipUnless(os.getenv("should_test"), "test only when required")
    def test_add(self):
        x, y = 2, 3

        with FlaskClient(app) as client:
            res = client.get(f"/add/{x}/{y}")
            self.assertEqual(res.data.decode(), str(x+y))

Existem dois pontos importantes no que escrevemos aqui:

  • Estamos usando uma biblioteca de terceiros, Flask;
  • O nosso teste só vai ser executado quando a variável de ambiente should_test estiver definida.

Então se tentarmos testar normalmente, o Python nos mostrará um erro dizendo que não encontra a biblioteca (partes omitidas para simplicidade):

$ python -m unittest -v test_app
test_app (unittest.loader._FailedTest) ... ERROR
ERROR: test_app (unittest.loader._FailedTest)
...
ModuleNotFoundError: No module named 'flask'
Ran 1 test in 0.000s
FAILED (errors=1)

Para que funcione, precisamos instalar a biblioteca. O faremos em um ambiente virtual:

$ python -m venv venv
$ . venv/bin/activate
$ pip install flask
$ python -m unittest -v test_app

E a sua saída:

$ python -m unittest -v test_app
test_add (test_app.TestApp) ... skipped 'test only when required'

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK (skipped=1)

Como esperado, o teste não executa a menos que a variável de ambiente esteja configurada.

$ should_test=1 python -m unittest -v test_app
test_add (test_app.TestApp) ... FAIL
AssertionError: '-1' != '5'
- -1
+ 5
Ran 1 test in 0.088s
FAILED (failures=1)

E essa é a nossa saída esperada, pois fizemos uma subtração em vez de uma adição no código da nossa rota. Deixaremos assim para ver como aparece no GitLab. Agora modificaremos o nosso .gitlab-ci.yml para que haja uma seção para o app:

stages:
  - test

default:
  image: python:slim-bullseye

test-ops:
  stage: test
  script:
    - python -m unittest -v test_ops

test-app:
  stage: test
  variables:
    should_test: "true"
  rules:
    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
  before_script:
    - python -m venv venv
    - . venv/bin/activate
    - pip install Flask==2.1.1
  script:
    - python -m unittest -v test_app

Nós:

  • adicionamos um objeto default, que possui uma imagem docker como único atributo;
  • removemos o atributo image do nosso job test-ops;
    • pois com o objeto default, definimos uma imagem para todos os jobs.
  • alteramos o script do job test-ops para testar apenas o ops.py;
  • criamos o job test-app, que é parecido com o test-ops mas com propriedades adicionais:
    • definimos um objeto variables com a variável de ambiente should_test;
    • definimos um objeto rules, que determina quando este job deve ser executado:
      • apenas quando o contexto do pipeline for um Merge Request;
    • definimos um objeto before_script, que diz à ferramenta o que deve ser feito antes de executarmos os passos do script:
      • criamos e ativamos um ambiente virtual, e
      • instalamos Flask nele.

Depois disso, podemos criar um novo branch e realizar o commit e push nele:

$ git checkout -b feature/add-app
$ git add -A
$ git commit -m "add app"
$ git push -u origin feature/add-app

Agora vá à página do seu repositório e abra um Merge Request para o seu branch principal. De volta à página de Pipelines você deve ver o seguinte:

imagem 6:
pipelines bem e mal sucedidos

Dois pipelines executados! Isso é por causa da regra que demos ao test-app. O job do test-ops é executado normalmente quando fazemos o push, mas o job do test-app só é acionado e executado quando estamos no contexto de um merge request. Perceba que o commit é o mesmo. Só muda mesmo o contexto.

A mensagem de erro do job mal-sucedido:

imagem 7:
mensagem de erro de job falhado

O mesmo erro que nós vimos acima, em nossa execução local.

Antes de corrigirmos o nosso app, vamos adicionar uma seção de clean up no nosso .gitlab-ci.yml.

stages:
  - test
  - clean-up

# algumas seções omitidas para simplicidade
default:
  ...

test-ops:
  ...

test-app:
  ...

clean-up:
  stage: clean-up
  rules:
    - when: always
  script:
    - echo "cleaning up..."
  • Criamos um novo stage, clean-up;
  • Criamos um novo job, clean-up;
  • Associamos os dois pela propriedade stage do job;
  • Por meio do objeto rules, definimos que este job deve sempre (always) ser executado;
  • Adicionamos um script simples, que apenas mostra um texto na tela.

Agora realizaremos o commit seguido do push e veremos o resultado na página de pipelines do projeto.

$ git add -A
$ git commit -m "add clean up stage and job"
$ git push

E, depois de executados os jobs, vemos o seguinte resultado:

imagem 8:
pipelines com 2 stages

Podemos ver que, agora, há o novo estágio presente e sendo executado. Normalmente, um pipeline interromperia sua execução no instante em que um stage falha. Mas, como dissemos à ferramenta que o clean-up deve ser executado sempre, o pipeline segue em frente até o final. Faça o teste: comente ou remova a regra que definimos para o job e commite-pushe novamente.
Vejamos a saída do terminal deste stage:

imagem 9:
saída do stage=clean-up job=clean-up

Agora só nos falta corrigir o erro que inserimos no nosso app.py para que o pipeline seja consertado:

def add(x: int, y: int):
-   return str(x - y)
+   return str(x + y)
$ git add -A
$ git commit -m "fix app error"
$ git push

Fazemos o push e aceitamos o merge request para que possamos ver os últimos 3 pipelines executados:

imagem 10:
resultados dos últimos três pipelines

E assim finalizamos o nosso primeiro projeto utilizando a ferramenta de CI/CD do GitLab!

Finalizando

O projeto que fizemos durante este post foi bem simples, e mesmo assim deu pra escrever bastante e apresentar algumas funcionalidades interessantes da ferramenta. Porém não acaba aqui; esta é só a ponta do iceberg. Existem muitas, muitas outras funcionalidades legais e interessantes disponíveis e prontas para uso. Se for do seu interesse saber mais, o melhor lugar é a referência oficial🇺🇸 deles. As seções sobre variáveis🇺🇸 e palaras chave do arquivo YAML🇺🇸 são especialmente relevantes.

E aí, quais processos vocês estão prontos para automatizar com essa ferramenta incrível?

Líder em Treinamento e serviços de Consultoria, Suporte e Implantação para o mundo open source. Conheça nossas soluções:

CURSOSCONSULTORIA

Anterior Invista na sua carreira com 50% de desconto em cursos de TI
Próxima Domine o Linux: Torne-se um Administrador de Sistemas Linux

About author

Você pode gostar também

Desenvolvimento

Domine o Vim: o editor de texto essencial para Sysadmins Linux

Para um Sysadmin Linux, possuir domínio de editores de texto via linha de comando é imprescindível. Constantemente temos a necessidade de alterar arquivos, visualizar  o contéudo, abrir mais de um

Desenvolvimento

Descubra como o Elastic APM pode melhorar a performance da sua aplicação

Heeey! E já que venho aqui sempre para falar de elasticsearch … Estou aqui para explorar o Elastic APM  :] Primeiramente, APM significa Monitoramento de performance de Aplicação (Application Performance

Desenvolvimento

Por que aprender Python pode impulsionar sua carreira em programação

Você que já automatiza suas rotinas com shell script precisa aprender Python. Dizem que se você quer trabalhar no Google o caminho mais fácil é aprender Python. Será que este