GKE Agent Sandbox の動的ストレージの遅延バインディングで Filestore エージェント ボリュームを使用する

動的ストレージの遅延バインディングを使用すると、要求時に実行中の事前ウォームアップされた Google Kubernetes Engine(GKE)Agent Sandbox Pod に Filestore エージェント ボリュームを直接挿入できます。標準の Kubernetes ボリューム アタッチメント ライフサイクルをバイパスすることで、動的遅延バインディングは Pod の再起動を必要とせずに 100 ミリ秒未満のストレージ アタッチメント レイテンシを実現します。

このアーキテクチャにより、高密度で低レイテンシのエージェント プラットフォームは次のことが可能になります。

  • Pod のコールド スタートとコンテナの初期化の遅延を解消します。
  • 永続ワークスペースをオンデマンドで動的にアタッチおよびデタッチします。
  • アイドル状態のエージェント セッションを一時停止または休止し、ファイル システムの状態を維持しながら、使用可能な事前ウォーミングされたサンドボックス Pod で再開します。

始める前に

  1. Filestore エージェント ボリュームの GKE 環境を設定するで初期設定を完了します。
  2. GKE クラスタがバージョン 1.36.0-gke.3302001 以降を実行していることを確認します。このバージョンでは、gVisor emptyDir マウント伝播に必要な force-shared アノテーションがサポートされています。
  3. volume-pool-sc StorageClassvolumeBindingMode: ImmediatereclaimPolicy: Delete を指定していることを確認します。

アーキテクチャの概要

遅延バインディング アーキテクチャは、次の 4 つのコンポーネントで構成されています。

  • プラットフォーム オーケストレーターまたはカスタム コントローラ: セッションのライフサイクルを管理するコントロール プレーン サービスまたは Kubernetes コントローラ。SandboxClaim イベントを監視し、テナント ボリュームのメタデータを解決し、ストレージ ノード デーモン API を呼び出してストレージをバインドまたはバインド解除し、削除ファイナライザーを管理します。
  • ストレージ ノード デーモン: マウント API を公開する各 gVisor ノードで実行される特権 DaemonSet
  • force-shared を使用した SandboxTemplate: ホストマウントを gVisor サンドボックス コンテナに動的に伝播できる gVisor サンドボックス Pod テンプレート。
  • SandboxWarmPool: 事前にウォームアップされた実行中のサンドボックス Pod のプール。クレーム時にマウント リクエストをすぐに受信できます。

ストレージ ノード デーモンをデプロイする

特権 DaemonSet を含む storage-node-daemon.yaml という名前のマニフェストを作成します。

次のようにマニフェストを適用します。

kubectl apply -f storage-node-daemon.yaml

SandboxTemplateSandboxWarmPool をデプロイする

テンプレートとウォームプールを含むマニフェストを sandbox-latebind.yaml という名前で作成します。

次のようにマニフェストを適用します。

kubectl apply -f sandbox-latebind.yaml

サンドボックスを要求してストレージを動的にバインドする

  1. 削除ファイナライザー(agent.sandbox/storage-cleanup)を含む late-bind-claim.yaml という名前のクレーム マニフェストを作成します。

    apiVersion: extensions.agents.x-k8s.io/v1alpha1
    kind: SandboxClaim
    metadata:
      name: late-bind-session-1
      namespace: default
      finalizers:
        - agent.sandbox/storage-cleanup
    spec:
      sandboxTemplateRef:
        name: late-bind-template
    

    請求を適用します。

    kubectl apply -f late-bind-claim.yaml
    
  2. agent-volume-pvc.yaml という名前の PVC マニフェストを使用してボリュームを動的にプロビジョニングします。

    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: session-1-pvc
      namespace: default
    spec:
      accessModes: [ReadWriteMany]
      storageClassName: volume-pool-sc
      resources:
        requests:
          storage: 1Gi
    

    PVC を適用します。

    kubectl apply -f agent-volume-pvc.yaml
    
  3. 割り当てられた Pod の UID、ノード、バッキング PV のエクスポートの詳細を取得します。

    POD_NAME=$(kubectl get pods \
      -l extensions.agents.x-k8s.io/claimed-by=late-bind-session-1 \
      -o jsonpath='{.items[0].metadata.name}')
    POD_UID=$(kubectl get pod "${POD_NAME}" -o jsonpath='{.metadata.uid}')
    NODE_NAME=$(kubectl get pod "${POD_NAME}" -o jsonpath='{.spec.nodeName}')
    PV_NAME=$(kubectl get pvc session-1-pvc -o jsonpath='{.spec.volumeName}')
    NFS_IP=$(kubectl get pv "${PV_NAME}" \
      -o jsonpath='{.spec.csi.volumeAttributes.ip}')
    NFS_PATH="/$(kubectl get pv "${PV_NAME}" \
      -o jsonpath='{.spec.csi.volumeAttributes.volume}')"
    
  4. Pod ホストのノード デーモンにマウント シグナルを送信します。

    DAEMON_POD=$(kubectl get pods -l app=storage-node-daemon \
      --field-selector spec.nodeName="${NODE_NAME}" \
      -o jsonpath='{.items[0].metadata.name}')
    kubectl exec "${DAEMON_POD}" -c daemon -- python3 -c "
    import urllib.request, json
    payload = json.dumps({
        'action': 'bind_nfs',
        'pod_uid': '${POD_UID}',
        'volume_name': 'workspace-volume',
        'sub_dir': 'user_data',
        'nfs_server': '${NFS_IP}',
        'nfs_path': '${NFS_PATH}'
    }).encode()
    req = urllib.request.Request(
        'http://localhost:9090',
        data=payload,
        headers={'Content-Type': 'application/json'})
    print(urllib.request.urlopen(req).read().decode())
    "
    
  5. ホストマウントがアクティブな間に早すぎるクリーンアップを防ぐため、要求された Pod に削除ファイナライザーを適用します。

    kubectl patch pod "${POD_NAME}" --type=merge \
      -p '{"metadata":{"finalizers":["agent.sandbox/storage-cleanup"]}}'
    
  6. 実行中の Pod 内のボリューム マウントを確認します。

    kubectl logs "${POD_NAME}" -c agent
    

    出力は、Volume が正常にマウントされたことを示しています。

    Waiting for late-bind signal...
    Filestore volume mounted successfully!
    drwxr-xr-x    2 1000     1000          4096 ... user_data
    

