kb.erickguedes.com
Bash e Shell Script: Automação Unix/Linux

Automação e Projetos Práticos

Aula 6 de 6

Script de Backup

Backup com rsync

cat > backup.sh << 'SCRIPT'
#!/bin/bash
set -euo pipefail

readonly BACKUP_SRC="${1:-/home/$USER}"
readonly BACKUP_DST="${2:-/mnt/backup}"
readonly BACKUP_NAME="backup-$(date +%Y%m%d-%H%M%S)"
readonly RETENTION_DAYS=30

# Verificar diretórios
for dir in "$BACKUP_SRC" "$BACKUP_DST"; do
    [ -d "$dir" ] || { echo "Diretório não existe: $dir"; exit 1; }
done

echo "Iniciando backup de $BACKUP_SRC para $BACKUP_DST/$BACKUP_NAME"

# Backup completo com rsync
rsync -avz --progress \
    --delete \
    --link-dest="$BACKUP_DST/latest" \
    "$BACKUP_SRC/" \
    "$BACKUP_DST/$BACKUP_NAME/"

# Atualizar link latest
rm -f "$BACKUP_DST/latest"
ln -s "$BACKUP_DST/$BACKUP_NAME" "$BACKUP_DST/latest"

echo "Backup concluído: $BACKUP_NAME"

# Rotação: deletar backups antigos
find "$BACKUP_DST" -maxdepth 1 -type d -name "backup-*" -mtime +$RETENTION_DAYS \
    -exec rm -rf {} \; \
    -exec echo "Removido backup antigo: {}" \;

echo "Rotação concluída (retenção: $RETENTION_DAYS dias)"
SCRIPT

chmod +x backup.sh

# Backup com compressão tar
cat > backup-tar.sh << 'SCRIPT'
#!/bin/bash
set -euo pipefail

BACKUP_SRC="$1"
BACKUP_FILE="backup-$(date +%Y%m%d-%H%M%S).tar.gz"
BACKUP_DIR="/backup"

# Criar diretório se não existir
mkdir -p "$BACKUP_DIR"

# Compactar com tar + gzip
tar -czf "$BACKUP_DIR/$BACKUP_FILE" \
    --exclude='*.log' \
    --exclude='node_modules' \
    --exclude='.cache' \
    -C "$(dirname "$BACKUP_SRC")" "$(basename "$BACKUP_SRC")"

echo "Backup criado: $BACKUP_DIR/$BACKUP_FILE"
ls -lh "$BACKUP_DIR/$BACKUP_FILE"
SCRIPT

Script de Deploy

Deploy com health check e rollback

cat > deploy.sh << 'SCRIPT'
#!/bin/bash
set -euo pipefail

readonly APP_DIR="/opt/app"
readonly BACKUP_DIR="/opt/app-backups"
readonly REPO_URL="https://github.com/org/app.git"
readonly BRANCH="main"
readonly HEALTH_URL="http://localhost:8080/health"
readonly MAX_RETRIES=12

deploy() {
    local commit_hash
    local timestamp
    timestamp=$(date +%Y%m%d-%H%M%S)

    echo "=== DEPLOY $timestamp ==="

    # 1. Pull latest
    echo "[1/5] Pull do código..."
    cd "$APP_DIR"
    git fetch origin
    commit_hash=$(git rev-parse --short origin/"$BRANCH")
    git checkout "$BRANCH"
    git pull origin "$BRANCH"

    # 2. Build
    echo "[2/5] Build..."
    npm ci --production
    npm run build

    # 3. Backup da versão atual
    echo "[3/5] Backup..."
    mkdir -p "$BACKUP_DIR"
    tar -czf "$BACKUP_DIR/pre-deploy-$timestamp.tar.gz" \
        --exclude=node_modules .

    # 4. Restart
    echo "[4/5] Restart do serviço..."
    sudo systemctl restart app

    # 5. Health check
    echo "[5/5] Health check..."
    local retry=0
    while [[ $retry -lt $MAX_RETRIES ]]; do
        if curl -sf "$HEALTH_URL" > /dev/null 2>&1; then
            echo "Deploy $commit_hash concluído com sucesso!"
            return 0
        fi
        echo "Aguardando serviço... ($((retry+1))/$MAX_RETRIES)"
        sleep 5
        ((retry++))
    done

    # Rollback
    echo "ERRO: Health check falhou. Iniciando rollback..."
    rollback
    return 1
}

rollback() {
    local latest_backup
    latest_backup=$(ls -t "$BACKUP_DIR"/pre-deploy-*.tar.gz 2>/dev/null | head -1)
    if [[ -z "$latest_backup" ]]; then
        echo "Nenhum backup disponível para rollback"
        exit 1
    fi
    echo "Restaurando backup: $latest_backup"
    cd "$APP_DIR"
    tar -xzf "$latest_backup"
    sudo systemctl restart app
    echo "Rollback concluído"
}

