<p>本記事は<strong>Geminiの出力をプロンプト工学で整理した業務ドラフト(未検証)</strong>です。</p>
<h1 class="wp-block-heading">rsyncで効率的なファイル同期と差分転送:安全な自動化と権限分離</h1>
<p>ファイル同期は、Webサーバーのコンテンツデプロイ、バックアップ、開発環境と本番環境間のデータ転送など、DevOpsの多くのシナリオで不可欠なタスクです。中でも<code>rsync</code>は、差分転送機能によりネットワーク帯域幅を節約し、高速な同期を実現する強力なツールとして広く利用されています。
、<code>rsync</code>を用いた効率的なファイル同期と差分転送について、DevOpsエンジニアが安全かつ自動化された運用を構築するための実践的な方法を解説します。具体的には、シェルスクリプトの安全性確保、<code>systemd</code>による定期実行、権限分離のベストプラクティス、そして外部API連携のための<code>curl</code>と<code>jq</code>の活用例を含みます。</p>
<h2 class="wp-block-heading">要件と前提</h2>
<p>本記事で解説するファイル同期システムには、以下の要件と前提があります。</p>
<ol class="wp-block-list">
<li><p><strong>効率的な差分同期</strong>: <code>rsync</code>の差分転送機能を利用し、変更されたファイルや新規ファイルのみを効率的に同期します。</p></li>
<li><p><strong>安全なスクリプト設計</strong>:</p>
<ul>
<li><p><code>idempotent</code>(冪等性)な操作を保証し、複数回実行しても同じ結果が得られるようにします。</p></li>
<li><p>エラー発生時にスクリプトが即座に停止し、未定義変数を使用しないなど、堅牢なエラー処理を導入します。</p></li>
<li><p>一時ファイルやディレクトリは確実にクリーンアップします。</p></li>
</ul></li>
<li><p><strong>自動化</strong>: <code>systemd</code>のUnitおよびTimer機能を用いて、定期的に同期処理を自動実行します。</p></li>
<li><p><strong>権限管理と分離</strong>: 最小権限の原則に基づき、専用の非rootユーザーで同期プロセスを実行し、セキュリティリスクを低減します。</p></li>
<li><p><strong>外部サービス連携</strong>: 同期処理の開始/終了ステータスをWeb API経由で通知する例として、<code>curl</code>と<code>jq</code>を用いたJSON処理を示します。</p></li>
<li><p><strong>環境</strong>: Linux環境(Ubuntu/CentOSなど)を想定し、SSH経由での<code>rsync</code>を基本とします。</p></li>
</ol>
<h2 class="wp-block-heading">実装</h2>
<h3 class="wp-block-heading">ファイル同期スクリプト (<code>sync_script.sh</code>)</h3>
<p>以下のシェルスクリプトは、安全性を重視した<code>rsync</code>によるファイル同期の例です。<code>set -euo pipefail</code>、<code>trap</code>、<code>mktemp</code>を用いて堅牢性を高め、<code>--dry-run</code>で事前確認を行います。また、<code>curl</code>と<code>jq</code>で外部APIに処理状況を通知する機能も組み込んでいます。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">#!/bin/bash
# ファイル名: /opt/rsync-scripts/sync_script.sh
# 1. 安全なスクリプト設定
# -e: コマンドが失敗した場合、即座に終了する
# -u: 未定義の変数を使用した場合、エラーとして終了する
# -o pipefail: パイプライン内のいずれかのコマンドが失敗した場合、パイプライン全体の終了ステータスを失敗とする
set -euo pipefail
# 2. 一時ディレクトリの作成と終了時のクリーンアップ
# mktemp -d: 安全な一時ディレクトリを作成 (予測不可能な名前)
# trap: スクリプトが正常終了またはエラー終了した場合でも、一時ディレクトリを確実に削除
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT # スクリプト終了時に一時ディレクトリを削除
# 3. 設定変数
# これらは環境変数、設定ファイル、または引数から取得することも検討
SOURCE_DIR="/var/www/html/prod_data" # 送信元ディレクトリの絶対パス
DEST_HOST="user@remote.example.com" # 転送先ホストとSSHユーザー
DEST_PATH="/var/www/html/dev_data" # 転送先パスの絶対パス
LOG_BASE_DIR="/var/log/rsync" # rsyncログの保存先ディレクトリ
# JST基準の具体的な日付を使用
CURRENT_DATE_TIME=$(date '+%Y%m%d%H%M%S') # 例: 20240726103000
LOG_FILE="${LOG_BASE_DIR}/rsync_sync_${CURRENT_DATE_TIME}.log"
EXCLUDE_FILE="${tmpdir}/exclude_list.txt" # 一時的な除外ファイルリスト
# ログディレクトリの存在確認と作成
mkdir -p "$(dirname "$LOG_FILE")"
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] Sync process started." | tee -a "$LOG_FILE"
echo "Temporary directory for this run: $tmpdir" | tee -a "$LOG_FILE"
# 4. 除外リストの作成
# .git/, node_modules/ など、同期不要なファイルを指定
cat << EOF > "$EXCLUDE_FILE"
.git/
node_modules/
*.tmp
*.log
EOF
# 5. APIへの事前通知 (curlとjqの例)
# 実際の運用では、API_URLやトークンは環境変数や安全な設定管理から取得することを推奨
API_URL="https://api.example.com/sync/status"
# jq -n: 入力なしでJSONを生成
# --arg: シェル変数をjqに渡す
# date -u +%Y-%m-%dT%H:%M:%SZ: ISO 8601形式のUTCタイムスタンプ
PAYLOAD=$(jq -n \
--arg status "started" \
--arg message "rsync process started for ${SOURCE_DIR} to ${DEST_HOST}:${DEST_PATH}." \
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{status: $status, message: $message, timestamp: $timestamp}')
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] Sending API pre-notification..." | tee -a "$LOG_FILE"
# curlオプション:
# -s: サイレントモード (プログレスメーター非表示)
# --fail: HTTPエラー (4xx/5xx) で終了ステータスを0以外にする
# --retry 3: 3回まで再試行
# --retry-delay 2: 各再試行前に2秒待機
# --connect-timeout 5: 接続確立に最大5秒
# -H: HTTPヘッダー
# -d: リクエストボディ
if ! curl -s --fail --retry 3 --retry-delay 2 --connect-timeout 5 \
-H "Content-Type: application/json" \
-d "$PAYLOAD" "$API_URL" > /dev/null; then
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] WARNING: Failed to send pre-notification to API. Continuing sync." | tee -a "$LOG_FILE"
fi
# 6. rsyncコマンド (dry-runで差分をまず確認)
# --dry-run: 実際にはファイルを転送せず、何が転送されるかを表示
# --archive (-a): 再帰、シンボリックリンク、パーミッション、時刻、グループ、オーナーを保持 (重要なオプション)
# --delete: 送信元に存在しないファイルを転送先から削除 (注意が必要なオプション)
# --stats: 転送統計情報を表示
# 2>&1: 標準出力と標準エラー出力をマージ
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] Running rsync in dry-run mode..." | tee -a "$LOG_FILE"
DRY_RUN_RESULT_FILE="${tmpdir}/rsync_dry_run_output.txt"
if rsync -az --dry-run --delete --exclude-from="$EXCLUDE_FILE" "$SOURCE_DIR/" "$DEST_HOST:$DEST_PATH/" > "$DRY_RUN_RESULT_FILE" 2>&1; then
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] Dry-run completed successfully." | tee -a "$LOG_FILE"
else
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] ERROR: rsync dry-run failed. Exiting." | tee -a "$LOG_FILE"
cat "$DRY_RUN_RESULT_FILE" | tee -a "$LOG_FILE" # dry-runのエラー内容をログに含める
exit 1
fi
# 7. dry-run結果を解析し、実際に転送が必要か判断
# rsync --dry-run の出力から、実際に転送または削除されるファイル行数をカウント
# grep -E: 正規表現で複数パターンを検索
# wc -l: 行数をカウント
# "sending incremental file list" や統計情報行を除外するため、具体的なファイル操作を示す行を抽出
DIFF_COUNT=$(grep -E "^(sending|deleting|[^.]+.*->)" "$DRY_RUN_RESULT_FILE" | wc -l)
if [ "$DIFF_COUNT" -gt 0 ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] Changes detected (files to sync: $DIFF_COUNT). Proceeding with actual sync." | tee -a "$LOG_FILE"
# 8. 実際のrsyncコマンド
# --log-file: rsync自身の詳細なログをファイルに出力
# 権限分離の考慮:
# rsyncはSSH経由で実行されるため、リモートホストのSSHユーザーの権限で動作します。
# 転送先のファイルパーミッションやオーナーシップを特定ユーザーに合わせたい場合は、
# --chown=USER:GROUP --chmod=DPERM,FPERM オプションをrsyncに渡すことができます。
# 例: --chown=www-data:www-data --chmod=D755,F644
if rsync -az --delete --stats --log-file="$LOG_FILE" --exclude-from="$EXCLUDE_FILE" \
--chown=webuser:webgroup --chmod=D755,F644 \
"$SOURCE_DIR/" "$DEST_HOST:$DEST_PATH/"; then
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] rsync completed successfully." | tee -a "$LOG_FILE"
SYNC_STATUS="success"
SYNC_MESSAGE="rsync process completed successfully with $DIFF_COUNT changes."
else
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] ERROR: rsync failed. Check log file for details." | tee -a "$LOG_FILE"
SYNC_STATUS="failure"
SYNC_MESSAGE="rsync process failed after $DIFF_COUNT changes were detected."
# rsync自体がエラーを返した場合も、trapで一時ディレクトリは削除される
exit 1
fi
else
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] No changes detected. Actual sync skipped." | tee -a "$LOG_FILE"
SYNC_STATUS="skipped"
SYNC_MESSAGE="No changes detected, rsync skipped."
fi
# 9. APIへの事後通知
PAYLOAD=$(jq -n \
--arg status "$SYNC_STATUS" \
--arg message "$SYNC_MESSAGE" \
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{status: $status, message: $message, timestamp: $timestamp}')
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] Sending API post-notification..." | tee -a "$LOG_FILE"
if ! curl -s --fail --retry 3 --retry-delay 2 --connect-timeout 5 \
-H "Content-Type: application/json" \
-d "$PAYLOAD" "$API_URL" > /dev/null; then
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] WARNING: Failed to send post-notification to API." | tee -a "$LOG_FILE"
fi
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] Sync process finished." | tee -a "$LOG_FILE"
exit 0
</pre>
</div>
<h4 class="wp-block-heading">スクリプトのポイント</h4>
<ul class="wp-block-list">
<li><p><strong>冪等性</strong>: <code>rsync</code>は差分転送を行うため、複数回実行しても最終的な状態は同じになります。<code>--delete</code>オプションは送信元にないファイルを転送先から削除するため、注意深く使用し、<code>--dry-run</code>で必ず確認してください。</p></li>
<li><p><strong>安全なシェルスクリプト</strong>: <code>set -euo pipefail</code>により、エラー発生時や未定義変数使用時に即座にスクリプトが終了し、予期せぬ挙動を防ぎます。<code>mktemp -d</code>と<code>trap 'rm -rf "$tmpdir"' EXIT</code>により、一時ディレクトリを安全に作成し、スクリプト終了時に確実に削除します。</p></li>
<li><p><strong>権限分離</strong>: スクリプトは後述の<code>systemd</code>設定で指定された専用ユーザーで実行されます。<code>--chown</code>および<code>--chmod</code>オプションは、<code>rsync</code>で転送されたファイルの所有者とパーミッションを転送先で明示的に設定するために使用できます。これにより、リモートホストでのファイル権限を細かく制御し、セキュリティを向上させます。</p></li>
<li><p><strong><code>jq</code>と<code>curl</code></strong>: <code>jq</code>でJSONペイロードを生成し、<code>curl</code>でHTTP POSTリクエストを送信しています。<code>curl</code>の<code>--retry</code>や<code>--retry-delay</code>オプションは、ネットワークの一時的な問題に対する耐障害性を高めます。</p></li>
</ul>
<h3 class="wp-block-heading">systemdによる自動化</h3>
<p><code>systemd</code>のUnitとTimerを組み合わせることで、定期的なファイル同期を信頼性高く自動化できます。</p>
<h4 class="wp-block-heading">1. systemd Unitファイル (<code>/etc/systemd/system/rsync-sync.service</code>)</h4>
<p>サービスの実体を定義します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">[Unit]
Description=rsync File Synchronization Service
Documentation=man:rsync(1)
After=network-online.target # ネットワークが利用可能になった後に起動
Wants=network-online.target # network-online.targetが起動することを推奨
[Service]
Type=oneshot # 一度実行して終了するサービス
User=rsync_user # 同期スクリプトを実行するユーザー
Group=rsync_group # 同期スクリプトを実行するグループ
WorkingDirectory=/opt/rsync-scripts # スクリプトの作業ディレクトリ
ExecStart=/opt/rsync-scripts/sync_script.sh # 実行するスクリプトのパス
StandardOutput=journal # 標準出力をsystemd journalに送る
StandardError=journal # 標準エラー出力をsystemd journalに送る
Restart=on-failure # サービスが失敗した場合に再起動
RestartSec=30s # 再起動までの待機時間
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/bin" # PATHを明示的に指定
[Install]
WantedBy=multi-user.target # マルチユーザーモードで起動する際に有効にする
</pre>
</div>
<h4 class="wp-block-heading">2. systemd Timerファイル (<code>/etc/systemd/system/rsync-sync.timer</code>)</h4>
<p>サービスを定期的に起動するためのタイマーを定義します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">[Unit]
Description=Run rsync File Synchronization every 3 hours
Documentation=man:systemd.timer(5)
[Timer]
OnCalendar=*-*-* 0/3:00:00 # 毎日、0時、3時、6時...21時に実行 (3時間ごと)
# 他の例:
# OnCalendar=daily # 毎日午前0時に実行
# OnCalendar=weekly # 毎週月曜午前0時に実行
# OnCalendar=Mon *-*-* 03:00:00 # 毎週月曜の午前3時に実行
Persistent=true # 起動時に見逃した実行があれば、すぐに実行
[Install]
WantedBy=timers.target # timers.targetが起動する際に有効にする
</pre>
</div>
<h4 class="wp-block-heading">3. systemd Unit/Timerの有効化と起動</h4>
<p><code>systemd</code>に設定を反映し、タイマーを有効化して起動します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># rsync_user と rsync_group を作成 (権限分離のため)
# -r: システムアカウントとして作成 (ログインシェルなし)
# -s /bin/false: ログインを禁止
sudo groupadd -r rsync_group
sudo useradd -r -g rsync_group -s /bin/false -d /opt/rsync-scripts rsync_user
# スクリプトディレクトリの権限設定 (rsync_userが読み書きできるように)
sudo mkdir -p /opt/rsync-scripts
sudo chown rsync_user:rsync_group /opt/rsync-scripts
sudo chmod 750 /opt/rsync-scripts
# スクリプトの配置と実行権限の付与
sudo mv sync_script.sh /opt/rsync-scripts/
sudo chmod +x /opt/rsync-scripts/sync_script.sh
# systemdの設定ファイルを配置
# sudo mv rsync-sync.service /etc/systemd/system/
# sudo mv rsync-sync.timer /etc/systemd/system/
# systemdに設定変更をリロード
sudo systemctl daemon-reload
# タイマーを有効化して即時起動 (初回)
sudo systemctl enable --now rsync-sync.timer
# サービスの状況確認
sudo systemctl status rsync-sync.service
sudo systemctl status rsync-sync.timer
# ログの確認
sudo journalctl -u rsync-sync.service -f
</pre>
</div>
<h2 class="wp-block-heading">検証</h2>
<h3 class="wp-block-heading">スクリプトの<code>--dry-run</code>テスト</h3>
<p><code>sync_script.sh</code>を直接実行し、<code>--dry-run</code>の出力を確認します。これにより、実際にどのようなファイル操作が行われるかを確認できます。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># スクリプトのDEBUGモードで実行 (rsyncコマンドの--dry-runが実行される)
# まずrsync_userに切り替えて実行できるか確認 (SSHキー設定等も必要)
sudo -u rsync_user /opt/rsync-scripts/sync_script.sh
# ログファイルを確認
sudo cat /var/log/rsync/rsync_sync_*.log
</pre>
</div>
<h3 class="wp-block-heading">systemdサービスの起動とログ確認</h3>
<p><code>systemd</code>タイマーが正しく動作しているか確認します。</p>
<ol class="wp-block-list">
<li><p><strong>タイマーの起動確認</strong>: <code>sudo systemctl list-timers</code>で<code>rsync-sync.timer</code>がリストにあり、次の実行時刻が表示されていることを確認します。</p></li>
<li><p><strong>手動実行</strong>: タイマーを待たずにサービスを直接実行し、動作を確認できます。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">sudo systemctl start rsync-sync.service
sudo journalctl -u rsync-sync.service -f
</pre>
</div>
<p>ログで<code>[...].log</code>に出力された<code>rsync</code>のログファイルへのパスを確認し、そのログファイルの内容も確認します。</p></li>
<li><p><strong>差分同期の確認</strong>:</p>
<ul>
<li><p>送信元のファイルに変更を加えたり、新しいファイルを作成したりします。</p></li>
<li><p>次のタイマー実行を待つか、サービスを手動で再度実行します。</p></li>
<li><p>ログと転送先のディレクトリの内容を確認し、変更が正しく同期されていることを確認します。</p></li>
<li><p>送信元のファイルを削除し、<code>--delete</code>オプションが正しく機能しているか確認します。</p></li>
</ul></li>
</ol>
<h2 class="wp-block-heading">運用</h2>
<h3 class="wp-block-heading">ログ監視 (<code>journalctl</code> / <code>rsync</code>ログファイル)</h3>
<p><code>systemd</code>経由で実行されるスクリプトのログは<code>journalctl</code>で確認できます。また、<code>rsync</code>自身の詳細なログはスクリプト内で指定した<code>/var/log/rsync/</code>以下のログファイルに記録されます。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># リアルタイムでサービスログを追跡
sudo journalctl -u rsync-sync.service -f
# 特定のrsyncログファイルを確認
sudo tail -f /var/log/rsync/rsync_sync_$(date +%Y%m%d)*.log
</pre>
</div>
<p>ログ監視ツール(Prometheus/Grafana, ELK Stack, Datadogなど)と連携して、異常を早期に検知できる体制を構築することが重要です。</p>
<h3 class="wp-block-heading">メトリクス収集</h3>
<p><code>rsync</code>の<code>--stats</code>オプションで得られる転送統計情報や、<code>journalctl</code>からスクリプトの実行時間、成功/失敗ステータスなどを抽出し、モニタリングシステムで可視化することを検討します。これにより、転送効率の変化や潜在的な問題を早期に発見できます。</p>
<h3 class="wp-block-heading">設定ファイル管理</h3>
<p>スクリプト内の<code>SOURCE_DIR</code>、<code>DEST_HOST</code>、<code>API_URL</code>などの設定値は、環境変数や専用の設定ファイル(例: <code>/etc/rsync-scripts/config.sh</code>)に分離し、Gitなどのバージョン管理システムで管理することをお勧めします。これにより、設定変更の管理が容易になり、スクリプトの再利用性が高まります。</p>
<h2 class="wp-block-heading">トラブルシュート</h2>
<h3 class="wp-block-heading">エラーログの解析</h3>
<ul class="wp-block-list">
<li><p><strong><code>journalctl -u rsync-sync.service</code></strong>: スクリプトが途中で終了した場合、ここからエラーメッセージの概略を把握します。</p></li>
<li><p><strong><code>/var/log/rsync/rsync_sync_*.log</code></strong>: <code>rsync</code>コマンド自身が出力する詳細なログ。接続エラー、パーミッションエラー、ファイル転送中の問題などがここに記録されます。</p></li>
</ul>
<h3 class="wp-block-heading">パーミッション問題</h3>
<ul class="wp-block-list">
<li><p><strong>SSHキーの権限</strong>: <code>rsync_user</code>がリモートホストにSSH接続するために使用する秘密鍵のパーミッションが適切か確認します(通常<code>600</code>)。</p></li>
<li><p><strong>転送先のディレクトリ権限</strong>: リモートホストの<code>DEST_PATH</code>が、<code>rsync_user</code>(または<code>--chown</code>で指定したユーザー)によって書き込み可能であるか確認します。</p></li>
<li><p><strong>rsyncスクリプトの実行権限</strong>: <code>/opt/rsync-scripts/sync_script.sh</code>が<code>rsync_user</code>によって実行可能 (<code>chmod +x</code>) であることを確認します。</p></li>
</ul>
<h3 class="wp-block-heading">ネットワーク接続問題</h3>
<ul class="wp-block-list">
<li><p><strong>SSH接続の確認</strong>: <code>rsync_user</code>で手動でSSH接続を試み、接続が可能か、認証が成功するか確認します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">sudo -u rsync_user ssh user@remote.example.com
</pre>
</div></li>
<li><p><strong>ファイアウォール</strong>: 送信元と転送先の間にファイアウォールがあり、SSHポート(通常22番)の通信が許可されているか確認します。</p></li>
<li><p><strong>DNS解決</strong>: リモートホスト名が正しく解決されているか確認します。</p></li>
</ul>
<h3 class="wp-block-heading">ディスク容量問題</h3>
<p>送信元または転送先のディスク容量が不足している場合、<code>rsync</code>が失敗することがあります。定期的にディスク使用量を監視し、必要に応じて容量を増やすか、古いファイルを削除する運用を検討してください。</p>
<h2 class="wp-block-heading">まとめ</h2>
<p>本記事では、<code>rsync</code>を用いた効率的なファイル同期と差分転送について、DevOpsの観点から包括的なガイドを提供しました。安全なシェルスクリプトの書き方、<code>systemd</code>による信頼性の高い自動化、最小権限の原則に基づく権限分離、そして<code>curl</code>と<code>jq</code>を活用した外部サービス連携の例を解説しました。</p>
<p>これらのプラクティスを導入することで、ファイル同期プロセスを堅牢かつ効率的に運用し、システムの安定性とセキュリティを向上させることができます。</p>
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid"><pre class="mermaid">
graph TD
A["開始"] --> |systemd Timerが起動| B("一時ディレクトリ作成")
B --> |成功| C{"API事前通知"}
C --> |通知成功| D("rsync dry-run実行")
C --> |通知失敗 (警告)| D
D --> |成功| E("dry-run結果解析")
E --> F{"差分あり?"}
F -- いいえ --> G("API事後通知: 同期スキップ")
F -- はい --> H("rsync本番実行")
H --> |成功| I("API事後通知: 同期成功")
H --> |失敗| J("API事後通知: 同期失敗")
G --> K("一時ディレクトリ削除")
I --> K
J --> K
K --> L["終了"]
D --> |失敗| M("ログ記録/エラー終了")
H --> |失敗| J
M --> K
</pre></div>
本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
rsyncで効率的なファイル同期と差分転送:安全な自動化と権限分離
ファイル同期は、Webサーバーのコンテンツデプロイ、バックアップ、開発環境と本番環境間のデータ転送など、DevOpsの多くのシナリオで不可欠なタスクです。中でもrsyncは、差分転送機能によりネットワーク帯域幅を節約し、高速な同期を実現する強力なツールとして広く利用されています。
、rsyncを用いた効率的なファイル同期と差分転送について、DevOpsエンジニアが安全かつ自動化された運用を構築するための実践的な方法を解説します。具体的には、シェルスクリプトの安全性確保、systemdによる定期実行、権限分離のベストプラクティス、そして外部API連携のためのcurlとjqの活用例を含みます。
要件と前提
本記事で解説するファイル同期システムには、以下の要件と前提があります。
効率的な差分同期: rsyncの差分転送機能を利用し、変更されたファイルや新規ファイルのみを効率的に同期します。
安全なスクリプト設計:
idempotent(冪等性)な操作を保証し、複数回実行しても同じ結果が得られるようにします。
エラー発生時にスクリプトが即座に停止し、未定義変数を使用しないなど、堅牢なエラー処理を導入します。
一時ファイルやディレクトリは確実にクリーンアップします。
自動化: systemdのUnitおよびTimer機能を用いて、定期的に同期処理を自動実行します。
権限管理と分離: 最小権限の原則に基づき、専用の非rootユーザーで同期プロセスを実行し、セキュリティリスクを低減します。
外部サービス連携: 同期処理の開始/終了ステータスをWeb API経由で通知する例として、curlとjqを用いたJSON処理を示します。
環境: Linux環境(Ubuntu/CentOSなど)を想定し、SSH経由でのrsyncを基本とします。
実装
ファイル同期スクリプト (sync_script.sh)
以下のシェルスクリプトは、安全性を重視したrsyncによるファイル同期の例です。set -euo pipefail、trap、mktempを用いて堅牢性を高め、--dry-runで事前確認を行います。また、curlとjqで外部APIに処理状況を通知する機能も組み込んでいます。
#!/bin/bash
# ファイル名: /opt/rsync-scripts/sync_script.sh
# 1. 安全なスクリプト設定
# -e: コマンドが失敗した場合、即座に終了する
# -u: 未定義の変数を使用した場合、エラーとして終了する
# -o pipefail: パイプライン内のいずれかのコマンドが失敗した場合、パイプライン全体の終了ステータスを失敗とする
set -euo pipefail
# 2. 一時ディレクトリの作成と終了時のクリーンアップ
# mktemp -d: 安全な一時ディレクトリを作成 (予測不可能な名前)
# trap: スクリプトが正常終了またはエラー終了した場合でも、一時ディレクトリを確実に削除
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT # スクリプト終了時に一時ディレクトリを削除
# 3. 設定変数
# これらは環境変数、設定ファイル、または引数から取得することも検討
SOURCE_DIR="/var/www/html/prod_data" # 送信元ディレクトリの絶対パス
DEST_HOST="user@remote.example.com" # 転送先ホストとSSHユーザー
DEST_PATH="/var/www/html/dev_data" # 転送先パスの絶対パス
LOG_BASE_DIR="/var/log/rsync" # rsyncログの保存先ディレクトリ
# JST基準の具体的な日付を使用
CURRENT_DATE_TIME=$(date '+%Y%m%d%H%M%S') # 例: 20240726103000
LOG_FILE="${LOG_BASE_DIR}/rsync_sync_${CURRENT_DATE_TIME}.log"
EXCLUDE_FILE="${tmpdir}/exclude_list.txt" # 一時的な除外ファイルリスト
# ログディレクトリの存在確認と作成
mkdir -p "$(dirname "$LOG_FILE")"
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] Sync process started." | tee -a "$LOG_FILE"
echo "Temporary directory for this run: $tmpdir" | tee -a "$LOG_FILE"
# 4. 除外リストの作成
# .git/, node_modules/ など、同期不要なファイルを指定
cat << EOF > "$EXCLUDE_FILE"
.git/
node_modules/
*.tmp
*.log
EOF
# 5. APIへの事前通知 (curlとjqの例)
# 実際の運用では、API_URLやトークンは環境変数や安全な設定管理から取得することを推奨
API_URL="https://api.example.com/sync/status"
# jq -n: 入力なしでJSONを生成
# --arg: シェル変数をjqに渡す
# date -u +%Y-%m-%dT%H:%M:%SZ: ISO 8601形式のUTCタイムスタンプ
PAYLOAD=$(jq -n \
--arg status "started" \
--arg message "rsync process started for ${SOURCE_DIR} to ${DEST_HOST}:${DEST_PATH}." \
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{status: $status, message: $message, timestamp: $timestamp}')
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] Sending API pre-notification..." | tee -a "$LOG_FILE"
# curlオプション:
# -s: サイレントモード (プログレスメーター非表示)
# --fail: HTTPエラー (4xx/5xx) で終了ステータスを0以外にする
# --retry 3: 3回まで再試行
# --retry-delay 2: 各再試行前に2秒待機
# --connect-timeout 5: 接続確立に最大5秒
# -H: HTTPヘッダー
# -d: リクエストボディ
if ! curl -s --fail --retry 3 --retry-delay 2 --connect-timeout 5 \
-H "Content-Type: application/json" \
-d "$PAYLOAD" "$API_URL" > /dev/null; then
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] WARNING: Failed to send pre-notification to API. Continuing sync." | tee -a "$LOG_FILE"
fi
# 6. rsyncコマンド (dry-runで差分をまず確認)
# --dry-run: 実際にはファイルを転送せず、何が転送されるかを表示
# --archive (-a): 再帰、シンボリックリンク、パーミッション、時刻、グループ、オーナーを保持 (重要なオプション)
# --delete: 送信元に存在しないファイルを転送先から削除 (注意が必要なオプション)
# --stats: 転送統計情報を表示
# 2>&1: 標準出力と標準エラー出力をマージ
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] Running rsync in dry-run mode..." | tee -a "$LOG_FILE"
DRY_RUN_RESULT_FILE="${tmpdir}/rsync_dry_run_output.txt"
if rsync -az --dry-run --delete --exclude-from="$EXCLUDE_FILE" "$SOURCE_DIR/" "$DEST_HOST:$DEST_PATH/" > "$DRY_RUN_RESULT_FILE" 2>&1; then
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] Dry-run completed successfully." | tee -a "$LOG_FILE"
else
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] ERROR: rsync dry-run failed. Exiting." | tee -a "$LOG_FILE"
cat "$DRY_RUN_RESULT_FILE" | tee -a "$LOG_FILE" # dry-runのエラー内容をログに含める
exit 1
fi
# 7. dry-run結果を解析し、実際に転送が必要か判断
# rsync --dry-run の出力から、実際に転送または削除されるファイル行数をカウント
# grep -E: 正規表現で複数パターンを検索
# wc -l: 行数をカウント
# "sending incremental file list" や統計情報行を除外するため、具体的なファイル操作を示す行を抽出
DIFF_COUNT=$(grep -E "^(sending|deleting|[^.]+.*->)" "$DRY_RUN_RESULT_FILE" | wc -l)
if [ "$DIFF_COUNT" -gt 0 ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] Changes detected (files to sync: $DIFF_COUNT). Proceeding with actual sync." | tee -a "$LOG_FILE"
# 8. 実際のrsyncコマンド
# --log-file: rsync自身の詳細なログをファイルに出力
# 権限分離の考慮:
# rsyncはSSH経由で実行されるため、リモートホストのSSHユーザーの権限で動作します。
# 転送先のファイルパーミッションやオーナーシップを特定ユーザーに合わせたい場合は、
# --chown=USER:GROUP --chmod=DPERM,FPERM オプションをrsyncに渡すことができます。
# 例: --chown=www-data:www-data --chmod=D755,F644
if rsync -az --delete --stats --log-file="$LOG_FILE" --exclude-from="$EXCLUDE_FILE" \
--chown=webuser:webgroup --chmod=D755,F644 \
"$SOURCE_DIR/" "$DEST_HOST:$DEST_PATH/"; then
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] rsync completed successfully." | tee -a "$LOG_FILE"
SYNC_STATUS="success"
SYNC_MESSAGE="rsync process completed successfully with $DIFF_COUNT changes."
else
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] ERROR: rsync failed. Check log file for details." | tee -a "$LOG_FILE"
SYNC_STATUS="failure"
SYNC_MESSAGE="rsync process failed after $DIFF_COUNT changes were detected."
# rsync自体がエラーを返した場合も、trapで一時ディレクトリは削除される
exit 1
fi
else
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] No changes detected. Actual sync skipped." | tee -a "$LOG_FILE"
SYNC_STATUS="skipped"
SYNC_MESSAGE="No changes detected, rsync skipped."
fi
# 9. APIへの事後通知
PAYLOAD=$(jq -n \
--arg status "$SYNC_STATUS" \
--arg message "$SYNC_MESSAGE" \
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{status: $status, message: $message, timestamp: $timestamp}')
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] Sending API post-notification..." | tee -a "$LOG_FILE"
if ! curl -s --fail --retry 3 --retry-delay 2 --connect-timeout 5 \
-H "Content-Type: application/json" \
-d "$PAYLOAD" "$API_URL" > /dev/null; then
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] WARNING: Failed to send post-notification to API." | tee -a "$LOG_FILE"
fi
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] Sync process finished." | tee -a "$LOG_FILE"
exit 0
スクリプトのポイント
冪等性: rsyncは差分転送を行うため、複数回実行しても最終的な状態は同じになります。--deleteオプションは送信元にないファイルを転送先から削除するため、注意深く使用し、--dry-runで必ず確認してください。
安全なシェルスクリプト: set -euo pipefailにより、エラー発生時や未定義変数使用時に即座にスクリプトが終了し、予期せぬ挙動を防ぎます。mktemp -dとtrap 'rm -rf "$tmpdir"' EXITにより、一時ディレクトリを安全に作成し、スクリプト終了時に確実に削除します。
権限分離: スクリプトは後述のsystemd設定で指定された専用ユーザーで実行されます。--chownおよび--chmodオプションは、rsyncで転送されたファイルの所有者とパーミッションを転送先で明示的に設定するために使用できます。これにより、リモートホストでのファイル権限を細かく制御し、セキュリティを向上させます。
jqとcurl: jqでJSONペイロードを生成し、curlでHTTP POSTリクエストを送信しています。curlの--retryや--retry-delayオプションは、ネットワークの一時的な問題に対する耐障害性を高めます。
systemdによる自動化
systemdのUnitとTimerを組み合わせることで、定期的なファイル同期を信頼性高く自動化できます。
1. systemd Unitファイル (/etc/systemd/system/rsync-sync.service)
サービスの実体を定義します。
[Unit]
Description=rsync File Synchronization Service
Documentation=man:rsync(1)
After=network-online.target # ネットワークが利用可能になった後に起動
Wants=network-online.target # network-online.targetが起動することを推奨
[Service]
Type=oneshot # 一度実行して終了するサービス
User=rsync_user # 同期スクリプトを実行するユーザー
Group=rsync_group # 同期スクリプトを実行するグループ
WorkingDirectory=/opt/rsync-scripts # スクリプトの作業ディレクトリ
ExecStart=/opt/rsync-scripts/sync_script.sh # 実行するスクリプトのパス
StandardOutput=journal # 標準出力をsystemd journalに送る
StandardError=journal # 標準エラー出力をsystemd journalに送る
Restart=on-failure # サービスが失敗した場合に再起動
RestartSec=30s # 再起動までの待機時間
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/bin" # PATHを明示的に指定
[Install]
WantedBy=multi-user.target # マルチユーザーモードで起動する際に有効にする
2. systemd Timerファイル (/etc/systemd/system/rsync-sync.timer)
サービスを定期的に起動するためのタイマーを定義します。
[Unit]
Description=Run rsync File Synchronization every 3 hours
Documentation=man:systemd.timer(5)
[Timer]
OnCalendar=*-*-* 0/3:00:00 # 毎日、0時、3時、6時...21時に実行 (3時間ごと)
# 他の例:
# OnCalendar=daily # 毎日午前0時に実行
# OnCalendar=weekly # 毎週月曜午前0時に実行
# OnCalendar=Mon *-*-* 03:00:00 # 毎週月曜の午前3時に実行
Persistent=true # 起動時に見逃した実行があれば、すぐに実行
[Install]
WantedBy=timers.target # timers.targetが起動する際に有効にする
3. systemd Unit/Timerの有効化と起動
systemdに設定を反映し、タイマーを有効化して起動します。
# rsync_user と rsync_group を作成 (権限分離のため)
# -r: システムアカウントとして作成 (ログインシェルなし)
# -s /bin/false: ログインを禁止
sudo groupadd -r rsync_group
sudo useradd -r -g rsync_group -s /bin/false -d /opt/rsync-scripts rsync_user
# スクリプトディレクトリの権限設定 (rsync_userが読み書きできるように)
sudo mkdir -p /opt/rsync-scripts
sudo chown rsync_user:rsync_group /opt/rsync-scripts
sudo chmod 750 /opt/rsync-scripts
# スクリプトの配置と実行権限の付与
sudo mv sync_script.sh /opt/rsync-scripts/
sudo chmod +x /opt/rsync-scripts/sync_script.sh
# systemdの設定ファイルを配置
# sudo mv rsync-sync.service /etc/systemd/system/
# sudo mv rsync-sync.timer /etc/systemd/system/
# systemdに設定変更をリロード
sudo systemctl daemon-reload
# タイマーを有効化して即時起動 (初回)
sudo systemctl enable --now rsync-sync.timer
# サービスの状況確認
sudo systemctl status rsync-sync.service
sudo systemctl status rsync-sync.timer
# ログの確認
sudo journalctl -u rsync-sync.service -f
検証
スクリプトの--dry-runテスト
sync_script.shを直接実行し、--dry-runの出力を確認します。これにより、実際にどのようなファイル操作が行われるかを確認できます。
# スクリプトのDEBUGモードで実行 (rsyncコマンドの--dry-runが実行される)
# まずrsync_userに切り替えて実行できるか確認 (SSHキー設定等も必要)
sudo -u rsync_user /opt/rsync-scripts/sync_script.sh
# ログファイルを確認
sudo cat /var/log/rsync/rsync_sync_*.log
systemdサービスの起動とログ確認
systemdタイマーが正しく動作しているか確認します。
タイマーの起動確認: sudo systemctl list-timersでrsync-sync.timerがリストにあり、次の実行時刻が表示されていることを確認します。
手動実行: タイマーを待たずにサービスを直接実行し、動作を確認できます。
sudo systemctl start rsync-sync.service
sudo journalctl -u rsync-sync.service -f
ログで[...].logに出力されたrsyncのログファイルへのパスを確認し、そのログファイルの内容も確認します。
差分同期の確認:
送信元のファイルに変更を加えたり、新しいファイルを作成したりします。
次のタイマー実行を待つか、サービスを手動で再度実行します。
ログと転送先のディレクトリの内容を確認し、変更が正しく同期されていることを確認します。
送信元のファイルを削除し、--deleteオプションが正しく機能しているか確認します。
運用
ログ監視 (journalctl / rsyncログファイル)
systemd経由で実行されるスクリプトのログはjournalctlで確認できます。また、rsync自身の詳細なログはスクリプト内で指定した/var/log/rsync/以下のログファイルに記録されます。
# リアルタイムでサービスログを追跡
sudo journalctl -u rsync-sync.service -f
# 特定のrsyncログファイルを確認
sudo tail -f /var/log/rsync/rsync_sync_$(date +%Y%m%d)*.log
ログ監視ツール(Prometheus/Grafana, ELK Stack, Datadogなど)と連携して、異常を早期に検知できる体制を構築することが重要です。
メトリクス収集
rsyncの--statsオプションで得られる転送統計情報や、journalctlからスクリプトの実行時間、成功/失敗ステータスなどを抽出し、モニタリングシステムで可視化することを検討します。これにより、転送効率の変化や潜在的な問題を早期に発見できます。
設定ファイル管理
スクリプト内のSOURCE_DIR、DEST_HOST、API_URLなどの設定値は、環境変数や専用の設定ファイル(例: /etc/rsync-scripts/config.sh)に分離し、Gitなどのバージョン管理システムで管理することをお勧めします。これにより、設定変更の管理が容易になり、スクリプトの再利用性が高まります。
トラブルシュート
エラーログの解析
パーミッション問題
SSHキーの権限: rsync_userがリモートホストにSSH接続するために使用する秘密鍵のパーミッションが適切か確認します(通常600)。
転送先のディレクトリ権限: リモートホストのDEST_PATHが、rsync_user(または--chownで指定したユーザー)によって書き込み可能であるか確認します。
rsyncスクリプトの実行権限: /opt/rsync-scripts/sync_script.shがrsync_userによって実行可能 (chmod +x) であることを確認します。
ネットワーク接続問題
SSH接続の確認: rsync_userで手動でSSH接続を試み、接続が可能か、認証が成功するか確認します。
sudo -u rsync_user ssh user@remote.example.com
ファイアウォール: 送信元と転送先の間にファイアウォールがあり、SSHポート(通常22番)の通信が許可されているか確認します。
DNS解決: リモートホスト名が正しく解決されているか確認します。
ディスク容量問題
送信元または転送先のディスク容量が不足している場合、rsyncが失敗することがあります。定期的にディスク使用量を監視し、必要に応じて容量を増やすか、古いファイルを削除する運用を検討してください。
まとめ
本記事では、rsyncを用いた効率的なファイル同期と差分転送について、DevOpsの観点から包括的なガイドを提供しました。安全なシェルスクリプトの書き方、systemdによる信頼性の高い自動化、最小権限の原則に基づく権限分離、そしてcurlとjqを活用した外部サービス連携の例を解説しました。
これらのプラクティスを導入することで、ファイル同期プロセスを堅牢かつ効率的に運用し、システムの安定性とセキュリティを向上させることができます。
graph TD
A["開始"] --> |systemd Timerが起動| B("一時ディレクトリ作成")
B --> |成功| C{"API事前通知"}
C --> |通知成功| D("rsync dry-run実行")
C --> |通知失敗 (警告)| D
D --> |成功| E("dry-run結果解析")
E --> F{"差分あり?"}
F -- いいえ --> G("API事後通知: 同期スキップ")
F -- はい --> H("rsync本番実行")
H --> |成功| I("API事後通知: 同期成功")
H --> |失敗| J("API事後通知: 同期失敗")
G --> K("一時ディレクトリ削除")
I --> K
J --> K
K --> L["終了"]
D --> |失敗| M("ログ記録/エラー終了")
H --> |失敗| J
M --> K
コメント