「別のバッチが出力するファイルが揃ってから処理を開始したいが、どう書けばいいか分からない」
こういった「条件が整うまで待機する」処理は、本番運用のシェルスクリプトでよく登場する。単純に
sleep 30で時間を埋める方法もあるが、必要以上に待たせたり、逆に待ち時間が足りずに失敗したりと、安定性に欠ける。この記事では、Linuxシェルスクリプトのwhileループを使ったポーリング設計を解説する。サービス起動待ち・ファイル生成待ち・URL応答待ちなど、現場でよく使うパターンをコード例付きで紹介し、タイムアウト設計・インターバル制御・ログ出力のベストプラクティスまでカバーする。動作確認はRHEL 9 / Rocky Linux 9 / Ubuntu 24.04 LTSで実施済みだ。
この記事のポイント
・whileループとsleepを組み合わせたポーリングの基本構造
・サービス起動・ファイル生成・URL応答待ちの実践パターン
・タイムアウトと上限回数で無限ループを確実に防ぐ設計
・ログ出力と終了コードで呼び出し元に状態を伝える設計
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜwhileループでポーリングするのか — 用途と設計の考え方
ポーリング(polling)とは、一定の間隔で状態を確認し続ける手法だ。「条件を満たしたらループを抜ける、満たさなければ待って再試行する」という繰り返しを実現する。シェルスクリプトでポーリングが必要になる代表的な場面を挙げる。
・DBサーバーを起動した後、アプリケーションを起動する前にポートの疎通を確認する
・別スクリプトが生成するファイルが揃ってから集計処理を実行する
・デプロイ後にURLにアクセスできるようになるまで待ってから次の工程に進む
・cronで起動するバッチ処理が、外部APIのメンテナンス時間帯を避けるために応答を待つ
単純な
sleepコマンドだけでは「決め打ちの待機時間」しか作れない。実際に条件が満たされたかどうかを確認せずに進むため、環境によっては待ちすぎたり、待ち時間が足りずに失敗したりする。whileループによるポーリングは、実際の状態を確認してから進む「インテリジェントな待機」だ。基本的な考え方:3つの要素を組み合わせる
・チェック条件: 何を確認するか(ポート・ファイル・URL等)
・インターバル: 何秒ごとに確認するか(
sleepの秒数)・タイムアウト: 何秒後に諦めるか(無限ループ防止)
この3要素を最初に変数として定義することで、後から調整しやすいスクリプトになる。
whileループとsleepによる基本的なポーリング構造
まず、ポーリングの基本構造を示す。この雛形を理解しておくと、各パターンに応用できる。#!/bin/bash TIMEOUT=60 # タイムアウトまでの最大秒数 INTERVAL=5 # チェック間隔(秒) elapsed=0 while true; do # 条件チェック(ここを用途に応じて書き換える) if <条件>; then echo "[OK] 条件成立(${elapsed}秒後)" exit 0 fi if [[ ${elapsed} -ge ${TIMEOUT} ]]; then echo "[ERROR] タイムアウト(${TIMEOUT}秒待機)" >&2 exit 1 fi echo "[WAIT] 条件未成立。${INTERVAL}秒後に再確認(経過: ${elapsed}秒)" sleep ${INTERVAL} elapsed=$((elapsed + INTERVAL)) done
・
while trueで永続ループを作り、条件成立時にexit 0で抜ける・
elapsed変数で経過時間を積算し、TIMEOUTに達したらexit 1で強制終了する・
TIMEOUTとINTERVALは変数化して先頭に集め、後から変更しやすくする・エラーメッセージは
>&2で標準エラー出力に流し、標準出力と分離するとログ処理が楽になるタイムアウトの判定を「チェック後」に行う理由は、1回目のチェックは必ず行いたいからだ。タイムアウトを先に判定すると、
elapsed=0、TIMEOUT=0のような設定ミスで最初のチェックも飛ばされてしまう。systemctlのサービス起動を待つポーリングの実装
本番運用で最もよく使うポーリングは「サービスの起動待ち」だ。たとえばDBサーバーを起動した後、アプリが接続を試みる前にサービスがactiveになっているかをポーリングで確認する。1. systemctl is-activeでサービス状態を確認する
systemctl is-active サービス名は、サービスがactive(稼働中)なら終了コード0を返す。この特性を利用してwhileループの条件にする。#!/bin/bash # サービス起動を最大60秒待機する SERVICE="mysql" TIMEOUT=60 INTERVAL=5 elapsed=0 echo "[INFO] ${SERVICE} の起動を待機します(タイムアウト: ${TIMEOUT}秒)" while true; do if systemctl is-active --quiet "${SERVICE}"; then echo "[OK] ${SERVICE} が起動しました(${elapsed}秒後)" exit 0 fi if [[ ${elapsed} -ge ${TIMEOUT} ]]; then echo "[ERROR] ${SERVICE} がタイムアウト内に起動しませんでした" >&2 exit 1 fi echo "[WAIT] ${SERVICE} 未起動。${INTERVAL}秒後に再確認(経過: ${elapsed}秒)" sleep ${INTERVAL} elapsed=$((elapsed + INTERVAL)) done
# mysql起動後、ポーリングスクリプトを呼び出す # root @ server01 ~ # systemctl start mysql && ./wait-service.sh [INFO] mysql の起動を待機します(タイムアウト: 60秒) [WAIT] mysql 未起動。5秒後に再確認(経過: 0秒) [OK] mysql が起動しました(5秒後) # タイムアウトした場合 # root @ server01 ~ # ./wait-service.sh [INFO] mysql の起動を待機します(タイムアウト: 60秒) [WAIT] mysql 未起動。5秒後に再確認(経過: 0秒) [WAIT] mysql 未起動。5秒後に再確認(経過: 5秒) [WAIT] mysql 未起動。5秒後に再確認(経過: 10秒) ...(省略) [ERROR] mysql がタイムアウト内に起動しませんでした # echo $? 1
2. ポート疎通でサービスの準備完了を確認する
systemctl is-activeはサービスプロセスの存在は確認できるが、実際に接続を受け付けているかは確認できない。実務上の注意点として、アプリケーションの接続確認はポートの疎通チェックで行う方が確実だ。サービスがactiveになっていても、初期化処理中でポートが開いていないことがある。ncコマンド(Netcat)を使ったポート疎通チェックの例:#!/bin/bash # ポート3306(MySQL)が応答するまで待機する HOST="127.0.0.1" PORT=3306 TIMEOUT=60 INTERVAL=3 elapsed=0 while true; do if nc -z -w2 "${HOST}" "${PORT}" 2>/dev/null; then echo "[OK] ${HOST}:${PORT} 接続確認(${elapsed}秒後)" exit 0 fi if [[ ${elapsed} -ge ${TIMEOUT} ]]; then echo "[ERROR] ${HOST}:${PORT} タイムアウト(${TIMEOUT}秒)" >&2 exit 1 fi echo "[WAIT] 未接続。${INTERVAL}秒後に再確認(経過: ${elapsed}秒)" sleep ${INTERVAL} elapsed=$((elapsed + INTERVAL)) done
-zオプションはゼロIOモード(接続確認のみ)、-w2は接続タイムアウトを2秒に設定する意味だ。ncが入っていない環境では/dev/tcpを使う代替手段もある。# ncがない環境では /dev/tcp で代替できる if (echo >/dev/tcp/127.0.0.1/3306) 2>/dev/null; then echo "ポート3306 応答あり" fi
ファイルの生成と書き込み完了を待つポーリングの実装
別プロセスやスクリプトが生成するファイルが揃ってから処理を開始したい場合のパターンだ。バッチ処理の連携や、外部プログラムが出力するファイルを集計する際によく使う。1. ファイルの存在確認
#!/bin/bash # 指定ファイルが生成されるまで最大300秒待機する TARGET_FILE="/var/tmp/export/daily_report.csv" TIMEOUT=300 INTERVAL=10 elapsed=0 while true; do if [[ -f "${TARGET_FILE}" ]]; then echo "[OK] ファイル確認: ${TARGET_FILE}(${elapsed}秒後)" exit 0 fi if [[ ${elapsed} -ge ${TIMEOUT} ]]; then echo "[ERROR] ファイル未生成のままタイムアウト: ${TARGET_FILE}" >&2 exit 1 fi echo "[WAIT] ファイル未生成。${INTERVAL}秒後に再確認(経過: ${elapsed}秒)" sleep ${INTERVAL} elapsed=$((elapsed + INTERVAL)) done
2. ファイルサイズが安定するまで待つ(書き込み完了の確認)
ファイルが存在するだけでなく、「書き込みが完了しているか」の確認が必要な場面もある。実務上の注意点として、ファイルが生成中(書き込み途中)でも-fのチェックはtrueになる。転送完了を確認するには、2回チェックしてサイズが変わっていないことを確認する手法が有効だ。#!/bin/bash # ファイルサイズが安定するまで待つ(書き込み完了確認) TARGET_FILE="/var/tmp/export/daily_report.csv" TIMEOUT=300 INTERVAL=10 STABLE_WAIT=3 # サイズ確認の間隔(秒) elapsed=0 while true; do if [[ -f "${TARGET_FILE}" ]]; then size1=$(stat -c%s "${TARGET_FILE}") sleep "${STABLE_WAIT}" size2=$(stat -c%s "${TARGET_FILE}") if [[ "${size1}" -gt 0 && "${size1}" -eq "${size2}" ]]; then echo "[OK] ファイル書き込み完了(サイズ: ${size2} bytes、${elapsed}秒後)" exit 0 fi fi if [[ ${elapsed} -ge ${TIMEOUT} ]]; then echo "[ERROR] タイムアウト(${TIMEOUT}秒)" >&2 exit 1 fi echo "[WAIT] ファイル書き込み中。${INTERVAL}秒後に再確認(経過: ${elapsed}秒)" sleep ${INTERVAL} elapsed=$((elapsed + INTERVAL)) done
stat -c%sでファイルサイズをバイト数で取得し、STABLE_WAIT秒後に再取得して変化がなければ書き込み完了と判断している。サイズが0バイトの場合(空ファイル)は完了とみなさない設計にするため、"${size1}" -gt 0の条件を加えている。URLの応答を待つポーリングの実装
Webアプリケーションのデプロイ後や再起動後に、サービスが実際にHTTPリクエストを処理できるようになるまで待つパターンだ。#!/bin/bash # URLが200を返すまで最大120秒待機する TARGET_URL="http://127.0.0.1:8080/health" TIMEOUT=120 INTERVAL=5 elapsed=0 while true; do HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 3 "${TARGET_URL}") if [[ "${HTTP_STATUS}" == "200" ]]; then echo "[OK] ${TARGET_URL} が応答(HTTP ${HTTP_STATUS})(${elapsed}秒後)" exit 0 fi if [[ ${elapsed} -ge ${TIMEOUT} ]]; then echo "[ERROR] タイムアウト。最後のHTTPステータス: ${HTTP_STATUS}" >&2 exit 1 fi echo "[WAIT] HTTP ${HTTP_STATUS}。${INTERVAL}秒後に再確認(経過: ${elapsed}秒)" sleep ${INTERVAL} elapsed=$((elapsed + INTERVAL)) done
curlの-sはサイレントモード(進捗非表示)、-o /dev/nullでレスポンスボディを捨て、-w "%{http_code}"でHTTPステータスコードだけを取得する。--connect-timeout 3で接続タイムアウトを3秒に設定し、ポーリング1回あたりがハングしないようにしている点がポイントだ。200以外のステータスコードも成功とみなしたい場合は、条件を変更する。
# 200または301(リダイレクト)を成功とみなす場合 if [[ "${HTTP_STATUS}" == "200" || "${HTTP_STATUS}" == "301" ]]; then echo "[OK] 応答確認(HTTP ${HTTP_STATUS})" exit 0 fi # 2xx系すべてを成功とみなす場合 if [[ "${HTTP_STATUS}" =~ ^2 ]]; then echo "[OK] 応答確認(HTTP ${HTTP_STATUS})" exit 0 fi
複数の待機をまとめた実践的なデプロイ前チェックスクリプト
実際の運用では「複数の条件すべてが満たされてから次の工程へ進む」というシナリオが多い。個々のポーリング関数をまとめたスクリプトの設計例を示す。#!/bin/bash # デプロイ前の複数条件チェックスクリプト # 使い方: ./pre-deploy-check.sh set -euo pipefail TIMEOUT=120 INTERVAL=5 # ポーリング汎用関数 wait_until() { local label="$1" local cmd="$2" local elapsed=0 echo "[CHECK] ${label} を確認中..." while true; do if eval "${cmd}" 2>/dev/null; then echo "[OK] ${label} 確認完了(${elapsed}秒後)" return 0 fi if [[ ${elapsed} -ge ${TIMEOUT} ]]; then echo "[ERROR] ${label} タイムアウト(${TIMEOUT}秒)" >&2 return 1 fi echo "[WAIT] 未確認。${INTERVAL}秒後に再確認(経過: ${elapsed}秒)" sleep ${INTERVAL} elapsed=$((elapsed + INTERVAL)) done } # 各条件のチェック実行 wait_until "MySQL(ポート3306)" "nc -z -w2 127.0.0.1 3306" wait_until "Redis(ポート6379)" "nc -z -w2 127.0.0.1 6379" wait_until "アプリのヘルスチェック" "curl -sf --connect-timeout 3 http://127.0.0.1:8080/health" echo "" echo "=== 全チェック完了。デプロイを続行します ===" exit 0
eval "${cmd}"でコマンド文字列を実行することで、条件チェックを関数の外から差し替えられるようにしている。この設計にしておくと、チェック対象が増えても関数本体を書き変えずに済む。実際のサーバーでの実行例:
# root @ server01 ~ # ./pre-deploy-check.sh [CHECK] MySQL(ポート3306) を確認中... [OK] MySQL(ポート3306) 確認完了(0秒後) [CHECK] Redis(ポート6379) を確認中... [WAIT] 未確認。5秒後に再確認(経過: 0秒) [OK] Redis(ポート6379) 確認完了(5秒後) [CHECK] アプリのヘルスチェック を確認中... [WAIT] 未確認。5秒後に再確認(経過: 0秒) [WAIT] 未確認。5秒後に再確認(経過: 5秒) [OK] アプリのヘルスチェック 確認完了(10秒後) === 全チェック完了。デプロイを続行します ===
ポーリングが終わらない時のトラブルシュート
タイムアウトせずに無限ループになっている場合
原因と対処:elapsedの積算が正しく行われていない場合に発生する。よくある間違いとして、elapsed=$((elapsed + INTERVAL))の代わりにelapsed=$(elapsed + INTERVAL)と書いてしまう(算術展開の$(( ))でなく$( )を使うミス)がある。またはTIMEOUT変数をループ内で誤って変更しているケースもある。デバッグ時は
set -xをスクリプト冒頭に追加すると、各変数の展開値と実行コマンドが表示され、問題箇所を特定しやすい。# デバッグモードで実行確認(-x は各コマンドと変数展開結果を表示する) # root @ server01 ~ # bash -x ./wait-service.sh + SERVICE=mysql + TIMEOUT=60 + INTERVAL=5 + elapsed=0 + true + systemctl is-active --quiet mysql + [[ 0 -ge 60 ]] + echo '[WAIT] mysql 未起動。5秒後に再確認(経過: 0秒)' [WAIT] mysql 未起動。5秒後に再確認(経過: 0秒) + sleep 5 + elapsed=5 ...
条件は満たされているのにループが抜けない場合
原因と対処:条件チェックのコマンドが常に失敗しているケースが多い。systemctl is-activeを使う場合はサービス名のスペルミス、--quietオプションの有無による挙動の違いを確認する。ファイルチェックであればパスの間違い(末尾スラッシュ有無・絶対パス・相対パスの混在)が原因になることがある。条件コマンドを単体で実行して終了コードを確認するのが確実だ。
# 条件コマンドを単体で実行して終了コードを確認する # root @ server01 ~ # systemctl is-active --quiet mysql; echo $? 0 # 0なら成功(サービス稼働中) # ファイルパスのチェック # root @ server01 ~ # [[ -f "/var/tmp/export/daily_report.csv" ]] && echo "exists" || echo "not found" not found # パスが違っていないか確認する # ncコマンドが入っているかどうかも確認する # root @ server01 ~ # which nc /usr/bin/nc
インターバルが長すぎて処理時間が伸びる場合
原因と対処:インターバルは「最短でいつ条件が成立しうるか」から逆算して決める。MySQLの起動は通常1~3秒で完了するため、INTERVAL=10にしてしまうと最大10秒の無駄待ちが生じる。一方、URLポーリングでは外部の応答時間やネットワーク遅延があるため、3~5秒程度が適切だ。条件が整うのが遅い処理ほどインターバルを長めにして、チェック処理自体がサーバーの負荷にならないよう調整する。まとめ
| 用途 | チェック手段 | 推奨インターバル | 推奨タイムアウト |
|---|---|---|---|
| サービス起動確認 | systemctl is-active --quiet |
3~5秒 | 30~60秒 |
| ポート疎通確認 | nc -z -w2 |
3~5秒 | 60~120秒 |
| ファイル生成待ち | [[ -f ファイル ]] |
5~10秒 | 60~300秒 |
| ファイル書き込み完了 | stat -c%sの2回比較 |
10秒 | 120~300秒 |
| URL応答確認 | curl -s -o /dev/null -w "%{http_code}" |
5秒 | 60~180秒 |
whileループによるポーリングはシンプルだが、インターバル・タイムアウト・ログ出力の3要素を意識して設計することで、本番環境でも安心して使えるスクリプトになる。
シェルスクリプト講座を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 次のページへ:シェルスクリプトのログ出力設計|ログレベル関数・タイムスタンプ・二重出力の実装パターン
- 前のページへ:シェルスクリプトのプロファイリング設計|PS4変数とtimeでボトルネックを特定する方法
- この記事の属するカテゴリ:シェルスクリプトへ戻る

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