Container orchestration scripts: docker-compose em Bash

1. Introdução à Automação de Docker-Compose com Bash

Automatizar o gerenciamento de ambientes Docker Compose com scripts Bash é uma prática essencial para equipes que buscam eficiência e reprodutibilidade. O Bash oferece simplicidade, portabilidade e controle fino sobre o ciclo de vida dos containers, sendo ideal para cenários como ambientes de desenvolvimento, pipelines de CI/CD e deploys rápidos em servidores.

Os pré-requisitos para acompanhar este artigo incluem Docker e Docker Compose instalados, além de conhecimento básico de Bash (variáveis, funções, condicionais). Vamos explorar scripts práticos que resolvem problemas reais de orquestração.

2. Scripts para Inicialização e Parada Controlada

A função start_services() verifica a existência do arquivo docker-compose.yml e carrega variáveis de ambiente antes de iniciar os serviços:

#!/bin/bash

start_services() {
    local compose_file="${1:-docker-compose.yml}"
    local env_file="${2:-.env}"

    if [[ ! -f "$compose_file" ]]; then
        echo "Erro: $compose_file não encontrado."
        exit 1
    fi

    if [[ -f "$env_file" ]]; then
        set -a
        source "$env_file"
        set +a
    fi

    echo "Iniciando serviços com $compose_file..."
    docker-compose -f "$compose_file" up -d

    if [[ $? -eq 0 ]]; then
        echo "Serviços iniciados com sucesso."
    else
        echo "Falha ao iniciar serviços."
        exit 1
    fi
}

Para parada controlada, a função stop_services() inclui timeout e logs:

stop_services() {
    local compose_file="${1:-docker-compose.yml}"
    local timeout="${2:-30}"

    echo "Parando serviços (timeout: ${timeout}s)..."
    docker-compose -f "$compose_file" stop -t "$timeout"

    if docker-compose -f "$compose_file" ps -q | grep -q .; then
        echo "Containers ainda em execução. Forçando parada..."
        docker-compose -f "$compose_file" down
    else
        echo "Todos os serviços parados com sucesso."
    fi
}

O tratamento de erros inclui verificação de containers em execução antes de iniciar novos:

if docker-compose -f "$compose_file" ps -q 2>/dev/null | grep -q .; then
    echo "Aviso: Containers já em execução. Execute stop primeiro."
    exit 1
fi

3. Gerenciamento de Múltiplos Ambientes (dev, staging, prod)

Para alternar entre ambientes, usamos seleção dinâmica de arquivos de compose:

select_environment() {
    local env="${1:-dev}"
    local base_file="docker-compose.yml"
    local override_file="docker-compose.${env}.yml"

    if [[ ! -f "$override_file" ]]; then
        echo "Erro: Arquivo $override_file não encontrado."
        exit 1
    fi

    export COMPOSE_FILE="${base_file}:${override_file}"
    echo "Ambiente $env configurado. Arquivos: $COMPOSE_FILE"

    # Validação da configuração
    docker-compose config -q
    if [[ $? -eq 0 ]]; then
        echo "Configuração válida."
    else
        echo "Configuração inválida. Corrija os erros."
        exit 1
    fi
}

Exemplo de uso com variáveis de ambiente:

# Uso: ./manage.sh dev up
case "${1:-dev}" in
    dev|staging|prod)
        select_environment "$1"
        shift
        docker-compose "$@"
        ;;
    *)
        echo "Ambiente inválido. Use: dev, staging ou prod."
        exit 1
        ;;
esac

4. Atualização e Rollback de Serviços

A função update_service() realiza pull de imagens, recreate e health check:

update_service() {
    local service="$1"
    local compose_file="${2:-docker-compose.yml}"

    echo "Atualizando serviço $service..."
    docker-compose -f "$compose_file" pull "$service"

    if [[ $? -ne 0 ]]; then
        echo "Falha ao baixar imagem. Abortando."
        return 1
    fi

    docker-compose -f "$compose_file" up -d --no-deps "$service"

    # Health check com timeout
    local timeout=60
    local elapsed=0
    while [[ $elapsed -lt $timeout ]]; do
        if docker-compose -f "$compose_file" ps "$service" | grep -q "Up"; then
            echo "Serviço $service atualizado e saudável."
            return 0
        fi
        sleep 5
        elapsed=$((elapsed + 5))
    done

    echo "Timeout: serviço $service não ficou saudável."
    return 1
}

Para rollback, salvamos logs antes da restauração:

