DevOps & CI/CD

Dominando Fundamentos de CI/CD: Pipelines, Stages e Boas Práticas em Projetos Reais

15 min de leitura

Dominando Fundamentos de CI/CD: Pipelines, Stages e Boas Práticas em Projetos Reais

Entendendo CI/CD: Do Conceito à Prática CI/CD é um conjunto de práticas que automatiza a entrega de software, eliminando etapas manuais e reduzindo erros humanos. O acrônimo representa Continuous Integration (Integração Contínua) e Continuous Delivery ou Continuous Deployment (Entrega ou Implantação Contínua). A diferença entre eles é sutil mas importante: CI/CD é o conjunto completo, CI é a prática de integrar código frequentemente, e CD pode significar tanto entregar código pronto para produção quanto implantar automaticamente. A razão pela qual CI/CD é tão valorizado na indústria é simples: reduz o tempo entre a escrita do código e sua chegada aos usuários, aumenta a confiabilidade do software e permite feedback rápido sobre problemas. Em um projeto tradicional sem CI/CD, um desenvolvedor pode trabalhar por semanas em uma branch isolada, e quando tenta integrar seu código, descobre conflitos complexos e bugs não detectados. Com CI/CD, pequenas mudanças são integradas diariamente, testadas automaticamente e, se passarem, podem ir para produção em horas. O

<h2>Entendendo CI/CD: Do Conceito à Prática</h2>

<p>CI/CD é um conjunto de práticas que automatiza a entrega de software, eliminando etapas manuais e reduzindo erros humanos. O acrônimo representa Continuous Integration (Integração Contínua) e Continuous Delivery ou Continuous Deployment (Entrega ou Implantação Contínua). A diferença entre eles é sutil mas importante: CI/CD é o conjunto completo, CI é a prática de integrar código frequentemente, e CD pode significar tanto entregar código pronto para produção quanto implantar automaticamente.</p>

<p>A razão pela qual CI/CD é tão valorizado na indústria é simples: reduz o tempo entre a escrita do código e sua chegada aos usuários, aumenta a confiabilidade do software e permite feedback rápido sobre problemas. Em um projeto tradicional sem CI/CD, um desenvolvedor pode trabalhar por semanas em uma branch isolada, e quando tenta integrar seu código, descobre conflitos complexos e bugs não detectados. Com CI/CD, pequenas mudanças são integradas diariamente, testadas automaticamente e, se passarem, podem ir para produção em horas.</p>

<h3>O Fluxo Básico</h3>

<p>Imagine este cenário: você faz um commit em seu repositório. Imediatamente, um servidor observa essa mudança, baixa o código, compila, executa testes automatizados, analisa qualidade de código, e se tudo passar, empacota a aplicação em um container ou artefato pronto para ser executado. Tudo isso sem nenhuma intervenção manual. Este é o CI/CD em ação.</p>

<h2>Pipelines: A Coluna Vertebral da Automação</h2>

<p>Um pipeline é uma série de passos executados em sequência ou paralelo para transformar código-fonte em um produto entregável. Cada passo é uma etapa específica que valida, testa ou prepara o código. A beleza de um pipeline é sua previsibilidade: você sabe exatamente o que acontece quando faz um commit, porque está tudo documentado e automatizado.</p>

<h3>Anatomia de um Pipeline</h3>

<p>Um pipeline típico possui três partes principais: <strong>source</strong> (onde o código vive), <strong>build</strong> (onde o código é compilado e testado), e <strong>deploy</strong> (onde o código é colocado em produção). Cada uma dessas partes pode ter múltiplos passos internos. O source é geralmente um repositório Git. O build é onde a mágica acontece: dependências são baixadas, testes rodam, artefatos são criados. O deploy leva esse artefato para servidores reais.</p>

<p>Vamos ver um exemplo concreto com GitHub Actions, que é gratuito e integrado ao GitHub:</p>

<pre><code class="language-yaml">name: CI/CD Pipeline

on:

push:

branches: [ main, develop ]

pull_request:

branches: [ main ]

jobs:

build:

