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.
Etapa | Tipo | Vamos implementar? |
Build | build | Sim |
Dependency Scanning | test | Sim |
Code Quality | test | Não |
Container Scanning | test | Não |
Secret Detection | test | Não |
Semgrep sast | test | Sim |
Test | test | Sim |
Docker Build | Container | Sim |
Docker Push | Container | Sim |
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
YAMLA 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
YAMLTestes 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
YAMLDocker
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
YAMLUltilizando 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
YAMLValidaçã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.