エンジニアリングノート

APFSクローンでクラウドMac作業領域を初期化する

APFSクローンでクラウドMac作業領域を初期化する

同じクラウドMacで依存関係の更新、プロジェクトの移行、破壊的なスクリプトを連続して実行する場合、最も厄介なのはビルド自体ではなく、クリーンな作業領域へ確実に戻す方法です。大規模なリポジトリを再チェックアウトすると大量の小さなファイルを再び読み込むことになり、以前のディレクトリをそのまま再利用すると、未追跡ファイル、生成物、誤った権限まで持ち越す可能性があります。作業ディレクトリがAPFSボリューム上にあるなら、検証済みの基準状態を維持し、コピーオンライト方式のクローンによってタスクごとに変更可能なコピーを作成できます。

APFSクローンが解決する問題

APFSクローンでは、作成直後の2つのファイルが基盤となるデータブロックを共有します。どちらか一方に書き込みが発生した時点で、変更された部分にだけ新しいブロックが割り当てられます。そのため、ソースコード、依存関係、静的ツールを含む基準状態を独立したディレクトリへ短時間でコピーでき、その後の変更が基準状態へ書き戻されることもありません。

これはハードリンクとは異なります。クローン後のファイルには独立したディレクトリエントリがあり、コピーを変更しても元のファイルは変化しません。また、基準状態とコピーは同じボリューム上に残るため、バックアップでもありません。ボリュームの破損、基準状態の誤削除、マシン全体のデータ消失が発生した場合、クローンでは別の場所から復旧できません。

APFSクローンは、キャッシュヒットを保証する仕組み、スナップショットシステム、データ保護手段ではなく、「低コストで作業用コピーを作成する」ための仕組みと考えてください。

du -sh が示すのはディレクトリの論理サイズであり、クローンによって実際に増えた物理容量を直接表すものではありません。容量の変化を監視する際は、APFSボリュームの空き容量も併せて確認し、タスクによる書き込みが続く場合に備えてクリーンアップのしきい値を設定します。

検証可能な基準状態を用意する

基準状態には、ビルド完了後に偶然残ったディレクトリをそのまま使うべきではありません。特定のコミットをチェックアウトし、依存関係を復元してから、単一のタスクだけに属する状態をすべて削除します。少なくとも、作業ツリーがクリーンであること、サブモジュールが正しい位置を指していることを確認し、基準となるコミットを記録してください。

set -euo pipefail

ROOT="$HOME/ci-workspaces"
BASE="$ROOT/baseline"
mkdir -p "$ROOT"

git -C "$BASE" status --porcelain
git -C "$BASE" submodule status --recursive
git -C "$BASE" rev-parse HEAD > "$BASE/.baseline-revision"
test -z "$(git -C "$BASE" status --porcelain)"

.baseline-revision 自体が未追跡ファイルになるため、リポジトリの外へ配置するか、チームで承認された無視ルールへあらかじめ追加する必要があります。より堅牢な構成は、コミット番号を $ROOT/metadata に保存し、$BASE が常にクリーンな作業ツリーの検査を通過するようにすることです。

基準状態に含めないもの

DerivedData、テスト結果バンドル、アーカイブファイル、一時キーチェーン、実行ログ、使用中のソケットは基準状態に含めないでください。これらには、絶対パス、プロセスの状態、タスク固有の認証情報が含まれていることがよくあります。依存関係のディレクトリを含めるかどうかは、その内容をロックファイルから一意に決定できるか、インストールスクリプトがマシン全体に関わるパスへ書き込むかによって判断します。

また、基準状態への書き込みは更新タスクだけに許可します。通常のビルドアカウントには読み取りを許可しても、その中でビルドコマンドを実行させるべきではありません。そうしなければ、1回の誤操作で以降のすべてのクローンが汚染される可能性があります。

タスクごとに独立したクローンを作成する

まずルートディレクトリが実際にAPFS上にあることを確認し、タスクIDを使って一意のディレクトリを生成します。コピー先のパスは存在していてはなりません。また、外部入力が削除対象のパスへ直接組み込まれないよう、タスクIDには安全な文字だけを許可します。

set -euo pipefail

ROOT="$HOME/ci-workspaces"
BASE="$ROOT/baseline"
JOB_ID="${BUILD_ID:?BUILD_ID is required}"

case "$JOB_ID" in
  *[!A-Za-z0-9._-]*) exit 64 ;;
esac

test "$(stat -f %T "$ROOT")" = "apfs"

