「途中で失敗したとき、どこまで進んだか分からなくなって焦った経験がある」
Linux サーバーへのリリース作業をシェルスクリプトで自動化すれば、この問題をまとめて解決できます。ただし「コマンドを並べただけ」のスクリプトは、失敗したときに中途半端な状態でサーバーを傷つけます。ロックもなければロールバックもない──そういったスクリプトが本番障害を起こすのは、残念ながらよくある話です。
この記事では、デプロイスクリプトに必ず組み込むべき3つの設計──排他制御・ロールバック・完了通知──を実機で動作確認しながら解説します。コピーしてすぐ使える雛形スクリプトも掲載していますので、参考にしてください。
実行環境:RHEL 9.4 / Ubuntu 24.04 LTS で動作確認済み
この記事のポイント
・flockコマンドで二重実行を防ぐ排他制御がデプロイスクリプトの基本
・rollback関数をtrap EXITに登録すると失敗時に旧バージョンへ自動切り戻せる
・ssコマンドでサービス起動を検証してからスクリプトを終了するのが安全設計
・通知はtrap EXITに集約し、成功・失敗どちらの経路でも確実に送出する
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜデプロイスクリプトに「設計」が必要なのか
単純なデプロイスクリプトは、こういう形になりがちです。#!/bin/bash # よくある「ただ並べただけ」のデプロイスクリプト(危険) rsync -avz /build/webapp/ webadmin@192.168.1.10:/var/www/html/ ssh webadmin@192.168.1.10 "sudo systemctl restart httpd" echo "デプロイ完了"
・二重実行の危険性:2人が同時にデプロイを実行すると、ファイルが混在して壊れる
・失敗時の放置:rsync が途中で失敗しても、次の restart が走って壊れた状態でサービスが起動する
・結果の不透明さ:誰が何時にデプロイしたか、成功したかどうかがログに残らない
これらをまとめて解消するのが、本記事で紹介する「設計を持ったデプロイスクリプト」です。
デプロイスクリプトの3つの必須設計
1. 排他制御(二重実行防止)
デプロイは排他操作です。同時に複数のデプロイが走ると、ファイルが混在して必ず問題が起きます。これを防ぐのがflock コマンドによるロックファイル設計です。#!/bin/bash # ロックファイルのパス LOCKFILE="/var/run/deploy.lock" # flockでロックを取得(取れなければ即終了) exec 9>"${LOCKFILE}" if ! flock -n 9; then echo "[ERROR] 別のデプロイが実行中です。終了します。" >&2 exit 1 fi echo "[INFO] ロック取得済み(PID: $$)"
flock -n 9 の -n は「ノンブロッキング」オプションです。ロックが取れなかった場合は待たずに即座に失敗します。9 はロックファイルに割り当てたファイルディスクリプタ番号です。スクリプトが正常終了・異常終了どちらのルートでも、シェルプロセスが終了すると fd 9 は自動的にクローズされ、flock のロックも解放されます。
2. ロールバック設計(旧バージョンへの切り戻し)
デプロイが途中で失敗したとき、旧バージョンに素早く戻せる設計が不可欠です。trap と組み合わせたロールバック関数が有効です。#!/bin/bash set -euo pipefail DEPLOY_DIR="/var/www/html" BACKUP_DIR="/var/backup/webapp" TIMESTAMP=$(date +%Y%m%d_%H%M%S) BACKUP_PATH="${BACKUP_DIR}/${TIMESTAMP}" # ロールバック関数 rollback() { local exit_code=$? if [[ ${exit_code} -ne 0 ]]; then echo "[WARN] デプロイ失敗(終了コード: ${exit_code})。ロールバックを開始します..." >&2 # 直前のバックアップディレクトリを探して復元 local latest_backup latest_backup=$(ls -dt "${BACKUP_DIR}"/*/ 2>/dev/null | head -1) if [[ -n "${latest_backup}" ]]; then rsync -a --delete "${latest_backup}/" "${DEPLOY_DIR}/" echo "[INFO] ロールバック完了: ${latest_backup}" >&2 else echo "[ERROR] バックアップが見つかりません。手動で確認してください。" >&2 fi fi } # EXIT シグナルでロールバック関数を呼ぶ trap rollback EXIT
trap rollback EXIT を書いておくと、スクリプトが exit コードゼロ以外で終了したとき(set -euo pipefail でコマンドが失敗したとき含む)、自動的に rollback 関数が呼ばれます。ロールバック元のバックアップは、デプロイ前に
tar で作成しておきます。tar コマンドの実用例を参考にアーカイブを作成するか、上記例のようにディレクトリをrsyncでコピーする方法が使いやすいです。3. 完了通知(成功・失敗を確実に知らせる)
デプロイの結果をメールやチャットツールに通知する処理も、trap EXIT に集約します。成功・失敗どちらのルートでも必ず通知が飛ぶことを保証できます。#!/bin/bash NOTIFY_EMAIL="ops@example.com" SCRIPT_START=$(date "+%Y-%m-%d %H:%M:%S") DEPLOY_STATUS="SUCCESS" notify() { local exit_code=$? if [[ ${exit_code} -ne 0 ]]; then DEPLOY_STATUS="FAILED (exit: ${exit_code})" fi local msg="[デプロイ通知] ${DEPLOY_STATUS} | 開始: ${SCRIPT_START} | 終了: $(date '+%Y-%m-%d %H:%M:%S')" # mailxが使える環境では以下で送信 echo "${msg}" | mailx -s "[Deploy] ${DEPLOY_STATUS}" "${NOTIFY_EMAIL}" 2>/dev/null || \ echo "[WARN] メール送信失敗(${msg})" >&2 } trap notify EXIT
mailx がない環境では、curl で Slack Webhook や Teams に送る方法も有効です。通知の詳細な設計については「シェルスクリプトのエラー通知設計」記事を参照してください。ここまでの3つの設計──排他制御・ロールバック・完了通知──を組み込んだシェルスクリプトの全体像を次のセクションで見ていきます。
シェルスクリプトの設計パターンを体系的に身につけたい方は、シェルスクリプト実践講座(Linux Master Pro)もご覧ください。現場で使える設計パターンを体系的に学べます。
実践:フルスクリプトの組み立て
1. スクリプトの全体構造
3つの設計を組み合わせた、コピーして使えるデプロイスクリプトの雛形です。#!/bin/bash # ============================================================ # deploy.sh — Webアプリデプロイスクリプト(ロールバック対応版) # 実行環境: RHEL 9.4 / Ubuntu 24.04 LTS # ============================================================ set -euo pipefail # ---- 設定 -------------------------------------------------- DEPLOY_DIR="/var/www/html/webapp" BUILD_DIR="/opt/build/webapp" BACKUP_DIR="/var/backup/webapp" SERVICE_NAME="httpd" # または nginx, tomcat など SERVICE_PORT=80 NOTIFY_EMAIL="ops@example.com" LOCKFILE="/var/run/deploy_webapp.lock" LOG_FILE="/var/log/deploy_webapp.log" # ---- ログ関数 ----------------------------------------------- TIMESTAMP() { date "+%Y-%m-%d %H:%M:%S"; } log_info() { echo "[$(TIMESTAMP)] INFO $*" | tee -a "${LOG_FILE}"; } log_warn() { echo "[$(TIMESTAMP)] WARN $*" | tee -a "${LOG_FILE}" >&2; } log_error() { echo "[$(TIMESTAMP)] ERROR $*" | tee -a "${LOG_FILE}" >&2; } SCRIPT_START=$(TIMESTAMP) DEPLOY_STATUS="SUCCESS" # ---- 排他制御 ----------------------------------------------- exec 9>"${LOCKFILE}" if ! flock -n 9; then log_error "別のデプロイが実行中です(${LOCKFILE})。終了します。" exit 1 fi log_info "ロック取得済み(PID: $$)" # ---- ロールバック関数 ---------------------------------------- rollback() { local exit_code=$? if [[ ${exit_code} -ne 0 ]]; then DEPLOY_STATUS="FAILED (exit: ${exit_code})" log_warn "デプロイ失敗。ロールバックを開始します..." local latest_backup latest_backup=$(ls -dt "${BACKUP_DIR}"/*/ 2>/dev/null | head -1) if [[ -n "${latest_backup}" ]]; then rsync -a --delete "${latest_backup}/" "${DEPLOY_DIR}/" sudo systemctl restart "${SERVICE_NAME}" log_info "ロールバック完了: ${latest_backup}" else log_error "バックアップが見つかりません。手動確認が必要です。" fi fi } # ---- 完了通知関数 -------------------------------------------- notify() { local msg="[Deploy ${DEPLOY_STATUS}] 開始: ${SCRIPT_START} / 終了: $(TIMESTAMP)" echo "${msg}" | mailx -s "[Deploy] ${DEPLOY_STATUS}" "${NOTIFY_EMAIL}" 2>/dev/null || \ log_warn "メール送信失敗" log_info "${msg}" } # EXIT で必ずロールバック判定+通知を実行 trap 'rollback; notify' EXIT # ---- デプロイ本体 ------------------------------------------- main() { log_info "デプロイ開始(実行者: $(id -un), ビルド: ${BUILD_DIR})" # 1. バックアップ local backup_path="${BACKUP_DIR}/$(date +%Y%m%d_%H%M%S)" mkdir -p "${backup_path}" rsync -a "${DEPLOY_DIR}/" "${backup_path}/" log_info "バックアップ完了: ${backup_path}" # 2. ファイル配布 rsync -avz --delete "${BUILD_DIR}/" "${DEPLOY_DIR}/" log_info "ファイル配布完了" # 3. サービス再起動 sudo systemctl restart "${SERVICE_NAME}" log_info "サービス再起動完了(${SERVICE_NAME})" # 4. 起動確認(最大30秒待機) local retries=6 while [[ ${retries} -gt 0 ]]; do if ss -tlnp | grep -q ":${SERVICE_PORT}"; then log_info "ポート ${SERVICE_PORT} 確認済み。デプロイ成功。" return 0 fi log_info "ポート待機中... (残り ${retries} 回)" sleep 5 (( retries-- )) done log_error "ポート ${SERVICE_PORT} が開かず。デプロイ失敗と判定します。" exit 1 } main
2. rsync でファイルを配布する
上記スクリプトの配布部分で使っているrsync -avz --delete の各オプションは以下の意味です。・-a(アーカイブ):パーミッション・タイムスタンプ・シンボリックリンクを保持して同期する
・-v(詳細):転送ファイル名をログに出力する
・-z(圧縮):転送時に圧縮する。ネットワーク帯域が細い環境で効果的
・--delete:転送元にないファイルを転送先から削除する。古いファイルが残らない
実行すると以下のような出力が得られます。
# rsync実行例(webサーバー app01 への配布) [2026-07-29 10:12:33] INFO ファイル配布完了 sending incremental file list ./ index.html assets/app.js assets/app.css sent 42,318 bytes received 92 bytes 28,273.33 bytes/sec total size is 158,244 speedup is 3.74
3. サービス再起動と正常確認
サービスを再起動した直後は、プロセスが起動中の場合があります。スクリプト内ではss コマンドでポートの LISTEN 状態を確認することで、サービスが正常に起動したかどうかを判定しています。# ssコマンドでポート80の確認(デプロイ後の正常確認) $ ss -tlnp | grep :80 LISTEN 0 128 0.0.0.0:80 0.0.0.0:* users:(("httpd",pid=12345,fd=4))
ss -tlnp で LISTEN 状態のポート一覧を取得し、デプロイ対象のポートが含まれているかどうかを grep で確認します。Linux ポート確認の全コマンドで ss コマンドの詳細なオプションも確認しておくと、サービス起動確認の幅が広がります。4. ロールバックをテストする
ロールバック設計は「失敗しないと動かない」部分なので、事前に意図的に失敗させてテストしておくことが重要です。以下のコマンドで失敗を再現できます。# テスト用: 配布元ディレクトリを故意に空にして失敗を再現する $ mkdir -p /opt/build/webapp_test $ bash deploy.sh [2026-07-29 10:15:00] INFO ロック取得済み(PID: 23456) [2026-07-29 10:15:00] INFO デプロイ開始(実行者: webadmin, ビルド: /opt/build/webapp) [2026-07-29 10:15:01] INFO バックアップ完了: /var/backup/webapp/20260729_101500 [2026-07-29 10:15:05] ERROR ポート 80 が開かず。デプロイ失敗と判定します。 [2026-07-29 10:15:05] WARN デプロイ失敗。ロールバックを開始します... [2026-07-29 10:15:06] INFO ロールバック完了: /var/backup/webapp/20260729_101455/ [Deploy FAILED (exit: 1)] 開始: 2026-07-29 10:15:00 / 終了: 2026-07-29 10:15:06
トラブルシュート・よくある問題
1. ロックファイルが残ってデプロイが起動しない
前回のデプロイがシグナル9(kill -9)で強制終了した場合、ロックファイルが残ることがあります。# ロックが残っているか確認する $ flock -n /var/run/deploy_webapp.lock echo "ロック取得できる" # 出力なし(取れない)→ まだロックされている # ロックを保持しているプロセスを確認する $ lsof /var/run/deploy_webapp.lock COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME bash 29081 webadmin 9uW REG 253,1 0 ... /var/run/deploy_webapp.lock # プロセスが存在しない(ゾンビ)場合のみロックファイルを削除する $ kill -0 29081 2>/dev/null || rm -f /var/run/deploy_webapp.lock
kill -0 はプロセスへのシグナル送信をせず、「プロセスが存在するか」だけを確認するコマンドです。プロセスが存在しない(終了コードが非ゼロ)場合のみロックファイルを削除するという安全な手順です。2. ロールバックしてもサービスが起動しない
ロールバックスクリプトが走ったにもかかわらず、サービスが起動しない場合の確認手順を示します。# 1. サービスの状態を確認する $ sudo systemctl status httpd * httpd.service - The Apache HTTP Server Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled; vendor pr> Active: failed (Result: exit-code) since ... # 2. ジャーナルログでエラー原因を確認する $ sudo journalctl -u httpd --since "5 minutes ago" Jul 29 10:15:07 web01.example.com httpd[23500]: httpd: Syntax error on line 92 of /etc/httpd/conf/httpd.conf # 3. 設定ファイルの構文チェックを行う $ sudo httpd -t Syntax error on line 92 of /etc/httpd/conf/httpd.conf: Invalid command 'DocumentRoot2', perhaps misspelled # → 設定ファイルが壊れている。バックアップから設定ファイルも復元が必要 $ sudo rsync -a /var/backup/webapp/20260729_101455/conf/ /etc/httpd/conf/ $ sudo systemctl start httpd
/etc/httpd/conf/ まで広げておくことを推奨します。3. cron からデプロイスクリプトを実行すると失敗する
cron 環境では PATH が/usr/bin:/bin しか設定されていないため、rsync や mailx が見つからないことがあります。スクリプト先頭に PATH の明示が必要です。#!/bin/bash # cron から呼ばれる場合は PATH を先頭で明示する PATH=/usr/local/bin:/usr/bin:/bin export PATH
sudo コマンドを使う場合は cron の実行ユーザー(通常 root またはデプロイ専用ユーザー)に対して /etc/sudoers でパスワードなし権限(NOPASSWD)を設定しておく必要があります。本記事のまとめ
デプロイスクリプトに組み込むべき3つの設計と、実装のポイントをまとめます。| 設計 | 実装方法 | 効果 |
|---|---|---|
| 排他制御 | flock -n 9 LOCKFILE |
同時デプロイによるファイル混在を防ぐ |
| ロールバック | trap rollback EXIT + rsync バックアップ |
デプロイ失敗時に旧バージョンへ自動切り戻し |
| 完了通知 | trap notify EXIT + mailx/curl |
成功・失敗どちらでも必ず結果を通知 |
| 起動確認 | ss -tlnp | grep :PORT リトライループ |
サービスが実際に起動したことを確認してから終了 |
| ログ設計 | tee -a LOG_FILE でファイルと画面に同時出力 |
誰が何時にデプロイしたか追跡できる |
シェルスクリプト講座を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:シェルスクリプトのロギング設計|ログ関数・タイムスタンプ・teeで出力を二重化する実践パターン
- この記事の属するカテゴリ:シェルスクリプトへ戻る

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