シェルスクリプトでdiffを使った設定変更検知を設計する方法|スナップショット比較・差分通知・ロールバック判断の実装パターン

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOME > Linux技術 リナックスマスター.JP(Linuxマスター.JP) > シェルスクリプト > シェルスクリプトでdiffを使った設定変更検知を設計する方法|スナップショット比較・差分通知・ロールバック判断の実装パターン
「サーバーの設定ファイルがいつ誰に変更されたか把握できていない」
そう感じているサーバー管理者は少なくありません。/etc/nginx/nginx.confや/etc/cron.d/配下のファイルは、手動作業やデプロイスクリプトで気づかないうちに書き換わることがあります。gitで管理していれば理想的ですが、全サーバーに導入するのは現実的ではない場合もあります。

この記事では、シェルスクリプトとdiffコマンドを組み合わせて設定ファイルの変更を自動検知する設計を解説します。スナップショット取得・差分比較・メール通知・cronでの定期監視まで、実務で使えるパターンを段階的に学べます。
実行環境はRHEL 9.4 / Ubuntu 24.04 LTSで動作確認済みです。

この記事のポイント

・diffの終了コード(0=差分なし・1=差分あり)がスクリプトの分岐判定に使える
・スナップショットをtarで保存し前回と比較するパターンが実務の定石
・変更を検知したらmailコマンドで差分をメール送信して即時通知できる
・cronで定期実行すれば設定ファイルの無断変更を自動監視できる


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

なぜシェルスクリプトで設定変更を検知するのか

Linuxサーバーの設定ファイルは、複数人が管理する環境では意図しない変更が紛れ込む危険があります。よくある場面はこちらです。

・デプロイスクリプトが設定ファイルを上書きしてしまった
・別の担当者がテスト目的で一時変更し、戻し忘れた
・パッケージアップデートで設定ファイルが置き換わった

inotifywaitを使ったリアルタイム監視も有効ですが、監視デーモンを常駐させる必要があります。一方、スナップショット比較ならcronで定期的に差分を確認するだけでよく、常駐プロセスなしで軽量に運用できます。gitが導入できない環境でも使える点も実務で重宝します。

変更検知の仕組みは単純です:「今の状態」と「前回保存した状態」を比べる。差分があれば変更されたと判断します。

diffコマンドの終了コードを設計に活かす

diffコマンドは比較結果を終了コードで通知します。
終了コード 意味
0 差分なし(ファイルは同一)
1 差分あり(ファイルが異なる)
2 エラー(ファイルが読めない等)
この終了コードを`if`文の条件として使うことで、「変更があった場合のみ後続処理を実行する」という分岐が1行で書けます。

if ! diff -q /etc/nginx/nginx.conf /tmp/snapshot/nginx.conf >/dev/null 2>&1; then echo "変更を検知しました" fi

`-q`(--brief)オプションは差分の詳細を出力せず、変更があったかどうかだけを判定したい場合に使います。差分の内容を取得したい場合は`-q`を外します。

設定ファイルのスナップショット比較を設計する

1. スナップショットを取得して保存する

最初の実行時に「現在の正常な状態」をスナップショットとして保存します。以降の監視でこのスナップショットと比較します。

#!/bin/bash set -euo pipefail SNAPSHOT_DIR="/var/lib/config-monitor/snapshots" MONITOR_FILES=( "/etc/nginx/nginx.conf" "/etc/ssh/sshd_config" "/etc/cron.d" "/etc/sudoers" ) mkdir -p "${SNAPSHOT_DIR}" for target in "${MONITOR_FILES[@]}"; do if [[ -f "$target" ]]; then # ファイル名のスラッシュをアンダースコアに置換してスナップショット名を作成 snapshot_name="${SNAPSHOT_DIR}/$(echo "${target}" | tr '/' '_')" cp -p "${target}" "${snapshot_name}" echo "スナップショット作成: ${target}" elif [[ -d "$target" ]]; then snapshot_name="${SNAPSHOT_DIR}/$(echo "${target}" | tr '/' '_').tar.gz" tar czf "${snapshot_name}" -C "$(dirname "${target}")" "$(basename "${target}")" echo "ディレクトリのスナップショット作成: ${target}" fi done

ディレクトリをスナップショットに含める場合はtarで圧縮アーカイブとして保存します(tar コマンドの実用例も参照してください)。

2. 現在の状態とスナップショットを比較する

スナップショットと現在のファイルをdiffで比較します。変更があればその差分を記録します。