# Executar
if [[ "${1:-}" == "rollback" ]]; then
    rollback
else
    deploy
fi
SCRIPT

chmod +x deploy.sh

Script de Monitoramento

cat > monitor.sh << 'SCRIPT'
#!/bin/bash
set -euo pipefail

readonly CPU_ALERT=80
readonly MEM_ALERT=90
readonly DISK_ALERT=85
readonly ALERT_EMAIL="[email protected]"
readonly LOG_FILE="/var/log/monitor.log"

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

check_cpu() {
    local usage
    usage=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d. -f1)
    if [[ $usage -gt $CPU_ALERT ]]; then
        log "ALERTA CPU: ${usage}% (limite: ${CPU_ALERT}%)"
        return 1
    fi
    log "CPU: ${usage}%"
}

check_memory() {
    local usage
    usage=$(free | awk '/Mem:/ {printf "%.0f", $3/$2 * 100}')
    if [[ $usage -gt $MEM_ALERT ]]; then
        log "ALERTA MEM: ${usage}% (limite: ${MEM_ALERT}%)"
        return 1
    fi
    log "MEM: ${usage}%"
}

check_disk() {
    df -h / | awk 'NR==2 {
        gsub(/%/,"",$5);
        if ($5+0 > '"$DISK_ALERT"') {
            system("echo ALERTA DISK: " $5 "% >> " "'"$LOG_FILE"'")
        }
    }'
}

check_process() {
    local proc="$1"
    if pgrep -x "$proc" > /dev/null; then
        log "Processo $proc: OK (PID $(pgrep -x "$proc" | head -1))"
    else
        log "ALERTA: Processo $proc não está rodando"
        return 1
    fi
}

send_alert() {
    local subject="$1"
    local body="$2"
    echo "$body" | mail -s "$subject" "$ALERT_EMAIL"
}

echo "=== MONITORAMENTO $(date) ==="
check_cpu
check_memory
check_disk
check_process "nginx"
check_process "postgres"

echo "Monitoramento concluído."
SCRIPT

chmod +x monitor.sh

Cron Jobs

# Editar crontab
crontab -e

# Formato: minuto hora dia-mês mês dia-semana comando
#          m     h    dom    mon   dow

# Executar todo dia às 3h
0 3 * * * /home/user/scripts/backup.sh

# A cada hora
0 * * * * /home/user/scripts/monitor.sh

# A cada 30 minutos
*/30 * * * * /home/user/scripts/health-check.sh

# A cada dia útil às 8h
0 8 * * 1-5 /home/user/scripts/daily-report.sh

# No reboot (@reboot)
@reboot /home/user/scripts/start-services.sh

# Logging em cron jobs
0 3 * * * /home/user/scripts/backup.sh >> /var/log/backup.log 2>&1

# Exemplo completo: criar arquivo de crontab
cat > my-cron << 'EOF'
# Backup diário às 3h com log
0 3 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1

# Monitoramento a cada 5 minutos (apenas se falhar)
*/5 * * * * /opt/scripts/monitor.sh 2>&1 | mail -s "Alerta" [email protected]

# Limpeza semanal (domingo às 2h)
0 2 * * 0 find /tmp -type f -atime +7 -delete

# Rotação de logs (todo dia 1o do mês)
0 1 1 * * /opt/scripts/rotate-logs.sh

# Verificação de SSL (toda segunda às 9h)
0 9 * * 1 /opt/scripts/check-ssl.sh
EOF

crontab my-cron  # Importar
crontab -l       # Listar
crontab -r       # Remover (cuidado!)

CI/CD — GitHub Actions com Bash

