Scripts de automação de deploy simples

1. Introdução à automação de deploy com Bash

1.1 O que é deploy automatizado e por que usar Bash?

Deploy automatizado é o processo de implantação de uma aplicação em ambiente de produção ou homologação sem intervenção manual repetitiva. O Bash é a escolha natural para essa tarefa por estar presente em praticamente todos os servidores Linux, ser maduro, confiável e permitir controle granular sobre cada etapa do processo.

Enquanto ferramentas como Ansible, Docker e Kubernetes oferecem abstrações poderosas, scripts Bash continuam sendo a base para tarefas rápidas, deploys simples e como camada de fallback em pipelines mais complexos.

1.2 Cenários comuns

Os scripts de deploy em Bash são amplamente utilizados para:
- Aplicações web estáticas (HTML, CSS, JavaScript)
- APIs REST em Node.js, PHP ou Python
- Microsserviços leves com poucas dependências
- Atualização de arquivos de configuração em servidores

1.3 Pré-requisitos

Antes de começar, certifique-se de ter:
- Acesso SSH ao servidor de destino
- rsync instalado (para transferência eficiente)
- git configurado com chaves SSH
- Variáveis de ambiente definidas (ou arquivo .env)

2. Estrutura básica de um script de deploy

2.1 Shebang e cabeçalho

#!/usr/bin/env bash
# deploy.sh - Script de deploy automatizado
# Autor: Seu Nome
# Data: 2025-03-01
# Versão: 1.0.0

set -euo pipefail

2.2 Variáveis de configuração

# Configurações do projeto
PROJECT_NAME="minha-app"
REMOTE_USER="deploy"
REMOTE_HOST="192.168.1.100"
REMOTE_PATH="/var/www/$PROJECT_NAME"
LOCAL_PATH="/home/user/projetos/$PROJECT_NAME"
BRANCH_DEFAULT="main"
RELEASES_DIR="$REMOTE_PATH/releases"
CURRENT_LINK="$REMOTE_PATH/current"
PREVIOUS_LINK="$REMOTE_PATH/previous"
KEEP_RELEASES=5

2.3 Funções principais

deploy() {
    echo "[INFO] Iniciando deploy de $PROJECT_NAME..."
    # Lógica principal será detalhada nas próximas seções
}

rollback() {
    echo "[INFO] Executando rollback..."
    # Reverte para versão anterior
}

check_status() {
    echo "[INFO] Verificando status do deploy..."
    # Valida se o deploy foi bem-sucedido
}

# Tratamento de erros
trap 'echo "[ERRO] Falha no deploy. Executando rollback..."; rollback; exit 1' ERR

3. Deploy via Git (clone e pull)

3.1 Clonagem em diretório temporário

deploy_via_git() {
    local release_dir="$RELEASES_DIR/$(date +%Y%m%d_%H%M%S)"
    local branch="${1:-$BRANCH_DEFAULT}"

    echo "[INFO] Clonando branch $branch..."
    git clone --depth 1 --branch "$branch" \
        "git@github.com:usuario/$PROJECT_NAME.git" "$release_dir"

    echo "[INFO] Release criada em: $release_dir"
}

3.2 Atualização incremental

deploy_via_pull() {
    local current_release=$(readlink -f "$CURRENT_LINK" 2>/dev/null || echo "")

    if [ -n "$current_release" ] && [ -d "$current_release/.git" ]; then
        echo "[INFO] Atualizando release existente..."
        cd "$current_release"
        git fetch origin
        git checkout "$BRANCH_DEFAULT"
        git pull origin "$BRANCH_DEFAULT"
    else
        deploy_via_git "$BRANCH_DEFAULT"
    fi
}

3.3 Validação de integridade

validate_release() {
    local release_dir="$1"

    # Verificar se arquivos essenciais existem
    if [ ! -f "$release_dir/package.json" ] && [ ! -f "$release_dir/index.php" ]; then
        echo "[ERRO] Arquivos essenciais não encontrados!"
        return 1
    fi

    # Verificar hash do commit
    local expected_hash=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
    echo "[INFO] Hash do deploy: $expected_hash"
}

4. Deploy com rsync e sincronização remota

4.1 Sincronização básica

sync_with_rsync() {
    local source_dir="$1"
    local target_dir="$2"

    rsync -avz --progress \
        -e "ssh -p 22" \
        --exclude=".git" \
        --exclude="node_modules" \
        --exclude=".env" \
        --exclude="cache" \
        --exclude="*.log" \
        "$source_dir/" \
        "$REMOTE_USER@$REMOTE_HOST:$target_dir/"
}

