シェルスクリプトでデプロイを自動化する方法|ロールバック・排他制御・通知を組み込んだリリース設計

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)シェルスクリプト > シェルスクリプトでデプロイを自動化する方法|ロールバック・排他制御・通知を組み込んだリリース設計
「デプロイのたびに手順書を見ながらコマンドを打つ作業が続いている」
「途中で失敗したとき、どこまで進んだか分からなくなって焦った経験がある」

Linux サーバーへのリリース作業をシェルスクリプトで自動化すれば、この問題をまとめて解決できます。ただし「コマンドを並べただけ」のスクリプトは、失敗したときに中途半端な状態でサーバーを傷つけます。ロックもなければロールバックもない──そういったスクリプトが本番障害を起こすのは、残念ながらよくある話です。

この記事では、デプロイスクリプトに必ず組み込むべき3つの設計──排他制御・ロールバック・完了通知──を実機で動作確認しながら解説します。コピーしてすぐ使える雛形スクリプトも掲載していますので、参考にしてください。

実行環境:RHEL 9.4 / Ubuntu 24.04 LTS で動作確認済み

この記事のポイント

・flockコマンドで二重実行を防ぐ排他制御がデプロイスクリプトの基本
・rollback関数をtrap EXITに登録すると失敗時に旧バージョンへ自動切り戻せる
・ssコマンドでサービス起動を検証してからスクリプトを終了するのが安全設計
・通知はtrap EXITに集約し、成功・失敗どちらの経路でも確実に送出する


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

なぜデプロイスクリプトに「設計」が必要なのか

単純なデプロイスクリプトは、こういう形になりがちです。

#!/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 "デプロイ完了"

問題は3つあります。

二重実行の危険性: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 しか設定されていないため、rsyncmailx が見つからないことがあります。スクリプト先頭に 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つの設計を組み込むだけで、デプロイ作業の安全性と可視性が大幅に上がります。本番環境に適用する前に、必ずステージング環境でロールバックも含めた通しテストを行ってください。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、20年以上の運用経験を持つ現役エンジニアが基礎から教えます。
シェルスクリプト講座を見る >>

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

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

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

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

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

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

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

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

この記事を書いた人

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

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

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