SureVM エンジニアリングノート

クラウドMac CIで一時ディレクトリとキャッシュを分離する

クラウドMac CIで一時ディレクトリとキャッシュを分離する

同じクラウドMacで複数のCIジョブを連続実行する場合、特に再現が難しい障害は、コードの不具合ではなく前のジョブが残した一時ファイルによって引き起こされることがあります。古いソケット、解放されていないロック、書き込み途中のキャッシュ、同名のエクスポート先ディレクトリなどが残っていると、同じコミットをビルドしても次のジョブで異なる結果になる可能性があります。専有物理ノードなら他のアカウントからの干渉は避けられますが、同一アカウント内で並列実行されるジョブまでは自動的に分離されません。そのため、ファイルシステム上の境界はパイプライン側で明示的に設ける必要があります。

ジョブ間で状態が漏れるディレクトリを特定する

「ワークスペースを空にした」ことと「環境が元に戻った」ことを同一視してはいけません。macOSのツールはワークスペース外の複数箇所にも状態を保存します。調査時には、少なくとも次の4種類の場所を確認してください。

まず、正常なビルドの前後でそれぞれ env | sort を実行し、実際に使用されている HOMETMPDIR、ワークスペースのパスを記録します。次に、いきなり再帰的な削除を行うのではなく、du -sh で関連ディレクトリのサイズを比較します。並列実行時にだけ失敗する場合は、固定されたファイル名、ポート番号、出力先パスを優先的に探してください。

分離の目的は、毎回すべてのキャッシュを削除することではありません。各ジョブが専用の書き込み可能なディレクトリを明確に所有し、自身が作成した内容だけを削除できるようにすることです。

ジョブごとに一意のルートディレクトリを作成する

ジョブIDには、CIシステムが提供する一意の番号を使用します。信頼できる番号がない場合は、mktemp で生成できます。ブランチ名だけを使用してはいけません。同じブランチで複数のビルドが同時に開始される可能性があるためです。

#!/bin/bash
set -euo pipefail

job_id="${CI_JOB_ID:-manual}"
job_root="$(mktemp -d "${TMPDIR:-/tmp}/surevm-ci.${job_id}.XXXXXX")"

export TMPDIR="${job_root}/tmp"
export DERIVED_DATA="${job_root}/DerivedData"
export MODULE_CACHE="${job_root}/ModuleCache"
export RESULT_BUNDLE="${job_root}/Results/Test.xcresult"

mkdir -p "$TMPDIR" "$DERIVED_DATA" "$MODULE_CACHE" "$(dirname "$RESULT_BUNDLE")"

cleanup() {
  local status=$?
  rm -rf "$job_root"
  exit "$status"
}

trap cleanup EXIT INT TERM

mktemp が作成するディレクトリは、デフォルトでは別のジョブと同じ名前になりません。クリーンアップ関数では、最初に終了コードを保存してからディレクトリを削除し、元の終了ステータスで終了します。これにより、テストの失敗がクリーンアップコマンドによって隠されるのを防げます。ワークスペース名に空白が含まれていても複数の引数に分割されないよう、パス変数は引用符で囲んでください。

HOMEを安易に上書きしない

HOME をジョブディレクトリに変更すれば完全に分離できるように見えますが、現在のユーザーが利用するKeychain、ツールの設定、完了済みのシステム初期化にアクセスできなくなる可能性があります。通常のコンパイルでは、特定のパッケージ管理ツールに専用キャッシュを指定できます。コード署名を伴う場合は、一般に HOME を維持し、ビルド成果物と明確に移行可能なキャッシュだけを分離します。

Xcodeの書き込み先を明示的に指定する

TMPDIR を設定するだけでは、DerivedDataは自動的に移動しません。重要なパスはビルドコマンドの引数として渡し、そのジョブが使用したディレクトリをログから直接確認できるようにします。

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Debug \
  -derivedDataPath "$DERIVED_DATA" \
  -clonedSourcePackagesDirPath "${job_root}/SourcePackages" \
  COMPILER_INDEX_STORE_ENABLE=NO \
  CLANG_MODULE_CACHE_PATH="$MODULE_CACHE" \
  test \
  -resultBundlePath "$RESULT_BUNDLE"

