シェルスクリプトの冪等設計|何度実行しても安全な初期設定スクリプトをステートファイルと条件分岐で実現する方法

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)シェルスクリプト > シェルスクリプトの冪等設計|何度実行しても安全な初期設定スクリプトをステートファイルと条件分岐で実現する方法
「サーバーセットアップスクリプトを2回実行したら、ユーザーが重複作成されてエラーになった」
「cronで動かしている初期化スクリプトが、2回目以降も毎回設定を上書きしてしまう」

セットアップや初期化を自動化するシェルスクリプトで、こういった問題に直面したことはないだろうか。原因のほとんどは「冪等性(べきとうせい)の欠如」にある。冪等とは「何度実行しても同じ結果になること」を指す。

この記事では、シェルスクリプトに冪等設計を組み込む3つの基本パターンと、サーバー初期設定スクリプトへの実践的な適用方法を解説する。CI/CDパイプラインでのデプロイ自動化から、障害後の再セットアップまで、そのまま使える設計を身につけてほしい。

この記事のポイント

・ステートファイルと条件分岐で「実行済みかどうか」を管理できる
・ユーザー・コマンド・ファイルは「存在確認してから作成」が鉄則
・設定ファイルの上書きは事前バックアップ+差分チェックで安全化できる
・冪等スクリプトはCI/CDや障害復旧にもそのまま再利用できる


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

なぜ冪等設計が必要なのか

シェルスクリプトによる自動化は便利だが、「一度きりの実行」を前提に書かれたスクリプトは危険だ。

たとえば、新規サーバーに useradd webuser を実行するスクリプトを書いたとする。初回は問題なく動く。しかし設定変更のために2回目を実行すると、「ユーザーが既に存在する」というエラーで処理が止まる。スクリプトがここで異常終了すれば、後続の重要な設定がスキップされることになる。

cronで定期実行されるスクリプトなら、この問題はさらに深刻だ。毎回同じ設定が上書きされ、手動で加えたカスタマイズが消えてしまう。

AnsibleをはじめとするIaCツールは冪等性を設計の中心に置いているが、シェルスクリプトではその責任は書き手にある。「2回目以降も安全に実行できるか」を常に意識して書く必要がある。

冪等設計の3つの基本パターン

1. ステートファイルで実行済みを記録する

最も汎用的なパターンが「ステートファイル(状態ファイル)」を使う方法だ。スクリプトの実行が完了したら、決められたパスにマーカーファイルを作成する。次回以降の実行時にそのファイルが存在するかチェックし、存在すれば処理をスキップする。

#!/bin/bash set -euo pipefail # ステートファイルのパス STATE_DIR="/var/lib/myapp" STATE_FILE="${STATE_DIR}/initialized.done" # ステートファイルが存在すれば終了 if [ -f "${STATE_FILE}" ]; then echo "[SKIP] 初期設定は既に完了しています(${STATE_FILE})" exit 0 fi echo "[RUN] 初期設定を開始します..." mkdir -p "${STATE_DIR}" # --- ここに初期設定の処理を書く --- # 完了マーカーを作成 touch "${STATE_FILE}" echo "[DONE] 初期設定が完了しました"

実際にサーバーで実行した出力例:

# 1回目の実行 $ sudo bash setup.sh [RUN] 初期設定を開始します... [DONE] 初期設定が完了しました # 2回目の実行(スキップされる) $ sudo bash setup.sh [SKIP] 初期設定は既に完了しています(/var/lib/myapp/initialized.done)

ステートファイルのパスは /var/lib/ 配下を推奨する。/tmp は再起動で消えるため、永続化が必要な状態管理には向かない。

スクリプトをバージョンアップした際に再実行させたい場合は、ファイル名にバージョンを含める設計が便利だ(例:initialized_v2.done)。

2. コマンド・ユーザー・設定の存在チェック

ステートファイルではなく、作成対象そのものの存在を確認する方法もある。より細粒度の冪等制御が可能で、部分的な失敗からの復旧にも強い。

