Linux: フェーズ4ミニプロジェクト — 自動バックアップスクリプト

フェーズ4の総合プロジェクトです。プロセス管理、パッケージ管理、アーカイブ/圧縮、Bashスクリプトすべてのスキルを組み合わせて、実用可能な自動バックアップツールを構築します。

📋 前提条件:まず以下を完了していること

1. このレッスンで学ぶこと


2. 小明のサーバーハードディスクが故障した

(1) 悩み:ハードディスク故障、データ消失

小明のサーバーのハードディスクが突然故障し、3日分のデータが失われました。バックアップがないことに気づきました — 手動バックアップは面倒すぎて、いつも忘れていました。

(2) 自動バックアップソリューション

彼はフェーズ4のスキルを使って完全なバックアップツールを書きました:

BASH
# Backup locally
./backup.sh -s /var/www -d /backup

# Backup remotely
./backup.sh -s /var/www -d user@backup:/backups

# Add to cron for daily execution
0 2 * * * /opt/scripts/backup.sh -s /var/www -d /backup

(3) 得られるもの:データ損失の恐怖から解放

バックアップスクリプトは毎日午前2時に自動実行され、リモートに差分同期されます。小明はついに安心して眠れるようになりました。


3. 知識ポイント

(1) バックアップスクリプトアーキテクチャ設計

良いバックアップスクリプトはモジュール化されているべきです:

TEXT

backup.sh
├── Config loading
├── Checks (directory/dependencies/space)
├── Compression (source → tar.gz)
├── Sync (local or remote rsync)
├── Cleanup (delete expired backups)
├── Logging
└── Notification (success/failure)

(2) 差分バックアップ戦略

TEXT
# Method 1: rsync --link-dest (recommended)
# Uses hard links; each day appears as a full directory but only stores changes
rsync -av --link-dest=/backup/yesterday/ /data/ /backup/today/

> 💡 ヒント:`rsync --link-dest`の差分バックアップの原理はハードリンクを活用 — 「今日の」バックアップ内の変更されていないファイルは「昨日の」ファイルへのハードリンクに過ぎず、追加ディスク容量を消費しません。変更されたファイルだけが実際に新規データとして書き込まれます。外見上は毎日フルディレクトリのように見えますが、ディスク使用量は差分に近い。

# Method 2: rsync incremental transfer
# rsync only transfers changed parts by default
rsync -avz --delete /data/ user@host:/backup/

> ⚠️ 警告:`rsync --delete`は送信先を送信元と完全に一致させます — 送信先にあって送信元にないファイルは削除されます。送信元パスを間違えた場合(例:末尾の`/`の漏れ)、送信先が空になる可能性があります。必ず先に`--dry-run`でプレビューしてください。

# Method 3: Date-named archives
backup-2026-07-07.tar.gz
backup-2026-07-08.tar.gz  # Two independent files, but more space

ℹ️ 注意:プロのバックアップ戦略は「3-2-1ルール」に従います — データ3コピー、2つの異なるストレージメディア、1つのオフサイトバックアップ。ローカルバックアップ + リモートrsyncが3-2-1戦略の基盤となります。ローカルのtar.gzだけでは不十分 — ハードディスク故障でソースデータとバックアップの両方が失われます。

(3) cronスケジューリング

TEXT
# cron format: minute hour day month weekday command
# Run backup daily at 2:00 AM
0 2 * * * /opt/scripts/backup.sh > /dev/null 2>&1

# Run every hour
0 * * * * /opt/scripts/backup-hourly.sh

# Every Monday at 3:00 AM
0 3 * * 1 /opt/scripts/backup-weekly.sh

(4) ▶ サンプル:バックアップ容量の確認

BASH
#!/bin/bash
# Check if backup directory has enough space

check_space() {
    local dest="$1"
    local min_gb="${2:-1}"
    
    local available=$(df -BG "$dest" | tail -1 | awk '{print $4}' | tr -d 'G')
    
    if [ "$available" -lt "$min_gb" ]; then
        echo "Error: Insufficient disk space (available: ${available}G, needed: ${min_gb}G)"
        return 1
    fi
    
    echo "Disk space sufficient: ${available}G"
    return 0
}

出力:

TEXT
Disk space sufficient: 50G

(5) ▶ サンプル:ロギング関数

TEXT
#!/bin/bash

LOG_FILE="/var/log/backup.log"

log() {
    local level="$1"
    local message="$2"
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}

log "INFO" "Backup started"
log "ERROR" "Backup failed: insufficient disk space"
log "INFO" "Backup complete"

(6) ▶ サンプル:ロック機構で重複実行を防止

TEXT
#!/bin/bash

LOCK_FILE="/tmp/backup.lock"

# Try to acquire lock
exec 200>"$LOCK_FILE"
flock -n 200 || {
    echo "Backup script is already running (lock file: $LOCK_FILE)"
    exit 1
}

