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
-
GNU Bash Manual - Conditional Constructs — Documentação oficial sobre estruturas condicionais em Bash, fundamental para implementar verificações idempotentes.
-
ShellCheck - Static Analysis for Shell Scripts — Ferramenta essencial para validar scripts Bash e evitar erros comuns que comprometem a idempotência.
-
Debian Policy Manual - Package Management — Referência oficial sobre gerenciamento de pacotes Debian, incluindo comandos como
dpkg --compare-versions. -
Systemd Documentation - systemctl — Documentação oficial do systemctl, cobrindo comandos como
is-active,is-enablede estratégias de gerenciamento de serviços. -
Idempotent Shell Scripts - DevOps Best Practices — Discussão técnica sobre operações idempotentes em scripts de automação, com exemplos práticos da comunidade DevOps.
-
Advanced Bash-Scripting Guide - Traps — Guia avançado sobre uso de traps em Bash para implementar rollback e tratamento de erros em scripts de provisionamento.