シェルスクリプトで設定ファイルを安全に更新する設計|バックアップ・原子的置換・trapロールバックの実装パターン

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)シェルスクリプト > シェルスクリプトで設定ファイルを安全に更新する設計|バックアップ・原子的置換・trapロールバックの実装パターン
「設定ファイルをsedで直接書き換えたら途中でエラーになり、ファイルが壊れて本番サービスが止まった」
「>リダイレクトで誤って入力と出力を同じファイルに指定し、設定が空になった」

Linuxサーバーの設定ファイルを直接上書きするのは、一度うまくいっていても突然裏切られるリスクを抱えている。特に、本番環境でcronから自動実行される場合、失敗に気づかないまま設定が壊れている状態が続くことがある。

この記事では、本番環境で安心して運用できるシェルスクリプトによる設定ファイル更新の設計パターンを解説する。バックアップ・原子的置換・trapによる自動ロールバックを組み合わせた実装例を順番に見ていこう。

動作確認環境:RHEL 9.4 / Rocky Linux 9.4 / Ubuntu 24.04 LTS
使用コマンド:mktemp, mv, diff, cp, sed, trap

この記事のポイント

・mktemp + mvの原子的置換で設定ファイルの破損リスクをなくせる
・trapをEXITに仕込むと失敗時にバックアップから自動ロールバックできる
・diffで差分確認してからmvする設計にすると意図しない変更を防げる
・バックアップの世代管理をfindで自動化することでディスク枯渇も防げる


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

なぜ設定ファイルの「直接上書き」が危険なのか

よく使われる設定変更のコマンドには、実は致命的なリスクが潜んでいる。

# ❌ 危険パターン1: 同じファイルを入力・出力に使う(確実に設定が消える) grep -v 'old_key' /etc/myapp.conf > /etc/myapp.conf # ❌ 危険パターン2: sed -i の直接書き換え(書き込み途中に失敗すると壊れる) sed -i 's/listen_port=8080/listen_port=9090/' /etc/myapp.conf

パターン1の問題:> リダイレクトはまず出力先ファイルをゼロバイトに切り詰めてから書き込みを開始する。入力と出力が同じファイルなら、読み込む前にファイルが空になり、結果も空ファイルになる。

パターン2の問題:sed -i は内部で一時ファイルを作成して書き込む。ただし、書き込み中にディスクが満杯になったり強制終了されたりすると中途半端な状態で終わる。バックアップなしで上書きした場合、元の設定に戻す方法がない。

これらのリスクを設計で根絶する方法を順番に解説していく。

安全な更新の基本設計(一時ファイル→mvの原子的置換)

設定ファイルを安全に更新する基本設計は「変更を一時ファイルに書き出してからmvで置換する」という3ステップだ。

1. 変更を一時ファイルに書き出す

mktemp で一時ファイルを作成し、そこに変更後の内容を書き出す。元ファイルには一切触れない。

CONF="/etc/myapp/myapp.conf" # 元ファイルと同じディレクトリに一時ファイルを作成する(重要) TMP=$(mktemp "${CONF}.XXXXXX") # sedの出力を一時ファイルへ(元ファイルは変更しない) sed 's/listen_port=8080/listen_port=9090/' "${CONF}" > "${TMP}"

mktemp "${CONF}.XXXXXX" のように元ファイルと同じディレクトリ配下にパスを指定する点が重要だ。mktemp だけだと /tmp に作られるが、/tmp/etc が別パーティションにある場合、後述の mv がクロスファイルシステム操作になり原子性が失われる。

2. diffで差分を事前確認する

一時ファイルへの書き出しが完了したら、diff で内容を確認してから置換する。

$ diff -u "${CONF}" "${TMP}" --- /etc/myapp/myapp.conf 2026-09-18 12:00:00.000000000 +0900 +++ /etc/myapp/myapp.conf.aBc123 2026-09-19 03:00:00.000000000 +0900 @@ -5,7 +5,7 @@ log_level=info -listen_port=8080 +listen_port=9090 max_connections=100

set -euo pipefail 環境では、diff が差分を検知すると終了コード1を返してスクリプトが止まる。差分の有無に関係なく処理を続けるには || true を付ける。

# set -e 環境でdiffを安全に使う diff_out=$(diff -u "${CONF}" "${TMP}" || true) if [[ -z "${diff_out}" ]]; then echo "差分なし。変更をスキップします" rm -f "${TMP}" exit 0 fi echo "適用する差分:" echo "${diff_out}"

3. mvで原子的に入れ替える

差分確認が終わったら mv で一時ファイルを元のパスに置換する。

# 原子的置換(同一ファイルシステム内であれば一瞬で完了) mv "${TMP}" "${CONF}" # mv成功後はTMPが存在しないのでtrapのクリーンアップ対象から外す TMP="" echo "設定ファイルを更新しました: ${CONF}"