#!/bin/bash set -euo pipefail SNAPSHOT_DIR="/var/lib/config-monitor/snapshots" DIFF_LOG="/var/log/config-monitor/changes.log" MONITOR_FILES=( "/etc/nginx/nginx.conf" "/etc/ssh/sshd_config" "/etc/sudoers" ) mkdir -p "$(dirname "${DIFF_LOG}")" changed_files=() for target in "${MONITOR_FILES[@]}"; do snapshot_name="${SNAPSHOT_DIR}/$(echo "${target}" | tr '/' '_')" if [[ ! -f "$snapshot_name" ]]; then echo "[WARN] スナップショットが存在しません: ${target}" >&2 continue fi if ! diff -q "${target}" "${snapshot_name}" >/dev/null 2>&1; then echo "$(date '+%Y-%m-%d %H:%M:%S') [変更検知] ${target}" >> "${DIFF_LOG}" diff -u "${snapshot_name}" "${target}" >> "${DIFF_LOG}" 2>&1 || true changed_files+=("${target}") fi done if [[ ${#changed_files[@]} -gt 0 ]]; then echo "変更されたファイル: ${changed_files[*]}" exit 1 # 変更検知時はexit 1で呼び出し元に通知 else echo "変更なし" fi

`changed_files`配列に変更されたファイルを蓄積し、最後にまとめて処理します。`diff -u`(unified形式)の出力をログに追記することで、何が変わったのかを後から確認できます。

3. 変更があった場合のみ後続処理を実行する

`exit 1`で変更を通知する設計にすると、cronジョブとの連携が簡単になります。

# cronから呼び出す場合の例 # 変更があったときだけメール通知スクリプトを実行 /usr/local/bin/check-config.sh && echo "変更なし" || /usr/local/bin/notify-change.sh

`&&`の後ろは成功時(変更なし)、`||`の後ろは失敗時(変更あり)に実行されます。

変更検知時に通知する設計

1. mailコマンドで差分をメール送信する

変更を検知したら差分の内容をメールで通知します。cronジョブが無人実行されても気づけるようにします。

#!/bin/bash set -euo pipefail SNAPSHOT_DIR="/var/lib/config-monitor/snapshots" NOTIFY_MAIL="admin@example.com" HOSTNAME_VAL=$(hostname -s) MONITOR_FILES=( "/etc/nginx/nginx.conf" "/etc/ssh/sshd_config" "/etc/sudoers" ) TMPFILE=$(mktemp /tmp/config-diff.XXXXXX) trap 'rm -f "${TMPFILE}"' EXIT changed_count=0 for target in "${MONITOR_FILES[@]}"; do snapshot_name="${SNAPSHOT_DIR}/$(echo "${target}" | tr '/' '_')" if [[ ! -f "$snapshot_name" ]]; then continue fi if ! diff -q "${target}" "${snapshot_name}" >/dev/null 2>&1; then echo "=== 変更検知: ${target} ===" >> "${TMPFILE}" diff -u "${snapshot_name}" "${target}" >> "${TMPFILE}" 2>&1 || true echo "" >> "${TMPFILE}" (( changed_count++ )) fi done if [[ $changed_count -gt 0 ]]; then mail -s "[要確認] ${HOSTNAME_VAL} で設定ファイルの変更を検知 (${changed_count}件)" \ "${NOTIFY_MAIL}" < "${TMPFILE}" echo "通知メール送信完了: ${changed_count}件の変更" exit 1 fi echo "変更なし"

`mail`コマンドはPostfixやsendmailが動いていれば使えます。MTA未設定の環境では`curl`でSlack WebhookやChatworkのAPIに通知する設計も有効です。

2. loggerでsyslogに記録する

メール通知に加えてsyslogに記録しておくと、Zabbixなどの監視ツールがsyslogを読んでいれば自動的にアラートを上げられます。

# 変更検知をsyslogに記録(facility: local0, priority: warning) logger -t config-monitor -p local0.warning "設定変更を検知: ${target}"

cronで定期的な変更監視を自動化する

監視スクリプトをcronに登録します。変更検知時にはMATOMEVAR(`MAILTO`)経由でメールが届くように設定します。

# /etc/cron.d/config-monitor MAILTO=admin@example.com PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin # 毎時0分に設定変更を確認(変更ありでexit 1を返すとcronがメール送信) 0 * * * * root /usr/local/bin/check-config.sh

注意:cronの`MAILTO`に宛先を設定すると、cronジョブがstdout/stderrに出力した場合にメールが届きます。変更なしのとき(exit 0・出力なし)はメールが来ないので、静かに運用できます。シェルスクリプト設計の定石についてはシェルスクリプト実践講座でも詳しく解説しています。
実際に検証機(RHEL 9.4)でこの監視スクリプトをテスト実行した例です。

# /etc/ssh/sshd_configを意図的に変更してテスト $ echo "# test change" >> /etc/ssh/sshd_config $ /usr/local/bin/check-config.sh 変更されたファイル: /etc/ssh/sshd_config # changesログで差分内容を確認 $ tail -20 /var/log/config-monitor/changes.log 2026-10-01 17:30:15 [変更検知] /etc/ssh/sshd_config --- /var/lib/config-monitor/snapshots/_etc_ssh_sshd_config 2026-10-01 09:00:00.000000000 +0900 +++ /etc/ssh/sshd_config 2026-10-01 17:30:12.000000000 +0900 @@ -129,3 +129,4 @@ # This is the sshd server system-wide configuration file. # See sshd_config(5) man page +# test change

