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-productionAs 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: manualVariáveis e segredos
No GitLab: Settings → CI/CD → Variables
| Variável | Tipo | Descrição |
|---|---|---|
K8S_TOKEN | Masked + Protected | Token do service account K8s |
K8S_SERVER | Variable | URL do cluster staging |
K8S_PROD_TOKEN | Masked + Protected | Token produção |
SLACK_WEBHOOK | Masked | Webhook 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.