4.2 Exclusão de diretórios desnecessários

EXCLUDE_LIST=(
    ".git"
    "node_modules"
    "vendor"
    ".cache"
    ".npm"
    "tests"
    "*.md"
    "docker-compose.yml"
    ".gitignore"
)

build_exclude_args() {
    local args=""
    for pattern in "${EXCLUDE_LIST[@]}"; do
        args+=" --exclude=$pattern"
    done
    echo "$args"
}

sync_precise() {
    local exclude_args=$(build_exclude_args)
    rsync -avz --delete --checksum \
        $exclude_args \
        -e "ssh -i ~/.ssh/deploy_key" \
        "$LOCAL_PATH/" \
        "$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/current/"
}

4.3 Uso de --delete e --checksum

sync_with_checksum() {
    echo "[INFO] Sincronizando com verificação de checksum..."
    rsync -avz --delete --checksum \
        --progress \
        --partial \
        --timeout=30 \
        "$LOCAL_PATH/" \
        "$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/"

    echo "[INFO] Sincronização concluída."
}

5. Gerenciamento de versões e rollback

create_symlinks() {
    local release_dir="$1"

    # Atualizar link 'previous' com o atual
    if [ -L "$CURRENT_LINK" ]; then
        ln -sfn "$(readlink -f "$CURRENT_LINK")" "$PREVIOUS_LINK"
    fi

    # Criar novo link 'current'
    ln -sfn "$release_dir" "$CURRENT_LINK"
    echo "[INFO] Link 'current' aponta para: $release_dir"
}

5.2 Rollback automático

rollback() {
    echo "[WARN] Executando rollback..."

    if [ -L "$PREVIOUS_LINK" ]; then
        local previous_release=$(readlink -f "$PREVIOUS_LINK")
        ln -sfn "$previous_release" "$CURRENT_LINK"
        echo "[INFO] Rollback realizado para: $previous_release"
    else
        echo "[ERRO] Nenhuma versão anterior disponível para rollback!"
        exit 1
    fi

    # Reiniciar serviço após rollback
    restart_service
}

5.3 Manutenção de histórico

cleanup_old_releases() {
    echo "[INFO] Limpando releases antigos..."

    local releases=($(ls -1t "$RELEASES_DIR" 2>/dev/null))
    local count=${#releases[@]}

    if [ "$count" -gt "$KEEP_RELEASES" ]; then
        local to_remove=$((count - KEEP_RELEASES))
        for ((i=KEEP_RELEASES; i<count; i++)); do
            rm -rf "${RELEASES_DIR}/${releases[$i]}"
            echo "[INFO] Removido release antigo: ${releases[$i]}"
        done
    fi
}

6. Hooks e pós-deploy

6.1 Comandos pós-sincronização

post_deploy_tasks() {
    local release_dir="$1"

    echo "[INFO] Executando tarefas pós-deploy..."

    ssh "$REMOTE_USER@$REMOTE_HOST" << EOF
        cd "$release_dir"

        # Instalar dependências
        if [ -f "package.json" ]; then
            npm install --production
        fi

        if [ -f "composer.json" ]; then
            composer install --no-dev
        fi

        # Executar migrações
        if [ -f "artisan" ]; then
            php artisan migrate --force
        fi

        # Build da aplicação
        if [ -f "package.json" ]; then
            npm run build
        fi
EOF
}

6.2 Notificações

send_notification() {
    local status="$1"
    local message="$2"

    # Notificação via Slack
    curl -s -X POST -H 'Content-type: application/json' \
        --data "{\"text\":\"[$status] Deploy $PROJECT_NAME: $message\"}" \
        "https://hooks.slack.com/services/SEU/WEBHOOK/AQUI"

    # Notificação por email (opcional)
    if command -v mail &> /dev/null; then
        echo "Deploy $PROJECT_NAME: $status - $message" | \
            mail -s "[Deploy] $PROJECT_NAME - $status" admin@exemplo.com
    fi
}

6.3 Limpeza de diretórios temporários

cleanup() {
    echo "[INFO] Limpando diretórios temporários..."

    # Remover diretórios temporários locais
    rm -rf /tmp/deploy_*

    # Limpar logs antigos no servidor
    ssh "$REMOTE_USER@$REMOTE_HOST" "find $REMOTE_PATH/logs -name '*.log' -mtime +7 -delete"

    cleanup_old_releases
}

7. Exemplo prático completo

7.1 Script completo para deploy de aplicação Node.js

#!/usr/bin/env bash
set -euo pipefail

# Configurações
PROJECT_NAME="api-node"
REMOTE_USER="deploy"
REMOTE_HOST="192.168.1.100"
REMOTE_PATH="/var/www/$PROJECT_NAME"
BRANCH="main"
RELEASES_DIR="$REMOTE_PATH/releases"
CURRENT_LINK="$REMOTE_PATH/current"
PREVIOUS_LINK="$REMOTE_PATH/previous"

# Funções
deploy() {
    local timestamp=$(date +%Y%m%d_%H%M%S)
    local release_dir="$RELEASES_DIR/$timestamp"

    echo "=== Iniciando deploy de $PROJECT_NAME ==="

    # Etapa 1: Clonar repositório
    echo "[1/5] Clonando repositório..."
    git clone --depth 1 --branch "$BRANCH" \
        "git@github.com:usuario/$PROJECT_NAME.git" "$release_dir"

    # Etapa 2: Instalar dependências
    echo "[2/5] Instalando dependências..."
    cd "$release_dir"
    npm install --production

    # Etapa 3: Build
    echo "[3/5] Executando build..."
    npm run build

    # Etapa 4: Sincronizar com servidor
    echo "[4/5] Sincronizando com servidor remoto..."
    rsync -avz --delete \
        --exclude=".git" --exclude="node_modules" --exclude=".env" \
        "$release_dir/" \
        "$REMOTE_USER@$REMOTE_HOST:$release_dir/"

    # Etapa 5: Atualizar links e reiniciar serviço
    echo "[5/5] Atualizando links e reiniciando..."
    ssh "$REMOTE_USER@$REMOTE_HOST" "
        ln -sfn $release_dir $CURRENT_LINK
        pm2 restart $PROJECT_NAME
    "

    echo "=== Deploy concluído com sucesso! ==="
}

rollback() {
    echo "=== Executando rollback ==="
    ssh "$REMOTE_USER@$REMOTE_HOST" "
        if [ -L $PREVIOUS_LINK ]; then
            ln -sfn \$(readlink -f $PREVIOUS_LINK) $CURRENT_LINK
            pm2 restart $PROJECT_NAME
            echo 'Rollback realizado com sucesso'
        else
            echo 'Nenhuma versão anterior disponível'
            exit 1
        fi
    "
}

# Execução
case "${1:-deploy}" in
    deploy)
        deploy
        ;;
    rollback)
        rollback
        ;;
    *)
        echo "Uso: $0 {deploy|rollback}"
        exit 1
        ;;
