Truques para escrever scripts de deploy idempotentes e seguros

1. Fundamentos da Idempotência em Deploy

Idempotência é a propriedade de uma operação que pode ser executada múltiplas vezes produzindo sempre o mesmo resultado final. Em scripts de deploy, isso significa que executar o mesmo script duas ou cem vezes deve deixar o sistema no mesmo estado desejado. Scripts idempotentes são críticos para deploys seguros porque permitem repetição sem medo de efeitos colaterais.

Scripts únicos (one-shot) assumem que o estado inicial é sempre o mesmo — uma premissa perigosa em ambientes reais. Scripts não idempotentes podem criar arquivos duplicados, sobrescrever configurações ou deixar serviços em estado inconsistente após uma falha. Rollbacks se tornam complexos porque não há garantia do que foi realmente modificado.

2. Estrutura Condicional e Verificações de Estado

O padrão fundamental para idempotência é verificar antes de agir. Sempre que possível, use o padrão "check-then-act" em vez de "act-and-handle".

# Padrão check-then-act: verifica antes de criar
if [ ! -f /etc/nginx/sites-available/meu-site.conf ]; then
    cp /tmp/meu-site.conf /etc/nginx/sites-available/
    echo "Arquivo de configuração criado"
else
    echo "Arquivo já existe, pulando criação"
fi

# Verificar se imagem Docker existe antes de rebuildar
if ! docker images --format '{{.Repository}}' | grep -q "^meu-app$"; then
    docker build -t meu-app:latest .
    echo "Imagem construída"
else
    echo "Imagem já existe, usando cache"
fi

Para operações que não podem ser verificadas previamente (como chamadas de API), use "act-and-handle" com idempotência no receptor:

# Criação de diretório é naturalmente idempotente
mkdir -p /var/log/meu-app

3. Uso de Flags e Modos Idempotentes em Ferramentas

Muitas ferramentas oferecem flags que tornam operações idempotentes:

# rsync com --ignore-existing para não sobrescrever
rsync -av --ignore-existing ./dist/ /var/www/meu-app/

# docker compose com --no-recreate para não recriar containers existentes
docker compose up -d --no-recreate

# Terraform com -auto-approve combinado com plan prévio
terraform plan -out=tfplan
if [ -f tfplan ]; then
    terraform apply -auto-approve tfplan
fi

O padrão --yes ou --non-interactive deve sempre ser combinado com verificações prévias para evitar execuções acidentais.

4. Gerenciamento de Estado com Arquivos de Lock e Marcadores

Arquivos marcadores são uma forma simples e eficaz de rastrear etapas concluídas:

# Arquivo de estado para migrações de banco
MIGRATION_STATE="/var/state/meu-app/migration-2024-01-01.done"

if [ ! -f "$MIGRATION_STATE" ]; then
    echo "Executando migração do banco..."
    mysql -u root -p$DB_PASSWORD meu_app < /tmp/migration.sql
    touch "$MIGRATION_STATE"
    echo "Migração concluída"
else
    echo "Migração já foi executada, pulando"
fi

Para evitar execuções simultâneas, use flock:

# Lockfile para evitar execuções paralelas
LOCKFILE="/tmp/deploy-meu-app.lock"
exec 200>"$LOCKFILE"
flock -n 200 || { echo "Outro deploy em execução"; exit 1; }

# ... código do deploy ...

flock -u 200

5. Tratamento Robusto de Erros e Rollback Seguro

Use set -e para abortar em qualquer erro e trap para capturar sinais:

#!/bin/bash
set -euo pipefail

ROLLBACK_NEEDED=false
cleanup() {
    if [ "$ROLLBACK_NEEDED" = true ]; then
        echo "Executando rollback..."
        # Rollback idempotente: só desfaz se existir
        if [ -f /tmp/backup-config.tar.gz ]; then
            tar -xzf /tmp/backup-config.tar.gz -C /etc/meu-app/
            echo "Rollback concluído"
        fi
    fi
}
trap cleanup EXIT

# Backup antes de modificar
tar -czf /tmp/backup-config.tar.gz /etc/meu-app/
ROLLBACK_NEEDED=true

# Operação principal
if ! cp /tmp/nova-config /etc/meu-app/config; then
    echo "Falha ao copiar configuração"
    exit 1
fi

ROLLBACK_NEEDED=false

6. Separação de Etapas e Ordem de Execução

Divida o script em fases claras:

# Fase 1: Pré-verificação
pre_check() {
    echo "Verificando conectividade..."
    if ! curl -s --connect-timeout 5 http://api.exemplo.com/health; then
        echo "API não está acessível"
        exit 1
    fi
    echo "Pré-verificação OK"
}

# Fase 2: Execução
deploy() {
    echo "Iniciando deploy..."
    # ... código de deploy ...
}

# Fase 3: Pós-validação
post_validate() {
    echo "Validando deploy..."
    if curl -s http://localhost:8080/health | grep -q "OK"; then
        echo "Deploy validado com sucesso"
    else
        echo "Falha na validação"
        exit 1
    fi
}

# Execução principal
pre_check
deploy
post_validate

7. Boas Práticas de Segurança em Scripts de Deploy

Use set -u para detectar variáveis não definidas e evite exposição de credenciais:

#!/bin/bash
set -euo pipefail

# Carregar secrets de forma segura
if [ -f /etc/meu-app/secrets.env ]; then
    source /etc/meu-app/secrets.env
    # Limpar variáveis após uso
    unset DB_PASSWORD
    unset API_KEY
fi

# Validar entrada de parâmetros
if [ -z "${1:-}" ]; then
    echo "Uso: $0 <ambiente>"
    exit 1
fi

# Sanitizar parâmetros para evitar injeção
AMBIENTE="$1"
case "$AMBIENTE" in
    dev|staging|production) ;;
    *) echo "Ambiente inválido"; exit 1 ;;
esac

8. Testes e Simulação de Deploy (Dry-Run)

Implemente modo --dry-run para simular sem efeitos colaterais:

DRY_RUN=false
if [ "${1:-}" = "--dry-run" ]; then
    DRY_RUN=true
fi

execute() {
    if [ "$DRY_RUN" = true ]; then
        echo "[DRY-RUN] $*"
    else
        "$@"
    fi
}

# Uso em todo o script
execute cp /tmp/nova-config /etc/meu-app/config
execute systemctl restart meu-app

# Comparar estado atual com desejado
if [ "$DRY_RUN" = true ]; then
    echo "Comparando arquivos..."
    diff -r /etc/meu-app/ /tmp/estado-desejado/ || true
fi

Use shellcheck para análise estática e testes unitários com bats:

# Instalar shellcheck
sudo apt-get install shellcheck
shellcheck meu-deploy.sh

# Teste unitário com bats
@test "Verifica se pré-verificação falha sem API" {
    run ./meu-deploy.sh
    [ "$status" -eq 1 ]
    [[ "$output" =~ "API não está acessível" ]]
}

Referências