mv は同一ファイルシステム内でのリネームであれば、カーネルレベルの単一操作として完了する。書き込み途中の状態は存在しない。これが「原子的」と呼ばれる理由だ。

シェルスクリプトを体系的に学びたい方は、シェルスクリプト実践講座(無料)をチェックしてほしい。設定管理・ログ出力・エラーハンドリングなど、現場で使える設計パターンを網羅している。

バックアップを自動作成する設計

4. 更新前にタイムスタンプ付きバックアップを保存する

mv で置換する前に、必ずバックアップを取っておく。タイムスタンプをファイル名に入れることで、いつの状態かを一目で確認できる。

BACKUP_DIR="/var/backup/conf" DATE=$(date +"%Y%m%d_%H%M%S") BACKUP="${BACKUP_DIR}/$(basename "${CONF}").${DATE}.bak" mkdir -p "${BACKUP_DIR}" cp "${CONF}" "${BACKUP}" echo "バックアップ作成: ${BACKUP}"

実行後のバックアップ確認:

$ ls -lt /var/backup/conf/ -rw-r--r-- 1 root root 2340 Sep 19 03:00 myapp.conf.20260919_030001.bak -rw-r--r-- 1 root root 2335 Sep 18 03:00 myapp.conf.20260918_030001.bak -rw-r--r-- 1 root root 2320 Sep 17 03:00 myapp.conf.20260917_030001.bak

5. バックアップの世代管理(findで古い世代を削除する)

バックアップが無制限に増えるのを防ぐため、一定世代を超えたものを削除する設計を入れる。

KEEP_BACKUPS=30 # 30世代(日次実行なら約1ヶ月分) CONF_BASENAME=$(basename "${CONF}") # タイムスタンプ順で新しい順に並べ、KEEP_BACKUPS件目以降を削除 ls -1t "${BACKUP_DIR}/${CONF_BASENAME}.*.bak" 2>/dev/null | tail -n +$((KEEP_BACKUPS + 1)) | while read -r OLD_BK; do echo "古いバックアップを削除: ${OLD_BK}" rm -f "${OLD_BK}" done

設定ファイルのバックアップをtarアーカイブにまとめて管理したい場合は tar コマンドの実用例 を参考にしてほしい。複数の設定ファイルを一括でアーカイブしたい場面では tar czf が便利だ。

trapで失敗時に自動ロールバックする設計

6. EXITシグナルをtrapして自動復旧する

set -e でコマンドが失敗するとスクリプトが即座に終了する。このとき trap ... EXIT を使えば、終了コードを確認してバックアップから自動復元できる。
以下が、これまでの設計要素をすべて組み合わせた完成版スクリプトだ。

#!/bin/bash set -euo pipefail # ── 設定 ───────────────────────────────────────────────── CONF="/etc/myapp/myapp.conf" BACKUP_DIR="/var/backup/conf" KEEP_BACKUPS=30 DATE=$(date +"%Y%m%d_%H%M%S") CONF_BASENAME=$(basename "${CONF}") BACKUP="${BACKUP_DIR}/${CONF_BASENAME}.${DATE}.bak" TMP="" # ── ログ関数 ───────────────────────────────────────────── log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*"; } log_error() { log "ERROR: $*" >&2; } # ── クリーンアップ関数(trap先) ────────────────────────── _cleanup() { local code=$? # 一時ファイルが残っていれば削除(通常はmvで消えているが異常終了時は残る) if [[ -n "${TMP}" && -f "${TMP}" ]]; then rm -f "${TMP}" fi if [[ $code -ne 0 ]]; then log_error "更新に失敗しました(終了コード: ${code})" if [[ -f "${BACKUP}" ]]; then log "バックアップから復元します: ${BACKUP}" cp "${BACKUP}" "${CONF}" log "復元完了" fi fi } trap _cleanup EXIT # ── バックアップ作成 ────────────────────────────────────── mkdir -p "${BACKUP_DIR}" cp "${CONF}" "${BACKUP}" log "バックアップ作成: ${BACKUP}" # ── 変更を一時ファイルに適用 ───────────────────────────── TMP=$(mktemp "${CONF}.XXXXXX") sed 's/listen_port=8080/listen_port=9090/' "${CONF}" > "${TMP}" # ── 差分確認 ───────────────────────────────────────────── diff_out=$(diff -u "${CONF}" "${TMP}" || true) if [[ -z "${diff_out}" ]]; then log "変更なし。スキップします" exit 0 fi log "適用する差分:" echo "${diff_out}" # ── 原子的置換 ─────────────────────────────────────────── mv "${TMP}" "${CONF}" TMP="" # trapのクリーンアップ対象から外す log "設定ファイルを更新しました: ${CONF}" # ── 世代管理 ───────────────────────────────────────────── ls -1t "${BACKUP_DIR}/${CONF_BASENAME}.*.bak" 2>/dev/null | tail -n +$((KEEP_BACKUPS + 1)) | while read -r OLD_BK; do log "古いバックアップを削除: ${OLD_BK}" rm -f "${OLD_BK}" done

