エンジニアリング記事

暗号化スパースイメージでMac CIワークスペースを分離する

暗号化スパースイメージでMac CIワークスペースを分離する

同じクラウドMacで複数のビルドジョブを連続実行する場合、最も見つけにくい問題はコンパイルエラーではありません。前のジョブが残したソースコード、一時証明書、テストデータ、キャッシュを、次のジョブが読み取ってしまうことです。実行のたびに rm -rf を行うだけでは十分に確実とはいえません。プロセスがファイルを使用し続けている可能性があり、隠しディレクトリは削除対象から漏れやすく、異常終了するとクリーンアップ処理自体が実行されないこともあります。より明確な境界を設けるには、ジョブごとに独立した暗号化スパースイメージをマウントし、チェックアウト、ビルド、一時生成物をすべてそのファイルシステム内に限定します。

スパースイメージによる分離を選ぶ理由

スパースイメージには大きな論理容量を設定できますが、ホストディスク上の使用量は実際に書き込まれたデータ量に応じて増加します。通常のファイルとして扱えるため、ジョブ番号を基準に特定、集計、削除しやすく、マウント後は独立したAPFSボリュームとして動作します。既存のビルドスクリプトは、通常、作業ディレクトリを変更するだけで対応できます。

暗号化は、イメージがマウントされていないときのデータ露出リスクを抑えます。一方、独立したマウントポイントはジョブ間のパス境界を確立します。どちらも実行ユーザーの分離や最小権限の原則に代わるものではありませんが、長期間共有される単一の作業ディレクトリより監査しやすくなります。

「イメージが暗号化されている」からといって、ジョブの実行中もアクセスできないわけではありません。イメージをマウントすると、適切なファイル権限を持つプロセスはその内容を読み取れます。そのため、runnerユーザー、ログ、シークレットの注入方法もあわせて管理する必要があります。

方式異常終了後の残留物ジョブ境界適したケース
共有ディレクトリを使用後に削除使用中のファイルや隠しディレクトリを削除し損ねやすいスクリプトの正確性に依存機密データを扱わない短時間ジョブ
ジョブごとの通常ディレクトリディレクトリがホストボリュームに残るパスは分かれるが、ファイルシステムは共通低リスクの並列ビルド
暗号化スパースイメージ残留マウントとイメージファイルを特定可能独立したファイルシステム明確なクリーンアップ境界が必要なCI

ジョブ名に基づくAPFSワークスペースを作成する

ジョブ番号は、パスにスラッシュ、空白、コマンド置換が入り込まないよう、事前に使用可能な文字を限定する必要があります。イメージディレクトリはrunnerユーザーだけが読み書きできる場所に置き、マウントポイントはジョブごとに個別に作成します。

set -euo pipefail
set +x
umask 077

JOB_KEY="$(printf '%s' "${CI_JOB_ID:?}" | tr -cd 'A-Za-z0-9._-')"
IMAGE_ROOT="$HOME/ci-images"
IMAGE_PATH="$IMAGE_ROOT/$JOB_KEY.sparsebundle"
MOUNT_PATH="/Volumes/ci-$JOB_KEY"

mkdir -p "$IMAGE_ROOT" "$MOUNT_PATH"
chmod 700 "$IMAGE_ROOT"

printf '%s' "${CI_VOLUME_PASSWORD:?}" |
  hdiutil create \
    -size 80g \
    -type SPARSEBUNDLE \
    -fs APFS \
    -volname "ci-$JOB_KEY" \
    -encryption AES-256 \
    -stdinpass \
    "$IMAGE_PATH"

80g は論理上の上限であり、作成直後に80GBを消費するわけではありません。上限には、ソースコード、依存関係、派生データ、アーカイブのピーク使用量を収め、さらに失敗時のログを保存できる余裕を持たせます。パスワードをコマンド引数、ファイル名、ビルドログに含めてはいけません。コマンドのエコーを無効にしてから、CIの保護された変数を標準入力経由で渡します。

マウント直後に検証する

作成に成功しても、正しいマウントポイントにマウントされたとは限りません。スクリプトで対象パスが実際にマウント済みのボリュームであることを確認し、ファイルシステムの種類も検証します。

printf '%s' "$CI_VOLUME_PASSWORD" |
  hdiutil attach \
    -stdinpass \
    -nobrowse \
    -mountpoint "$MOUNT_PATH" \
    "$IMAGE_PATH"

mount | grep -F "on $MOUNT_PATH "
diskutil info "$MOUNT_PATH" | grep -E 'File System Personality|Volume Name'
mkdir -p "$MOUNT_PATH/src" "$MOUNT_PATH/output" "$MOUNT_PATH/tmp"

以後、ソースコードのチェックアウト先、ビルド出力先、ジョブ単位の一時ディレクトリをすべてこのボリュームに設定します。パッケージマネージャーの共有読み取り専用キャッシュはボリューム外に置けますが、ジョブによって変更される可能性があるキャッシュはすべてボリューム内にコピーし、並行書き込みによる汚染を防ぎます。