esac

7.2 Execução com argumentos

# Executar deploy
./deploy.sh

# Executar rollback
./deploy.sh rollback

# Exemplo com variáveis de ambiente
ENV=production BRANCH=main ./deploy.sh

7.3 Saída esperada

=== Iniciando deploy de api-node ===
[1/5] Clonando repositório...
Cloning into '/var/www/api-node/releases/20250301_143022'...
[2/5] Instalando dependências...
added 1250 packages in 15s
[3/5] Executando build...
Build completed in 8s
[4/5] Sincronizando com servidor remoto...
sent 2456789 bytes  received 1234 bytes  12345.67 bytes/sec
[5/5] Atualizando links e reiniciando...
pm2 restarted api-node
=== Deploy concluído com sucesso! ===

8. Boas práticas e considerações finais

8.1 Versionamento do script de deploy

Mantenha o script de deploy no mesmo repositório da aplicação ou em um repositório separado de infraestrutura. Isso garante rastreabilidade e facilita a colaboração da equipe.

# Exemplo de versionamento
git tag v1.0.0-deploy-script
git push origin v1.0.0-deploy-script

8.2 Segurança

Nunca hardcode senhas ou chaves no script. Use:
- Variáveis de ambiente
- Arquivos .env protegidos
- Gerenciadores de secrets como HashiCorp Vault
- Chaves SSH com passphrase e ssh-agent

# Exemplo seguro
export DEPLOY_KEY=$(cat ~/.ssh/deploy_key)
ssh -i <(echo "$DEPLOY_KEY") user@host

8.3 Integração com CI/CD

Para ambientes mais complexos, integre seu script Bash com:
- GitHub Actions: Execute o script como step em workflows
- GitLab CI: Utilize o script em jobs do pipeline
- Jenkins: Chame o script via shell step

# Exemplo de integração com GitHub Actions
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Execute deploy script
        run: |
          chmod +x deploy.sh
          ./deploy.sh
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}

O Bash continua sendo uma ferramenta indispensável para automação de deploy, especialmente em cenários simples e controlados. Com as técnicas apresentadas neste artigo, você pode construir scripts robustos, seguros e de fácil manutenção para suas aplicações.

Referências