#!/bin/bash set -euo pipefail # ユーザーの存在チェック(idコマンドの終了コードを使う) if id "webuser" &>/dev/null; then echo "[SKIP] ユーザー webuser は既に存在します" else useradd -r -s /sbin/nologin webuser echo "[CREATE] ユーザー webuser を作成しました" fi # コマンドの存在チェック(command -v で確認) if command -v nginx &>/dev/null; then echo "[SKIP] nginx はインストール済みです" else dnf install -y nginx echo "[INSTALL] nginx をインストールしました" fi # ディレクトリの存在チェック TARGET_DIR="/etc/myapp/conf.d" if [ -d "${TARGET_DIR}" ]; then echo "[SKIP] ディレクトリ ${TARGET_DIR} は既に存在します" else mkdir -p "${TARGET_DIR}" echo "[CREATE] ディレクトリ ${TARGET_DIR} を作成しました" fi

id コマンドはユーザーが存在する場合に終了コード0を返す。command -v はコマンドがPATHに存在する場合に0を返す。いずれも if 文と組み合わせることで、存在確認と処理のスキップを簡潔に記述できる。

3. 設定ファイルの安全な上書き

設定ファイルを配置する場合、既存のファイルを無条件に上書きすると、手動でのカスタマイズが失われる。差分チェックを組み込んで「変更がある場合だけ更新する」設計が冪等スクリプトの基本だ。

#!/bin/bash set -euo pipefail CONF_SRC="/opt/myapp/templates/myapp.conf" CONF_DST="/etc/myapp/myapp.conf" # 配置先が存在し、かつ内容が同じならスキップ if [ -f "${CONF_DST}" ] && diff -q "${CONF_SRC}" "${CONF_DST}" &>/dev/null; then echo "[SKIP] ${CONF_DST} は最新の状態です" exit 0 fi # 既存ファイルをバックアップしてから上書き if [ -f "${CONF_DST}" ]; then cp "${CONF_DST}" "${CONF_DST}.$(date +%Y%m%d_%H%M%S).bak" echo "[BACKUP] 既存の設定ファイルをバックアップしました" fi cp "${CONF_SRC}" "${CONF_DST}" echo "[UPDATE] ${CONF_DST} を更新しました"

diff -q は2つのファイルが同一であれば終了コード0を返す。これを利用して、実際に変更がある場合だけ更新処理を走らせる。バックアップにはタイムスタンプを付与し、複数世代を保持できるようにする。

実践:サーバー初期設定スクリプトを冪等化する

ここでは3つのパターンを組み合わせて、実務で使える初期設定スクリプトを作る。

1. ユーザー作成の冪等化

create_user() { local username="$1" local uid="$2" if id "${username}" &>/dev/null; then echo "[SKIP] ユーザー ${username} は既に存在します($(id "${username}"))" return 0 fi useradd -r -u "${uid}" -s /sbin/nologin "${username}" echo "[CREATE] ユーザー ${username} (uid=${uid}) を作成しました" } create_user "webuser" 8080 create_user "appuser" 8081

実際のサーバーでの実行結果(RHEL 9.4 / Rocky Linux 9.4 で動作確認済み):

# webuser が未作成の場合 [CREATE] ユーザー webuser (uid=8080) を作成しました [CREATE] ユーザー appuser (uid=8081) を作成しました # 2回目の実行(どちらもスキップ) [SKIP] ユーザー webuser は既に存在します(uid=8080(webuser) gid=8080(webuser) groups=8080(webuser)) [SKIP] ユーザー appuser は既に存在します(uid=8081(appuser) gid=8081(appuser) groups=8081(appuser))

2. パッケージインストールの冪等化

RHEL/Rocky Linux 環境では rpm -q コマンドでパッケージのインストール済みを確認できる。rpm コマンドの使い方も合わせて参照してほしい。

install_package() { local pkg="$1" if rpm -q "${pkg}" &>/dev/null; then echo "[SKIP] ${pkg} はインストール済みです($(rpm -q "${pkg}"))" return 0 fi echo "[INSTALL] ${pkg} をインストールします..." dnf install -y "${pkg}" echo "[DONE] ${pkg} のインストールが完了しました" } install_package "nginx" install_package "rsync"

実際のサーバーでの実行結果:

# 1回目:未インストール [INSTALL] nginx をインストールします... Complete! [DONE] nginx のインストールが完了しました # 2回目:インストール済み [SKIP] nginx はインストール済みです(nginx-1.22.1-3.module+el9.2.0+13177+da0eeee8.x86_64) [SKIP] rsync はインストール済みです(rsync-3.2.3-19.el9.x86_64)

3. 設定ファイル配置の冪等化

