Automatizar o deploy de aplicações é uma das tarefas mais importantes para um desenvolvedor ou administrador de sistemas. Um script de deploy bem estruturado reduz erros humanos, garante consistência e economiza tempo. Nesta aula, vamos construir um script de deploy simples, mas completo, em Bash, abordando as etapas fundamentais, verificações de pré-requisitos, mecanismos de rollback e logging adequado.

O script que criaremos simulará o deploy de uma aplicação web para um servidor de produção, mas os conceitos podem ser adaptados para qualquer tipo de aplicação. Vamos focar em boas práticas, como uso de funções, tratamento de erros e modularização, para que o script seja fácil de manter e estender.

Etapas

Um script de deploy típico segue uma sequência lógica de etapas. Vamos dividir o processo em funções para melhor organização e reutilização. As etapas principais são:

  • Preparação: definir variáveis, configurar o ambiente e garantir que o script seja executado com as permissões adequadas.
  • Backup: criar uma cópia da versão atual da aplicação, para permitir rollback.
  • Atualização: copiar os novos arquivos para o diretório de produção.
  • Pós-deploy: executar comandos como migrações de banco, limpeza de cache, etc.
  • Verificação: testar se a aplicação está funcionando corretamente.

Abaixo, um esqueleto do script com as funções para cada etapa:

#!/bin/bash

# Script de deploy simples

set -euo pipefail

# === Variáveis de configuração ===
APP_DIR="/var/www/minha_app"
BACKUP_DIR="/backups/minha_app"
RELEASE_DIR="/tmp/minha_app_release"

# === Funções ===

preparar() {
    echo "[PREPARAÇÃO] Iniciando deploy..."
    # Verificar se o diretório de release existe
    if [ ! -d "$RELEASE_DIR" ]; then
        echo "Erro: Diretório de release $RELEASE_DIR não encontrado." >&2
        exit 1
    fi
    # Verificar se o diretório da aplicação existe
    if [ ! -d "$APP_DIR" ]; then
        echo "Erro: Diretório da aplicação $APP_DIR não encontrado." >&2
        exit 1
    fi
}

backup() {
    echo "[BACKUP] Criando backup da versão atual..."
    local timestamp=$(date +%Y%m%d_%H%M%S)
    local backup_file="$BACKUP_DIR/app_$timestamp.tar.gz"
    mkdir -p "$BACKUP_DIR"
    tar -czf "$backup_file" -C "$APP_DIR" .
    echo "Backup criado em $backup_file"
}