runs-on: ubuntu-latest

steps:

  • uses: actions/checkout@v3
  • name: Setup Node.js

uses: actions/setup-node@v3

with:

node-version: &#039;18&#039;

  • name: Install Dependencies

run: npm install

  • name: Run Unit Tests

run: npm run test

  • name: Run Linter

run: npm run lint

  • name: Build Application

run: npm run build

  • name: Upload Artifacts

uses: actions/upload-artifact@v3

with:

name: build-output

path: dist/

deploy:

needs: build

runs-on: ubuntu-latest

if: github.ref == &#039;refs/heads/main&#039;

steps:

  • uses: actions/download-artifact@v3

with:

name: build-output

  • name: Deploy to Production

run: |

echo &quot;Deploying to production...&quot;

Aqui você colocaria seu script de deploy real</code></pre>

<p>Observe a estrutura: primeiro fazemos checkout do código, configuramos o Node.js, instalamos dependências, rodamos testes, linting, build, e se tudo passar, armazenamos os artefatos. Depois, em um job separado que só executa na branch main, fazemos o deploy. Isso garante que apenas código que passou em todos os testes vai para produção.</p>

<h3>Triggers: Quando o Pipeline Executa</h3>

<p>Triggers definem quando um pipeline começa a rodar. No exemplo acima, o pipeline executa quando há um push nas branches main ou develop, ou quando há um pull request. Você pode ser mais específico ainda: executar apenas quando arquivos em diretórios específicos mudam, em horários agendados, ou manualmente.</p>

<pre><code class="language-yaml">on:

push:

paths:

  • &#039;src/**&#039;
  • &#039;package.json&#039;
  • &#039;.github/workflows/**&#039;

schedule:

  • cron: &#039;0 2 *&#039; # Executa diariamente às 2 da manhã

workflow_dispatch: # Permite execução manual</code></pre>

<h2>Stages: Dividindo Responsabilidades</h2>

<p>Um stage é um agrupamento lógico de passos dentro de um pipeline. Enquanto um passo é uma ação individual (como &quot;rodar testes&quot;), um stage é uma fase maior do pipeline que agrupa relacionados. Stages são cruciais para organização e para entender em qual fase o pipeline falhou.</p>

<h3>Estrutura de Stages Recomendada</h3>

<p>A maioria dos pipelines modernos usa uma estrutura de stages assim: <strong>Checkout</strong> (preparação), <strong>Build</strong> (compilação), <strong>Test</strong> (testes unitários, integração), <strong>Quality</strong> (análise estática, cobertura), <strong>Package</strong> (criar artefatos), <strong>Deploy Staging</strong> (ambiente de teste), <strong>Smoke Tests</strong> (testes rápidos pós-deploy), <strong>Deploy Production</strong> (produção real).</p>

<p>Vamos ver isso com GitLab CI, que é excelente para stage management:</p>

<pre><code class="language-yaml">stages:

  • build
  • test
  • quality
  • package
  • deploy_staging
  • deploy_production

variables:

DOCKER_IMAGE: registry.gitlab.com/mycompany/myapp

build_app:

stage: build

image: node:18-alpine

script:

  • npm install
  • npm run build

artifacts:

paths:

  • dist/

expire_in: 1 hour

only:

  • branches

unit_tests:

stage: test

image: node:18-alpine

dependencies:

  • build_app

script:

  • npm install
  • npm run test:unit

coverage: &#039;/Coverage: \d+\.\d+%/&#039;

integration_tests:

stage: test

image: node:18-alpine

dependencies:

  • build_app

script:

  • npm run test:integration

services:

  • postgres:14
  • redis:7

sonarqube_analysis:

stage: quality

image: sonarsource/sonar-scanner-cli:latest

script:

  • sonar-scanner -Dsonar.projectKey=myapp -Dsonar.host.url=$SONAR_HOST

only:

  • merge_requests

package_docker:

stage: package

image: docker:latest

services:

  • docker:dind

