クラウドMac CIを専用ユーザー、権限、launchctlで分離する

クラウドMac CIを専用ユーザー、権限、launchctlで分離する

同じクラウドMacでビルドジョブを連続して処理する場合、最も見つけにくい問題はコンパイルエラーとは限りません。前のジョブが残した状態によって、次のジョブが「偶然成功」してしまうことがあります。たとえば、依存関係がすでにキャッシュされている、バックグラウンドサービスが引き続きポートを占有している、一時的な認証情報が削除されていない、GUIセッションにユーザー単位のエージェントが残っている、といった状態です。この種のパイプラインは短期的には安定して見えても、別のマシンへ移行したりジョブの実行順序を変えたりすると、問題が一気に表面化します。対策は、最後に強引な削除コマンドを1行追加することではありません。まず実行ユーザー、ディレクトリ、キャッシュ、セッションドメインの境界を明確にし、それぞれの境界に検証手順を設けます。

分離すべき境界を先に定義する

少なくとも、実行ユーザー、作業ディレクトリ、一時ディレクトリ、キャッシュ、バックグラウンドプロセスという5種類の状態を確認する必要があります。CIサービス自体はシステムレベルのデーモンから起動してもかまいませんが、実際のビルドを常時管理者権限で実行すべきではありません。より安全なのは、ci_runner のような管理者権限を持たない標準ユーザーを作成し、チェックアウト、コンパイル、テストをすべてそのユーザーで実行する方法です。

分離の目的は、ジョブから「マシン全体を見えなくする」ことではありません。あるジョブが生成した可変状態が、明示されていないまま次のジョブへ影響するのを防ぐことです。

後から比較できるよう、まずベースラインを記録します。

id
umask
printf 'HOME=%s\nTMPDIR=%s\n' "$HOME" "$TMPDIR"
launchctl print "gui/$(id -u)" >/tmp/launchctl-baseline.txt 2>&1 || true
ps -axo user,pid,ppid,command

runnerで少数の特権操作が必要な場合は、対象コマンドを管理されたスクリプトへ集約し、特定の操作だけに権限を付与します。ビルド全体を高い権限で直接実行してはいけません。ビルドスクリプトからシステム全体のツールチェーン、グローバルなネットワーク設定、ほかのユーザーのディレクトリを変更することも避けます。

ジョブごとに専用ワークスペースを作成する

ジョブディレクトリは、予測困難でありながら監査可能なジョブ識別子から生成し、スラッシュ、空白、制御文字を拒否します。ディレクトリの権限は 700 に設定し、一時ファイルもジョブディレクトリ内へ配置して、複数の並行ジョブがシステムの一時パスを共有しないようにします。

set -eu

case "${RUN_ID:-}" in
  ""|*[!A-Za-z0-9._-]*)
    echo "Invalid RUN_ID" >&2
    exit 64
    ;;
esac

RUN_ROOT="/Users/ci_runner/Jobs/$RUN_ID"
install -d -m 700 "$RUN_ROOT"
install -d -m 700 "$RUN_ROOT/src" "$RUN_ROOT/tmp" "$RUN_ROOT/cache"

export HOME="/Users/ci_runner"
export TMPDIR="$RUN_ROOT/tmp/"
export XDG_CACHE_HOME="$RUN_ROOT/cache"
umask 077

すべてのジョブが書き込める共有ディレクトリにワークスペースを置かないでください。読み取り専用のシードキャッシュを共有する必要がある場合は、ジョブ開始時にローカルキャッシュへコピーし、ビルドではそのコピーだけを変更します。これにより、ウォームアップ済みのキャッシュを活用しながら、失敗したジョブによる共有ベースラインの汚染を防げます。

命名規則ではなく権限で分離する

ディレクトリ名にジョブ番号が含まれていても、安全性は保証されません。stat -f '%Su %Sp %N' "$RUN_ROOT" で所有者と権限を確認し、ls -lde で追加のACLを検査します。継承されたルールが見つかった場合は、まず継承元を確認し、管理された初期化処理を通じて不要な権限を削除します。ビルドスクリプト内で再帰的に権限を緩和してはいけません。

キャッシュを3種類の有効期間に分ける

すべてのキャッシュを削除するとパイプラインが遅くなり、すべてを再利用すると汚染の影響が拡大します。実際には、有効期間に応じて次のように分類できます。

| 種類 | 例 | 推奨する境界 | 終了時の処理 | |---|---|---|---| | ジョブ単位 | 一時的な派生ファイル、テスト添付ファイル | 現在の `RUN_ID` のみ | 検証後に削除 | | ブランチ単位 | 再構築可能な依存関係のダウンロード | リポジトリとブランチの複合キー | 期限後にローテーション | | マシン単位 | 読み取り専用ツールパッケージ、固定SDKインデックス | 管理者が保守するシードディレクトリ | ビルドジョブからは書き込み不可 |

キャッシュキーには、少なくともツールチェーンのバージョン、依存関係ロックファイルのダイジェスト、ターゲットアーキテクチャを含めます。ブランチ名だけをキーにすると、ツールチェーンを切り替えた後も古い成果物がヒットします。キャッシュの復元後は、ロックファイルのダイジェスト、主要バイナリのアーキテクチャ、ディレクトリの所有者などを簡易検証します。条件を満たさないコピーは、その場で修復しようとせず破棄してください。