アンマウントを終了処理ではなくライフサイクルとして実装する

クリーンアップをスクリプトの最終行だけに置いてはいけません。コンパイルエラー、タイムアウト、終了シグナルによってジョブが途中で終了する可能性があるためです。終了フックを使って同期、使用状況の確認、アンマウントを一元的に実行し、失敗時には診断に必要な情報を残します。

cleanup_workspace() {
  set +e
  sync
  if mount | grep -Fq "on $MOUNT_PATH "; then
    lsof +D "$MOUNT_PATH" > "$IMAGE_ROOT/$JOB_KEY.lsof.txt" 2>/dev/null
    hdiutil detach "$MOUNT_PATH"
  fi
  rmdir "$MOUNT_PATH" 2>/dev/null
}

trap cleanup_workspace EXIT INT TERM
export TMPDIR="$MOUNT_PATH/tmp"
cd "$MOUNT_PATH/src"

ディレクトリが大きい場合、lsof +D は時間がかかることがあります。まず既知のビルド、テスト、パッケージングプロセスを確認し、アンマウントに失敗した場合にのみ完全なスキャンを実行できます。最初から強制アンマウントを使用してはいけません。書き込み中のプロセスを見えなくし、不完全な生成物を残すおそれがあります。

並行実行、容量、失敗時の残留物を処理する

同じジョブを再実行すると、以前のイメージが残っている場合があります。安全な方法は直接上書きすることではなく、まずマウント済みかどうかを確認することです。マウント済みであれば新しいジョブを停止して競合を記録し、マウントされていなければ保持ポリシーに従ってアーカイブまたは削除します。2回の実行が同じイメージを参照しないよう、ジョブ番号には今回の実行番号も含めます。

3つのチェックを設ける

第一に、ジョブの開始前に hdiutil info の結果を確認し、同名のボリュームがないことを確かめます。第二に、ビルド中はホストボリュームとマウント済みボリュームの空き容量を同時に監視します。スパースイメージは拡大するため、論理ボリュームに空きがあっても、ホストボリュームに空きがあるとは限りません。第三に、ジョブ終了後にイメージディレクトリをスキャンし、診断用として明示的にマークされた失敗サンプルだけが残っている状態にします。

次のコマンドで、2つの階層の容量を区別できます。

df -h "$MOUNT_PATH"
df -h "$IMAGE_ROOT"
du -sh "$IMAGE_PATH"
hdiutil info

特定の種類のジョブが頻繁に上限へ達する場合は、論理容量を増やす前に、長期保存が不要な中間生成物を分離します。むやみにイメージを拡大しても、ホストディスクが枯渇するタイミングを先送りするだけです。

セキュリティ境界と導入チェックリスト

シークレットをプロセスへ渡すのは作成時とマウント時だけにし、完了後は現在のshellから直ちにエクスポートを解除します。ビルドスクリプトで環境変数を出力してはならず、パスワードをボリューム内へコピーしてもいけません。イメージファイルの権限は 600 以下に制限し、イメージのルートディレクトリは 700 に保ちます。

本番導入前に、次の項目を1つずつ確認します。

  • runnerが専用の非管理者アカウントを使用している。
  • ジョブ番号が許可リストでフィルタリングされ、パストラバーサルが発生しない。
  • 作成、マウント、ビルド、アンマウントがそれぞれ個別に失敗でき、明確な状態を返す。
  • EXITINTTERM がすべて同じクリーンアップ関数を実行する。
  • アンマウントに失敗した場合、すぐに強制処理せず、先に使用中のプロセスを記録する。
  • APFSボリューム、スパースイメージファイル、ホストボリュームの容量を同時に監視する。
  • 失敗サンプルに保持期限を設定し、期限後はイメージ全体を削除する。
  • ログにパスワード、秘密鍵、完全な認証情報、機密性の高いソースコード断片が含まれていない。

まず、機密データを含まない小規模なプロジェクトを使い、正常完了、コンパイル失敗、手動終了、ディスク容量が上限に近づく場合の4つの経路を検証します。4つすべての状況でイメージを特定し、状態を説明し、回収まで完了できて初めて、この分離方式を実運用可能と判断できます。

よくある質問

暗号化スパースイメージだけで権限分離も完了しますか?

完了しません。保存時の作業データ保護には有効ですが、専用実行ユーザー、最小権限、秘密情報の安全な注入、ジョブ終了時のアンマウントも必要です。

失敗したジョブのイメージをアンマウントできない場合はどうしますか?

lsofでマウント先を使用中のプロセスを特定し、対象ジョブのプロセスだけを停止します。sync後に通常の切断を再試行し、強制切断は書き込み停止を確認した後に限定します。

専用クラウドMac

開発、ビルド、リモートデスクトップに専用物理マシンを選択

3種類のApple Silicon構成を比較し、注文時にノード、利用期間、ストレージの追加オプションを選択できます。

レンタルプランを選ぶ