script:

  • docker build -t $DOCKER_IMAGE:$CI_COMMIT_SHA .
  • docker push $DOCKER_IMAGE:$CI_COMMIT_SHA
  • docker tag $DOCKER_IMAGE:$CI_COMMIT_SHA $DOCKER_IMAGE:latest
  • docker push $DOCKER_IMAGE:latest

only:

  • main

deploy_staging:

stage: deploy_staging

image: bitnami/kubectl:latest

script:

  • kubectl set image deployment/myapp myapp=$DOCKER_IMAGE:$CI_COMMIT_SHA

environment:

name: staging

url: https://staging.myapp.com

only:

  • main

smoke_tests_staging:

stage: deploy_staging

image: curlimages/curl:latest

script:

- curl -f https://staging.myapp.com/health | | exit 1 - curl -f https://staging.myapp.com/api/version || exit 1

when: on_success

retry:

max: 3

when: script_failure

deploy_production:

stage: deploy_production

image: bitnami/kubectl:latest

script:

  • kubectl set image deployment/myapp myapp=$DOCKER_IMAGE:$CI_COMMIT_SHA --namespace=production

environment:

name: production

url: https://myapp.com

only:

  • main

when: manual</code></pre>

<p>Observe como os stages são claramente separados. O estágio <code>test</code> tem dois jobs que rodam em paralelo (unit_tests e integration_tests). O estágio <code>deploy_production</code> tem <code>when: manual</code>, o que significa que alguém precisa clicar um botão para deployer para produção — uma boa prática de segurança.</p>

<h3>Dependências Entre Stages</h3>

<p>Stages podem depender um do outro. Um stage só inicia quando todos os jobs do stage anterior completam com sucesso. Se um test falhar, o package não executa. Se o package falhar, nenhum deploy acontece. Isso garante que código quebrado nunca chega a produção.</p>

<h2>Boas Práticas Essenciais</h2>

<p>Ter um pipeline é bom. Ter um pipeline bem feito é excelente. Existem padrões e práticas que separam empresas que entregam qualidade daquelas que entregam caos frequentemente.</p>

<h3>1. Feedback Rápido</h3>

<p>Um pipeline que leva 2 horas para rodar destrói a produtividade. Desenvolvedores precisam de feedback em minutos, não horas. A estratégia é paralelizar: coloque testes independentes em jobs paralelos, divida testes em suites que rodem separadamente. Se seu pipeline leva muito tempo, identifique os gargalos e otimize ou divida.</p>

<pre><code class="language-yaml"># Exemplo: testes em paralelo

test_unit:

stage: test

script:

  • npm run test:unit

parallel: 5 # Divide os testes em 5 execuções paralelas

test_integration:

stage: test

script:

  • npm run test:integration

parallel: 3</code></pre>

<h3>2. Idempotência: Deploy Deve Ser Seguro</h3>

<p>Um deploy deve ser seguro de rodar múltiplas vezes. Se você executa o deploy duas vezes seguidas, o resultado final deve ser o mesmo. Isso significa usar declarative configuration (Kubernetes, Terraform) ao invés de scripts imperativos que modificam estado.</p>

<pre><code class="language-hcl"># Terraform - declarativo e idempotente

resource &quot;aws_ecs_service&quot; &quot;app&quot; {

name = &quot;myapp&quot;

cluster = aws_ecs_cluster.main.id

task_definition = aws_ecs_task_definition.app.arn

desired_count = 3

launch_type = &quot;FARGATE&quot;

network_configuration {

subnets = aws_subnet.private[*].id

security_groups = [aws_security_group.app.id]

assign_public_ip = false

}

}</code></pre>

<p>Se você executar isso duas vezes, o Terraform reconhece que o estado já existe e faz nada. Isso é seguro.</p>

<h3>3. Controle de Acesso e Segredos</h3>

<p>Nunca coloque senhas, tokens, chaves de API em repositórios. Use secrets management. GitHub Actions, GitLab CI, e Jenkins têm formas de armazenar segredos com segurança.</p>

<pre><code class="language-yaml"># GitHub Actions com secrets

  • name: Deploy

env:

DATABASE_PASSWORD: ${{ secrets.DB_PASSWORD }}

