「デプロイスクリプトを実行したら、想定外のサーバーに対してサービス停止コマンドが走った」
こうした事故は「実行前に何が起きるか確認できていれば防げた」ケースがほとんどです。外部ツールには rsync
--dry-run や Ansible の --check モードがありますが、自作スクリプトにも同じ仕組みを組み込めます。この記事では、DRY RUNフラグと実行ラッパー関数を使って、危険な操作(削除・上書き・ネットワーク送信)を安全にテストできるシェルスクリプトの設計パターンを解説します。RHEL 9.4 / Ubuntu 24.04 LTSで動作確認済みです。
この記事のポイント
・DRY_RUN変数とexecute()関数で危険なコマンドの実行を一元制御できる
・--dry-runフラグで「実行しない」モードに切り替え、何が起きるかを事前確認できる
・DRY RUN時は[DRY-RUN]プレフィックスをログに出力して実行内容を可視化する
・バックアップ・デプロイスクリプトへそのまま転用できる実践パターンを紹介
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
DRY RUNモードとは何か
DRY RUNモード(試し実行・模擬実行)とは、実際の変更をシステムに加えることなく、「このスクリプトを実行したら何が起きるか」をログに出力するだけで終わらせる実行モードです。代表的な例として、
rsync --dry-run は実際のファイル転送を行わずに転送リストだけを表示し、Ansible の --check モードは実際の変更を加えずに適用内容だけを報告します。自作スクリプトで DRY RUN モードを実装すると、次の3つのメリットがあります。
・本番適用前の確認:削除・上書き・サービス停止といった取り返しのつかない操作を、まずログだけで確認できる
・CI/CDでの事前検証:パイプラインの中でドライラン結果を diff 出力として保存し、レビューに組み込める
・新人オペレーターの安全教育:「まず --dry-run で確認してから実行する」という習慣を強制できる
現場ではバックアップスクリプト・デプロイスクリプト・データ移行スクリプトなど、実行ミスが許されないスクリプトほど DRY RUN モードを持つべきです。
DRY RUN実装の基本構造
DRY RUN を実装する基本構造は「DRY_RUN変数」と「execute()関数」の2点セットです。危険なコマンドを execute() 経由で呼び出すだけで、DRY_RUN フラグの有無に応じて実行・スキップを自動制御できます。1. DRY_RUN変数とexecute()関数を定義する
まず、スクリプト冒頭に DRY_RUN 変数を宣言し、execute() 関数を定義します。#!/bin/bash set -euo pipefail # DRY RUN フラグ(デフォルトは false) DRY_RUN=false # 実行ラッパー関数 # DRY_RUN=true の時はコマンドを実行せずログだけ出力する execute() { if "${DRY_RUN}"; then echo "[DRY-RUN] $*" else "$@" fi }
execute() { ... } というシンプルな構造で、引数に渡したコマンドをそのまま実行できます。DRY_RUN が true の場合は echo "[DRY-RUN] $*" でコマンド内容を出力するだけです。使い方は次のとおりです。
# 通常実行: 実際にファイルを削除する execute rm -f /tmp/old_backup.tar.gz # DRY_RUN=true の場合の出力例: # [DRY-RUN] rm -f /tmp/old_backup.tar.gz
2. --dry-runオプションを引数で受け取る
次に、コマンドライン引数--dry-run を受け取って DRY_RUN 変数を切り替える処理を実装します。#!/bin/bash set -euo pipefail DRY_RUN=false # 引数パース for arg in "$@"; do case "${arg}" in --dry-run|-n) DRY_RUN=true ;; --help|-h) echo "Usage: $0 [--dry-run] [--help]" echo " --dry-run, -n : 実際の変更を行わず内容を確認する" exit 0 ;; *) echo "[ERROR] 不明なオプション: ${arg}" >&2 exit 1 ;; esac done execute() { if "${DRY_RUN}"; then echo "[DRY-RUN] $*" else "$@" fi }
--dry-run と -n(rsync と同じ短縮オプション)の両方に対応することで、rsync に慣れたエンジニアが直感的に使えます。3. DRY RUNログで実行内容を可視化する
DRY RUN時のログには、[DRY-RUN] プレフィックスを必ず付けます。このプレフィックスがあると、あとでログを grep したときに DRY RUN の実行記録だけを抜き出せます。# DRY RUN ログの確認例 ./backup.sh --dry-run 2>&1 | grep "DRY-RUN" # 出力例: # [DRY-RUN] rsync -avz /data/www/ user@backup-server:/backup/www/ # [DRY-RUN] rm -f /backup/www/20260601.tar.gz # [DRY-RUN] gzip -c /tmp/db_dump.sql > /backup/db/20260808.sql.gz
--dry-run を外して本番実行するという確認フローが定着します。実践例:rsyncバックアップスクリプトをDRY RUN対応にする
実際のバックアップスクリプトに DRY RUN を組み込んだ例を示します。RHEL 9.4 の検証機で動作確認したものです。#!/bin/bash set -euo pipefail # ---- 設定 ---- SRC_DIR="/data/www" DEST_USER="backup" DEST_HOST="192.168.1.50" DEST_DIR="/backup/www" RETENTION_DAYS=30 LOG_FILE="/var/log/backup.log" # ---- DRY RUN 制御 ---- DRY_RUN=false for arg in "$@"; do case "${arg}" in --dry-run|-n) DRY_RUN=true ;; --help|-h) echo "Usage: $0 [--dry-run]" exit 0 ;; esac done execute() { if "${DRY_RUN}"; then echo "[DRY-RUN] $*" else "$@" fi } log() { local level="$1"; shift local prefix="" "${DRY_RUN}" && prefix="[DRY-RUN] " || true echo "$(date '+%Y-%m-%d %H:%M:%S') [${level}] ${prefix}$*" | tee -a "${LOG_FILE}" } # ---- メイン処理 ---- log INFO "バックアップ開始: ${SRC_DIR} -> ${DEST_USER}@${DEST_HOST}:${DEST_DIR}" # rsync 転送(DRY_RUN=true なら rsync にも --dry-run を追加) RSYNC_OPTS="-avz --delete" "${DRY_RUN}" && RSYNC_OPTS="${RSYNC_OPTS} --dry-run" || true # shellcheck disable=SC2086 execute rsync ${RSYNC_OPTS} "${SRC_DIR}/" "${DEST_USER}@${DEST_HOST}:${DEST_DIR}/" # 古いバックアップの削除(RETENTION_DAYS日以上前のtar.gz) execute find "${DEST_DIR}" -name "*.tar.gz" -mtime "+${RETENTION_DAYS}" -delete log INFO "バックアップ完了"
・rsync 自体にも --dry-run を渡す:スクリプト側の DRY RUN だけでは rsync の「実際の転送リスト」が見えません。rsync 本体にも
--dry-run を渡すことで、どのファイルが転送されるかを正確に確認できます・log() 関数も DRY RUN を認識する:
"${DRY_RUN}" && prefix="[DRY-RUN] " || true という書き方で、DRY_RUN=true の時だけログに [DRY-RUN] プレフィックスを付加しています実行時の確認フローは次のとおりです。
# ステップ1: DRY RUNで内容確認 ./backup.sh --dry-run # 出力例(実際の変更はなし): # 2026-08-08 12:00:00 [INFO] [DRY-RUN] バックアップ開始 ... # [DRY-RUN] rsync -avz --delete --dry-run /data/www/ backup@192.168.1.50:/backup/www/ # (rsync の転送予定ファイル一覧がここに表示される) # [DRY-RUN] find /backup/www -name "*.tar.gz" -mtime +30 -delete # ステップ2: 内容に問題がなければ本番実行 ./backup.sh
DRY RUN設計の応用パターン
4. 確認系コマンドはDRY RUNでも実行する(safe_execute)
大規模スクリプトでは、副作用のない「確認系」コマンド(stat、ls、curl GET など)は DRY RUN でも実際に実行して情報を得たい場合があります。そのような場合はsafe_execute() と execute() を使い分けます。# DRY RUN でも実際に実行する(読み取り専用操作に使う) safe_execute() { "$@" } # DRY RUN 時はスキップする(書き込み・削除等に使う) execute() { if "${DRY_RUN}"; then echo "[DRY-RUN] $*" else "$@" fi } # 使い分け例: safe_execute stat /data/www # DRY RUN でも実行(確認のため) execute rm -rf /backup/old/ # DRY RUN 時はスキップ
5. trapと組み合わせてDRY RUNでも後片付けを確実に行う
DRY RUN モードでも一時ファイルを作成するケースがあります。trap を使って終了時に一時ファイルを確実に削除する設計は、DRY RUN と関係なく有効です。TMP_DIR="$(mktemp -d)" # trap は DRY RUN に関係なく常に実行(一時ファイルを必ず削除する) trap 'rm -rf "${TMP_DIR}"' EXIT INT TERM # DRY RUN でも一時ファイルは作成して内容を確認する curl -sf "https://example.com/api/data" > "${TMP_DIR}/data.json" safe_execute cat "${TMP_DIR}/data.json" # 内容確認(DRY RUN でも表示) # 本番への書き込みだけ DRY RUN でスキップ execute cp "${TMP_DIR}/data.json" /etc/app/config.json
よくあるミスと対策
【ミス1】execute()の外でdestructiveな操作を直接呼ぶ
DRY RUN を導入しても、execute() を経由せず直接 rm や cp を呼んでしまうと、DRY RUN モードでも実際に実行されてしまいます。実装後に次のコマンドで DRY RUN バイパス箇所を確認できます。
# execute() を通さない破壊的操作の呼び出しを検出する grep -En "^[[:space:]]*(rm|cp|mv|rsync|find) " backup.sh | grep -v "execute" # 出力があれば要修正(execute() でラッピングする)
【ミス2】DRY RUNでもAPIリクエストや通知が送信される
「通知・API呼び出し・メール送信」などの外部への副作用を execute() で制御し忘れると、DRY RUN 中にアラートメールが本当に飛んでしまいます。外部へのリクエストは必ず execute() 経由にするか、DRY RUN 時は専用のテスト用エンドポイントに向けるように設計します。
【ミス3】DRY RUN変数をサブシェル内で変更しても反映されない
パイプの途中や( ) のサブシェル内で DRY_RUN を変更しても、親シェルには反映されません。DRY_RUN はスクリプト最上部で宣言し、変更は引数パース時に1か所だけで行います。本記事のまとめ
シェルスクリプトへの DRY RUN 設計の要点を整理します。| やりたいこと | 実装方法 | 注意点 |
|---|---|---|
| DRY RUNフラグを宣言する | DRY_RUN=false をスクリプト冒頭に配置する |
サブシェル内で変更しないこと |
| 危険なコマンドをラップする | execute rm -f /path/to/file の形式で呼び出す |
rm/cp/mv/rsync等すべてを経由させる |
| 引数で切り替える | ./script.sh --dry-run または -n で起動する |
-n は rsync と同じ慣習で使いやすい |
| rsync にも dry-run を伝える | "${DRY_RUN}" && RSYNC_OPTS="${RSYNC_OPTS} --dry-run" || true |
転送予定リストを確認できて情報量が増える |
| 確認系操作は常に実行する | safe_execute stat /data/www の形式で呼び出す |
stat/ls/curl GET など副作用なし操作向け |
| DRY RUNバイパスを検出する | grep -En "^[[:space:]]*(rm|cp)" script.sh | grep -v execute |
実装後の確認フローとして組み込む |
シェルスクリプトを体系的に学びたい方は、シェルスクリプト実践ガイド も合わせて活用してほしい。
シェルスクリプト講座を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:シェルスクリプトでcurlとjqを使ってREST APIを自動操作する方法|認証ヘッダー・エラー処理・ページング設計の実装パターン
- この記事の属するカテゴリ:シェルスクリプトへ戻る

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