CIでコードインデックスが不要なら、インデックスストアを無効にして不要な書き込みを減らせます。テスト結果はジョブのルートディレクトリ内に保存します。ただし、失敗後に添付ファイルをアップロードする場合は、クリーンアップを開始する前にアーカイブを完了させる必要があります。より確実なのは、「テストを実行する—診断ファイルをパイプラインの成果物領域へコピーする—コピー結果を検証する—終了する」という流れです。

共有キャッシュは読み取り専用、ジョブキャッシュは書き込み可能にする

依存関係を再利用する必要がある場合は、検証済みの共有キャッシュをシードとして扱えます。ジョブの開始時に専用ディレクトリへコピーまたは復元し、ビルド中はジョブ側のコピーだけに書き込みます。ビルドが成功した後、独立したステップで共有版を更新します。2つの xcodebuild プロセスが同じモジュールキャッシュやビルドデータベースへ同時に書き込む構成は避けてください。

異常終了時にも調査材料を残す

trap 'rm -rf ...' EXIT をそのまま使用すると、失敗した瞬間に調査対象が削除されます。実運用では、終了コード、ディスクの空き容量、ディレクトリサイズ、終了していない関連プロセスを含む簡潔な診断一覧を先に生成し、その後で残すファイルを判断します。環境変数にはトークンや署名関連の値が含まれる可能性があるため、ログへすべて出力してはいけません。

クリーンアップ前に、次の処理を実行できます。

{
  echo "exit_status=$status"
  df -h "$job_root"
  du -sh "$job_root"/* 2>/dev/null || true
  find "$job_root" -type s -print
} > "${ARTIFACT_DIR}/job-cleanup.txt"

ARTIFACT_DIR はジョブのルートディレクトリ外に配置し、アップロードと定期的な削除はパイプライン側で行います。失敗時の状態を残す必要がある場合は、診断ディレクトリを圧縮してから元のディレクトリを削除できます。その際は、ログとパスに含まれる機密情報をマスキングしてください。

残留監査で分離の有効性を検証する

変更後は、同じコミットを2回連続で実行し、2回目の開始前に前のジョブIDに対応するディレクトリが残っていないことを確認します。並列実行では、2つのジョブの TMPDIR、DerivedData、結果バンドルのパスが完全に異なることも確認してください。

受け入れ確認は、次の5項目に固定できます。各ジョブに一意のルートディレクトリがあること、Xcodeのパスがすべて明示されていること、共有キャッシュに並列書き込みがないこと、成功時と失敗時の両方で終了フックが実行されること、診断成果物をコピーしてからクリーンアップすることです。それでもディスク使用量が増え続ける場合は、削除範囲を広げるのではなく、ジョブIDを手掛かりに境界へ含まれていないツールのディレクトリを特定します。

SureVMの専有物理Macでは、この方法が長期間稼働するビルドノードに特に適しています。マシン単位のツールチェーンを維持しながら、ジョブごとに変化する状態を追跡可能かつ削除可能なディレクトリ内へ限定できます。最終的な判断基準は単純です。前のジョブが成功、失敗、中断のいずれで終わっても、次のジョブの入力条件を変えてはなりません。

よくある質問

すべてのCIジョブで同じ一時ディレクトリを使うと何が起きますか?

固定パスを共有すると、並列ジョブが同名ファイルを上書きし、失敗したジョブのロックファイルやソケットが次の処理に残ります。システムの一時領域配下にジョブ専用ディレクトリを作る方法が安全です。

TMPDIRだけを分離すればDerivedDataの指定は不要ですか?

不要にはなりません。XcodeはDerivedDataやモジュールキャッシュを別の場所に保存できるため、それぞれを明示的にジョブ専用パスへ向ける必要があります。

署名を行うジョブでもHOMEを分離できますか?

署名ジョブでは安易に変更しない方が安全です。現在のユーザーに紐づくKeychain設定が必要になるため、まず一時領域とビルドキャッシュだけを分離してください。

SureVM クラウドMac

ビルド、開発、実験に使える専用物理Macノードを選択

Mac mini M4の2つの構成、4種類の契約期間、注文可能な5つのノードを比較し、実際のワークフローに合うプランを選べます。

レンタルプランを選ぶ