セッションを一時停止、再開する

エージェント セッションが終了するか、休止状態になると、オーケストレーターは、Kubernetes が Pod を終了または再利用できるようにする前に、ホスト ストレージをマウント解除する必要があります。

  1. バックグラウンドで SandboxClaim の削除を開始します。ファイナライザのため、Kubernetes は削除対象の要求をマークしますが、Pod の終了を一時停止します。

    kubectl delete sandboxclaim late-bind-session-1 --wait=false
    
  2. ホストノードで NFS 共有のマウントを解除します。

    kubectl exec "${DAEMON_POD}" -c daemon -- python3 -c "
    import urllib.request, json
    payload = json.dumps({
        'action': 'unbind',
        'pod_uid': '${POD_UID}',
        'volume_name': 'workspace-volume',
        'sub_dir': 'user_data'
    }).encode()
    req = urllib.request.Request(
        'http://localhost:9090',
        data=payload,
        headers={'Content-Type': 'application/json'})
    print(urllib.request.urlopen(req).read().decode())
    "
    
  3. SandboxClaim と Pod の両方からファイナライザを削除して、終了を完了します。

    kubectl patch sandboxclaim late-bind-session-1 --type=merge \
      -p '{"metadata":{"finalizers":[]}}'
    kubectl patch pod "${POD_NAME}" --type=merge \
      -p '{"metadata":{"finalizers":[]}}'
    

セッションを後で再開するには、新しい事前ウォーミングされた Pod を要求し、既存の PVC(session-1-pvc)を使用してバインド リクエストを送信します。新しいサンドボックス Pod は、保存されたワークスペースの状態にすぐにアクセスできます。

本番環境の考慮事項とカスタム コントローラの設計

このドキュメントの手動コマンドは、動的遅延バインディングの低レベルのメカニズムを示しています。このアーキテクチャを本番環境で確実に実行するには、アプリケーションのセッション ライフサイクルに合わせてカスタマイズされたカスタム Kubernetes コントローラまたはプラットフォーム オーケストレーターを開発する必要があります。

本番環境のコントローラとノード デーモンを設計する際は、次のアーキテクチャ パターンを実装します。

調整とファイナライザーのライフサイクルを自動化する

ストレージ ノード デーモンは、標準の Kubernetes Container Storage Interface(CSI)ライフサイクル管理の外部でホストレベルのマウントを実行するため、Kubelet は Pod の emptyDir 内のアクティブなマウントを認識しません。マウントがアクティブな状態で Pod が削除されると、Kubelet は emptyDir ディレクトリを削除できず、Device or resource busy エラーが発生し、Pod が Terminating 状態のままになります。

