シェルスクリプトのretry設計|コマンド失敗時の自動再試行とタイムアウト制御の実装パターン

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOME > Linux技術 リナックスマスター.JP(Linuxマスター.JP) > シェルスクリプト > シェルスクリプトのretry設計|コマンド失敗時の自動再試行とタイムアウト制御の実装パターン
「curl が接続エラーで終わった。もう1回やったら通ったのに……」
ネットワーク瞬断・一時的なサーバー過負荷・レート制限。シェルスクリプトが本番環境で失敗する原因の多くは「一時的な失敗」です。
そのたびに手動で再実行するのは、自動化の意味がありません。

この記事では、シェルスクリプトのretry設計について、基本のwhileループから指数バックオフ・timeoutコマンドとの組み合わせまで、実務で即使える実装パターンを解説します。RHEL 9.4 / Ubuntu 24.04 LTSで動作確認済みです。

この記事のポイント

・while ループ+$? でコマンドの失敗を検知し、自動で再試行できる
・最大試行回数・待機時間・指数バックオフの3軸で retry を設計する
・timeout コマンドで無限待ちを防ぎ、retry の内側に組み込む
・trap と組み合わせて中断時にもクリーンアップが走る設計にする


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

なぜシェルスクリプトに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

retryと組み合わせる場合は、retryの内側にtimeoutを置きます。

#!/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 ホスト ポート サービス起動待ち
retry設計の3つの軸は「最大試行回数(MAX_RETRY)」「待機戦略(固定待機 or 指数バックオフ)」「タイムアウト(timeoutコマンド)」です。
この3つを組み合わせることで、一時的な失敗には自動で対処しつつ、恒久的な障害は早期に検知して終了できるスクリプトになります。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、
シェルスクリプトの設計から実運用まで体系的に学べます。
>> シェルスクリプト実践講座の詳細はこちら

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

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

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

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

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

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

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

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

この記事を書いた人

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

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

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