rollback_to_version() {
    local service="$1"
    local version="$2"
    local compose_file="${3:-docker-compose.yml}"

    # Salva logs atuais
    docker-compose -f "$compose_file" logs --tail=50 "$service" > "rollback_${service}_$(date +%Y%m%d_%H%M%S).log"

    # Altera a tag no arquivo compose (exemplo simples)
    sed -i "s|image: ${service}:.*|image: ${service}:${version}|" "$compose_file"

    docker-compose -f "$compose_file" up -d --no-deps "$service"

    echo "Rollback do serviço $service para versão $version concluído."
}

5. Logs e Diagnóstico Automatizado

Coleta centralizada de logs com filtragem por serviço:

collect_logs() {
    local service="$1"
    local lines="${2:-100}"
    local output_file="logs_${service}_$(date +%Y%m%d_%H%M%S).log"

    docker-compose logs --tail="$lines" "$service" > "$output_file"
    echo "Logs salvos em $output_file"
}

Geração de relatório de saúde dos serviços:

health_report() {
    echo "=== Relatório de Saúde dos Serviços ==="
    echo "Data: $(date)"
    echo ""

    docker-compose ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}"

    echo ""
    echo "Uso de recursos (últimos 30 segundos):"
    docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}"
}

6. Backup e Restauração de Volumes

Função para backup de volumes nomeados:

backup_volumes() {
    local backup_dir="${1:-./backups}"
    local timestamp=$(date +%Y%m%d_%H%M%S)

    mkdir -p "$backup_dir"

    docker volume ls --format "{{.Name}}" | while read volume; do
        echo "Backup do volume: $volume"
        docker run --rm -v "$volume:/source" -v "$backup_dir:/backup" alpine \
            tar czf "/backup/${volume}_${timestamp}.tar.gz" -C /source .
    done

    echo "Backups concluídos em $backup_dir"
}

Restauração com validação de integridade:

restore_volumes() {
    local backup_file="$1"
    local volume_name="$2"

    if [[ ! -f "$backup_file" ]]; then
        echo "Erro: Arquivo de backup não encontrado."
        exit 1
    fi

    # Valida integridade do arquivo
    tar tzf "$backup_file" > /dev/null 2>&1
    if [[ $? -ne 0 ]]; then
        echo "Erro: Arquivo de backup corrompido."
        exit 1
    fi

    docker volume create "$volume_name"
    docker run --rm -v "$volume_name:/target" -v "$(pwd):/backup" alpine \
        tar xzf "/backup/$backup_file" -C /target

    echo "Volume $volume_name restaurado de $backup_file"
}

Integração com cron para backups periódicos:

# No crontab: 0 2 * * * /path/to/backup_volumes.sh /backups

7. Deploy Contínuo com Git Hooks e CI/CD

Script de deploy automático via Git hook (post-receive):

#!/bin/bash
# .git/hooks/post-receive

while read oldrev newrev refname; do
    if [[ "$refname" == "refs/heads/main" ]]; then
        echo "Deploy automático iniciado para branch main..."

        # Verifica mudanças no docker-compose.yml
        if git diff --name-only "$oldrev" "$newrev" | grep -q "docker-compose.yml"; then
            echo "Arquivo docker-compose.yml modificado. Recriando serviços..."
            docker-compose up -d --force-recreate
        else
            echo "Apenas código alterado. Reconstruindo imagens..."
            docker-compose build
            docker-compose up -d
        fi

        # Health check
        sleep 10
        if docker-compose ps | grep -q "Exit"; then
            echo "Falha no deploy. Iniciando rollback..."
            git revert --no-edit "$newrev"
            docker-compose up -d
        else
            echo "Deploy bem-sucedido!"
        fi
    fi
done

Pipeline CI/CD simplificado:

# test.sh
docker-compose run --rm test

# build.sh
docker-compose build

# deploy.sh
docker-compose up -d

# rollback.sh (em caso de falha)
git revert HEAD --no-edit
docker-compose up -d --force-recreate

8. Boas Práticas e Extensões

Modularização com biblioteca de funções reutilizáveis:

# lib/docker-compose-lib.sh

check_docker() {
    if ! command -v docker &> /dev/null; then
        echo "Docker não encontrado."
        exit 1
    fi
}

check_compose() {
    if ! command -v docker-compose &> /dev/null; then
        echo "Docker Compose não encontrado."
        exit 1
    fi
}

cleanup() {
    echo "Limpando recursos..."
    docker-compose down -v
}

trap cleanup EXIT INT TERM

Dicas de segurança:

# Evitar hardcoded secrets: usar .env com permissões restritas
# Exemplo: chmod 600 .env

# Usar docker secrets em modo swarm
# docker secret create db_password /path/to/secret

# Validar variáveis de ambiente obrigatórias
required_vars=("DB_PASSWORD" "API_KEY")
for var in "${required_vars[@]}"; do
    if [[ -z "${!var}" ]]; then
        echo "Erro: Variável $var não definida."
        exit 1
    fi
done

Referências