atualizar() {
    echo "[ATUALIZAÇÃO] Copiando novos arquivos..."
    cp -r "$RELEASE_DIR"/* "$APP_DIR"/
}

pos_deploy() {
    echo "[PÓS-DEPLOY] Executando comandos pós-deploy..."
    # Exemplo: migração de banco
    # (cd "$APP_DIR" && php artisan migrate --force)
    # Limpeza de cache
    # (cd "$APP_DIR" && php artisan cache:clear)
    echo "Pós-deploy concluído."
}

verificar() {
    echo "[VERIFICAÇÃO] Testando aplicação..."
    # Exemplo: verificar se o site responde
    if curl -f http://localhost/ > /dev/null; then
        echo "Aplicação está no ar."
    else
        echo "Erro: Aplicação não responde." >&2
        exit 1
    fi
}

# === Execução principal ===
preparar
backup
atualizar
pos_deploy
verificar

echo "Deploy concluído com sucesso!"

Note o uso de set -euo pipefail para interromper o script em caso de erro, evitar variáveis não definidas e capturar falhas em pipes. Isso torna o script mais seguro.

Verificações

Verificações são essenciais para evitar deploys quebrados. Vamos adicionar mais verificações além das básicas, como checagem de espaço em disco, permissões de escrita e integridade dos arquivos. Também podemos verificar se o processo do servidor web está rodando.

Implementaremos uma função verificacoes_pre_deploy que realiza várias checagens e aborta o deploy se algo estiver errado. Exemplos:

  • Verificar se o usuário atual tem permissões de escrita no diretório da aplicação.
  • Verificar espaço em disco suficiente no diretório de destino.
  • Verificar se os arquivos de release estão presentes e não vazios.
  • Verificar se o serviço web (ex.: nginx) está ativo.
verificacoes_pre_deploy() {
    echo "[VERIFICAÇÕES] Executando verificações..."
    
    # 1. Verificar permissões de escrita
    if [ ! -w "$APP_DIR" ]; then
        echo "Erro: Sem permissão de escrita em $APP_DIR" >&2
        exit 1
    fi
    
    # 2. Verificar espaço em disco (pelo menos 1GB livre)
    local espaco=$(df --output=avail "$APP_DIR" | tail -1 | tr -d ' ')
    if [ "$espaco" -lt 1048576 ]; then
        echo "Erro: Espaço em disco insuficiente em $APP_DIR" >&2
        exit 1
    fi
    
    # 3. Verificar se o diretório de release não está vazio
    if [ -z "$(ls -A "$RELEASE_DIR")" ]; then
        echo "Erro: Diretório de release $RELEASE_DIR está vazio." >&2
        exit 1
    fi
    
    # 4. Verificar se o nginx está ativo (exemplo)
    if ! systemctl is-active --quiet nginx; then
        echo "Erro: Serviço nginx não está ativo." >&2
        exit 1
    fi
    
    echo "Verificações concluídas com sucesso."
}

Inclua essa função no início do fluxo, antes do backup. É importante que as verificações sejam robustas e forneçam mensagens claras de erro.

Rollback básico

Rollback é a capacidade de reverter o deploy para a versão anterior em caso de falha. Em nosso script, faremos um rollback básico, restaurando o backup criado anteriormente. Vamos criar a função rollback que restaura o backup mais recente.

Vamos modificar o script para detectar falhas durante a atualização ou pós-deploy e, nesses casos, executar o rollback automaticamente. Para isso, usaremos uma armadilha (trap) para capturar erros.

# Variável para armazenar o backup criado
BACKUP_FILE=""

backup() {
    ...
    BACKUP_FILE="$backup_file"
}

rollback() {
    echo "[ROLLBACK] Restaurando backup $BACKUP_FILE ..."
    if [ -n "$BACKUP_FILE" ] && [ -f "$BACKUP_FILE" ]; then
        rm -rf "$APP_DIR"/*
        tar -xzf "$BACKUP_FILE" -C "$APP_DIR"
        echo "Rollback concluído."
    else
        echo "Erro: Nenhum backup disponível para rollback." >&2
        exit 1
    fi
}

# Função de erro global
on_error() {
    echo "Erro no comando '$BASH_COMMAND' na linha $LINENO." >&2
    rollback
    exit 1
}

trap on_error ERR

Com o trap, qualquer erro não tratado no script (desde que não esteja em um bloco if que já trate) disparará a função on_error, que fará o rollback e sairá. Isso garante que, se algo der errado durante a atualização ou pós-deploy, a aplicação volte ao estado anterior.

Também podemos permitir rollback manual, passando uma opção ao script, como --rollback.

Logging

Logging é fundamental para auditar deploys e diagnosticar problemas. Vamos adicionar logs com timestamps e níveis (INFO, ERRO, etc.), gravando em um arquivo de log e também exibindo no console. Podemos usar uma função log que centraliza a formatação.

Criaremos um diretório de logs e um arquivo com data. Exemplo:

#!/bin/bash

LOG_DIR="/var/log/minha_app"
LOG_FILE="$LOG_DIR/deploy_$(date +%Y%m%d_%H%M%S).log"

log() {
    local nivel="$1"
    shift
    local mensagem="$*"
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$nivel] $mensagem" | tee -a "$LOG_FILE"
}

# Exemplo de uso
log "INFO" "Iniciando deploy..."
log "ERRO" "Falha na verificação de espaço."

Vamos integrar os logs ao script, substituindo os echo por chamadas a log. Também registraremos o início e fim do deploy, e quaisquer erros. Garanta que o diretório de logs exista e tenha permissão de escrita.

preparar() {
    log "INFO" "Iniciando deploy..."
    mkdir -p "$LOG_DIR"
    ...
}

backup() {
    log "INFO" "Criando backup..."
    ...
    log "INFO" "Backup criado em $backup_file"
}

atualizar() {
    log "INFO" "Copiando novos arquivos..."
    ...
}

# Em on_error, registrar o erro
on_error() {
    log "ERRO" "Erro no comando '$BASH_COMMAND' na linha $LINENO."
    rollback
    exit 1
}

Dessa forma, cada deploy fica registrado com todos os passos, facilitando o monitoramento e a resolução de problemas.

Boas práticas e observações finais

Ao criar scripts de deploy, considere as seguintes boas práticas:

  • Use set -euo pipefail para evitar falhas silenciosas.
  • Centralize as configurações em variáveis no topo do script.
  • Use funções para organizar as etapas e facilitar a manutenção.
  • Implemente verificações robustas antes de qualquer alteração no ambiente.
  • Sempre tenha um mecanismo de rollback, mesmo que simples.
  • Registre logs detalhados para auditoria.
  • Teste o script em um ambiente de staging antes de usar em produção.
  • Considere usar ferramentas como rsync para transferências mais eficientes, ou gerenciadores de deploy como Ansible, mas um script simples pode ser suficiente para projetos menores.

Com essas práticas, seu script será confiável e fácil de depurar.

Referências

Exercícios

  1. Escreva um script simples que faça backup de um diretório usando tar e registre a operação em um arquivo de log com data.

    ✓ Resposta:
    #!/bin/bash
    
    set -euo pipefail
    
    DIR="/home/user/dados"
    LOG_FILE="/var/log/backup.log"
    
    log() {
        echo "$(date '+%Y-%m-%d %H:%M:%S') - $*" | tee -a "$LOG_FILE"
    }
    
    if [ ! -d "$DIR" ]; then
        log "ERRO: Diretório $DIR não existe"
        exit 1
    fi
    
    TIMESTAMP=$(date +%Y%m%d_%H%M%S)
    BACKUP_FILE="/backups/dados_$TIMESTAMP.tar.gz"
    
    tar -czf "$BACKUP_FILE" -C "$(dirname "$DIR")" "$(basename "$DIR")"
    log "Backup criado em $BACKUP_FILE"
    
  2. Adicione uma função de rollback ao script do exercício 1, que restaure o backup mais recente.

    ✓ Resposta:
    #!/bin/bash
    
    set -euo pipefail
    
    DIR="/home/user/dados"
    BACKUP_DIR="/backups"
    LOG_FILE="/var/log/backup.log"
    
    log() {
        echo "$(date '+%Y-%m-%d %H:%M:%S') - $*" | tee -a "$LOG_FILE"
    }
    
    backup() {
        if [ ! -d "$DIR" ]; then
            log "ERRO: Diretório $DIR não existe"
            exit 1
        fi
        TIMESTAMP=$(date +%Y%m%d_%H%M%S)
        BACKUP_FILE="$BACKUP_DIR/dados_$TIMESTAMP.tar.gz"
        mkdir -p "$BACKUP_DIR"
        tar -czf "$BACKUP_FILE" -C "$(dirname "$DIR")" "$(basename "$DIR")"
        log "Backup criado em $BACKUP_FILE"
    }
    
    rollback() {
        # Encontra o backup mais recente
        LATEST=$(ls -t "$BACKUP_DIR"/dados_*.tar.gz 2>/dev/null | head -1)
        if [ -z "$LATEST" ]; then
            log "ERRO: Nenhum backup encontrado"
            exit 1
        fi
        log "Restaurando backup $LATEST"
        rm -rf "$DIR"/*
        tar -xzf "$LATEST" -C "$(dirname "$DIR")"
        log "Restauração concluída"
    }
    
    # Uso: ./script.sh backup|rollback
    case "${1:-backup}" in
        backup) backup ;;
        rollback) rollback ;;
        *) echo "Uso: $0 {backup|rollback}" >&2; exit 1 ;;
    esac
    
  3. Crie uma função de verificação que confirme se o diretório de destino tem permissão de escrita e pelo menos 100MB livres.

    ✓ Resposta:
    verificar() {
        local dir="$1"
        if [ ! -w "$dir" ]; then
            echo "Erro: Sem permissão de escrita em $dir" >&2
            return 1
        fi
        local espaco=$(df --output=avail "$dir" | tail -1 | tr -d ' ')
        if [ "$espaco" -lt 102400 ]; then
            echo "Erro: Espaço insuficiente em $dir" >&2
            return 1
        fi
        echo "Verificações OK"
    }
    
  4. Implemente um logging que grave mensagens com níveis (INFO, ERRO) em um arquivo, usando a função log.

    ✓ Resposta:
    LOG_FILE="/var/log/meu_script.log"
    
    log() {
        local nivel="$1"
        shift
        local mensagem="$*"
        echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$nivel] $mensagem" | tee -a "$LOG_FILE"
    }
    
    # Exemplos de uso
    log "INFO" "Iniciando processo"
    log "ERRO" "Falha ao conectar"
    
  5. Escreva um script que faça o deploy de um site estático, copiando arquivos de um diretório de release para o diretório web, com backup e rollback automático em caso de erro.

    ✓ Resposta:
    #!/bin/bash
    
    set -euo pipefail
    
    RELEASE_DIR="/tmp/site_release"
    WEB_DIR="/var/www/html"
    BACKUP_DIR="/backups/site"
    LOG_FILE="/var/log/deploy.log"
    
    log() {
        local nivel="$1"; shift
        echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$nivel] $*" | tee -a "$LOG_FILE"
    }
    
    verificacoes() {
        [ -d "$RELEASE_DIR" ] || { log "ERRO" "Release dir não existe"; exit 1; }
        [ -w "$WEB_DIR" ] || { log "ERRO" "Sem permissão em $WEB_DIR"; exit 1; }
    }
    
    backup() {
        mkdir -p "$BACKUP_DIR"
        local ts=$(date +%Y%m%d_%H%M%S)
        BACKUP_FILE="$BACKUP_DIR/site_$ts.tar.gz"
        tar -czf "$BACKUP_FILE" -C "$WEB_DIR" .
        log "INFO" "Backup criado: $BACKUP_FILE"
    }
    
    rollback() {
        log "ERRO" "Executando rollback"
        if [ -n "${BACKUP_FILE:-}" ] && [ -f "$BACKUP_FILE" ]; then
            rm -rf "$WEB_DIR"/*
            tar -xzf "$BACKUP_FILE" -C "$WEB_DIR"
            log "INFO" "Rollback concluído"
        else
            log "ERRO" "Nenhum backup disponível"
        fi
        exit 1
    }
    
    trap 'rollback' ERR
    
    verificacoes
    backup
    cp -r "$RELEASE_DIR"/* "$WEB_DIR"/
    log "INFO" "Deploy realizado com sucesso"