Environment provisioning: scripts idempotentes

1. Fundamentos da Idempotência em Scripts de Provisionamento

1.1 Definição de idempotência

Idempotência é a propriedade de uma operação que pode ser aplicada múltiplas vezes sem alterar o resultado além da primeira aplicação. Em scripts de provisionamento de ambientes, isso significa que executar o mesmo script várias vezes produz exatamente o mesmo estado final, sem causar erros ou duplicações.

1.2 Diferença entre scripts destrutivos e seguros

Scripts não idempotentes frequentemente causam problemas:

# Script DESTRUTIVO (não idempotente)
echo "deb http://example.com/repo buster main" >> /etc/apt/sources.list
apt update
apt install nginx -y
systemctl start nginx

Cada execução adiciona uma nova linha ao sources.list e tenta instalar pacotes já existentes. Scripts idempotentes verificam antes de agir:

# Script IDEMPOTENTE
if ! grep -q "example.com/repo" /etc/apt/sources.list; then
    echo "deb http://example.com/repo buster main" >> /etc/apt/sources.list
    apt update
fi

if ! dpkg -l | grep -q "nginx"; then
    apt install nginx -y
fi

if ! systemctl is-active --quiet nginx; then
    systemctl start nginx
fi

1.3 Ciclo de vida de um script idempotente

O padrão fundamental é: verificar → aplicar → verificar novamente. Cada ação deve ser precedida por uma checagem do estado atual e seguida por uma confirmação da mudança.

2. Estrutura de Verificação Pré-Execução (Guard Clauses)

2.1 Uso de if, test e [[ ]]

# Verificação de arquivo existente
if [[ ! -f "/etc/nginx/nginx.conf" ]]; then
    cp /etc/nginx/nginx.conf.default /etc/nginx/nginx.conf
    echo "Arquivo de configuração criado"
fi

# Verificação de pacote instalado
if ! command -v docker &> /dev/null; then
    curl -fsSL https://get.docker.com | sh
fi

2.2 Funções auxiliares para checagem

# Função para verificar instalação de pacote
is_package_installed() {
    dpkg -l "$1" 2>/dev/null | grep -q "^ii"
}

# Função para verificar serviço ativo
is_service_active() {
    systemctl is-active --quiet "$1"
}

# Uso combinado
if ! is_package_installed "nginx"; then
    apt install nginx -y
fi

if ! is_service_active "nginx"; then
    systemctl start nginx
fi

2.3 Padrão "skip if already done"

install_nodejs() {
    if command -v node &> /dev/null; then
        echo "Node.js já instalado, pulando..."
        return 0
    fi

    curl -fsSL https://deb.nodesource.com/setup_18.x | bash -
    apt install nodejs -y
}

3. Gerenciamento Idempotente de Pacotes e Repositórios

3.1 Verificação antes de instalar

install_package_if_missing() {
    local package="$1"

    if ! dpkg -l "$package" 2>/dev/null | grep -q "^ii"; then
        echo "Instalando $package..."
        apt install "$package" -y
    else
        echo "$package já está instalado"
    fi
}

install_package_if_missing "git"
install_package_if_missing "curl"

3.2 Adição condicional de repositórios

add_repository_if_missing() {
    local repo_line="$1"
    local repo_file="$2"

    if [[ ! -f "$repo_file" ]]; then
        echo "$repo_line" > "$repo_file"
        apt update
        echo "Repositório adicionado"
    else
        echo "Repositório já existe"
    fi
}

# Exemplo: Adicionar repositório Docker
add_repository_if_missing \
    "deb [arch=amd64] https://download.docker.com/linux/debian buster stable" \
    "/etc/apt/sources.list.d/docker.list"

3.3 Tratamento de versões específicas

install_specific_version() {
    local package="$1"
    local required_version="$2"

    current_version=$(dpkg -l "$package" 2>/dev/null | awk '/^ii/ {print $3}')

    if [[ -z "$current_version" ]]; then
        apt install "$package=$required_version" -y
    elif dpkg --compare-versions "$current_version" lt "$required_version"; then
        apt install "$package=$required_version" -y
    else
        echo "Versão $current_version é suficiente"
    fi
}

install_specific_version "nginx" "1.24.0-1"

4. Configuração de Serviços e Systemd de Forma Idempotente

4.1 Verificação de estado do serviço

ensure_service_running() {
    local service="$1"

    if ! systemctl is-enabled --quiet "$service"; then
        systemctl enable "$service"
        echo "Serviço $service habilitado"
    fi

    if ! systemctl is-active --quiet "$service"; then
        systemctl start "$service"
        echo "Serviço $service iniciado"
    fi
}

ensure_service_running "nginx"

4.2 Aplicação de configurações baseadas em checksum

deploy_config_if_changed() {
    local source="$1"
    local target="$2"

    if [[ ! -f "$target" ]] || ! cmp -s "$source" "$target"; then
        cp "$source" "$target"
        echo "Configuração atualizada"
        return 0
    fi
    echo "Configuração já está atualizada"
    return 1
}

if deploy_config_if_changed "/templates/nginx.conf" "/etc/nginx/nginx.conf"; then
    systemctl reload nginx
fi

4.3 Uso de systemctl is-active e systemctl is-enabled

restart_service_if_needed() {
    local service="$1"
    local config_file="$2"
    local checksum_file="/var/run/${service}_checksum"

    current_md5=$(md5sum "$config_file" | cut -d' ' -f1)

    if [[ ! -f "$checksum_file" ]] || [[ "$current_md5" != "$(cat $checksum_file)" ]]; then
        systemctl restart "$service"
        echo "$current_md5" > "$checksum_file"
        echo "Serviço $service reiniciado devido a mudanças na configuração"
    fi
}

