Bem-vindo à aula 94 do nosso curso de DevOps! Hoje vamos colocar em prática tudo o que aprendemos até aqui, construindo um pipeline de CI/CD completo e funcional. Você vai entender como automatizar o fluxo desde o momento em que um desenvolvedor faz um commit até a publicação da aplicação em produção, com testes, build de imagem e deploy automatizado.

Um pipeline CI/CD é a espinha dorsal de uma operação DevOps eficiente. Ele garante que cada mudança no código seja integrada, testada e entregue de forma rápida e confiável. Ao final desta aula, você terá um pipeline modelo que pode ser adaptado aos seus projetos, usando ferramentas como GitHub Actions, GitLab CI ou Jenkins, mas com foco em conceitos universais.

Do commit ao deploy

O pipeline começa no momento em que um desenvolvedor envia um commit para o repositório. A partir daí, uma série de etapas automatizadas são acionadas. A primeira delas é a integração contínua (CI), que envolve a compilação do código, a execução de testes e a análise estática. Depois, a entrega contínua (CD) cuida do empacotamento e da preparação do artefato para deploy, e finalmente o deploy contínuo automatiza a publicação em produção.

Vamos ver um exemplo de configuração de pipeline usando GitHub Actions, que é uma ferramenta popular e integrada ao GitHub. O arquivo de workflow define os eventos que disparam o pipeline, os jobs e os passos de cada job. Aqui está um exemplo básico de um workflow que roda testes e faz o build:

name: CI/CD Pipeline

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up Python
        uses: actions/setup-python@v2
        with:
          python-version: '3.9'
      - name: Install dependencies
        run: |
          pip install -r requirements.txt
      - name: Run tests
        run: |
          pytest

  build-and-deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Build Docker image
        run: |
          docker build -t myapp:${{ github.sha }} .
      - name: Push image to registry
        run: |
          docker tag myapp:${{ github.sha }} myregistry/myapp:latest
          docker push myregistry/myapp:latest
      - name: Deploy to production
        run: |
          ssh user@server "docker pull myregistry/myapp:latest && docker stop myapp || true && docker run -d --rm -p 80:80 myregistry/myapp:latest"

Esse pipeline é acionado em push para a branch main e em pull requests. O job de teste roda primeiro, e o job de build e deploy só é executado se os testes passarem. Isso garante que apenas código testado vá para produção.

Testes

Os testes são uma parte crítica do pipeline. Eles garantem que o código não quebrou funcionalidades existentes e que atende aos requisitos. Existem vários tipos de testes: unitários, de integração, de contrato, de ponta a ponta, etc. Em um pipeline, é comum rodar testes unitários e de integração em estágios iniciais, e testes de aceitação ou E2E antes do deploy.

No exemplo anterior, usamos pytest para testes unitários. Mas você pode usar qualquer framework de teste. O importante é que os testes sejam rápidos e confiáveis. Se os testes forem lentos, o pipeline fica lento, e isso prejudica a produtividade do time. Por isso, é comum paralelizar os testes em múltiplos jobs ou usar cache de dependências.

Vamos ver um exemplo de como configurar testes paralelos em GitHub Actions:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: [3.8, 3.9]
    steps:
      - uses: actions/checkout@v2
      - name: Set up Python
        uses: actions/setup-python@v2
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install dependencies
        run: |
          pip install -r requirements.txt
      - name: Run tests
        run: |
          pytest -n auto

Nesse exemplo, testamos em múltiplas versões do Python em paralelo, e usamos o plugin pytest-xdist para distribuir os testes entre os núcleos da CPU. Isso acelera a execução.

Build de imagem

Depois que os testes passam, o próximo passo é construir a imagem Docker da aplicação. Uma imagem Docker é um pacote executável que contém o código, as dependências, e o ambiente de execução. Construir a imagem é importante porque ela será usada para rodar a aplicação em qualquer ambiente, garantindo consistência.

No pipeline, o build da imagem deve ser otimizado. Use camadas e cache para evitar rebuilds desnecessários. Por exemplo, copie os arquivos de dependências primeiro e instale-as antes de copiar o código-fonte. Isso aproveita o cache do Docker. Veja um exemplo de Dockerfile eficiente:

FROM python:3.9-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["python", "app.py"]

