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).
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).
.gitlab-ci.yml
Explicando o nosso 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
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.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.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 nometest-ops
em baixo do nome de seu stage,Test
.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.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.
No ponto 6 iniciamos o nosso objeto
script
, que consiste de uma lista desteps
(passos). Cada step deve ser um comando shell válido.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 jobtest-ops
;- pois com o objeto
default
, definimos uma imagem para todos os jobs.
- pois com o objeto
- alteramos o script do job
test-ops
para testar apenas oops.py
; - criamos o job
test-app
, que é parecido com otest-ops
mas com propriedades adicionais:- definimos um objeto
variables
com a variável de ambienteshould_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 doscript
:- criamos e ativamos um ambiente virtual, e
- instalamos Flask nele.
- definimos um objeto
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:
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:
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:
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:
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:
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:
About author
Você pode gostar também
Curso Python Fundamentals: Sua porta de entrada para o mundo Python
Com foco em atrair diversos perfis e atender à uma demanda crescente por profissionais que conheçam a linguagem, o curso foi remodelado para ser a sua porta de entrada para
DevSecOps: 6 passos para implementar segurança no desenvolvimento de software
A cultura DevOps se tornou um marco na história da tecnologia, tanto que muitos recrutadores começaram a utilizar este nome até para descrição de vagas. Mas onde fica a segurança
Integração de Chat Funcional com Ferramenta Rocket.Chat: Passo a Passo
Dando continuidade o nosso post anterior onde entendemos como utilizar as APIs do Rocket.Chat para manipular o Omnichannel, agora iremos integrar um chat funcional com essa ferramenta. Irei partir do