「夜間バッチで全サーバーの状態確認をまとめて実施したいが、1台でもタイムアウトしたら次のサーバーへ進めない」
この問題を解決するのが、シェルスクリプトによるSSH並列実行設計です。
&でコマンドをバックグラウンドに回し、wait $pidで個別の終了コードを収集する仕組みを使えば、20台のサーバーへのコマンド実行が数秒で完了します。この記事では、SSH並列実行の基本パターンから、タイムアウト制御・一時ファイルによる結果格納・障害サーバーの自動検知まで、設計層を順に解説します。RHEL 9.4 / Ubuntu 24.04 LTSで動作確認済みです。
この記事のポイント
・&とwait ${PIDS[$i]}でSSH接続を並列化して終了コードを個別収集できる
・mktemp + trap EXITで一時ファイルを確実にクリーンアップする設計が基本
・timeoutとConnectTimeoutの2段階で応答しないサーバーへの待ちを制限する
・障害サーバーを自動検知してFAILEDカウントをexit値として返す設計が推奨
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜ複数サーバーへのSSH操作を並列化すべきか
10台のサーバーに順番にSSH接続してdf -hの結果を確認すると、1台あたり2秒かかれば合計20秒、タイムアウトが発生すれば数分かかることもあります。並列化すると最も遅いサーバーの応答時間だけで済むため、待ち時間を大幅に削減できます。並列SSH実行が特に有効な場面は次の3つです。
・定期ヘルスチェック: ディスク使用量・メモリ残量・プロセス死活を全サーバーで一斉確認する
・一斉設定確認: 設定ファイルのハッシュ値やパッケージバージョンを全サーバーで比較する
・障害時の初動調査: 問題発生直後に全サーバーのログやプロセス状態を同時に取得する
シェルスクリプトで並列SSH実行を設計する際に押さえるべき3点があります。
・終了コードの収集:
&で起動したプロセスのPIDを保持し、wait $pidで個別に終了コードを取得する・タイムアウト制御: 応答しないサーバーが1台あるだけでスクリプト全体が止まらないよう制限を設ける
・出力の分離: 標準出力と標準エラーを一時ファイルに格納し、並列実行時の出力混在を防ぐ
基本パターン:SSH並列実行の設計
1. forループとバックグラウンドジョブ(&)の基本
最初に理解すべき基本形は、forループの各コマンドを&でバックグラウンドに回し、waitで全ジョブの終了を待つパターンです。#!/bin/bash set -uo pipefail SERVERS=("srv01.example.local" "srv02.example.local" "srv03.example.local") for SERVER in "${SERVERS[@]}"; do # -n: バックグラウンド実行時の標準入力リダイレクト(必須) ssh -n -o ConnectTimeout=5 "$SERVER" "df -h /" & done wait echo "全サーバーの処理が完了しました"
sshに-nオプションを付けているのは、バックグラウンド実行時に標準入力を/dev/nullへリダイレクトするためです。これがないと、sshが対話的入力を待って処理が止まることがあります。ただし、この形では各サーバーの終了コードを個別に取得できません。いずれかのサーバーでエラーが発生しても、スクリプトはそれを検知できないまま終了します。
2. PIDを使った終了コードの収集
各サーバーへのSSH接続のPIDを配列に記録し、wait ${PIDS[$i]}で個別の終了コードを収集します。set -eを使うと、sshがエラー終了した瞬間にスクリプト全体が止まってしまいます。並列実行ではset -eを使わず、waitの戻り値を手動で確認する設計にします。#!/bin/bash set -uo pipefail # -e は意図的に外す(後述) SERVERS=("srv01.example.local" "srv02.example.local" "srv03.example.local") declare -a PIDS=() # バックグラウンドでSSHを起動してPIDを記録する for SERVER in "${SERVERS[@]}"; do ssh -n -o ConnectTimeout=5 "$SERVER" "df -h /" & PIDS+=($!) # $! は直前のバックグラウンドジョブのPID done # 各PIDの終了コードを個別に収集する FAILED=0 for i in "${!PIDS[@]}"; do wait "${PIDS[$i]}" # このwaitは対象PIDの終了コードを返す RC=$? SERVER="${SERVERS[$i]}" if [ "$RC" -ne 0 ]; then echo "[FAIL] ${SERVER} (終了コード: ${RC})" FAILED=$((FAILED + 1)) else echo "[OK] ${SERVER}" fi done echo "---" echo "完了: ${#SERVERS[@]} サーバー中 $FAILED 件失敗" exit $FAILED
${!PIDS[@]}はPIDS配列のインデックス(0、1、2...)を返します。インデックスでSERVERS配列とPIDS配列を対応させているため、どのPIDがどのサーバーのものかを追跡できます。wait $pidはそのPIDのプロセスが終了するまで待ち、終了コードを返します。この形でforループを回すことで、並列に起動した全プロセスの成功・失敗を個別に把握できます。3. 結果を一時ファイルに格納するパターン
並列実行すると、複数のSSH接続の出力が混在してターミナルに表示されます。実務では各サーバーの出力を一時ファイルに分けて格納し、全プロセスの終了後に集約して表示する設計が安全です。#!/bin/bash set -uo pipefail SERVERS=("srv01.example.local" "srv02.example.local" "srv03.example.local") declare -a PIDS=() # 一時ディレクトリを作成してEXIT時に自動削除する WORKDIR=$(mktemp -d /tmp/ssh_parallel.XXXXXX) trap 'rm -rf "$WORKDIR"' EXIT for SERVER in "${SERVERS[@]}"; do ssh -n -o ConnectTimeout=5 "$SERVER" "df -h / | tail -1" > "$WORKDIR/${SERVER}.out" 2> "$WORKDIR/${SERVER}.err" & PIDS+=($!) done FAILED=0 for i in "${!SERVERS[@]}"; do wait "${PIDS[$i]}" RC=$? SERVER="${SERVERS[$i]}" if [ "$RC" -eq 0 ]; then printf "[OK] %-30s %s\n" "$SERVER" "$(cat "$WORKDIR/${SERVER}.out")" else printf "[FAIL] %-30s %s\n" "$SERVER" "$(cat "$WORKDIR/${SERVER}.err")" FAILED=$((FAILED + 1)) fi done echo "---" echo "完了: ${#SERVERS[@]} サーバー中 $FAILED 件失敗" exit $FAILED
mktemp -dでユニークな一時ディレクトリを作成し、trap 'rm -rf "$WORKDIR"' EXITでスクリプト終了時(正常・異常どちらの場合も)に自動削除します。この組み合わせは、並列処理で生じる一時ファイルのクリーンアップ漏れを防ぐシェルスクリプトの定石です。タイムアウト制御の組み込み設計
SSH接続には2種類のタイムアウトがあります。TCP接続確立時のタイムアウトと、コマンド実行全体のタイムアウトです。両方を設定しないと、応答しないサーバーに引っかかってスクリプトが長時間ブロックされます。・ConnectTimeout: TCP接続確立の最大待ち時間(SSHオプション)。デフォルトはOSのTCPタイムアウトに依存して120秒以上になることがある
・timeoutコマンド: SSH接続後のコマンド実行を含む全体のタイムアウト。長時間かかるコマンドや想定外のプロンプト待ちを防ぐ
2段階のタイムアウト設計を組み込んだ例を示します。
# ConnectTimeout=5 で接続確立を5秒以内に制限 # timeout 10 でSSH全体(接続+コマンド実行)を10秒以内に制限 # BatchMode=yes でパスワードプロンプトを出さず即座に失敗させる timeout 10 ssh -n \ -o ConnectTimeout=5 \ -o StrictHostKeyChecking=no \ -o BatchMode=yes \ "$SERVER" "df -h / | tail -1" \ > "$WORKDIR/${SERVER}.out" \ 2> "$WORKDIR/${SERVER}.err" & PIDS+=($!)
BatchMode=yesを設定すると、パスワード入力プロンプトや鍵の確認メッセージが表示された場合に自動で失敗扱いにします。並列実行時に対話的なプロンプトが出るとスクリプトが止まるため、自動化スクリプトでは必須の設定です。StrictHostKeyChecking=noは初回接続時のホスト鍵確認をスキップします。セキュリティリスクがあるため、管理対象サーバーのホスト鍵をssh-keyscan -H サーバーIP >> ~/.ssh/known_hostsで事前登録したうえでStrictHostKeyChecking=yesに戻すことを推奨します。実践スクリプト:ディスク使用量の一斉確認
SSH並列実行の完成形として、5台のサーバーへのディスク使用量確認スクリプトを示します。#!/bin/bash # disk-check-all.sh - 全サーバーのディスク使用量を並列確認する set -uo pipefail # 管理対象サーバーリスト SERVERS=( "srv01.example.local" "srv02.example.local" "srv03.example.local" "srv04.example.local" "srv05.example.local" ) CONNECT_TIMEOUT=5 # TCP接続の最大待ち秒数 EXEC_TIMEOUT=10 # SSHコマンド全体の最大待ち秒数 WARN_PERCENT=80 # 使用率がこの値以上なら警告表示する declare -a PIDS=() WORKDIR=$(mktemp -d /tmp/disk_check.XXXXXX) trap 'rm -rf "$WORKDIR"' EXIT echo "=== ディスク使用量チェック開始 ($(date '+%Y-%m-%d %H:%M:%S')) ===" # 全サーバーへ並列でSSH接続する for SERVER in "${SERVERS[@]}"; do timeout "$EXEC_TIMEOUT" ssh -n \ -o "ConnectTimeout=${CONNECT_TIMEOUT}" \ -o StrictHostKeyChecking=no \ -o BatchMode=yes \ "$SERVER" \ "df -h / | awk 'NR==2{print \$5, \$1, \$2, \$3, \$4}'" \ > "$WORKDIR/${SERVER}.out" \ 2> "$WORKDIR/${SERVER}.err" & PIDS+=($!) done # 結果を収集して表示する FAILED=0 WARN=0 for i in "${!SERVERS[@]}"; do wait "${PIDS[$i]}" RC=$? SERVER="${SERVERS[$i]}" if [ "$RC" -ne 0 ]; then printf "[FAIL] %-28s %s\n" "$SERVER" "$(cat "$WORKDIR/${SERVER}.err")" FAILED=$((FAILED + 1)) continue fi USAGE=$(cat "$WORKDIR/${SERVER}.out") PERCENT=$(echo "$USAGE" | awk '{print $1}' | tr -d '%') if [ "$PERCENT" -ge "$WARN_PERCENT" ]; then printf "[WARN] %-28s %s <- 使用率 %s%%\n" "$SERVER" "$USAGE" "$PERCENT" WARN=$((WARN + 1)) else printf "[OK] %-28s %s\n" "$SERVER" "$USAGE" fi done echo "===" echo "完了: ${#SERVERS[@]} サーバー中 $FAILED 件失敗 / $WARN 件警告" [ "$FAILED" -gt 0 ] && exit 1 exit 0
=== ディスク使用量チェック開始 (2026-08-10 11:24:36) === [OK] srv01.example.local 56% /dev/sda1 50G 28G 22G [OK] srv02.example.local 63% /dev/sda1 50G 31G 19G [FAIL] srv03.example.local ssh: connect to host srv03.example.local: Connection timed out [WARN] srv04.example.local 91% /dev/sda1 80G 73G 7G <- 使用率 91% [OK] srv05.example.local 48% /dev/sda1 80G 38G 42G === 完了: 5 サーバー中 1 件失敗 / 1 件警告
並列実行の設計パターンを体系的に学びたい方は、シェルスクリプト実践講座(Linux Master Pro)もご覧ください。現場で即使える設計の「型」を実機ハンズオンで身につけられます。
トラブルシュート:よくある失敗と対処
StrictHostKeyCheckingで接続がブロックされる
初回SSH接続時や既知のホスト鍵が変更された場合、次のメッセージが表示されて接続がブロックされます。The authenticity of host 'srv01.example.local (192.168.1.101)' can't be established. ED25519 key fingerprint is SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx This key is not known by any other names. Are you sure you want to continue connecting (yes/no/[fingerprint])?
・短期対処:
-o StrictHostKeyChecking=noを追加してホスト鍵チェックをスキップする(テスト用途のみ)・正式対処:
ssh-keyscan -H サーバーIP >> ~/.ssh/known_hostsでホスト鍵を事前登録し、StrictHostKeyChecking=yesに戻す本番環境で
StrictHostKeyChecking=noを使い続けると中間者攻撃のリスクがあります。スクリプトが安定稼動したら必ず正式対処に切り替えてください。SSH公開鍵が配布されていないサーバーがある
BatchMode=yesを設定している場合、パスワード認証が必要なサーバーへの接続は即座に失敗します。# エラー出力例 srv02.example.local: Permission denied (publickey,gssapi-keyex,gssapi-with-mic).
ssh-copy-idコマンドで公開鍵を対象サーバーへ配布することです。# 公開鍵を配布する(初回のみパスワード入力が必要) ssh-copy-id -i ~/.ssh/id_ed25519.pub ユーザー名@srv02.example.local
ファイアウォールでSSHポートがブロックされている
SSHデフォルトポート(22番)がファイアウォールで制限されていると、ConnectTimeoutまで待った後に接続失敗になります。対象サーバーのSSHポートが疎通できているかを確認するには、Linux ポート確認の全コマンドで紹介しているssコマンドやncコマンドが役立ちます。# ncコマンドでSSHポート(22番)の疎通確認(0: 成功 / 1: 失敗) nc -zv -w 3 srv03.example.local 22
digコマンドによる名前解決テストで原因を切り分けられます。本記事のまとめ
| やりたいこと | コマンド / 設定 |
|---|---|
| SSHコマンドをバックグラウンドで並列実行する | ssh -n ... "$SERVER" "コマンド" & |
| バックグラウンドジョブのPIDを配列に記録する | PIDS+=($!) |
| PIDを指定して終了コードを個別収集する | wait "${PIDS[$i]}" |
| 一時ディレクトリを安全に作成する | WORKDIR=$(mktemp -d /tmp/xxx.XXXXXX) |
| スクリプト終了時に一時ファイルを自動削除する | trap 'rm -rf "$WORKDIR"' EXIT |
| 接続確立のタイムアウトを設定する | ssh -o ConnectTimeout=5 ... |
| コマンド全体にタイムアウトを設定する | timeout 10 ssh ... |
| パスワードプロンプトを出さずに失敗させる | ssh -o BatchMode=yes ... |
シェルスクリプト講座を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 次のページへ:シェルスクリプトでディスク使用量を定期監視・自動クリーンアップする設計|閾値アラートと古いログ削除の実装パターン
- 前のページへ:シェルスクリプトのDRY RUN設計|--dry-runフラグと実行関数で危険な操作を安全にテストする方法
- この記事の属するカテゴリ:シェルスクリプトへ戻る

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