CIコントローラーからクラウドMacに対して、バージョン確認、コードの取得、ビルドの開始、成果物の収集を連続して実行する際、コマンドごとに新しいSSH接続を確立すると、公開鍵のネゴシエーション、認証、セッション初期化が毎回繰り返されます。1回あたりの遅延は目立たなくても、数十個の短いコマンドを連続実行すると、ハンドシェイクにかかる時間が無視できない割合を占めます。SSH組み込みのControlMasterを使えば、後続コマンドで認証済みTCP接続を再利用できますが、ソケットの分離、無効な接続の検出、終了時のクリーンアップも併せて設計する必要があります。
最適化対象を確認する
接続の再利用が適しているのは、「同じCIジョブが数分間にわたって同じMacへ繰り返しアクセスする」ケースです。ジョブが長時間のビルドを1回実行するだけなら、ハンドシェイクを最適化しても効果は限定的です。一方、コントローラーが十数個の短いコマンドを順番に実行する場合は、通常、接続を再利用するメリットが大きくなります。
まず、接続を再利用しない場合の処理を記録します。
time ssh -o BatchMode=yes build-mac 'sw_vers -productVersion'
ssh -vv -o BatchMode=yes build-mac 'true' 2>ssh-debug.log
デバッグログを確認し、鍵交換と認証の段階が繰り返し発生していないか調べます。1回のコマンド実行で偶然得られた所要時間だけを比較せず、少なくとも複数回連続して実行し、「接続の確立」と「リモートコマンドの実行」にかかった時間を分けて評価してください。
ControlMasterが削減するのは接続確立のコストです。Xcodeのコンパイル、依存関係の解決、ファイル転送そのものが高速化されるわけではありません。
最小限の再利用設定を作成する
CIコントローラーの ~/.ssh/config に、ビルドホスト専用のエイリアスを作成します。%C は接続パラメーターをハッシュ化するため、ユーザー名やホスト名が長すぎてUnix Socketのパス作成に失敗する問題を回避できます。
Host build-mac
HostName mac.example.internal
User ci-runner
BatchMode yes
ControlMaster auto
ControlPersist 10m
ControlPath ~/.ssh/control/%C
ConnectTimeout 15
ServerAliveInterval 30
ServerAliveCountMax 3
StrictHostKeyChecking yes
UserKnownHostsFile ~/.ssh/known_hosts_ci
ディレクトリを準備する際は、必ずアクセス権を制限します。
install -d -m 700 "$HOME/.ssh/control"
chmod 600 "$HOME/.ssh/known_hosts_ci"
ssh build-mac 'printf "%s\n" ready'
ssh -O check build-mac
最初のコマンドでマスター接続が作成され、ssh -O check ではマスタープロセスの状態が返されます。ControlPersist 10m は、最後のセッションが終了してからマスター接続を最長10分間維持する設定です。ジョブが無期限にその接続へ依存できるという意味ではありません。
ホストIDを固定する
手間を省くために StrictHostKeyChecking=no を設定してはいけません。管理された経路でホストの公開鍵を取得し、CI専用の known_hosts に登録してください。ノードの再構築や鍵のローテーション時には、明確に定めた更新手順を使用します。接続の再利用で減らせるのは後続のハンドシェイクだけであり、最初の接続における信頼境界は変わりません。
並列ジョブごとにソケットを分離する
共有Runnerで最も起こりやすい問題は、すべてのジョブが ~/.ssh/control/%C を共有することです。2つのパイプラインが同じホストへ同時に接続すると、一方が作成したマスター接続をもう一方が再利用する可能性があります。その状態で片方のジョブが終了操作を実行すると、もう片方のジョブの接続まで切断されます。
より安全な方法は、ジョブごとに独立したディレクトリを作成し、コマンドラインから ControlPath を上書きすることです。
set -euo pipefail
job_id="${CI_JOB_ID:-local-$$}"
control_dir="${TMPDIR:-/tmp}/ssh-control-${job_id}"
install -d -m 700 "$control_dir"
ssh_opts=(
-o ControlMaster=auto
-o ControlPersist=10m
-o "ControlPath=${control_dir}/%C"
-o BatchMode=yes
)
ssh "${ssh_opts[@]}" build-mac 'xcodebuild -version'
ssh "${ssh_opts[@]}" build-mac 'git --version'
ジョブ識別子には信頼できるパイプラインコンテキストから取得した値を使い、短い文字列に制限する必要があります。ブランチ名をそのままパスへ組み込んではいけません。スラッシュ、空白、長すぎる名前によってソケットを作成できなくなる可能性があります。
無効な接続を検出して安全に再確立する
ネットワークの切り替え、リモートホストの再起動、コントロールプロセスの異常終了が発生すると、ソケットファイルが残っていても、対応する接続が使用できなくなる場合があります。処理コマンドを実行する前に接続を確認し、失敗した場合は現在のジョブ専用のソケットディレクトリを削除してから、接続を再確立します。
if ! ssh "${ssh_opts[@]}" -O check build-mac >/dev/null 2>&1; then
rm -rf "$control_dir"
install -d -m 700 "$control_dir"
ssh "${ssh_opts[@]}" build-mac 'true'
fi
削除できるのは、現在のジョブが作成したディレクトリだけです。共有ディレクトリをクリーンアップするために rm -rf ~/.ssh/control/* を使用してはいけません。また、ビルドコマンドを無条件に再試行しないでください。接続が切断された時点で、リモートコマンドがすでに開始されている可能性があります。安全な方法は、冪等なチェックだけを自動的に再試行し、プロセス、ロックファイル、ビルド番号などを使って元のジョブの状態を確認することです。
サーバー側のセッション上限を確認する
1本のマスター接続で複数の論理セッションを処理できますが、数に制限がないわけではありません。同時実行するコマンドが多すぎると、サーバーが新しいchannelを拒否する場合があります。まずジョブ単位のSSH同時実行数を制限し、そのうえでサーバー側ポリシーの調整が必要か評価してください。セッション上限の引き上げをデフォルトの解決策にしてはいけません。
終了時に確実にクリーンアップする
正常終了時は制御コマンドを使ってマスター接続を閉じます。異常終了時にもジョブ用ディレクトリを削除する必要があります。Shellスクリプトでは、trap を使って処理を一元化できます。
cleanup() {
ssh "${ssh_opts[@]}" -O exit build-mac >/dev/null 2>&1 || true
rm -rf "$control_dir"
}
trap cleanup EXIT INT TERM
最終確認では、次の4項目を検証します。連続するコマンドが実際に同じマスター接続を使用していること、並列実行される2つのジョブが異なるディレクトリを使用していること、リモートホストの再起動後に接続を再確立できること、ジョブのキャンセル後にソケットが残っていないことです。SetMini上の自動化ジョブでも同じ原則に従い、コンソールで現在選択可能な構成を確認したうえで、接続レイヤーを検出、再確立、クリーンアップが可能なインフラとして扱ってください。長期間無効にならない隠れた状態として扱うべきではありません。
よくある質問
ControlPersistは長く設定するほど効果的ですか?
いいえ。CIでは同一ジョブ内の連続コマンドを覆う5~15分程度から始めます。長すぎる保持時間は、古いソケットや別ジョブからの誤利用を増やします。
並列ジョブで同じControlPathを共有できますか?
推奨しません。ジョブごとに権限700の専用ソケットディレクトリを作り、終了処理でssh -O exitを実行してから削除します。
接続再利用によってホスト鍵検証は省略されますか?
省略されません。最初のマスター接続ではStrictHostKeyChecking=yesを有効にし、管理されたknown_hostsで接続先を検証します。
いつでも接続できるクラウドMacに開発環境を移行
2種類のApple Silicon構成と4つのデータセンターから選択できます。リソースは他の利用者と共有されず、実際の利用状況はコンソールでリアルタイムに確認できます。