Projeto: sistema de backup completo
LidoNesta aula, vamos construir um sistema de backup completo em Shell Script, cobrindo configuração, compressão, rotação, logging e agendamento. Você aprenderá a criar um script robusto, reutilizável e seguro para automatizar backups de diretórios, com rotação de versões antigas, log detalhado e integração com cron.
Nesta aula, vamos desenvolver um projeto prático e completo: um sistema de backup em Shell Script para Linux. Este sistema será capaz de realizar backups de um ou mais diretórios, comprimir os arquivos, manter um histórico com rotação (eliminando backups antigos), gerar logs detalhados e ser agendável via cron. Ao final, você terá um script profissional que pode ser adaptado para uso real em servidores.
O script será construído de forma modular, com configurações centralizadas, funções bem definidas e tratamento de erros. Vamos explorar boas práticas de programação shell, como uso de variáveis, laços, condições, e integração com comandos padrão do Linux (tar, find, mkdir, etc.).
Configuração
O primeiro passo é definir a configuração do backup. Em scripts de produção, é comum separar as configurações do código. Podemos fazer isso usando um arquivo externo (por exemplo, backup.conf) ou definindo variáveis no início do script. Para este projeto, optaremos por um arquivo de configuração externo, que facilita a manutenção sem editar o script em si.
As configurações principais incluem: diretórios de origem (o que será copiado), diretório de destino (onde os backups serão armazenados), prefixo do nome dos arquivos de backup, número de backups a manter (rotação), e opções de compressão. Também podemos incluir configurações de log, como caminho do arquivo de log e nível de verbosidade.
Vamos criar o arquivo backup.conf com as seguintes variáveis:
# backup.conf - Configurações do sistema de backup
# Diretórios de origem (separados por espaço)
ORIGEM="/home/usuario/documentos /etc/nginx"
# Diretório de destino
DESTINO="/mnt/backups"
# Prefixo para os arquivos de backup
PREFIXO="backup"
# Número de backups a manter (rotação)
ROTACAO=7
# Método de compressão: tar.gz, tar.bz2, tar.xz
COMPRESSOR="tar.gz"
# Arquivo de log
LOG_FILE="/var/log/backup.log"
No script principal, vamos carregar essas variáveis usando source (ou .). Isso permite que o script seja reutilizável e fácil de configurar. Também vamos definir variáveis internas, como data e hora atual, e criar a estrutura de diretórios necessária.
#!/bin/bash
# Carrega as configurações
source "$(dirname "$0")/backup.conf"
# Gera timestamp
DATA=$(date +%Y%m%d_%H%M%S)
NOME_BACKUP="${PREFIXO}_${DATA}.${COMPRESSOR}"
# Cria diretório de destino, se não existir
mkdir -p "$DESTINO" || { echo "Erro ao criar diretório de destino"; exit 1; }
Compressão e rotação
A compressão é feita com o comando tar, que pode criar arquivos compactados nos formatos .tar.gz, .tar.bz2, etc. Vamos usar um case para selecionar o parâmetro correto do tar de acordo com o valor de COMPRESSOR.
Para a rotação, precisamos manter apenas os últimos N backups. Podemos listar os arquivos de backup no diretório de destino, ordená-los por data e excluir os mais antigos além do limite. O comando ls com ordenação e head/tail são úteis, mas para segurança, usaremos find com -name e -mtime ou simplesmente listar e contar.
Implementaremos uma função que: (1) executa o tar com as opções adequadas; (2) verifica se o backup foi criado com sucesso; (3) chama a função de rotação.
# Função para realizar o backup
fazer_backup() {
echo "Iniciando backup em $(date)"
# Seleciona opções do tar conforme o compressor
case "$COMPRESSOR" in
tar.gz) TAR_OPTS="-czf" ;;
tar.bz2) TAR_OPTS="-cjf" ;;
tar.xz) TAR_OPTS="-cJf" ;;
*) echo "Compressor inválido: $COMPRESSOR"; exit 1 ;;
esac
# Executa o tar
tar $TAR_OPTS "$DESTINO/$NOME_BACKUP" $ORIGEM
if [ $? -eq 0 ]; then
echo "Backup criado: $DESTINO/$NOME_BACKUP"
rotacionar_backups
else
echo "Erro ao criar backup"
exit 1
fi
}
# Função para rotacionar backups antigos
rotacionar_backups() {
echo "Verificando rotação (máximo de $ROTACAO backups)"
# Lista os backups do tipo prefixo_* e ordena por nome (que contém data)
# O nome começa com prefixo_ e a data é no formato YYYYMMDD_HHMMSS, então ordenação alfabética funciona.
BACKUPS=$(ls -1 "$DESTINO"/${PREFIXO}_* 2>/dev/null | sort)
CONTAGEM=$(echo "$BACKUPS" | wc -l)
# Se houver mais backups que o permitido, remove os mais antigos
if [ "$CONTAGEM" -gt "$ROTACAO" ]; then
EXCESSO=$((CONTAGEM - ROTACAO))
for ARQ in $(echo "$BACKUPS" | head -n "$EXCESSO"); do
rm -f "$ARQ"
echo "Removendo backup antigo: $ARQ"
done
fi
}
Logging
Registrar as ações do script é fundamental para auditoria e depuração. Vamos criar uma função de log que escreve mensagens com timestamp e nível (INFO, ERRO, AVISO) no arquivo definido em LOG_FILE. Também podemos espelhar as mensagens na saída padrão, se desejado.
Usaremos tee para salvar no arquivo e exibir no terminal, ou simplesmente redirecionar. Para robustez, vamos garantir que o diretório do log exista e que o arquivo possa ser gravado.
# Função de log
log() {
local NIVEL="$1"
shift
local MENSAGEM="$@"
local TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
echo "[$TIMESTAMP] [$NIVEL] $MENSAGEM" | tee -a "$LOG_FILE"
}
# Exemplo de uso
log "INFO" "Iniciando backup"
log "ERRO" "Falha ao criar diretório"
É importante redirecionar erros do script para o log. Podemos usar exec 2>> "$LOG_FILE" para fazer com que todos os erros do shell sejam anexados ao log, mas isso pode poluir o log com mensagens do sistema. Melhor é usar nossa função de log para mensagens controladas e redirecionar apenas a saída de comandos específicos quando necessário.
No script final, substituiremos os echo por chamadas à função log.
Agendamento
Para executar o backup automaticamente, usamos o cron. O cron é um agendador de tarefas do Linux. Vamos criar uma entrada no crontab para executar o script diariamente em um horário específico, por exemplo, às 2h da manhã.
Para editar o crontab do usuário: crontab -e. Adicione a linha:
0 2 * * * /caminho/para/backup.sh
Certifique-se de que o script tenha permissão de execução (chmod +x backup.sh) e que o interpretador bash esteja no shebang. Também é importante que o script seja executado com o ambiente adequado; podemos definir variáveis de ambiente no próprio script ou usar source de um arquivo de perfil.
Vamos criar um script principal que chama as funções e adiciona o agendamento como parte do projeto (opcional). Podemos incluir uma função que instala a entrada no crontab, mas isso deve ser feito com cuidado para não duplicar entradas.
# Função para instalar o cron (opcional)
instalar_cron() {
local SCRIPT="$(realpath "$0")"
local LINHA="0 2 * * * $SCRIPT"
( crontab -l 2>/dev/null | grep -v "$SCRIPT"; echo "$LINHA" ) | crontab -
log "INFO" "Cron instalado: $LINHA"
}
# Chamada principal
main() {
log "INFO" "Iniciando script de backup"
fazer_backup
log "INFO" "Backup concluído"
}
# Executa a função principal
main "$@"
Boas práticas e observações finais
Ao desenvolver scripts de backup, considere as seguintes boas práticas:
- Use
set -euo pipefailpara sair em caso de erro, tratar variáveis não definidas e falhas em pipes. Isso torna o script mais previsível. - Valide as configurações: verifique se os diretórios de origem existem e são legíveis.
- Teste o script manualmente antes de agendar.
- Considere usar
rsyncpara backups incrementais, mas o tar é suficiente para backups completos. - Proteja o arquivo de configuração se contiver senhas (não é o caso aqui).
- Mantenha os logs centralizados e faça rotação de logs também, para não encher o disco.
- Use timestamps no nome dos arquivos para evitar sobrescrita.
- Teste a restauração periodicamente para garantir que os backups são válidos.
Agora, vamos montar o script completo. O código final integra todas as partes e pode ser usado como base para um sistema real.
#!/bin/bash
# backup.sh - Sistema de backup completo
set -euo pipefail
# Carrega configurações
SCRIPT_DIR="$(dirname "$(realpath "$0")")"
source "$SCRIPT_DIR/backup.conf"
# Função de log
log() {
local NIVEL="$1"
shift
local MENSAGEM="$@"
local TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
echo "[$TIMESTAMP] [$NIVEL] $MENSAGEM" | tee -a "$LOG_FILE"
}
# Função para criar diretório de destino
criar_destino() {
mkdir -p "$DESTINO" || { log "ERRO" "Falha ao criar diretório de destino"; exit 1; }
}
# Função para realizar o backup
fazer_backup() {
log "INFO" "Iniciando backup..."
local DATA=$(date +%Y%m%d_%H%M%S)
local NOME_BACKUP="${PREFIXO}_${DATA}.${COMPRESSOR}"
local CAMINHO_BACKUP="$DESTINO/$NOME_BACKUP"
case "$COMPRESSOR" in
tar.gz) TAR_OPTS="-czf" ;;
tar.bz2) TAR_OPTS="-cjf" ;;
tar.xz) TAR_OPTS="-cJf" ;;
*) log "ERRO" "Compressor inválido: $COMPRESSOR"; exit 1 ;;
esac
if tar $TAR_OPTS "$CAMINHO_BACKUP" $ORIGEM 2>>"$LOG_FILE"; then
log "INFO" "Backup criado: $CAMINHO_BACKUP"
else
log "ERRO" "Falha ao criar backup"
exit 1
fi
rotacionar_backups
}
# Função para rotacionar backups
rotacionar_backups() {
log "INFO" "Verificando rotação (máximo de $ROTACAO backups)"
local BACKUPS=( "$DESTINO"/${PREFIXO}_* )
local CONTAGEM=${#BACKUPS[@]}
if [ "$CONTAGEM" -gt "$ROTACAO" ]; then
# Ordena por nome (timestamp) e remove os mais antigos
local EXCESSO=$((CONTAGEM - ROTACAO))
# Usa ls -t para ordenar por tempo de modificação (mais recentes primeiro)
local ANTIGOS=$(ls -1t "$DESTINO"/${PREFIXO}_* | tail -n "$EXCESSO")
for ARQ in $ANTIGOS; do
rm -f "$ARQ"
log "INFO" "Removendo backup antigo: $ARQ"
done
fi
}
# Função para instalar cron (opcional)
instalar_cron() {
local SCRIPT="$(realpath "$0")"
local LINHA="0 2 * * * $SCRIPT"
( crontab -l 2>/dev/null | grep -v "$SCRIPT"; echo "$LINHA" ) | crontab -
log "INFO" "Cron instalado: $LINHA"
}
# Função principal
main() {
criar_destino
fazer_backup
log "INFO" "Backup concluído com sucesso"
}
# Executa a função principal
main "$@"
Referências
- Bash Reference Manual
- tar(1) - Linux manual page
- crontab(1) - Linux manual page
- cron(5) - Linux manual page
- GNU Coreutils: date
- GNU Coreutils: tee
- ShellCheck - shell script analysis tool
Exercícios
Explique a importância de usar
set -euo pipefailem um script de backup e dê um exemplo de como cada opção ajuda a evitar problemas.✓ Resposta:set -efaz o script sair imediatamente se um comando retornar um status de erro, evitando que falhas passem despercebidas.set -utrata o uso de variáveis não definidas como erro, prevenindo erros de digitação.set -o pipefailfaz com que o status de saída de um pipeline seja o do último comando que falhou, garantindo que erros em qualquer parte do pipeline sejam detectados. Exemplo: setarfalhar em um pipeline, o script sairá em vez de continuar com um backup corrompido.No script, a rotação usa
ls -1tpara listar os backups por tempo de modificação. Explique por que isso é preferível a ordenar por nome, e em que situação a ordenação por nome seria mais adequada.✓ Resposta:ls -1tordena por data de modificação (mais recentes primeiro), o que é mais confiável se os nomes dos arquivos forem alterados manualmente ou se houver backups de fontes diferentes com timestamps semelhantes. A ordenação por nome é adequada quando os nomes seguem um padrão estritamente cronológico (como YYYYMMDD_HHMMSS) e não há alterações externas, mas pode falhar se houver múltiplos backups no mesmo segundo ou se o relógio do sistema for ajustado.A função
instalar_cronusagrep -v "$SCRIPT"para evitar duplicatas. Explique como essa linha funciona e o que aconteceria se não usássemos ogrep.✓ Resposta: A linha( crontab -l 2>/dev/null | grep -v "$SCRIPT"; echo "$LINHA" ) | crontab -primeiro obtém o crontab atual (ignorando erros), filtra as linhas que contêm o caminho do script (para remover entradas antigas), adiciona a nova linha com o agendamento e envia tudo de volta para o crontab. Sem ogrep -v, cada execução adicionaria uma nova entrada, resultando em múltiplos backups agendados e causando execuções duplicadas.Modifique o script para que ele aceite um argumento de linha de comando
--dry-runque apenas simula o backup, mostrando o que seria feito sem realmente criar o arquivo. Implemente a lógica usando uma variávelDRY_RUNe condicionais.✓ Resposta:#!/bin/bash # ... configurações ... DRY_RUN=false if [[ "$1" == "--dry-run" ]]; then DRY_RUN=true fi fazer_backup() { # ... if $DRY_RUN; then log "INFO" "DRY RUN: tar $TAR_OPTS $CAMINHO_BACKUP $ORIGEM" else if tar $TAR_OPTS "$CAMINHO_BACKUP" $ORIGEM; then log "INFO" "Backup criado: $CAMINHO_BACKUP" else log "ERRO" "Falha ao criar backup" exit 1 fi fi # ... rotação também deve ser simulada }Escreva um trecho de código que valide se os diretórios de origem existem e são legíveis antes de iniciar o backup. Use um laço
fore a funçãologpara reportar erros.✓ Resposta:validar_origens() { for DIR in $ORIGEM; do if [ ! -d "$DIR" ]; then log "ERRO" "Diretório não existe: $DIR" exit 1 elif [ ! -r "$DIR" ]; then log "ERRO" "Diretório sem permissão de leitura: $DIR" exit 1 fi done log "INFO" "Diretórios de origem válidos" } # Chame validar_origens antes de fazer_backup