ls コマンドでファイルの存在と状態を確認する場面も多い。詳細はls コマンドの基本オプションも参照してほしい。

deploy_config() { local src="$1" local dst="$2" # 配置先ディレクトリを作成(-p で冪等) mkdir -p "$(dirname "${dst}")" # 内容が同じならスキップ if [ -f "${dst}" ] && diff -q "${src}" "${dst}" &>/dev/null; then echo "[SKIP] $(basename "${dst}") は最新の状態です" return 0 fi # バックアップしてから上書き if [ -f "${dst}" ]; then cp "${dst}" "${dst}.$(date +%Y%m%d_%H%M%S).bak" fi cp "${src}" "${dst}" echo "[UPDATE] ${dst} を配置しました" } deploy_config "/opt/myapp/templates/nginx.conf" "/etc/nginx/nginx.conf" deploy_config "/opt/myapp/templates/app.conf" "/etc/myapp/app.conf"

冪等設計を組み合わせた完全スクリプト例

上記3パターンを統合した、実際に使えるサーバー初期設定スクリプトを示す。

#!/bin/bash # server-init.sh サーバー初期設定スクリプト(冪等設計) # 実行環境: RHEL 9.4 / Rocky Linux 9.4 set -euo pipefail # 定数定義 SCRIPT_NAME="$(basename "$0")" STATE_DIR="/var/lib/server-init" LOG_FILE="/var/log/server-init.log" log() { local level="$1"; shift echo "[$(date '+%Y-%m-%d %H:%M:%S')] [${level}] $*" | tee -a "${LOG_FILE}" } # ステートファイルによる実行管理 mkdir -p "${STATE_DIR}" run_once() { local step_name="$1" local state_file="${STATE_DIR}/${step_name}.done" if [ -f "${state_file}" ]; then log "SKIP" "ステップ [${step_name}] は完了済みです" return 0 fi log "RUN" "ステップ [${step_name}] を開始します" "${@:2}" touch "${state_file}" log "DONE" "ステップ [${step_name}] が完了しました" } # 個別処理関数 step_create_users() { if ! id webuser &>/dev/null; then useradd -r -u 8080 -s /sbin/nologin webuser log "INFO" "ユーザー webuser を作成しました" fi } step_install_packages() { for pkg in nginx rsync; do if ! rpm -q "${pkg}" &>/dev/null; then dnf install -y "${pkg}" log "INFO" "${pkg} をインストールしました" fi done } step_deploy_configs() { local nginx_conf="/etc/nginx/conf.d/myapp.conf" if [ ! -f "${nginx_conf}" ]; then cp "/opt/myapp/templates/myapp.conf" "${nginx_conf}" log "INFO" "nginx 設定ファイルを配置しました" fi } # メイン処理(run_once でステップ管理) run_once "create_users" step_create_users run_once "install_packages" step_install_packages run_once "deploy_configs" step_deploy_configs log "INFO" "全ての初期設定が完了しました"

実際のサーバーでの実行ログ(/var/log/server-init.log):

# 1回目の実行 [2026-08-05 10:22:10] [RUN] ステップ [create_users] を開始します [2026-08-05 10:22:10] [INFO] ユーザー webuser を作成しました [2026-08-05 10:22:10] [DONE] ステップ [create_users] が完了しました [2026-08-05 10:22:11] [RUN] ステップ [install_packages] を開始します [2026-08-05 10:22:14] [INFO] nginx をインストールしました [2026-08-05 10:22:16] [INFO] rsync をインストールしました [2026-08-05 10:22:16] [DONE] ステップ [install_packages] が完了しました [2026-08-05 10:22:16] [RUN] ステップ [deploy_configs] を開始します [2026-08-05 10:22:16] [INFO] nginx 設定ファイルを配置しました [2026-08-05 10:22:16] [DONE] ステップ [deploy_configs] が完了しました [2026-08-05 10:22:16] [INFO] 全ての初期設定が完了しました # 2回目の実行(全ステップスキップ) [2026-08-05 10:30:05] [SKIP] ステップ [create_users] は完了済みです [2026-08-05 10:30:05] [SKIP] ステップ [install_packages] は完了済みです [2026-08-05 10:30:05] [SKIP] ステップ [deploy_configs] は完了済みです [2026-08-05 10:30:05] [INFO] 全ての初期設定が完了しました

run_once 関数がポイントだ。各ステップにステートファイルを割り当て、完了済みのステップは自動的にスキップする。どのステップで失敗しても、再実行時には失敗した箇所から再開できる。

トラブルシュート:冪等スクリプトが意図通りに動かない場合

1. ステートファイルが残って再実行できない

手順のやり直しが必要になった場合、ステートファイルを削除すれば対象ステップを再実行できる。

# 特定ステップのステートを削除して再実行 $ sudo rm /var/lib/server-init/install_packages.done $ sudo bash server-init.sh [2026-08-05 11:05:01] [SKIP] ステップ [create_users] は完了済みです [2026-08-05 11:05:01] [RUN] ステップ [install_packages] を開始します [2026-08-05 11:05:05] [DONE] ステップ [install_packages] が完了しました [2026-08-05 11:05:05] [SKIP] ステップ [deploy_configs] は完了済みです [2026-08-05 11:05:05] [INFO] 全ての初期設定が完了しました # 全ステートを削除して最初からやり直す $ sudo rm -f /var/lib/server-init/*.done

2. [ -f ] と [ -e ] の使い分けミスによる誤判定

存在チェックに使う条件式は目的に合わせて正しく使い分けることが重要だ。

[ -f ファイル ]:通常ファイルが存在する場合に真(ディレクトリは偽)
[ -d ディレクトリ ]:ディレクトリが存在する場合に真
[ -e パス ]:ファイル・ディレクトリ問わず存在する場合に真(シンボリックリンクも含む)
[ -L シンボリックリンク ]:シンボリックリンクが存在する場合に真

ステートファイルの確認には -f を使う。-e は同名のディレクトリが存在する場合にも真になるため、誤検知が起きることがある。

# NG:-e は同名ディレクトリでも真になる if [ -e "/var/lib/myapp/initialized.done" ]; then echo "スキップ" # ディレクトリが存在してもスキップされてしまう fi # OK:-f は通常ファイルのみ真 if [ -f "/var/lib/myapp/initialized.done" ]; then echo "スキップ" # ファイルのみスキップ fi

3. set -e とサブシェルの組み合わせで意図しない終了が起きる

set -euo pipefail を使う場合、存在確認のコマンドが非ゼロを返すと即終了してしまうことがある。

# NG:id コマンドが終了コード1を返した瞬間にスクリプトが終了する set -e id "webuser" # ユーザーが存在しない場合、ここで exit 1 が起きる # OK:if 文の中では set -e の即時終了が適用されない if id "webuser" &>/dev/null; then echo "存在する" else echo "存在しない" fi

存在確認コマンドは必ず if 文または || と組み合わせて使うこと。set -e 環境では、単独で実行したコマンドが非ゼロを返すとスクリプトが終了する。

本記事のまとめ

シェルスクリプトに冪等設計を組み込む3つのパターンと使い所をまとめる。
パターン 主な手法 適した場面
ステートファイル管理 touch done_file / [ -f done_file ] スクリプト全体・ステップ単位の完了管理
存在確認後に作成 id / rpm -q / [ -d ] ユーザー・パッケージ・ディレクトリの冪等化
差分チェック後に更新 diff -q src dst / cp + bak 設定ファイルの安全な配置・更新
冪等設計はAnsibleなどのIaCツールが標準で提供する機能だが、シェルスクリプトでも同じ思想を実現できる。「何度実行しても安全」なスクリプトは、障害後の復旧や設定変更のやり直しに強く、チームで運用するシステムの信頼性を大きく高める。

シェルスクリプトの設計技術をもっと体系的に学びたい方は、シェルスクリプト実践ガイド も合わせて活用してほしい。

冪等設計の前に、Linuxの動作原理そのものを体系的に理解する

冪等スクリプトを安定して動かすには、ユーザー管理・パッケージ管理・ファイルシステムという基盤の仕組みを正確に把握していることが前提になります。個別のコマンドをその場で調べながら書いていると、「なぜこの条件式で判定できるのか」という根拠が曖昧なまま実装が進み、思わぬ場面でスクリプトが壊れます。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、『Linuxサーバー構築入門マニュアル(図解60P)』を完全無料でプレゼントしています。

「独学の時間がもったいない」「プロから直接、現場の技術を最短で学びたい」という本気の方には、2日で実務レベルのスキルが身につく【初心者向けハンズオンセミナー】も開催しています。

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

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

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

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

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

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

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

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

この記事を書いた人

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

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

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