# ... backup logic ...

# Lock is automatically released when script exits

(7) ▶ サンプル:期限切れバックアップのクリーンアップ

TEXT
#!/bin/bash

cleanup_old_backups() {
    local backup_dir="$1"
    local retention_days="${2:-30}"
    
    log "INFO" "Cleaning backups older than $retention_days days..."
    
    local count=0
    while IFS= read -r -d '' file; do
        rm -f "$file"
        count=$((count + 1))
    done < <(find "$backup_dir" -name "*.tar.gz" -type f -mtime +$retention_days -print0)
    
    if [ $count -gt 0 ]; then
        log "INFO" "Cleaned up $count expired files"
    fi
}

(8) ▶ 総合例:完全なバックアップツールスクリプト

BASH
#!/bin/bash
# backup.sh - Automated backup tool
# Usage: ./backup.sh -s <source_dir> -d <dest_dir> [-r <retention_days>] [-e <exclude_pattern>] [-v]

set -euo pipefail

# ====== Configuration ======
VERSION="1.0.0"
LOG_FILE="/var/log/backup.log"
LOCK_FILE="/tmp/backup.lock"
DEFAULT_RETENTION=30

# ====== Colors ======
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

# ====== Functions ======

usage() {
    cat << EOF
Backup Tool v${VERSION}

Usage: $0 -s <source_dir> -d <dest_dir> [options]

Required:
  -s <source_dir>      Source directory to back up
  -d <dest_dir>        Backup destination (local path or user@host:/path)

Options:
  -r <days>            Retention days (default: $DEFAULT_RETENTION)
  -e <pattern>         Exclude pattern (can be used multiple times, e.g., -e node_modules -e .git)
  -v                   Verbose output
  -t                   Test mode (no actual backup created)
  -h                   Show help
EOF
    exit 0
}

log() {
    local level="$1"
    local message="$2"
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    echo -e "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}

info()    { log "INFO" "$1"; }
success() { log "INFO" "${GREEN}$1${NC}"; }
warn()    { log "WARN" "${YELLOW}$1${NC}"; }
error()   { log "ERROR" "${RED}$1${NC}"; }

check_deps() {
    for cmd in rsync tar; do
        if ! command -v "$cmd" &> /dev/null; then
            error "Required command '$cmd' not found"
            exit 1
        fi
    done
}

check_disk_space() {
    local dir="$1"
    local min_gb="${2:-1}"
    
    if [[ "$dir" != *:* ]]; then
        local avail=$(df -BG "$dir" 2>/dev/null | tail -1 | awk '{print $4}' | tr -d 'G')
        if [ "$avail" -lt "$min_gb" ]; then
            error "Insufficient disk space: ${avail}G available, ${min_gb}G needed"
            return 1
        fi
        info "Disk space: ${avail}G available"
    fi
}

cleanup_old() {
    local dir="$1"
    local days="$2"
    
    if [[ "$dir" != *:* ]]; then
        local count=$(find "$dir" -name "*.tar.gz" -type f -mtime +$days -delete -print | wc -l)
        if [ $count -gt 0 ]; then
            info "Cleaned up $count expired backups"
        fi
    fi
}

# ====== Lock mechanism ======
exec 200>"$LOCK_FILE"
flock -n 200 || {
    error "Backup script is already running (PID: $(cat $LOCK_FILE 2>/dev/null))"
    exit 1
}
echo $$ > "$LOCK_FILE"

# ====== Signal handling ======
cleanup() {
    rm -f "$LOCK_FILE"
    info "Backup script exiting"
}
trap cleanup EXIT INT TERM

# ====== Parameter parsing ======
SOURCE=""
DEST=""
RETENTION=$DEFAULT_RETENTION
EXCLUDE=()
VERBOSE=false
DRY_RUN=false

while getopts "s:d:r:e:vth" opt; do
    case $opt in
        s) SOURCE="$OPTARG" ;;
        d) DEST="$OPTARG" ;;
        r) RETENTION="$OPTARG" ;;
        e) EXCLUDE+=("$OPTARG") ;;
        v) VERBOSE=true ;;
        t) DRY_RUN=true ;;
        h) usage ;;
        *) usage ;;
    esac
done

# ====== Validation ======
if [ -z "$SOURCE" ] || [ -z "$DEST" ]; then
    error "Must specify -s (source directory) and -d (destination)"
    usage
fi

if [ ! -d "$SOURCE" ]; then
    error "Source directory '$SOURCE' does not exist"
    exit 1
fi

# ====== Main logic ======
info "========================================"
info "Backup Tool v${VERSION}"
info "Source: $SOURCE"
info "Destination: $DEST"
info "Retention: $RETENTION days"
if [ "$DRY_RUN" = true ]; then
    warn "Test mode - no actual backup will be created"
fi
info "========================================"