WORK="$ROOT/jobs/$JOB_ID"
DERIVED="$ROOT/derived/$JOB_ID"

test ! -e "$WORK"
mkdir -p "$ROOT/jobs" "$DERIVED"
cp -cR "$BASE" "$WORK"

xcodebuild \
  -workspace "$WORK/App.xcworkspace" \
  -scheme App \
  -derivedDataPath "$DERIVED" \
  build

cp -cR はクローンコピーを要求しますが、実際の動作はコピー元とコピー先のパス、およびファイルシステムに左右されます。コピー元とコピー先のディレクトリは、同じAPFSボリューム上になければなりません。ボリュームをまたぐ場合は、コピーオンライトの処理として扱うべきではありません。導入前に、大きなファイルを含むテスト用ディレクトリを使い、コピー時間とボリューム容量の変化を確認できます。

並行タスクと可変状態を分離する

作業領域が独立していても、すべての状態が分離されているとは限りません。ビルドツールは、ユーザーディレクトリ内のキャッシュ、一時ディレクトリ、設定ファイルを読み書きすることがあります。少なくとも、タスクごとに個別の DerivedData と結果ディレクトリを割り当ててください。また、複数のタスクで同じシミュレータのデバイスセットや同じ出力先パスを共有しないようにします。

並行実行数に応じた容量を見積もる

クローンを作成した直後に増える使用量はわずかですが、コンパイル済みオブジェクト、依存関係の書き換え、アーカイブによって新しいブロックが急速に増加します。並行実行数の上限は、基準状態の論理サイズではなく、最悪の場合に発生する書き込み増分を基準に決める必要があります。タスクを開始する前にボリュームの空き容量を確認し、ビルド完了後に変化を記録できます。

df -h "$ROOT"
du -sh "$DERIVED" "$WORK"

タスクが多数の依存関係ファイルを書き換える場合、クローンによる容量面の利点は徐々に小さくなります。この場合は、不変の依存関係を基準状態に残し、頻繁に変化する生成ディレクトリをタスク専用のパスへ移します。クローン率を維持するためだけに、書き込み可能なキャッシュを共有してはいけません。

安全に回収し、基準状態を定期的に再構築する

クリーンアップスクリプトの最優先事項は「素早く削除すること」ではなく、タスクのルートディレクトリを絶対に越えないことです。削除前に、パスのプレフィックス、ディレクトリの存在、タスクIDをすべて検証してください。空になる可能性がある変数を使って、再帰削除を直接実行してはいけません。

set -euo pipefail

ROOT="$HOME/ci-workspaces"
WORK="$ROOT/jobs/${BUILD_ID:?BUILD_ID is required}"
DERIVED="$ROOT/derived/${BUILD_ID:?BUILD_ID is required}"

case "$WORK" in
  "$ROOT"/jobs/*) rm -rf -- "$WORK" ;;
  *) exit 64 ;;
esac

case "$DERIVED" in
  "$ROOT"/derived/*) rm -rf -- "$DERIVED" ;;
  *) exit 64 ;;
esac

基準状態の更新は、新しいディレクトリを使って行います。対象コミットをチェックアウトし、再現可能な方法で依存関係をインストールして検証を実施した後、現在の基準状態と置き換えます。既存の基準状態をその場で更新しながら使い続けてはいけません。更新が中断すると、新旧の状態が混在し、正否を判断できなくなります。

最後に確認すべき項目は4つあります。基準状態のコミットを追跡でき、作業ツリーがクリーンに保たれていること。各タスクに一意の作業領域と生成ディレクトリがあること。ボリュームの空き容量が最大並行実行時の書き込みを吸収できること。クリーンアップ処理が管理されたパスだけを受け付けることです。これらの条件を満たして初めて、APFSクローンは一見高速なだけのコピー手法ではなく、再現可能なエンジニアリング手順になります。

よくある質問

APFSクローンはビルドキャッシュやバックアップの代わりになりますか?

なりません。同じAPFSボリューム上に書き換え可能な複製を素早く作る仕組みであり、変更ブロックは追加容量を消費します。重要データには別のバックアップが必要です。

基準領域にDerivedDataを含めないのはなぜですか?

DerivedDataには絶対パス、インデックス、タスク固有の状態が残ります。ビルドごとに専用ディレクトリを指定する方が、分離と再現性を保ちやすくなります。

専有物理ノード

クラウドMacでこのワークフローの検証を続ける

3種類のApple Silicon構成から、最適なメモリ、ストレージ、ノード、利用期間を選べます。

構成を選んで注文する