ネットワーク瞬断・一時的なサーバー過負荷・レート制限。シェルスクリプトが本番環境で失敗する原因の多くは「一時的な失敗」です。
そのたびに手動で再実行するのは、自動化の意味がありません。
この記事では、シェルスクリプトのretry設計について、基本のwhileループから指数バックオフ・timeoutコマンドとの組み合わせまで、実務で即使える実装パターンを解説します。RHEL 9.4 / Ubuntu 24.04 LTSで動作確認済みです。
この記事のポイント
・while ループ+$? でコマンドの失敗を検知し、自動で再試行できる
・最大試行回数・待機時間・指数バックオフの3軸で retry を設計する
・timeout コマンドで無限待ちを防ぎ、retry の内側に組み込む
・trap と組み合わせて中断時にもクリーンアップが走る設計にする
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜシェルスクリプトにretry設計が必要なのか
シェルスクリプトで自動化を組む際、意外と見落とされがちなのが「一時的な失敗」への対処です。例えば次のような状況を考えてください。
・外部APIへのcurlがネットワーク瞬断で失敗した
・rsyncによるファイル転送がサーバー過負荷で途中で止まった
・デプロイ後のサービス起動確認が、プロセスが立ち上がりきる前に走って失敗した
こうした一時的な失敗に対して、スクリプトがそのまま
exit 1 で終わってしまうと、cronやCI/CDパイプラインがエラー扱いになり、後続の処理が止まります。一方で、「とりあえず何度でも再試行し続ける」設計は、障害が恒久的なものだったときに無限ループになる危険があります。
retry設計の目的は「一時的な失敗には自動で対処しつつ、恒久的な失敗は早期に検知して終了する」という2つを両立させることです。
retryの基本実装:whileループと終了コード
1. 終了コード($?)とretryの関係
Linuxコマンドは終了時に「終了コード(exit status)」を返します。成功なら0、失敗なら 1 以上の非ゼロ値です。retry設計の基本は「コマンドの終了コードを確認し、非ゼロなら再試行する」というシンプルな仕組みです。
# 終了コードの確認例 $ curl https://192.0.2.1/api/status curl: (7) Failed to connect to 192.0.2.1 port 443: Connection refused $ echo $? 7 $ ls /tmp/work.txt ls: cannot access '/tmp/work.txt': No such file or directory $ echo $? 2
2. 基本的なretryループ
最もシンプルなretry実装は、forループと終了コードの組み合わせです。#!/bin/bash MAX_RETRY=3 RETRY_WAIT=5 SUCCESS=0 for i in $(seq 1 $MAX_RETRY); do curl -sf https://192.0.2.1/api/status if [ $? -eq 0 ]; then SUCCESS=1 echo "成功(試行 $i 回目)" break fi echo "失敗(試行 $i 回目)。${RETRY_WAIT}秒後に再試行します..." sleep $RETRY_WAIT done if [ $SUCCESS -ne 1 ]; then echo "ERROR: ${MAX_RETRY}回試行したが成功しなかった" >&2 exit 1 fi
失敗(試行 1 回目)。5秒後に再試行します... 失敗(試行 2 回目)。5秒後に再試行します... 成功(試行 3 回目)
retry関数化:再利用できる設計にする
retry処理を毎回ループで書くと保守性が下がります。関数として切り出すのが実務的な設計です。#!/bin/bash # retry関数: 指定コマンドを最大 MAX_RETRY 回実行する # 使い方: retry <コマンド> [引数...] MAX_RETRY=${MAX_RETRY:-3} RETRY_WAIT=${RETRY_WAIT:-5} retry() { local attempt=1 until "$@"; do if [ $attempt -ge $MAX_RETRY ]; then echo "ERROR: '$*' は ${MAX_RETRY}回試行したが成功しなかった" >&2 return 1 fi echo "WARN: '$*' 失敗(試行 ${attempt}/${MAX_RETRY})。${RETRY_WAIT}秒後に再試行..." attempt=$((attempt + 1)) sleep $RETRY_WAIT done } # 呼び出し例 retry curl -sf https://192.0.2.1/api/status retry rsync -az /data/ backup@192.0.2.10:/backup/
until "$@" は「コマンドが成功するまでループを続ける」構文です。引数をすべてコマンドとして渡すため、任意のコマンドに対してretryを適用できます。シェルスクリプトの設計技法を体系的に学びたい方は、シェルスクリプト実践講座も参考にしてください。
指数バックオフの実装
固定の待機時間(sleep 5)では、多数のクライアントが同時にリトライすると、サーバーに集中してかえって負荷が高まることがあります(Thundering Herd問題)。指数バックオフ(Exponential Backoff)は、再試行のたびに待機時間を指数的に増やす戦略です。
#!/bin/bash # 指数バックオフ付きretry関数 # 待機時間: 1回目→2秒、2回目→4秒、3回目→8秒(MAX_WAITで上限設定) MAX_RETRY=${MAX_RETRY:-5} BASE_WAIT=${BASE_WAIT:-2} MAX_WAIT=${MAX_WAIT:-60} retry_backoff() { local attempt=1 local wait_time=$BASE_WAIT until "$@"; do if [ $attempt -ge $MAX_RETRY ]; then echo "ERROR: '$*' は ${MAX_RETRY}回試行したが成功しなかった" >&2 return 1 fi echo "WARN: '$*' 失敗(試行 ${attempt}/${MAX_RETRY})。${wait_time}秒待機..." sleep $wait_time attempt=$((attempt + 1)) wait_time=$((wait_time * 2)) if [ $wait_time -gt $MAX_WAIT ]; then wait_time=$MAX_WAIT fi done } retry_backoff curl -sf https://192.0.2.1/api/status
WARN: 'curl -sf https://192.0.2.1/api/status' 失敗(試行 1/5)。2秒待機... WARN: 'curl -sf https://192.0.2.1/api/status' 失敗(試行 2/5)。4秒待機... WARN: 'curl -sf https://192.0.2.1/api/status' 失敗(試行 3/5)。8秒待機... # 4回目で成功
timeoutコマンドとの組み合わせ
retry設計で見落とされやすいのが「コマンドが終了せずに長時間ハングする」ケースです。curlのデフォルトはタイムアウトなしで、接続が確立しない場合に数分間待ち続けることがあります。
timeout コマンドを使うと、指定秒数でコマンドを強制終了させられます。# timeoutの基本: 10秒以内にcurlが完了しなければ強制終了 $ timeout 10 curl -sf https://192.0.2.1/api/status $ echo $? 124 # タイムアウトした場合の終了コードは 124
#!/bin/bash # retry + timeout の組み合わせ # 1回あたりのタイムアウト: 10秒、最大3回試行 MAX_RETRY=3 RETRY_WAIT=5 CMD_TIMEOUT=10 retry_with_timeout() { local attempt=1 until timeout "$CMD_TIMEOUT" "$@"; do local exit_code=$? if [ $exit_code -eq 124 ]; then echo "WARN: '$*' タイムアウト(${CMD_TIMEOUT}秒)(試行 ${attempt}/${MAX_RETRY})" else echo "WARN: '$*' 失敗(終了コード ${exit_code})(試行 ${attempt}/${MAX_RETRY})" fi if [ $attempt -ge $MAX_RETRY ]; then echo "ERROR: ${MAX_RETRY}回試行したが成功しなかった" >&2 return 1 fi attempt=$((attempt + 1)) sleep $RETRY_WAIT done } retry_with_timeout curl -sf https://192.0.2.1/api/status
実務で使うretryパターン集
1. curl:APIコール(HTTPステータスコードまで検証する)
curlには--retry オプションが組み込まれていますが、これはネットワーク層の失敗のみを対象とします。HTTPステータス500系のサーバーエラーはcurl自体は成功扱い(終了コード0)になるため、アプリケーション層のエラーにはスクリプト側でretryを実装する必要があります。
#!/bin/bash # HTTPステータスコードまで検証するcurl retry retry_curl() { local url=$1 local attempt=1 local wait_time=2 while [ $attempt -le 5 ]; do local response response=$(curl -sf -w " %{http_code}" "$url" 2>/dev/null) local curl_exit=$? local http_code http_code=$(echo "$response" | tail -1) local body body=$(echo "$response" | sed '$d') if [ $curl_exit -eq 0 ] && [ "$http_code" -eq 200 ]; then echo "$body" return 0 fi echo "WARN: curl失敗(試行 ${attempt}/5)HTTP=${http_code} exit=${curl_exit}。${wait_time}秒待機..." >&2 sleep $wait_time wait_time=$((wait_time * 2)) attempt=$((attempt + 1)) done echo "ERROR: APIコールが5回失敗した: $url" >&2 return 1 } retry_curl "https://api.example.com/v1/status"
2. サービス起動待ち(ncによるポート疎通確認)
デプロイ後にサービスの起動完了を待つ場面は頻繁にあります。#!/bin/bash # PostgreSQL(5432番ポート)が開くまで待つ WAIT_HOST="127.0.0.1" WAIT_PORT=5432 MAX_RETRY=30 RETRY_WAIT=2 echo "PostgreSQLの起動を待っています..." for i in $(seq 1 $MAX_RETRY); do if nc -z "$WAIT_HOST" "$WAIT_PORT" 2>/dev/null; then echo "ポート ${WAIT_PORT} が開きました(${i}回目で成功)" break fi if [ $i -eq $MAX_RETRY ]; then echo "ERROR: ${MAX_RETRY}回試行したがポート${WAIT_PORT}が開かなかった" >&2 exit 1 fi echo "待機中... (${i}/${MAX_RETRY})" sleep $RETRY_WAIT done
PostgreSQLの起動を待っています... 待機中... (1/30) 待機中... (2/30) 待機中... (3/30) ポート 5432 が開きました(4回目で成功)
3. rsync:転送失敗時の自動再試行
#!/bin/bash # rsync の転送を最大3回retry # --partial: 途中まで転送したファイルを保持(再開のため) # --timeout=30: rsync自体のI/Oタイムアウト MAX_RETRY=3 SRC="/data/backup/" DST="backup@192.0.2.10:/backup/$(date +%Y%m%d)/" for i in $(seq 1 $MAX_RETRY); do rsync -az --partial --timeout=30 "$SRC" "$DST" if [ $? -eq 0 ]; then echo "rsync 完了(試行 ${i}回目)" exit 0 fi echo "WARN: rsync 失敗(試行 ${i}/${MAX_RETRY})" [ $i -lt $MAX_RETRY ] && sleep 10 done echo "ERROR: rsync が${MAX_RETRY}回失敗した" >&2 exit 1
trapとの連携:中断時のクリーンアップ
retry処理中に Ctrl+C やSIGTERMで中断された場合、一時ファイルや途中状態が残ることがあります。trap と組み合わせることで、中断時にも確実にクリーンアップが走る設計にできます。#!/bin/bash # trapとretryの組み合わせ # 中断時(SIGINT/SIGTERM/EXIT)に一時ファイルを削除する TMPFILE=$(mktemp /tmp/retry_work.XXXXXX) cleanup() { echo "クリーンアップ中..." rm -f "$TMPFILE" } trap cleanup EXIT SIGINT SIGTERM MAX_RETRY=3 RETRY_WAIT=5 for i in $(seq 1 $MAX_RETRY); do curl -sf https://192.0.2.1/api/data -o "$TMPFILE" if [ $? -eq 0 ]; then echo "取得完了。処理を続行します..." cp "$TMPFILE" /var/data/latest.json break fi echo "WARN: 取得失敗(試行 ${i}/${MAX_RETRY})。${RETRY_WAIT}秒後に再試行..." sleep $RETRY_WAIT done # EXIT トラップにより cleanup() が自動で呼ばれる
trap cleanup EXIT を設定しておくと、スクリプトが正常終了・エラー終了・シグナル受信のいずれで終わっても cleanup() が実行されます。retryとtrapの組み合わせは、本番スクリプトの信頼性を大きく高める設計パターンです。retryが正しく動かない時の確認ポイント
retry設計を実装してもうまく動かない場合、次の3点を確認してください。1. set -e 環境ではuntilが動かない
スクリプト冒頭にset -e や set -euo pipefail を書いている場合、コマンドが非ゼロで終了した瞬間にスクリプト自体が終了します。until "$@" の内部でコマンドが失敗すると、retryループに入る前にスクリプトが死んでしまいます。# set -e 環境での対処: || true でエラーを一時的に無効化する set -euo pipefail retry() { local attempt=1 # || true により set -e の即時終了を回避する until "$@" || false; do [ $attempt -ge "${MAX_RETRY:-3}" ] && return 1 attempt=$((attempt + 1)) sleep "${RETRY_WAIT:-5}" done }
2. timeoutの終了コード124を「成功」と誤判定する
timeout がタイムアウトした時の終了コードは 124 です。retryループ内でtimeoutを使う場合、
$? の確認を省略すると、タイムアウトも「通常のエラー」として同じ待機時間でリトライし続けてしまいます。タイムアウトが多発する場合は、
CMD_TIMEOUT の値を大きくするか、接続先の問題を先に調査してください。3. コマンドが常にexit 0を返している
curl -s(サイレントモード)では、HTTPステータスが400系・500系でも終了コードが0になります。curl -sf(-fオプション追加)にすることで、HTTPエラー時に非ゼロを返すようになります。# -f なし: HTTPエラーでも exit 0 → retryが機能しない curl -s https://192.0.2.1/api/status # -f あり: HTTPエラーで exit 22 → retryが動く curl -sf https://192.0.2.1/api/status
本記事のまとめ
| 設計パターン | 実装例 | 用途 |
|---|---|---|
| 基本retry | for i in $(seq 1 $MAX_RETRY); do ... done |
シンプルな再試行 |
| retry関数 | until "$@"; do ... done |
任意コマンドへの汎用retry |
| 指数バックオフ | wait_time=$((wait_time * 2)) |
Thundering Herd回避 |
| タイムアウト制御 | timeout 10 コマンド |
hang防止・無限待ち回避 |
| trapとの連携 | trap cleanup EXIT SIGINT SIGTERM |
中断時のクリーンアップ保証 |
| ポート疎通確認 | nc -z ホスト ポート |
サービス起動待ち |
この3つを組み合わせることで、一時的な失敗には自動で対処しつつ、恒久的な障害は早期に検知して終了できるスクリプトになります。
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:シェルスクリプトでdiffを使った設定変更検知を設計する方法|スナップショット比較・差分通知・ロールバック判断の実装パターン
- この記事の属するカテゴリ:シェルスクリプトへ戻る

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