# 1. Check dependencies and space
check_deps
check_disk_space "$DEST"

# 2. Generate filename
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="backup_$(basename "$SOURCE")_${TIMESTAMP}.tar.gz"

# 3. Build exclude arguments
EXCLUDE_ARGS=""
for pattern in "${EXCLUDE[@]}"; do
    EXCLUDE_ARGS="$EXCLUDE_ARGS --exclude=$pattern"
done

# 4. Create compressed backup
if [ "$DRY_RUN" = false ]; then
    info "Creating compressed backup: $BACKUP_NAME"
    
    # Local backup
    if [[ "$DEST" != *:* ]]; then
        mkdir -p "$DEST"
        eval tar -czf "${DEST}/${BACKUP_NAME}" $EXCLUDE_ARGS "$SOURCE"
        success "✅ Local backup complete: ${DEST}/${BACKUP_NAME}"
        
        # Sync to remote
        if [ "$VERBOSE" = true ]; then
            ls -lh "${DEST}/${BACKUP_NAME}"
        fi
    else
        # Remote backup - compress locally then rsync
        local_temp="/tmp/${BACKUP_NAME}"
        eval tar -czf "$local_temp" $EXCLUDE_ARGS "$SOURCE"
        info "Transferring to remote: $DEST"
        rsync -avz --progress "$local_temp" "${DEST}/${BACKUP_NAME}"
        rm -f "$local_temp"
        success "✅ Remote backup complete: ${DEST}/${BACKUP_NAME}"
    fi
else
    warn "Test mode: Skipping backup creation"
fi

# 5. Clean up expired backups
if [ "$DRY_RUN" = false ] && [ $RETENTION -gt 0 ]; then
    cleanup_old "$DEST" $RETENTION
fi

# 6. Backup summary
info "Backup summary:"
info "  Source: $SOURCE"
info "  Destination: $DEST"
if [ "$DRY_RUN" = false ]; then
    info "  File: ${BACKUP_NAME}"
    if [[ "$DEST" != *:* ]]; then
        info "  Size: $(du -h "${DEST}/${BACKUP_NAME}" | cut -f1)"
    fi
fi
info "========================================"

success "✅ Backup operation complete"
TEXT
# Usage examples

# 1. Basic usage
./backup.sh -s /var/www/myapp -d /backup

# 2. Exclude directories + retain 7 days
./backup.sh -s /var/www/myapp -d /backup -e node_modules -e .git -r 7

# 3. Remote backup
./backup.sh -s /var/www -d user@backup-server:/backups -v

# 4. Test mode (no actual backup)
./backup.sh -s /var/www -d /backup -t

# 5. Add to cron (daily at 2 AM)
crontab -e
# Add:
0 2 * * * /opt/scripts/backup.sh -s /var/www/myapp -d /backup -e node_modules -r 30 > /dev/null 2>&1

❓ よくある質問

Q: バックアップスクリプトが同時に2回実行されるのを防ぐには? A: ファイルロック(flock)を使用:スクリプト開始時にロックファイルを作成しようとします。ロックが既に存在する場合は終了します。これにより、バックアップ完了前にcronが再度トリガーしても、2つのバックアップが同時実行されることはありません。

Q: バックアップファイルの保持期間はどのくらいにすべき? A: ストレージ容量とコンプライアンス要件によります。一般的な戦略:日次バックアップ30日間、週次バックアップ3ヶ月間、月次バックアップ1年間。--link-destや異なるcron頻度で実装します。

Q: バックアップの整合性を検証するには? A: sha256sumでチェックサムファイルを生成:sha256sum backup.tar.gz > backup.sha256。復元時にsha256sum -c backup.sha256で検証。

Q: rsyncは転送中断時に再開する? A: はい。rsyncはデフォルトで差分転送を行います。転送中のファイルが中断されても、次回のrsync実行で未完了部分のみ転送されます。--partialを追加すると部分的に転送されたファイルが保持されます。

Q: リモートサーバーバックアップに必要な設定は? A: 1) SSH鍵認証(ssh-keygen + ssh-copy-id)。2) リモートの宛先ディレクトリが存在する。3) リモートユーザーに書き込み権限がある。4) オプション:SSHパスワードレスログインの設定。


📖 まとめ


📝 練習問題

  1. 基本(難易度 ⭐):テストディレクトリとファイルを作成し、backup.sh/tmp/test-backup/にバックアップし、-e*.logファイルを除外して除外が機能したことを確認する
  2. 中級(難易度 ⭐⭐):cronで毎時バックアップを実行するように設定(3回以上の実行を観察)、バックアップをリモートrsyncに変更(複数マシンが利用可能な場合)
  3. 上級(難易度 ⭐⭐⭐):スクリプトにメールやシステム通知(wallmail、webhook)を追加し、バックアップ整合性検証(sha256sum)を実装し、バックアップ失敗時に自動アラートを送信
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%