API_KEY: ${{ secrets.EXTERNAL_API_KEY }}

run: ./deploy.sh</code></pre>

<p>Os secrets são mascarados nos logs (aparecem como <code>***</code>), então você não expõe credenciais acidentalmente quando compartilha logs com o time.</p>

<h3>4. Rastreabilidade e Auditoria</h3>

<p>Cada deploy deve ser rastreável. Você deve saber exatamente qual commit foi deployado, por quem, quando, e para onde. Armazene essa informação.</p>

<pre><code class="language-bash">#!/bin/bash

Script de deploy com auditoria

COMMIT_SHA=$(git rev-parse --short HEAD)

COMMIT_MESSAGE=$(git log -1 --pretty=%B)

DEPLOYED_BY=${CI_COMMIT_AUTHOR:-unknown}

DEPLOYMENT_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)

echo &quot;Deployment Log&quot;

echo &quot;==============&quot;

echo &quot;Commit: $COMMIT_SHA&quot;

echo &quot;Message: $COMMIT_MESSAGE&quot;

echo &quot;By: $DEPLOYED_BY&quot;

echo &quot;Time: $DEPLOYMENT_TIME&quot;

echo &quot;Environment: $DEPLOY_ENV&quot;

Armazene isso em um banco de dados ou arquivo de auditoria</code></pre>

<h3>5. Ambiente de Staging Idêntico à Produção</h3>

<p>Erros só aparecem em produção se seu staging não é idêntico. Use containers (Docker) para garantir que o mesmo artefato roda em qualquer lugar. Tenha dados de teste realistas em staging.</p>

<pre><code class="language-dockerfile"># Dockerfile - mesmo para todos os ambientes

FROM node:18-alpine

WORKDIR /app

COPY package*.json ./

RUN npm ci --only=production

COPY dist ./dist

EXPOSE 3000

CMD [&quot;node&quot;, &quot;dist/server.js&quot;]</code></pre>

<p>O container roda igual em seu notebook, em staging, e em produção. Nenhuma surpresa.</p>

<h3>6. Testes em Múltiplas Camadas</h3>

<p>Não confie apenas em testes unitários. Uma pirâmide de testes é: muitos testes unitários (rápidos, baratos), alguns testes de integração (validam componentes juntos), poucos testes E2E (validam o fluxo completo do usuário).</p>

<pre><code class="language-javascript">// Teste unitário - função pura

describe(&#039;calculatePrice&#039;, () =&gt; {

it(&#039;should apply discount correctly&#039;, () =&gt; {

const price = calculatePrice(100, 0.1);

expect(price).toBe(90);

});

});

// Teste de integração - com banco de dados

