Saltar para o conteúdo
CI/CD com GitLab CI: Pipeline Completo do Zero
DevOps

CI/CD com GitLab CI: Pipeline Completo do Zero

20 de maio de 2024·Paulo de Paula

Um pipeline de CI/CD bem construído é a diferença entre deploys confiantes e deploys tensos. GitLab CI define o pipeline como código no próprio repositório — versiona junto com a aplicação, é revisado em PRs, e qualquer desenvolvedor consegue entender o que acontece em cada deploy.

Estrutura do .gitlab-ci.yml

# .gitlab-ci.yml
stages:
  - test       # testes e qualidade
  - build      # build da imagem Docker
  - deploy-staging
  - deploy-production

As stages executam sequencialmente. Jobs dentro da mesma stage executam em paralelo.

Stage de testes com paralelismo

# ── Testes ──────────────────────────────────────────────────────
lint:
  stage: test
  image: node:20-alpine
  cache:
    key:
      files: [package-lock.json]
    paths: [node_modules/]
  before_script:
    - npm ci --prefer-offline
  script:
    - npm run lint
    - npm run typecheck
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == "main"'

unit-tests:
  stage: test
  image: node:20-alpine
  cache:
    key:
      files: [package-lock.json]
    paths: [node_modules/]
  services:
    - name: postgres:16-alpine
      alias: postgres
      variables:
        POSTGRES_DB: testdb
        POSTGRES_USER: test
        POSTGRES_PASSWORD: test
  variables:
    DATABASE_URL: "postgresql://test:test@postgres:5432/testdb"
    NODE_ENV: test
  before_script:
    - npm ci --prefer-offline
    - npm run db:migrate
  script:
    - npm run test:coverage
  coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
      junit: coverage/junit.xml
    expire_in: 7 days
  parallel: 3                    # GitLab divide os testes automaticamente
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == "main"'

Build da imagem Docker

# ── Build ────────────────────────────────────────────────────────
build-image:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  variables:
    IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    IMAGE_LATEST: $CI_REGISTRY_IMAGE:latest
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    # BuildKit para cache de layers
    - docker buildx build
        --cache-from $IMAGE_LATEST
        --cache-to type=inline
        --tag $IMAGE_TAG
        --tag $IMAGE_LATEST
        --push
        .
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
    - if: '$CI_COMMIT_TAG'

Dockerfile otimizado para CI

# Dockerfile
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package.json .

USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]

Deploy automático para staging

# ── Staging ──────────────────────────────────────────────────────
deploy-staging:
  stage: deploy-staging
  image: bitnami/kubectl:latest
  environment:
    name: staging
    url: https://staging.meusite.com.br
  variables:
    IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  before_script:
    - kubectl config set-cluster k8s --server="$K8S_SERVER"
    - kubectl config set-credentials admin --token="$K8S_TOKEN"
    - kubectl config set-context deploy --cluster=k8s --user=admin
    - kubectl config use-context deploy
  script:
    - kubectl set image deployment/minha-api
        api=$IMAGE_TAG
        -n staging
    - kubectl rollout status deployment/minha-api -n staging --timeout=120s
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

Deploy manual para produção

# ── Produção ─────────────────────────────────────────────────────
deploy-production:
  stage: deploy-production
  image: bitnami/kubectl:latest
  environment:
    name: production
    url: https://meusite.com.br
  variables:
    IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  before_script:
    - kubectl config set-cluster k8s --server="$K8S_PROD_SERVER"
    - kubectl config set-credentials admin --token="$K8S_PROD_TOKEN"
    - kubectl config set-context deploy --cluster=k8s --user=admin
    - kubectl config use-context deploy
  script:
    - kubectl set image deployment/minha-api
        api=$IMAGE_TAG
        -n production
    - kubectl rollout status deployment/minha-api -n production --timeout=180s
  when: manual              # requer clique humano na UI do GitLab
  allow_failure: false
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual

Variáveis e segredos

No GitLab: Settings → CI/CD → Variables

VariávelTipoDescrição
K8S_TOKENMasked + ProtectedToken do service account K8s
K8S_SERVERVariableURL do cluster staging
K8S_PROD_TOKENMasked + ProtectedToken produção
SLACK_WEBHOOKMaskedWebhook para notificações
# Notificação no Slack ao final do pipeline
notify:
  stage: .post         # estágio especial: roda após todos os outros
  image: alpine
  script:
    - apk add --no-cache curl
    - |
      curl -X POST $SLACK_WEBHOOK \
        -H 'Content-type: application/json' \
        -d "{\"text\": \"Deploy *$CI_PROJECT_NAME* concluído — <$CI_PIPELINE_URL|Pipeline #$CI_PIPELINE_ID>\"}"
  when: on_success
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

Proteção de branch

Em Settings → Repository → Protected Branches, configure main:

  • Allowed to merge: Maintainers
  • Allowed to push: No one (só via MR)
  • Require pipeline to succeed: ✓

Isso garante que nenhum código vai para main sem passar pelos testes — e nenhum deploy acontece sem aprovação de código via Merge Request.