カスタム コントローラは、ファイナライザー(agent.sandbox/storage-cleanup など)を使用して厳格な状態マシンを自動化する必要があります。

  1. クレームの作成とバインディング:
    • 作成時にすべての SandboxClaim に静的ファイナライザーをアタッチします。
    • Kubernetes API で SandboxClaim のステータス更新を監視します。クレームがウォームプール Pod にバインドされたら、割り当てられた pod_uidnodeName、バッキング Filestore ボリューム属性を抽出します。
    • 認証済みの bind リクエストを、ターゲット ノードで実行されているストレージ ノード デーモンに送信します。
    • 実行中の Pod オブジェクトに動的ファイナライザーを追加するために、直ちにパッチを適用します。SandboxTemplate 仕様では静的 Pod ファイナライザーがサポートされていないため、Pod が削除されても SandboxClaim がアクティブな状態が続くノードのドレイン イベントやリスケジューリング イベント中に Pod を保護するには、Pod を動的にパッチ適用する必要があります。
  2. 正常終了と削除の処理:
    • SandboxClaim リソースと Pod リソースの両方で deletionTimestamp を監視します。
    • 削除または強制排除が検出されたら、ノード デーモンの unbind エンドポイントを呼び出して、ホスト ディレクトリ(umount -l)をクリーンにマウント解除します。
    • アンマウントが成功し、保留中の書き込みがすべてフラッシュされたことを確認してから、PodSandboxClaim にパッチを適用してファイナライザーを削除します。これにより、グレースフルな削除、GKE ノードのアップグレード、Spot VM のプリエンプション、メモリ不足(OOM)による強制終了など、クリーンな削除を実現できます。

ストレージ ノード デーモンを保護して強化する

  • kubectl exec を認証済み API に置き換えます。本番環境では、kubectl exec を使用したり、デーモンを localhost にバインドしたりしないでください。相互 TLS(mTLS)または Kubernetes ServiceAccount トークン認証で保護されたクラスタ ネットワークを介して専用の gRPC または HTTPS エンドポイントを公開するように、ストレージ ノード デーモンを構成します。
  • デーモン Namespace とネットワーク アクセスを分離する: 制限付きの管理 Namespace(default やテナント Namespace ではなく、sandbox-storage-system など)に特権 storage-node-daemon DaemonSet をデプロイします。カスタム コントローラ Pod からのみデーモン API への上り(内向き)を許可し、サンドボックス化されたエージェント Pod からのすべてのトラフィックをブロックする Kubernetes NetworkPolicy ルールを適用します。
  • 事前作成されたコンテナ イメージを使用する: initContainer で実行時に nfs-common などのパッケージをインストールしないようにします。必要なマウント ユーティリティがすべてプリインストールされた、ビルド済みの不変コンテナ イメージを使用して、ノードの起動遅延と外部リポジトリの依存関係を解消します。

ストレージ割り当てとマルチテナント分離を適用する

  • エージェントごとのストレージ使用量をモニタリングする: 共有 ReadWriteMany(RWX)Filestore ボリュームのサブディレクトリを emptyDir に動的にバインド マウントする場合、標準の Kubernetes emptyDir.sizeLimit 設定では、マウントされた NFS パスにエージェントごとのストレージ割り当てを適用できません。単一の暴走エージェントが共有ボリュームを使い果たしてサービス拒否(DoS)を引き起こすのを防ぐには、オーケストレーターでディレクトリ割り当てのモニタリングを実装するか、ボリューム プールを使用して専用ボリュームをプロビジョニングします。
  • さまざまなワークスペース アクセスモードに合わせてマウント ペイロードを調整する: コントローラは、ノード デーモンに送信されるパラメータを変更することで、複数のエージェント ストレージ トポロジをサポートできます。
    • プライベート分離ワークスペース: 一意のテナント サブディレクトリまたは専用 PVC を、読み取り / 書き込み権限を持つ単一のサンドボックス Pod にバインドします。
    • コラボレーション ワークスペース: リアルタイムのファイル共有のために、複数の連携エージェント Pod 間で同じ共有 RWX サブディレクトリを同時にバインドします。
    • 探索ブランチ ワークスペース: ベース テンプレート ディレクトリを読み取り専用(ro)としてマウントし、エージェントがゴールデン コピーを変更せずに共有アセットを読み取れるようにします。同時に、新しい書き込みを別の書き込み可能なスクラッチ パスまたは復元時のコピー ディレクトリに転送します。

ポイントインタイム スナップショットとクリーンアップを調整する

  • スナップショットの前に書き込みの静止を実現する: データの破損なしに一貫性のある特定の時点のワークスペース スナップショットを取得するには、ワークスペース ディレクトリをアーカイブする前、または Filestore スナップショットをトリガーする前に、アンバインド ワークフローを開始(またはファイル システム バッファをフラッシュ)して、アクティブな書き込みを一時停止する必要があります。
  • テナントのプロビジョニング解除を自動化する: ユーザー セッションまたはワークスペースが完全に期限切れになったら、コントローラがまずすべてのノードでアクティブなマウントをバインド解除してから、非同期バックグラウンド タスクを実行して、テナントの永続ディレクトリをバッキング ボリュームから削除するようにします。

動的ファイナライザー管理、マルチテナント分離、スナップショット復元ワークフローを示す完全なリファレンス実装については、GitHub の GKE Sandbox の遅延バインディング ストレージの例をご覧ください。

次のステップ