Desenvolvimento

Construindo uma CI para um projeto Java com Quarkus

Já iniciou o desenvolvimento do seu projeto Java? Por exemplo usando Quarkus? Se sim, como próximo passo ao desenvolvimento devemos ter uma CI/CD ou, em outras palavras, continuous integration e continuous deployment e neste artigo queremos abordar a CI e mostrar como pode ser simples e eficiente para o fluxo de desenvolvimento.

Este artigo é parte integrante do nosso fluxo onde saímos do zero e vamos até ao CD, caso não tenha visto as outras partes recomendamos a leitura e participação:

Uma vez com nosso projeto, vamos estruturar nosso fluxo da seguinte forma:

  • Etapas desejadas na CI
  • Construção da CI
  • Validação da CI

Pré-Requisitos

A CI deste projeto será focada no Gitlab CI e por isso uma conta ou instância é necessario, além disso, para fins de teste o cli e docker instalados também são necessários. Para nosso projeto vamos usar os runners compartilhados do gitlab porém caso não possa usar ou esteja usando uma instância auto hospedada será necessario instalar e configurar um runner docker.

Etapas Desejadas em uma CI

Abaixo vamos colocar algumas das etapas que é bom ter em uma CI, estamos nos baseando em partes no Auto DevOps do Gitlab, colaremos uma coluna indicando se vamos implementar na nossa CI.

EtapaTipoVamos implementar?
Buildbuild Sim
Dependency ScanningtestSim
Code QualitytestNão
Container ScanningtestNão
Secret DetectiontestNão
Semgrep sasttestSim
TesttestSim
Docker BuildContainerSim
Docker PushContainerSim

Build

Como o nome sugere, é aqui que vamos gerar o nosso pacote, seja ele uma compilação ou não.

Dependency Scanning

Nem sempre podemos conseguimos controlar todas as dependências de um projeto e é por esse motivo que devemos ter Dependency Scanning como parte da sua CI, pois é ela que vai procurar falhas de segurança nessas dependências e te avisar caso tenha.

Container Scanning

Com um objetivo similar ao Dependency Scanning porém com uma atuação totalmente diferente, nesse caso ele não analisa as dependências do seu projeto e sim o container, imagem, que sua CI construiu e busca por falhas, seja na camada da sua aplicação ou em camadas anteriores.

Secret Detection

Mesmo que seu projeto não tenha o código fonte aberto, não é seguro deixar senhas diretamente no código fonte e essa etapa basicamente tenta garantir isso.

Test

Aqui entra a execução dos testes escritos na sua aplicação, vamos apenas validar se todos os testes estão passando conforme previsto ou se algo mudou no meio do caminho, talvez com um commit novo ou um MR, que fez com que o resultado esperado pela aplicação mude e desta forma evitando erros em produção.

Docker Build

Como o nome sugere, nessa etapa vamos contruir uma imagem docker, caso você tenha um projeto que tenha que gerar uma, esse é um fluxo indispensável.

Docker Push

Complementar ao ponto anterior, é nesse ponto que vamos enviar a imagem para nosso gerenciador de imagens por exemplo Jfrog Artifactory, Artifact Registry do Google ou em nosso caso GitLab container registry.

Construção da CI

Se essa for sua primeira CI recomendamos que leia: Como Criar uma Integração Contínua com GitLab em 5 Passos Simples primeiro onde explicamos a estrutura de uma CI, neste artigo vamos considerar que já entende o básico da construção de uma CI.

Etapas

stages:
  - build
  - test-code
  - test
  - docker-build
  - docker-push
YAML

A organização da nossa CI não segue exatamente uma regra, apenas estamos organizando de forma que os primeiros jobs vão virar dependências do que vem depois.

Build

Para o job de build vamos usar a imagem do Quarkus para gerar o pacote nativo do binário do Quarkus, poderíamos usar uma variável porém não vamos reutilizar e por isso colocamos direto na linha 3, como resultado desse build teremos artefatos na pasta target e como cache usaremos o .m2 que terá como objetivo não ter que baixar as dependências se o commit não mudar.

package:
  image:
    name: quay.io/quarkus/ubi-quarkus-mandrel-builder-image:jdk-21
    entrypoint:
      - ""
  variables:
    MAVEN_OPTS: >-
      -Dhttps.protocols=TLSv1.2
      -Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository
      -Dorg.slf4j.simpleLogger.showDateTime=true
      -Djava.awt.headless=true
    MAVEN_CLI_OPTS: >-
      --batch-mode
      --errors
      --fail-at-end
      --show-version
      --no-transfer-progress
      -DinstallAtEnd=true
      -DdeployAtEnd=true
  stage: build
  script:
    - ./mvnw package -Dnative -DskipTests
  artifacts:
    paths:
      - target
  cache:
    key: $CI_COMMIT_SHA
    policy: push
    paths:
      - .m2/repository
YAML

Testes unitários

Similar ao Build porém para este artigo vamos usar a funcionalidade de extensão de jobs para que você tenha um job centralizado e genérico e crie os específicos como build e tests.

test:
  extends: package
  stage:  test-code
  script:
    - ./mvnw test -Dmaven.install.skip=true
YAML

Docker

A primeira pergunta que pode surgir é porque separamos o docker build e docker push se podemos executar tudo em uma única etapa? É algo lógico sim, porém ao separar esses jobs isso nos permite que após a criação da imagem podemos rodar um scanner nela e se passar, ou seja sem risco de segurança na imagem, poderemos fazer a publicação sem riscos da imagem.

docker_build:
  image: docker:git
  stage: docker-build
  dependencies: ["package"]
  services:
    - docker:dind
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker build -f src/main/docker/Dockerfile.native-micro -t $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG .
    - docker save -o ${CI_PROJECT_NAME}.tar "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
  artifacts:
    paths:
      - ${CI_PROJECT_NAME}.tar

docker_push:
  image: docker:git
  stage: docker-push
  dependencies: ["docker_build"]
  services:
    - docker:dind
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker load -i ${CI_PROJECT_NAME}.tar;
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
  artifacts:
    paths:
      - ${CI_PROJECT_NAME}.tar
YAML

Ultilizando Jobs do Gitlab Auto DevOps

Como dito acima, vamos utilizar 2 jobs do Auto DevOps e para fazer o uso é bem simples, precisamos primeiro importar ele e depois definir o job e em qual etapa queremos rodar ela, dizemos isso tanto com o sast quanto no dependency scanning.

include:
  - template: Security/SAST.gitlab-ci.yml
  - template: Security/Dependency-Scanning.gitlab-ci.yml
  
sast:
  stage: test-code

gemnasium-maven-dependency_scanning:
  stage: test-code
  dependencies: ["package"]
  rules:
    - when: on_success
YAML

Validação

Após a construção de uma CI o próximo passo é saber que ela está funcionando e o gitlab oferece isso de forma bem simples atravez do CI Lint. Acesse em seu projeto Build -> Pipelines -> CI Lint e cole o seu código, como resultado devemos ter algo assim:

Conclusão

O que achou dessa CI? Achava que era mais complexo? E respondendo o nosso questionamento inicial, com uma CI bem estruturada podemos trazer mais velocidade para o fluxo de trabalho, onde o desenvolvedor consegue de forma simples e rápida descobrir e simular o ambiente mais próximo do cliente através da CI. Deixe nos comentários o que achou desta CI e para acessar este projeto também disponibilizamos ele no Gitlab.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.