cat > .github/workflows/deploy.yml << 'YAML'
name: Deploy

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Test
        run: |
          set -euo pipefail
          bash -n scripts/*.sh
          shellcheck scripts/*.sh
          echo "Todos os testes passaram!"

  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy via SSH
        run: |
          set -euo pipefail
          echo "${{ secrets.SSH_KEY }}" > /tmp/deploy_key
          chmod 600 /tmp/deploy_key
          ssh -i /tmp/deploy_key user@server '
            cd /opt/app
            git pull origin main
            npm ci --production
            npm run build
            sudo systemctl restart app
          '
YAML

Argument Parsing com getopts

cat > getopts-example.sh << 'SCRIPT'
#!/bin/bash
set -euo pipefail

usage() {
    cat <<EOF
Uso: $0 [-v] [-o arquivo] [-n nome] [-d] [-h]

Opções:
  -o ARQUIVO   Arquivo de saída (obrigatório)
  -n NOME      Nome do projeto (padrão: projeto)
  -v           Modo verboso
  -d           Modo debug
  -h           Ajuda
EOF
    exit 0
}

# Valores padrão
OUTPUT_FILE=""
PROJECT_NAME="projeto"
VERBOSE=false
DEBUG=false

# Parse com getopts
while getopts "o:n:vdh" opt; do
    case $opt in
        o) OUTPUT_FILE="$OPTARG" ;;
        n) PROJECT_NAME="$OPTARG" ;;
        v) VERBOSE=true ;;
        d) DEBUG=true ;;
        h) usage ;;
        \?) echo "Opção inválida: -$OPTARG" >&2; usage ;;
        :) echo "Opção -$OPTARG requer argumento" >&2; exit 1 ;;
    esac
done

# Validar argumentos obrigatórios
[[ -n "$OUTPUT_FILE" ]] || { echo "Erro: -o é obrigatório"; usage; }

$VERBOSE && echo "Verbose ativado"
$DEBUG && set -x

shift $((OPTIND-1))
echo "Argumentos restantes: $@"

echo "Configuração:"
echo "  Output: $OUTPUT_FILE"
echo "  Nome:   $PROJECT_NAME"
echo "  Debug:  $DEBUG"
SCRIPT

chmod +x getopts-example.sh
./getopts-example.sh -o saida.txt -n MeuProjeto -v

Lab: Pipeline CI/CD Completo com Bash

cat > pipeline.sh << 'SCRIPT'
#!/bin/bash
set -euo pipefail

# ─── Configuração ─────────────────────────────────────────────
readonly APP_NAME="myapp"
readonly APP_DIR="/opt/$APP_NAME"
readonly REPO="https://github.com/org/$APP_NAME.git"
readonly BRANCH="${CI_COMMIT_BRANCH:-main}"
readonly BUILD_DIR="/tmp/$APP_NAME-build"
readonly BACKUP_DIR="/opt/$APP_NAME-backups"
readonly HEALTH_URL="http://localhost:8080/health"
readonly SLACK_WEBHOOK="${SLACK_WEBHOOK:-}"

log()  { echo "[$(date +%H:%M:%S)] $*"; }
error(){ echo "[ERRO] $*" >&2; exit 1; }

# ─── Etapas ───────────────────────────────────────────────────
stage_lint() {
    log "Lint: verificando scripts..."
    for script in scripts/*.sh; do
        bash -n "$script" || error "Sintaxe inválida: $script"
        if command -v shellcheck &>/dev/null; then
            shellcheck --severity=style "$script"
        fi
    done
    log "Lint: OK"
}

stage_test() {
    log "Test: executando testes..."
    # Assumindo framework de teste
    if [[ -f "tests/run.sh" ]]; then
        bash tests/run.sh
    fi
    log "Test: OK"
}

stage_build() {
    log "Build: compilando..."
    rm -rf "$BUILD_DIR"
    mkdir -p "$BUILD_DIR"

    cp -r src/ package.json "$BUILD_DIR/"
    (cd "$BUILD_DIR" && npm ci --production)
    log "Build: OK"
}

stage_deploy() {
    log "Deploy: publicando..."

    # Backup da versão atual
    if [[ -d "$APP_DIR" ]]; then
        local backup_file="$BACKUP_DIR/pre-deploy-$(date +%Y%m%d-%H%M%S).tar.gz"
        mkdir -p "$BACKUP_DIR"
        tar -czf "$backup_file" -C "$APP_DIR" .
        log "Backup criado: $backup_file"
    fi

    # Deploy
    rsync -az --delete "$BUILD_DIR/" "$APP_DIR/"
    sudo systemctl restart "$APP_NAME"

    log "Deploy: OK"
}

stage_health() {
    log "Health: verificando serviço..."
    local retry=0
    while [[ $retry -lt 12 ]]; do
        if curl -sf "$HEALTH_URL" > /dev/null 2>&1; then
            log "Health: serviço saudável"
            return 0
        fi
        sleep 5
        ((retry++))
    done

    error "Health check falhou após deploy - necessário rollback manual"
}

stage_notify() {
    if [[ -n "$SLACK_WEBHOOK" ]]; then
        curl -sf -X POST -H 'Content-type: application/json' \
            --data "{\"text\":\"✅ Deploy $APP_NAME ($BRANCH) concluído\"}" \
            "$SLACK_WEBHOOK" || true
    fi
    log "Notificação enviada"
}

# ─── Main ────────────────────────────────────────────────────
main() {
    log "=== PIPELINE $APP_NAME ($BRANCH) ==="
    trap 'error "Pipeline falhou na linha $LINENO"' ERR

    stage_lint
    stage_test
    stage_build
    stage_deploy
    stage_health
    stage_notify

    log "=== PIPELINE CONCLUÍDO COM SUCESSO ==="
}

main "$@"
SCRIPT

chmod +x pipeline.sh

Scripts de automação combinam rsync/tar para backup, health check com curl para deploy, cron para agendamento, getopts para argumentos, e traps para rollback. Idempotência é essencial em automação.