シェルスクリプトで複数サーバーへのSSH並列実行を設計する方法|結果集約とエラー検知の実践パターン

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)シェルスクリプト > シェルスクリプトで複数サーバーへのSSH並列実行を設計する方法|結果集約とエラー検知の実践パターン
「5台のサーバーでdf -hを同時に実行して結果を一覧で見たい。でもsshを1台ずつ順番に叩いていたら5倍の時間がかかってしまう」
「夜間バッチで全サーバーの状態確認をまとめて実施したいが、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値として返す設計が推奨


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

なぜ複数サーバーへの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

RHEL 9.4の実機5台で実行した出力例を示します(ホスト名はマスク済み)。

=== ディスク使用量チェック開始 (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 件警告

srv03は接続タイムアウトで失敗、srv04はディスク使用率91%で警告として検知できています。このスクリプトをcronに登録しておくことで、全サーバーのディスク状態を定期的に一元管理できます。

並列実行の設計パターンを体系的に学びたい方は、シェルスクリプト実践講座(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])?

並列実行時にこのプロンプトが出るとスクリプトが入力待ちになります。対処は2段階です。

短期対処: -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

接続先サーバーのホスト名が正引きできない場合は、Linux DNS 設定の基本も参照してください。/etc/resolv.confの設定確認や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 ...
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、20年以上の運用経験を持つ現役エンジニアが基礎から教えます。
シェルスクリプト講座を見る >>

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

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

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

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

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

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

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

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

この記事を書いた人

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

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

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