describe(&#039;User Service&#039;, () =&gt; {

it(&#039;should save and retrieve user&#039;, async () =&gt; {

const user = await userService.create({ name: &#039;John&#039; });

const retrieved = await userService.getById(user.id);

expect(retrieved.name).toBe(&#039;John&#039;);

});

});

// Teste E2E - browser automatizado

describe(&#039;Login Flow&#039;, () =&gt; {

it(&#039;should login user and redirect to dashboard&#039;, async () =&gt; {

await page.goto(&#039;https://app.com/login&#039;);

await page.fill(&#039;[name=&quot;email&quot;]&#039;, &#039;user@example.com&#039;);

await page.fill(&#039;[name=&quot;password&quot;]&#039;, &#039;password123&#039;);

await page.click(&#039;button[type=&quot;submit&quot;]&#039;);

await page.waitForNavigation();

expect(page.url()).toContain(&#039;/dashboard&#039;);

});

});</code></pre>

<h3>7. Configuração por Ambiente</h3>

<p>Sua aplicação provavelmente precisa de configurações diferentes em dev, staging e produção (URLs, log levels, features flags). Use variáveis de ambiente, não hardcode.</p>

<pre><code class="language-javascript">// config.js

const config = {

development: {

apiUrl: &#039;http://localhost:3001&#039;,

logLevel: &#039;debug&#039;,

enableFeatures: [&#039;beta-feature&#039;],

},

staging: {

apiUrl: &#039;https://api-staging.myapp.com&#039;,

logLevel: &#039;info&#039;,

enableFeatures: [&#039;beta-feature&#039;],

},

production: {

apiUrl: &#039;https://api.myapp.com&#039;,

logLevel: &#039;warn&#039;,

enableFeatures: [],

},

};

module.exports = config[process.env.NODE_ENV || &#039;development&#039;];</code></pre>

<h3>8. Monitoramento e Rollback Automático</h3>

<p>Após um deploy, monitore a aplicação. Se taxa de erros subir, métricas caírem, ou alertas disparam, faça rollback automático para a versão anterior. Isso protege usuários de deploys ruins.</p>

<pre><code class="language-yaml"># Exemplo conceitual de rollback automático

deploy_and_monitor:

stage: deploy

script:

  • kubectl rollout status deployment/myapp # Aguarda conclusão do deploy
  • sleep 60 # Aguarda 1 minuto
- ERROR_RATE=$(curl -s http://prometheus:9090/api/v1/query?query=error_ratejq &#039;.data.result[0].value[1]&#039;)
if [ $(echo &quot;$ERROR_RATE &gt; 5&quot;bc) -eq 1 ]; then

echo &quot;Error rate above 5%, rolling back...&quot;

kubectl rollout undo deployment/myapp

exit 1

fi</code></pre>

<h2>Conclusão</h2>

<p>Aprendemos que CI/CD é muito mais que apenas rodar testes automaticamente. É uma cultura e um conjunto de práticas que aceleram entrega, aumentam confiabilidade e reduzem estresse de deployments. Os três pontos principais são: <strong>Pipelines bem estruturados automatizam e organizam o fluxo de código</strong>, garantindo que cada commit passa por validações rigorosas antes de chegar aos usuários. <strong>Stages dividem responsabilidades e permitem feedback granular</strong>, dizendo exatamente em qual fase algo falhou. <strong>Boas práticas como feedback rápido, idempotência, segredos seguros e testes em múltiplas camadas</strong> transformam um pipeline básico em um sistema confiável que a empresa inteira pode confiar.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://docs.github.com/en/actions" target="_blank" rel="noopener noreferrer">GitHub Actions Documentation</a> — Documentação oficial do GitHub Actions com exemplos detalhados</li>

<li><a href="https://docs.gitlab.com/ee/ci/" target="_blank" rel="noopener noreferrer">GitLab CI/CD Documentation</a> — Guia completo de CI/CD com GitLab</li>

<li><a href="https://itrevolution.com/the-devops-handbook/" target="_blank" rel="noopener noreferrer">The DevOps Handbook</a> — Livro referência sobre práticas DevOps e pipelines</li>

<li><a href="https://12factor.net/" target="_blank" rel="noopener noreferrer">12 Factor App</a> — Metodologia essencial para aplicações cloud-native com boas práticas de configuração</li>

<li><a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/" target="_blank" rel="noopener noreferrer">Kubernetes Deployment Strategies</a> — Documentação sobre estratégias de deploy seguras</li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em DevOps & CI/CD

Dominando Horizontal Pod Autoscaler e Vertical Pod Autoscaler em Kubernetes em Projetos Reais
Dominando Horizontal Pod Autoscaler e Vertical Pod Autoscaler em Kubernetes em Projetos Reais

Entendendo Autoscaling em Kubernetes Autoscaling é um dos pilares da infraest...

Guia Completo de GitLab CI/CD: Pipelines, Runners e Integração com Kubernetes
Guia Completo de GitLab CI/CD: Pipelines, Runners e Integração com Kubernetes

Introdução ao GitLab CI/CD: Automação de Ponta a Ponta GitLab CI/CD é um sist...

ConfigMaps e Secrets em Kubernetes: Gerenciando Configuração na Prática
ConfigMaps e Secrets em Kubernetes: Gerenciando Configuração na Prática

ConfigMaps e Secrets em Kubernetes: Gerenciando Configuração Quando você trab...