リモートチームでは、クラウドMac上のCI runnerを常時稼働するインフラとして扱うことがよくあります。しかし、runner自体にもアップデートが必要です。最も危険なのは、ジョブの実行中にプログラムディレクトリを上書きして再起動する方法です。ビルドプロセスが旧ファイルを使い続ける一方で、スケジューラープロセスは新しいコンポーネントを読み込み、再現が難しい混在状態に陥る可能性があります。より安全なのは、先にジョブをドレインし、不変のバージョンディレクトリとアトミックなシンボリックリンク切り替えを使う方法です。
まずアップデートの安全境界を定義する
制御可能なアップデートには、少なくとも4つの条件が必要です。新しいジョブを受け付けないこと、実行中のジョブを自然に完了させること、新旧バージョンを共存させられること、切り替えに失敗した場合に即座に復旧できることです。ここでいう「ドレイン」とは、ワークスペースを空にしたり、ビルドを強制終了したりすることではありません。ジョブの境界でrunnerを一時停止することを指します。
アップデート中に最も重要なのは「サービスが起動しているか」ではなく、「旧ジョブが終了し、新しいジョブがまだ開始されていないか」という状態です。
作業を始める前に、現在のバージョン、runnerのプロセスID、実行中のジョブ識別子、ワークディレクトリを記録します。ジョブにXcodeビルドが含まれる場合は、xcodebuild、テストプロセス、アーカイブのエクスポート用サブプロセスが残っていないかも確認してください。最上位のrunnerがアイドル状態かどうかだけで判断してはいけません。runnerが終了しても、不適切なスクリプトによってサブプロセスがバックグラウンドに残ることがあるためです。
アップグレードの判定基準は、明確なルールとして定めておくことを推奨します。現在のジョブに許容する最長実行時間、手動確認に切り替えるまでの待機時間、ジョブ受付を再開する前に合格すべきスモークテストを決めてください。これらのルールは障害発生時にその場で決めるのではなく、各プロジェクトの所要時間に応じてチームが事前に策定します。
プログラム、設定、ワークスペースを分離する
runnerプログラムはバージョンごとに独立したディレクトリへ配置し、永続的な設定とビルド用ワークスペースはバージョンディレクトリの外に置きます。単純な構成例は次のとおりです。
/opt/ci-runner/
├── current -> releases/runner-current
├── previous-target
├── drain
├── active.pid
├── config/
├── work/
└── releases/
├── runner-old/
└── runner-current/
configには登録情報と非公開設定を保存し、アクセス権はrunnerの実行ユーザーに限定します。workにはチェックアウト先と一時的なビルド内容を保存し、releasesにはプログラムだけを配置します。トークンを各バージョンディレクトリへコピーしたり、アップデートパッケージでワークスペースを上書きしたりしないでください。
このように分離すれば、アップグレードで変更するのはcurrentの参照先だけです。旧バージョンは完全な状態で残るため、再ダウンロードや再インストールをせずにロールバックできます。旧バージョンの削除は観察期間が終わるまで延期し、検証済みの直近バージョンを少なくとも1つ保持してください。
wrapperでジョブ境界に合わせてドレインする
最も制御しやすいのは、runnerが一度に1件のジョブだけを実行し、終了後にwrapperが次のジョブを受け付けるか判断するモデルです。単一ジョブ実行用のオプションはrunnerによって異なるため、実際の起動コマンドをまず$RUNNER_CMDとしてラップし、そのうえで共通のループを使用します。
#!/bin/zsh
set -u
root=/opt/ci-runner
lock="$root/wrapper.lock"
mkdir "$lock" 2>/dev/null || exit 1
cleanup() {
rm -f "$root/active.pid"
rmdir "$lock" 2>/dev/null
}
trap cleanup EXIT INT TERM
while [[ ! -e "$root/drain" ]]; do
"$root/current/bin/$RUNNER_CMD" run --once &
child=$!
print -r -- "$child" > "$root/active.pid"
if wait "$child"; then
status=0
else
status=$?
fi
rm -f "$root/active.pid"
if (( status != 0 )); then
sleep 10
fi
done
アップデートの準備時には、touch /opt/ci-runner/drainを実行します。wrapperは現在の子プロセスが完了するまで待ちますが、次のループは開始しません。その後active.pidを読み取り、kill -0 PIDでプロセスがまだ存在するか確認します。PIDファイルが空でも、マシンがアイドル状態とは限りません。そのジョブに関連するビルドやテストのサブプロセスも確認する必要があります。
runnerが単一ジョブ後に終了するモードをサポートしていない場合は、runner標準の新規ジョブ受付停止機能を使用し、停止後にジョブがプリフェッチされないことを検証してください。確認できない場合は、まず隔離環境で長時間ジョブを再現し、ドレイン時の動作を観察します。
アップデートパッケージを検証してアトミックに切り替える
新しいバージョンは一時ディレクトリへ展開し、検証が完了してから名前を変更してreleasesへ配置します。ダウンロードパッケージのチェックサムには、チームが信頼するリリース記録の値を使用してください。同じ未検証の圧縮ファイルの隣で、その場で生成した値を使ってはいけません。
root=/opt/ci-runner
release="$root/releases/runner-next"
archive=/tmp/runner-next.tar.gz
expected="$EXPECTED_SHA256"
actual="$(shasum -a 256 "$archive" | awk '{print $1}')"
[[ "$actual" = "$expected" ]] || exit 2
mkdir -p "$release"
tar -xzf "$archive" -C "$release"
"$release/bin/$RUNNER_CMD" --version || exit 3
old="$(readlink "$root/current")"
print -r -- "$old" > "$root/previous-target"
ln -sfn "$release" "$root/current.next"
mv -fh "$root/current.next" "$root/current"
シンボリックリンクを切り替える前に、新しいディレクトリ内のプログラムが実行可能であること、アーキテクチャが正しいこと、外部化した設定を読み込めることを確認します。切り替え処理自体は短時間で完了させ、展開、依存関係のインストール、ネットワーク経由のダウンロードをクリティカルセクションに含めないでください。アップデートに設定の移行も必要な場合は、まず一時ファイルへコピーし、形式を検証してからアトミックに置き換えます。
展開による権限の変動を防ぐ
圧縮パッケージには、異なる所有者や権限が含まれていることがあります。切り替え前に、runner以外のユーザーがプログラムファイルを変更できないことを確認してください。機密設定は通常600、設定ディレクトリは通常700にします。同時に、ワークディレクトリの所有者が従来のrunnerユーザーのままであることも確認し、新バージョンの初回実行時になって書き込みエラーが発覚する事態を防ぎます。
スモークテストと迅速なロールバック
切り替え後も、すぐにdrainを削除しないでください。まず、制御されたスモークジョブを手動で1回実行します。最低限、runnerが設定を読み込めること、一時ワークスペースを作成できること、開発ツールを呼び出せること、最小構成のビルドを完了できること、成果物を保存できること、正常にクリーンアップして終了できることを検証します。
ログを確認するときは、特にパスに注意します。新しいプロセスはcurrentが指す新ディレクトリから起動する一方、キャッシュ、設定、ワークスペースは引き続き固定パスへ保存される必要があります。ログ内で旧バージョンと新バージョンのパスが混在している場合は、旧プロセスがまだ終了していません。この状態ではジョブ受付を再開できません。
失敗した場合は、previous-targetから旧リンクの参照先を読み取り、同じ一時リンク方式で切り戻してから、最小ジョブを再実行します。新バージョンのディレクトリを直接修正しながら試行しないでください。不変リリースという前提が崩れるためです。旧バージョンへの復旧を確認してから、失敗したバージョンを削除し、リリースパッケージを作り直します。
検証に合格したら、rm -f /opt/ci-runner/drainを実行し、プロセスマネージャーからwrapperを再起動します。最後に、少なくとも1件の実ジョブについて、受付、ビルド、成果物のアーカイブ、終了ステータスを確認し、バージョン番号、切り替え結果、ロールバック先をアップデート記録へ残します。runnerがオンラインと表示された時点で作業を終えるのではなく、ここまで完了して初めてアップデート終了と判断します。
よくある質問
CI runnerを同じディレクトリへ上書き更新してはいけない理由は何ですか?
実行中のジョブが新旧ファイルを混在して読み込む可能性があり、正常な旧版も失われます。バージョン別ディレクトリなら切り替えと復旧を分離できます。
ドレイン時に実行中のビルドを停止する必要はありますか?
ありません。ドレインは次のジョブ取得だけを止め、現在の子プロセスは完了させます。強制停止を検討する前にログとプロセス状態を保存します。
更新後に最低限確認すべき項目は何ですか?
runnerのバージョンと登録状態、ワークスペースへの書き込み、最小ビルド、成果物の保存、機密設定の権限を確認し、失敗時は旧版へ戻します。
独占利用できるApple Siliconビルドマシンが必要ですか?
Xcodeビルド、iOS CI、リモート開発、自動化タスク向けに、独占利用の物理ノードを日単位、週単位、月単位、または四半期単位でレンタルできます。