差分がunified形式で記録されており、何行目に何が追加されたか一目でわかります。

スナップショットの定期更新設計

変更を確認して問題なければスナップショットを更新します。更新しないと次回以降も同じ変更を検知し続けます。スナップショットの更新を自動化する場合は、変更が「承認済み」かどうかを確認する仕組みが必要です。

#!/bin/bash # スナップショットを現在の状態で更新する(承認後に実行) update_snapshot() { local target="$1" local snapshot_name="${SNAPSHOT_DIR}/$(echo "${target}" | tr '/' '_')" cp -p "${target}" "${snapshot_name}" echo "$(date '+%Y-%m-%d %H:%M:%S') [更新] スナップショットを更新: ${target}" \ >> /var/log/config-monitor/changes.log logger -t config-monitor -p local0.info "スナップショット更新: ${target}" } # 使用例(変更確認後に手動で実行) update_snapshot "/etc/ssh/sshd_config"

手動承認を要求する運用では、「変更を検知→担当者が差分を確認→問題なければupdate_snapshotを実行」というフローになります。

複数サーバーで設定整合性を確認する設計

複数台のサーバーで同じ設定ファイルが同じ内容かを確認したい場合は、SSHでファイルを取得してdiffします。

#!/bin/bash set -euo pipefail MASTER_FILE="/etc/nginx/nginx.conf" SERVERS=("web01.example.com" "web02.example.com" "web03.example.com") SSH_USER="deploy" for server in "${SERVERS[@]}"; do remote_file=$(ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 \ "${SSH_USER}@${server}" "cat ${MASTER_FILE}" 2>/dev/null) if ! diff -q \ <(echo "$remote_file") \ "${MASTER_FILE}" >/dev/null 2>&1; then echo "[WARN] ${server}: ${MASTER_FILE} が基準と異なります" >&2 diff <(echo "$remote_file") "${MASTER_FILE}" || true else echo "[OK] ${server}: 整合性確認済み" fi done

プロセス置換(`<(cmd)`)を使うことで一時ファイルを作らずにSSH出力をdiffに渡せます。

トラブルシュート

1. 常に差分が出力されてアラートが止まらない

タイムスタンプや一時的なコメントが含まれる設定ファイルでは、内容は変わっていなくてもファイルの更新日時が変わりdiffが差分を検出することがあります。`diff -q`は内容の比較なのでタイムスタンプ問題は起きませんが、設定ファイル内に動的な値(バージョン番号・自動生成コメント)が含まれている場合は誤検知の原因になります。

対処は2通りです。
・問題の行をdiffの`--ignore-matching-lines`で除外する
・動的な行を正規化した状態で比較する

# タイムスタンプ行を無視して比較(例: # Generated: 2026-10-01 という行を除外) diff -q \ <(grep -v '^# Generated:' "${snapshot}") \ <(grep -v '^# Generated:' "${target}") >/dev/null 2>&1

2. rootのみ読めるファイルの比較でPermission deniedになる

/etc/shadowや/etc/sudoersはrootのみ読み取り可能です。スクリプトをroot権限で実行するか、sudoersでスクリプトの実行を許可します。cronの`root`ユーザー行として登録すれば権限の問題は解消します。

3. diffのexit 1が set -e で誤終了する

`set -euo pipefail`を使っている場合、`diff`が差分を検出してexit 1を返すとスクリプトが終了してしまいます。

# NGパターン: set -e 下でdiffが差分を検出するとスクリプト終了 diff -u "${snapshot}" "${target}" # OKパターン: || true でexit 1を吸収する diff -u "${snapshot}" "${target}" >> "${DIFF_LOG}" || true # または条件式内なら set -e の影響を受けない if ! diff -q "${snapshot}" "${target}" >/dev/null 2>&1; then # 変更処理 fi

まとめ

やりたいこと 実装
変更の有無だけを確認 diff -q ファイル1 ファイル2 >/dev/null 2>&1
差分内容をログに記録 diff -u スナップショット 現在 >> ログ || true
変更を検知したらメール通知 mail -s "件名" 宛先 < 差分ファイル
syslogに記録 logger -t タグ -p local0.warning "メッセージ"
変更なしのcron実行は静か exit 0+出力なしでMATOMEVARへの通知が抑制される
set -e 下でdiffを使う diff ... || true または if文内で使う
スナップショット比較型の変更検知は、gitやinotifywaitが使えない環境でも導入できる軽量な仕組みです。「変更をすべて記録しておきたい」「変更があったらすぐに気づきたい」というサーバー管理の基本ニーズを、シェルスクリプトとdiffだけで満たせます。

現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、設定変更検知・trapによる安全設計・バックアップ自動化まで、シェルスクリプトの実践的なノウハウをセミナーでお伝えしています。

>> シェルスクリプト完全マスター 詳細はこちら

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。