No pipeline, o build da imagem pode ser feito com o comando docker build e depois enviado para um registro de contêineres, como Docker Hub ou Amazon ECR. É importante taggear a imagem com o SHA do commit para rastreabilidade. No exemplo do pipeline, usamos ${{ github.sha }} para isso.

Deploy automatizado

O deploy automatizado é a etapa final do pipeline. Existem várias estratégias: deploy contínuo, que publica automaticamente após os testes; ou deploy manual, que requer aprovação humana. Para ambientes de produção, é comum usar aprovação manual, mas em ambientes de desenvolvimento ou staging, o deploy automático é preferível.

O deploy pode ser feito de várias maneiras: via SSH, usando Kubernetes, com ferramentas de IaC como Terraform, ou até mesmo com serviços gerenciados como AWS CodeDeploy. No exemplo, usamos SSH para conectar ao servidor e atualizar o contêiner. Mas é importante considerar estratégias mais robustas, como blue-green deployment ou canary release, para minimizar riscos.

Vamos ver um exemplo de deploy usando Docker Compose em um servidor remoto:

deploy:
  needs: build-and-deploy
  runs-on: ubuntu-latest
  steps:
    - name: Deploy to production
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.HOST }}
        username: ${{ secrets.USERNAME }}
        key: ${{ secrets.KEY }}
        script: |
          docker pull myregistry/myapp:latest
          docker-compose up -d --force-recreate

Nesse caso, usamos uma ação SSH para executar comandos no servidor. O docker-compose garante que a aplicação seja recriada com a nova imagem. Lembre-se de usar secrets para armazenar credenciais.

Boas práticas e observações finais

Ao construir pipelines CI/CD, algumas boas práticas são essenciais: manter o pipeline rápido, usar cache de dependências, ter testes confiáveis, versionar as configurações do pipeline, e sempre ter logs claros para depuração. Além disso, é fundamental ter um bom monitoramento do pipeline e da aplicação em produção.

Outro ponto importante é a segurança: nunca exponha credenciais no código, use secrets do ambiente de CI, e faça varreduras de vulnerabilidades nas imagens. Finalmente, documente o pipeline para que toda a equipe entenda o processo.

Referências

Exercícios

  1. Exercício 1: Explique a diferença entre integração contínua e entrega contínua. Dê um exemplo de cada.
  2. ✓ Resposta: Integração contínua (CI) é a prática de automatizar a integração de código frequentemente, com builds e testes automáticos. Exemplo: rodar testes unitários a cada push. Entrega contínua (CD) é a prática de garantir que o software possa ser liberado para produção a qualquer momento, com deploy automatizado em ambientes de staging. Exemplo: após testes, gerar um artefato pronto para deploy.
  3. Exercício 2: Escreva um trecho de pipeline (em bash) que rode testes unitários, construa uma imagem Docker e faça o push para um registro.
  4. ✓ Resposta:
    # Exemplo de script bash para CI
    pytest
    if [ $? -eq 0 ]; then
      docker build -t myapp:$CI_COMMIT_SHA .
      docker push myregistry/myapp:$CI_COMMIT_SHA
    else
      echo "Testes falharam"
      exit 1
    fi
  5. Exercício 3: Quais são as vantagens de usar uma imagem Docker para deploy? Cite pelo menos três.
  6. ✓ Resposta: 1) Consistência: a aplicação roda igual em qualquer ambiente. 2) Isolamento: dependências são empacotadas, evitando conflitos. 3) Escalabilidade: fácil de replicar e orquestrar com Kubernetes.
  7. Exercício 4: Explique como você faria um deploy zero-downtime usando blue-green deployment. Descreva os passos.
  8. ✓ Resposta: No blue-green deployment, temos dois ambientes idênticos: blue (atual) e green (novo). Passos: 1) Implante a nova versão no ambiente green. 2) Rode testes no green. 3) Mude o balanceador de carga para apontar para o green. 4) Se tudo der certo, desative o blue ou mantenha como standby. Isso permite rollback rápido.
  9. Exercício 5: Crie um comando bash para fazer deploy de uma aplicação em um servidor remoto via SSH, puxando a última imagem e recriando o contêiner.
  10. ✓ Resposta:
    ssh user@server "docker pull myregistry/myapp:latest && docker stop myapp || true && docker rm myapp || true && docker run -d --name myapp -p 80:80 myregistry/myapp:latest"