機密情報をキャッシュに含めてはいけません。短期認証情報はジョブ環境を通じて注入し、必要な子プロセスのスコープ内だけに存在させます。ログにも環境全体を出力してはいけません。ジョブ終了時には関連する変数を明示的に unset し、生成ファイルがアーカイブディレクトリへ混入していないことを確認します。

launchctlのセッションドメインを正しく理解する

macOSのバックグラウンドエージェントは、システムドメイン、ユーザードメイン、GUIセッションドメインのいずれかに属する場合があります。ビルドを ci_runner で実行していても、そこから起動したすべてのエージェントが想定したドメインに配置されるとは限りません。まず現在のユーザー識別子を確認し、その後で対象ドメインを調べます。

uid="$(id -u ci_runner)"
sudo -u ci_runner launchctl print "user/$uid" >/tmp/user-domain.txt

if launchctl print "gui/$uid" >/dev/null 2>&1; then
  sudo -u ci_runner launchctl print "gui/$uid" >/tmp/gui-domain.txt
fi

コマンドラインだけで完結するビルドは、通常GUIセッションに依存しません。シミュレータやGUIを必要とするテストでは、最初に有効なセッションが存在することを確認します。エージェントを繰り返し起動して、セッションの欠如を覆い隠してはいけません。ジョブが一時的に起動したサービスはPIDを保存し、終了処理で通常の終了シグナルを送ります。プロセス名だけで一括終了すると、同じマシン上のほかのジョブまで誤って停止させる可能性があります。

ジョブをまたいで残留するプロセスを特定する

ジョブの開始前と終了後にそれぞれ ps のスナップショットを取得し、ユーザー、親プロセス、作業ディレクトリに基づいて関連付けます。ポートの確認だけでは不十分です。待受ポートを持たないファイル監視プロセスやテストデーモンもリソースを消費するためです。終了処理後も残っており、コマンドラインが現在の RUN_ROOT を指しているプロセスは、黙って無視せず失敗として扱います。

失敗を検出できる終了時検証を設ける

クリーンアップ処理では元のジョブ終了コードを保持すると同時に、クリーンアップ中の異常も観測可能にする必要があります。確認は次の固定順序に分けることを推奨します。

  1. ジョブが起動した子プロセスを停止し、終了を待ちます。
  2. ワークスペース内にソケット、マウントポイント、権限が不適切なファイルがないか確認します。
  3. テストレポートをワークスペース外の管理されたアーカイブディレクトリへコピーします。
  4. ジョブの一時認証情報と環境変数を削除します。
  5. パスのプレフィックスを検証してから、ジョブディレクトリを削除します。
  6. プロセスとlaunchctlの状態を再取得し、ベースラインとの差分を比較します。

削除前には必ずパスを保護してください。

cleanup() {
  case "$RUN_ROOT" in
    /Users/ci_runner/Jobs/*)
      rm -rf -- "$RUN_ROOT"
      ;;
    *)
      echo "Refusing unsafe cleanup path" >&2
      return 1
      ;;
  esac
}
trap cleanup EXIT HUP INT TERM

検証スクリプトは、パイプラインを成功扱いにするためにすべてのエラーを握りつぶしてはいけません。アーカイブの失敗、残留プロセス、ディレクトリ所有者の変化は、いずれも明確な非ゼロステータスを返す必要があります。SoarMacでCIを継続運用する場合は、まずコンソールで現在選択できる構成を確認し、並行ジョブによるメモリとディスクの負荷に応じてrunnerを分割するか判断します。どの構成を使用する場合でも、ユーザーと状態の境界は一貫させてください。

稼働開始前のチェックリスト

初回導入時には、内容が異なり手順が同じ2つのジョブを連続して実行します。1つ目のジョブでは、意図的にキャッシュファイル、バックグラウンドプロセス、一時変数を作成します。2つ目のジョブから、明示されていないファイルを読み取れず、前のジョブのプロセスや認証情報も引き継げないことを確認してください。最後に、次の項目を確認します。

これらの確認を安定して繰り返せるようになれば、パイプラインの成功は、マシン上に偶然残っていた状態ではなく、明示された入力によるものになります。その後、並行数の調整、ジョブの移行、実行ディレクトリの変更を行っても、同じ境界を使って差異の発生源をすばやく判断できます。

よくある質問

CIジョブごとにmacOSユーザーを作成する必要がありますか?

通常は不要です。まずrunner専用の標準ユーザーを用意し、各ジョブに権限700の作業ディレクトリを割り当てます。相互に信頼しないチームや異なる機密区分を扱う場合だけ、実行ユーザーを分けます。

ワークスペースの削除だけでは不十分なのはなぜですか?

一時領域、ユーザーキャッシュ、認証情報、子プロセス、launchctlセッションにも状態が残るためです。終了処理では各境界を確認し、検証済みのジョブパスだけを削除します。

次のタスクを実行

ワークロードに合わせてクラウドMacを選ぶ

チップ、メモリ、ストレージ、契約期間、ノードを確認して、Apple Silicon搭載の専有物理マシンを注文しましょう。

モデルを選んで注文する