trapの動作確認:スクリプトをテストするなら存在しないファイルを参照させて意図的に失敗させると、バックアップから自動復元されることを確認できる。

$ sudo bash update-conf.sh [2026-09-19 03:00:01] バックアップ作成: /var/backup/conf/myapp.conf.20260919_030001.bak [2026-09-19 03:00:01] 適用する差分: --- /etc/myapp/myapp.conf 2026-09-18 12:00:00.000000000 +0900 +++ /etc/myapp/myapp.conf.xY7kPq 2026-09-19 03:00:01.000000000 +0900 @@ -5,7 +5,7 @@ log_level=info -listen_port=8080 +listen_port=9090 max_connections=100 [2026-09-19 03:00:01] 設定ファイルを更新しました: /etc/myapp/myapp.conf

汎用的な更新関数に整理する

同じパターンを何度も書かないよう、汎用関数に整理しておくと便利だ。標準入力で新しい内容を受け取るインターフェースにすると、変換コマンドを外から自由に差し込める。

# 設定ファイルを安全に更新する汎用関数(標準入力から新しい内容を受け取る) safe_update_conf() { local conf="$1" local backup_dir="${CONF_BACKUP_DIR:-/var/backup/conf}" local date_str date_str=$(date +"%Y%m%d_%H%M%S") local backup="${backup_dir}/$(basename "${conf}").${date_str}.bak" local tmp tmp=$(mktemp "${conf}.XXXXXX") [[ -f "${conf}" ]] || { echo "ERROR: ファイルが存在しません: ${conf}" >&2; return 1; } mkdir -p "${backup_dir}" cp "${conf}" "${backup}" # 標準入力から新しい内容を受け取る cat > "${tmp}" local diff_out diff_out=$(diff -u "${conf}" "${tmp}" || true) if [[ -z "${diff_out}" ]]; then rm -f "${tmp}" return 0 fi mv "${tmp}" "${conf}" echo "更新完了: ${conf}(バックアップ: ${backup})" } # 使用例: パイプで新しい内容を渡す sed 's/listen_port=8080/listen_port=9090/' /etc/myapp/myapp.conf | safe_update_conf /etc/myapp/myapp.conf # 使用例: ヒアドキュメントで内容を渡す safe_update_conf /etc/myapp/myapp.conf << 'EOF' listen_port=9090 log_level=info max_connections=100 EOF

この関数をライブラリファイル(例: /usr/local/lib/conf-utils.sh)に保存しておけば、複数のスクリプトから source するだけで使いまわせる。

よくあるエラーと対処法

「mv: cannot move '...' across filesystems」エラー

mv: cannot move '/tmp/myapp.conf.aBcDeF' to '/etc/myapp/myapp.conf': Invalid cross-device link

原因:mktemp だけで呼ぶと /tmp に一時ファイルが作られる。/tmp/etc が別パーティション(XFS/ext4のLVM分割など)の場合、クロスデバイスのmvはコピー+削除になり原子性が失われる。

対処:mktemp "${CONF}.XXXXXX" のように設定ファイルと同じディレクトリに一時ファイルを作成する。

「diff: exit code 1 でスクリプトが止まる」

set -e 環境では diff が差分を検知すると終了コード1を返して停止する。
対処:diff ... || true を付ける。または変数に代入する形 diff_out=$(diff ... || true) を使う。

「バックアップから復元されたが設定が古すぎる」

世代数を超えたバックアップが削除されていた場合、古い世代からしか復元できないことがある。

対処:KEEP_BACKUPS を調整する。重要度の高い設定ファイルは世代数を増やすか、別途永続バックアップを取る設計を検討する。

本記事のまとめ

設定ファイルを安全に更新するシェルスクリプトの設計要素をまとめる。
やりたいこと 実装パターン
破損なしで変更を適用する mktemp "${CONF}.XXXXXX" + mv "${TMP}" "${CONF}"
変更前後の差分を確認する diff_out=$(diff -u "${CONF}" "${TMP}" || true)
失敗時にバックアップから復元する trap _cleanup EXITcp "${BACKUP}" "${CONF}"
一時ファイルの残骸を確実に削除する trap _cleanup EXITrm -f "${TMP}"
バックアップの世代を自動管理する ls -1t ... | tail -n +N | while read で古い世代を削除
複数スクリプトから再利用する 汎用関数化して source でライブラリ読み込み
設定ファイルの更新は「動けばよい」ではなく、「失敗しても安全に戻せる」設計が本番品質の基準だ。mktemp + mv の組み合わせと trap によるロールバックを覚えておくだけで、現場で起きるほとんどの設定事故を防ぐことができる。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、20年以上の運用経験を持つ現役エンジニアが基礎から教えます。
シェルスクリプト講座を見る >>

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

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

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

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

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

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

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

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

この記事を書いた人

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

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

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