5. Manipulação Segura de Arquivos de Configuração

5.1 Uso de sed com flags de idempotência

# Substituir apenas se a linha não existir
add_line_if_missing() {
    local file="$1"
    local line="$2"

    if ! grep -qF "$line" "$file"; then
        echo "$line" >> "$file"
        echo "Linha adicionada"
    fi
}

# Substituir valor apenas se diferente
update_config_value() {
    local file="$1"
    local key="$2"
    local value="$3"

    if grep -q "^${key}=" "$file"; then
        sed -i "s/^${key}=.*/${key}=${value}/" "$file"
    else
        echo "${key}=${value}" >> "$file"
    fi
}

5.2 Estratégias de backup e rollback

safe_config_update() {
    local file="$1"
    local backup="${file}.bak.$(date +%Y%m%d%H%M%S)"

    cp "$file" "$backup"

    if ! sed -i 's/old_value/new_value/' "$file"; then
        cp "$backup" "$file"
        echo "Rollback realizado devido a erro"
        return 1
    fi

    echo "Backup salvo em $backup"
}

5.3 Geração de arquivos a partir de templates

generate_config_from_template() {
    local template="$1"
    local output="$2"
    local temp_file=$(mktemp)

    export APP_PORT="${APP_PORT:-8080}"
    export DB_HOST="${DB_HOST:-localhost}"

    envsubst < "$template" > "$temp_file"

    if [[ ! -f "$output" ]] || ! diff -q "$temp_file" "$output" &>/dev/null; then
        cp "$temp_file" "$output"
        echo "Arquivo de configuração gerado"
    fi

    rm "$temp_file"
}

6. Tratamento de Usuários, Grupos e Permissões

6.1 Criação condicional de usuários

create_user_if_missing() {
    local username="$1"

    if ! id "$username" &>/dev/null; then
        useradd -m -s /bin/bash "$username"
        echo "Usuário $username criado"
    else
        echo "Usuário $username já existe"
    fi
}

create_group_if_missing() {
    local groupname="$1"

    if ! getent group "$groupname" &>/dev/null; then
        groupadd "$groupname"
        echo "Grupo $groupname criado"
    fi
}

6.2 Ajuste de permissões apenas quando necessário

set_permissions_if_needed() {
    local path="$1"
    local expected_owner="$2"
    local expected_group="$3"
    local expected_perms="$4"

    current_owner=$(stat -c "%U" "$path")
    current_group=$(stat -c "%G" "$path")
    current_perms=$(stat -c "%a" "$path")

    [[ "$current_owner" != "$expected_owner" ]] && chown "$expected_owner" "$path"
    [[ "$current_group" != "$expected_group" ]] && chgrp "$expected_group" "$path"
    [[ "$current_perms" != "$expected_perms" ]] && chmod "$expected_perms" "$path"
}

6.3 Gerenciamento de chaves SSH

add_ssh_key_if_missing() {
    local user="$1"
    local key="$2"
    local auth_file="/home/$user/.ssh/authorized_keys"

    mkdir -p "/home/$user/.ssh"

    if ! grep -qF "$key" "$auth_file" 2>/dev/null; then
        echo "$key" >> "$auth_file"
        chmod 600 "$auth_file"
        chown "$user:$user" "$auth_file"
        echo "Chave SSH adicionada para $user"
    fi
}

7. Logging, Rollback e Idempotência em Falhas

7.1 Estrutura de logging

log() {
    local status="$1"
    local message="$2"
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$status] $message"
}

# Uso
log "OK" "Nginx configurado corretamente"
log "SKIP" "Docker já estava instalado"
log "FAIL" "Falha ao instalar pacote XYZ"

7.2 Implementação de traps para rollback

#!/bin/bash
set -e

cleanup() {
    log "FAIL" "Erro detectado, iniciando rollback..."
    if [[ -f "$BACKUP_FILE" ]]; then
        cp "$BACKUP_FILE" "$CONFIG_FILE"
        log "OK" "Rollback concluído"
    fi
}

trap cleanup ERR

CONFIG_FILE="/etc/app/config.conf"
BACKUP_FILE="/tmp/config.backup"

cp "$CONFIG_FILE" "$BACKUP_FILE"
# ... operações de configuração ...

7.3 Funções de dry-run

DRY_RUN=false

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

# Uso
run apt install nginx -y
run systemctl start nginx

8. Boas Práticas e Padrões Avançados

8.1 Uso de set -euo pipefail

#!/bin/bash
set -euo pipefail

# -e: Sai em caso de erro
# -u: Trata variáveis não definidas como erro
# -o pipefail: Falha no pipeline se qualquer comando falhar

shopt -s nullglob  # Expansão de glob vazio não gera erro

8.2 Modularização em bibliotecas

# lib/idempotent.sh
is_installed() { ... }
ensure_service() { ... }
safe_config() { ... }

# provision.sh
source lib/idempotent.sh

ensure_service "nginx"
safe_config "/templates/nginx.conf" "/etc/nginx/nginx.conf"

8.3 Testes de idempotência

test_idempotency() {
    local script="$1"

    echo "Execução 1:"
    bash "$script"

    echo "Execução 2:"
    bash "$script"

    echo "Se o script é idempotente, ambas as execuções produzem o mesmo resultado"
}

test_idempotency "provision.sh"

Referências