Projeto: pipeline CI/CD completo
LidoNesta aula, você vai construir um pipeline CI/CD completo, desde o commit até o deploy automatizado, cobrindo testes, build de imagem e deploy. Vamos explorar cada etapa com exemplos práticos em bash e boas práticas para garantir entregas rápidas e seguras.
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 autoNesse 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-recreateNesse 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
- Documentação oficial do GitHub Actions
- Documentação do Docker Build
- Documentação do pytest
- Documentação do GitLab CI
- Documentação do Jenkins
- Documentação do AWS CodeDeploy
Exercícios
- Exercício 1: Explique a diferença entre integração contínua e entrega contínua. Dê um exemplo de cada.
- 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.
- Exercício 3: Quais são as vantagens de usar uma imagem Docker para deploy? Cite pelo menos três.
- Exercício 4: Explique como você faria um deploy zero-downtime usando blue-green deployment. Descreva os passos.
- 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.
# 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
fissh 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"