こういうトラブルは、現場で1度は遭遇します。
シェルスクリプトから設定ファイルへの直接リダイレクト(>)や sed -i での上書きは、書き換え途中で処理が止まると中途半端なファイルが残ります。
その状態でサービスが起動すると、設定が壊れたままで動き続けます。
この記事では、RHEL 9系/Ubuntu 24.04 LTS を想定環境として、mktemp と mv を使ったアトミックな設定ファイル書き換え設計を解説します。
「一時ファイルに書ききってから mv で差し替える」という1つの原則で、ほとんどのデータ消失リスクを排除できます。
この記事のポイント
・ mktemp と mv でアトミックな設定ファイル書き換えができる
・ mktemp は対象と同じディレクトリに作ると mv がアトミックになる
・ chmod/chown --reference で権限・所有者を一時ファイルに引き継げる
・ trap EXIT と set -euo pipefail で失敗時の一時ファイル削除が漏れない
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜ設定ファイルの直接書き換えは危険なのか
設定ファイルを書き換えるコードを書くとき、以下のような実装はよく見かけます。# よく見かける実装 echo "new_setting=value" > /etc/myapp/config.conf # 直接リダイレクト sed -i 's/old_value/new_value/' /etc/myapp/config.conf # sed -i での上書き
1. ディスクフル時にファイルが空になる
>(リダイレクト)で既存ファイルに書き込もうとすると、シェルはまずファイルを空に切り詰めてから書き込みを始めます。書き込み中にディスクが満杯になると、空のファイルだけが残ります。
その後 /etc/myapp/config.conf を読もうとすると空ファイルになっており、設定ゼロの状態でサービスが動き始めます。
2. sed -i の罠
sed -i は Linux の GNU sed では一時ファイルを使って差し替えるため、一見安全に見えます。ただし、GNU sed の -i が作る一時ファイルの場所は実装依存で、同じパーティションに書くとは保証されません。
スクリプトがシグナルで強制終了された場合に一時ファイルが残ることもあります。
また POSIX の sed には -i オプションが定義されておらず、macOS の BSD sed とは引数の書き方が異なります(移植性の問題)。
3. 並行実行時に中途半端な状態を読まれる
別のプロセスが書き換え中のファイルを読みにいくと、書きかけの内容を読んでしまいます。特に複数の cron ジョブが同じ設定ファイルを操作するケースで問題になります。
アトミック書き換えの基本パターン(mktempとmv)
これらの問題を回避する方法は、「一時ファイルに書ききってから mv で差し替える」です。mv(rename システムコール)は同じパーティション内であれば、ファイルシステムレベルで原子的に実行されます。
つまり、ファイルは古い内容か新しい内容のどちらかが見える状態しか取れません。書き換え途中の内容が他のプロセスに見えることがなくなります。
1. mktempで安全な一時ファイルを作る
一時ファイルを /tmp/config.tmp のような固定名にしてはいけません。並行実行時に別のプロセスが同じ名前を使っている可能性があり、競合が発生します。
mktemp コマンドを使うと、ランダムなサフィックスが付いた一時ファイルを安全に作れます。
# mktemp で一時ファイルを作る # テンプレートを対象ファイルと同じディレクトリにすることが重要 tmpfile=$(mktemp /etc/myapp/config.conf.XXXXXX)
別のパーティション(例:/tmp と /etc が別のパーティション)をまたぐと、mv はファイルをコピーして削除する処理に変わり、アトミック性が失われます。
2. 一時ファイルに書き込んでから mv で差し替える
target="/etc/myapp/config.conf" tmpfile=$(mktemp "${target}.XXXXXX") # 一時ファイルに新しい内容を書く cat > "$tmpfile" << 'EOF' # /etc/myapp/config.conf new_setting=value timeout=30 EOF # mv でアトミックに差し替える(同じパーティション内なら rename システムコール) mv "$tmpfile" "$target"
・書き込み中に障害が起きても、差し替え前のファイルは影響を受けない
・mv が完了した瞬間から、他のプロセスは新しい内容だけを読む
3. 権限と所有者を引き継ぐ
mktemp が作ったファイルのデフォルト権限は 600(rw-------)です。元のファイルが 644 や 640 の場合は、mv の前に権限と所有者を合わせておく必要があります。
target="/etc/myapp/config.conf" tmpfile=$(mktemp "${target}.XXXXXX") # 元ファイルの権限・所有者を一時ファイルにコピーする chmod --reference="$target" "$tmpfile" chown --reference="$target" "$tmpfile" # 新しい内容を書く cat > "$tmpfile" << 'EOF' new_setting=value EOF mv "$tmpfile" "$target"
RHEL 9 / Ubuntu 24.04 では使えますが、古い BusyBox 環境や POSIX 厳守の環境では stat で権限を取得して数値で渡す方法を使います。
実装例:設定ファイルを安全に更新する関数
ここまでの要素を組み合わせた、再利用できる関数の形で示します。この関数は、シェルスクリプトの実装で繰り返し使える「アトミック書き換えの型」です。
シェルスクリプトの設計パターンをさらに体系的に身につけたい方は、シェルスクリプト実践ガイドもあわせてご覧ください。
#!/bin/bash set -euo pipefail # アトミック書き換え関数 # 使い方: atomic_write ターゲットファイル 新しい内容を出力するコマンド... # 例: atomic_write /etc/myapp/config.conf cat /tmp/new_config.conf atomic_write() { local target="$1" shift local tmpfile # 対象と同じディレクトリに作る(mv のアトミック性を保証するため) tmpfile=$(mktemp "${target}.XXXXXX") # 失敗時に一時ファイルを確実に削除するための trap trap 'rm -f "$tmpfile"' EXIT # 権限・所有者を引き継ぐ if [[ -f "$target" ]]; then chmod --reference="$target" "$tmpfile" chown --reference="$target" "$tmpfile" fi # 新しい内容を書く($@ に渡したコマンドを実行して一時ファイルに書く) "$@" > "$tmpfile" # アトミックに差し替える mv "$tmpfile" "$target" trap - EXIT # 差し替え完了後は trap を解除する } # 使用例 atomic_write /etc/myapp/config.conf cat /tmp/new_config.conf
1. バックアップを取ってから更新する
設定ファイルを書き換える前に、バックアップを取る習慣を付けておくと安心です。特に /etc 配下の設定ファイルは、バックアップなしで書き換えると元に戻す手段がなくなります。
target="/etc/myapp/config.conf" backup="${target}.$(date +%Y%m%d%H%M%S).bak" # バックアップを取る cp -p "$target" "$backup" echo "バックアップ: $backup" # アトミック書き換えで更新する tmpfile=$(mktemp "${target}.XXXXXX") trap 'rm -f "$tmpfile"' EXIT chmod --reference="$target" "$tmpfile" chown --reference="$target" "$tmpfile" sed 's/old_value/new_value/' "$target" > "$tmpfile" mv "$tmpfile" "$target" trap - EXIT
2. 設定構文の検証を挟む
差し替える前に、一時ファイルの内容を検証するステップを加えると、構文エラーの設定ファイルへの切り替えを防げます。target="/etc/nginx/nginx.conf" tmpfile=$(mktemp "${target}.XXXXXX") trap 'rm -f "$tmpfile"' EXIT chmod --reference="$target" "$tmpfile" chown --reference="$target" "$tmpfile" # 新しい設定を一時ファイルに書く cat > "$tmpfile" << 'EOF' worker_processes auto; events { worker_connections 1024; } http { include /etc/nginx/conf.d/*.conf; } EOF # 構文検証:一時ファイルを本番パスとして nginx に読ませる if nginx -t -c "$tmpfile" 2>&1; then mv "$tmpfile" "$target" trap - EXIT echo "更新完了" else echo "構文エラー: 更新を中止します" >&2 exit 1 fi
検証が通ったときだけ mv を実行します。nginx 以外のサービスも、同様の「検証してから差し替える」の流れを使えます。
set -euo pipefailと組み合わせた完全な実装
シェルスクリプト全体のエラーハンドリング設計と組み合わせると、より堅牢になります。set -euo pipefail を先頭に書いておくと、tmpfile に書き込む処理が途中で失敗した場合も、trap EXIT が発動して一時ファイルを削除します。
#!/bin/bash set -euo pipefail TARGET="/etc/myapp/config.conf" BACKUP="${TARGET}.$(date +%Y%m%d%H%M%S).bak" TMPFILE="" cleanup() { [[ -n "$TMPFILE" && -f "$TMPFILE" ]] && rm -f "$TMPFILE" } trap cleanup EXIT # バックアップ cp -p "$TARGET" "$BACKUP" # 一時ファイルに書く(対象と同じディレクトリに作ることでアトミック mv が使える) TMPFILE=$(mktemp "${TARGET}.XXXXXX") chmod --reference="$TARGET" "$TMPFILE" chown --reference="$TARGET" "$TMPFILE" # 新しい内容を生成して書く(環境変数から設定を組み立てる例) cat > "$TMPFILE" << EOF # Generated by $(basename "$0") on $(date) timeout=${APP_TIMEOUT:-30} max_connections=${APP_MAX_CONN:-100} log_level=${APP_LOG_LEVEL:-info} EOF # アトミックに差し替える mv "$TMPFILE" "$TARGET" TMPFILE="" # mv 完了後は cleanup で削除しないよう空にする echo "更新完了: $TARGET"
set -euo pipefail と trap ERR によるエラーハンドリングの詳細は、シェルスクリプトの set -euo pipefail と trap ERR によるエラーハンドリング設計で解説しています。
「mv: cannot move」が出た時の対処法
1. 別パーティションへの mv で失敗する
# 出力例(環境により異なります) $ mv /tmp/config.conf.XXXXXX /etc/myapp/config.conf mv: cannot move '/tmp/config.conf.XXXXXX' to '/etc/myapp/config.conf': Invalid cross-device link
一時ファイルを対象ファイルと同じディレクトリに作るよう、テンプレートを "${TARGET}.XXXXXX" に修正してください。
2. 権限エラーで mv が失敗する
# 出力例(環境により異なります) $ mv "$tmpfile" /etc/myapp/config.conf mv: cannot move '...' to '/etc/myapp/config.conf': Permission denied
3. trap EXIT が発動して一時ファイルが消えたとき
set -e が有効な状態で、一時ファイルへの書き込みコマンドが失敗した場合、trap EXIT が発動して一時ファイルが削除されます。これは設計通りの動作です。ログを確認して書き込み失敗の原因(ディスクフル・権限不足など)を特定してください。
本記事のまとめ
| やりたいこと | 実装の型 |
|---|---|
| 安全な一時ファイルを作る | mktemp "${TARGET}.XXXXXX" |
| 元ファイルの権限を引き継ぐ | chmod --reference="$TARGET" "$TMPFILE" |
| 元ファイルの所有者を引き継ぐ | chown --reference="$TARGET" "$TMPFILE" |
| アトミックに差し替える | mv "$TMPFILE" "$TARGET" |
| 失敗時に一時ファイルを削除する | trap 'rm -f "$TMPFILE"' EXIT |
| 書き換え前のバックアップを取る | cp -p "$TARGET" "${TARGET}.$(date +%Y%m%d%H%M%S).bak" |
シェルスクリプトの実装設計から、安全な運用スクリプトの書き方まで、シェルスクリプト実践ガイドで体系的に学べます。
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:シェルスクリプトのcoproc設計|bash組み込みの双方向パイプで対話プロセスへの繰り返し入力を実装する方法
- この記事の属するカテゴリ:シェルスクリプトへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます