Dynamic storage late-binding lets you inject Filestore agent volumes directly into running, pre-warmed Google Kubernetes Engine (GKE) Agent Sandbox pods upon claim. By bypassing the standard Kubernetes volume attachment lifecycle, dynamic late-binding achieves under 100 ms storage attachment latency without requiring pod restarts.
This architecture enables high-density, low-latency agent platforms to:
- Eliminate pod cold-start and container initialization delays.
- Dynamically attach and detach persistent workspaces on demand.
- Pause or hibernate idle agent sessions and resume them on any available pre-warmed sandbox pod while preserving file system state.
Before you begin
- Complete the initial setup in Set up GKE environment for Filestore agent volumes.
- Verify that your GKE cluster runs version
1.36.0-gke.3302001or higher. This version supports theforce-sharedannotation required for gVisoremptyDirmount propagation. - Verify that your
volume-pool-scStorageClassspecifiesvolumeBindingMode: ImmediateandreclaimPolicy: Delete.
Architecture overview
The late-binding architecture consists of four components:
- Platform orchestrator or custom controller: A control-plane service or
Kubernetes controller that manages session lifecycles. It watches for
SandboxClaimevents, resolves tenant volume metadata, calls the storage node daemon API to bind or unbind storage, and manages deletion finalizers. - Storage node daemon: A privileged
DaemonSetrunning on each gVisor node that exposes a mount API. SandboxTemplatewithforce-shared: A gVisor sandbox pod template which lets the host mounts propagate dynamically into the gVisor sandbox container.SandboxWarmPool: A pool of pre-warmed running sandbox pods ready to receive mount requests instantly upon claim.
Deploy the storage node daemon
Create a manifest named storage-node-daemon.yaml containing the privileged
DaemonSet:
Apply the manifest:
kubectl apply -f storage-node-daemon.yaml
Deploy the SandboxTemplate and SandboxWarmPool
Create a manifest named sandbox-latebind.yaml containing the template and
warm pool:
Apply the manifest:
kubectl apply -f sandbox-latebind.yaml
Claim a sandbox and dynamically bind storage
Create a claim manifest named
late-bind-claim.yamlthat includes a deletion finalizer (agent.sandbox/storage-cleanup):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-templateApply the claim:
kubectl apply -f late-bind-claim.yaml
Dynamically provision a volume using a PVC manifest named
agent-volume-pvc.yaml:apiVersion: v1 kind: PersistentVolumeClaim metadata: name: session-1-pvc namespace: default spec: accessModes: [ReadWriteMany] storageClassName: volume-pool-sc resources: requests: storage: 1GiApply the PVC:
kubectl apply -f agent-volume-pvc.yaml
Retrieve the assigned pod UID, node, and backing PV export details:
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}')"Send the mount signal to the node daemon on the pod host:
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()) "Apply a deletion finalizer to the claimed pod to prevent premature cleanup while the host mount is active:
kubectl patch pod "${POD_NAME}" --type=merge \ -p '{"metadata":{"finalizers":["agent.sandbox/storage-cleanup"]}}'Verify the volume mount inside the running pod:
kubectl logs "${POD_NAME}" -c agentThe output confirms that the volume was mounted successfully:
Waiting for late-bind signal... Filestore volume mounted successfully! drwxr-xr-x 2 1000 1000 4096 ... user_data
Pause and resume a session
When an agent session concludes or enters hibernation, your orchestrator must unmount the host storage before allowing Kubernetes to terminate or recycle the pod.
Initiate deletion of the
SandboxClaimin the background. Because of the finalizer, Kubernetes marks the claim for deletion but pauses pod termination:kubectl delete sandboxclaim late-bind-session-1 --wait=false
Unmount the NFS share on the host node:
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()) "Remove the finalizers from both the
SandboxClaimand the pod to complete termination:kubectl patch sandboxclaim late-bind-session-1 --type=merge \ -p '{"metadata":{"finalizers":[]}}' kubectl patch pod "${POD_NAME}" --type=merge \ -p '{"metadata":{"finalizers":[]}}'
To resume the session later, claim a new pre-warmed pod and send the bind
request using the existing PVC (session-1-pvc). The new sandbox pod
immediately gains access to the preserved workspace state.
Production considerations and custom controller design
The manual commands in this document demonstrate the low-level mechanics of dynamic late-binding. To run this architecture reliably in production, you must develop a custom Kubernetes controller or platform orchestrator tailored to your application's session lifecycle.
When designing your production controller and node daemon, implement the following architectural patterns:
Automate the reconciliation and finalizer lifecycle
Because the storage node daemon performs host-level mounts outside of standard
Kubernetes Container Storage Interface (CSI) lifecycle management, Kubelet is
unaware of active mounts inside the pod's emptyDir. If a pod is deleted while
the mount is active, Kubelet fails to remove the emptyDir directory and raises
a Device or resource busy error, leaving the pod stuck in a Terminating
state.
Your custom controller must automate a strict state machine using finalizers
(such as agent.sandbox/storage-cleanup):
- Claim creation and binding:
- Attach a static finalizer to every
SandboxClaimupon creation. - Watch the Kubernetes API for
SandboxClaimstatus updates. When a claim binds to a warm pool pod, extract the assignedpod_uid,nodeName, and backing Filestore volume attributes. - Send an authenticated
bindrequest to the storage node daemon running on the target node. - Immediately patch the running
Podobject to add the dynamic finalizer. BecauseSandboxTemplatespecifications don't support static pod finalizers, patching the pod dynamically is required to protect the pod during node drains or rescheduling events where the pod is evicted but theSandboxClaimremains active.
- Attach a static finalizer to every
- Graceful termination and eviction handling:
- Watch for
deletionTimestampon bothSandboxClaimandPodresources. - When a deletion or eviction is detected, call the node daemon's
unbindendpoint to cleanly unmount the host directory (umount -l). - Verify that the unmount succeeded and that all pending writes have
flushed before patching the
PodandSandboxClaimto remove their finalizers. This can help you achieve clean teardown across graceful claim deletions, GKE node upgrades, Spot VM preemptions, and out-of-memory (OOM) kills.
- Watch for
Secure and harden the storage node daemon
- Replace
kubectl execwith authenticated APIs: In production, don't usekubectl execor bind the daemon tolocalhost. Configure the storage node daemon to expose a dedicated gRPC or HTTPS endpoint over the cluster network secured with mutual TLS (mTLS) or KubernetesServiceAccounttoken authentication. - Isolate daemon namespaces and network access: Deploy the privileged
storage-node-daemonDaemonSetin a restricted administrative namespace (for example,sandbox-storage-system) rather than thedefaultor tenant namespaces. Apply KubernetesNetworkPolicyrules that permit ingress to the daemon API exclusively from your custom controller pods and block all traffic from sandboxed agent pods. - Use pre-baked container images: Avoid installing packages like
nfs-commonat runtime in aninitContainer. Use a prebuilt, immutable container image with all required mount utilities pre-installed to eliminate node startup delays and external repository dependencies.
Enforce storage quotas and multi-tenant isolation
- Monitor per-agent storage usage: When you dynamically bind-mount a
subdirectory from a shared
ReadWriteMany(RWX) Filestore volume into anemptyDir, standard KubernetesemptyDir.sizeLimitsettings can't enforce per-agent storage quotas on the mounted NFS path. To prevent a single runaway agent from exhausting the shared volume and causing a denial of service (DoS), implement directory quota monitoring in your orchestrator or provision dedicated volumes using volume pools. - Adapt mount payloads for different workspace access modes: Your
controller can support multiple agent storage topologies by varying the
parameters sent to the node daemon:
- Private isolated workspaces: Bind a unique tenant subdirectory or dedicated PVC to a single sandbox pod with read-write permissions.
- Collaborative workspaces: Concurrently bind the same shared RWX subdirectory across multiple coordinating agent pods for real-time file sharing.
- Exploration branching workspaces: Mount a base template directory
as read-only (
ro) so agents can read shared assets without modifying the golden copy, while routing new writes to a separate writable scratch path or copy-on-restore directory.
Coordinate point-in-time snapshots and cleanup
- Achieve write quiescence before snapshots: To capture consistent point-in-time workspace snapshots without data corruption, your orchestrator should pause active writes by initiating the unbind workflow (or flushing file system buffers) before archiving the workspace directory or triggering a Filestore snapshot.
- Automate tenant deprovisioning: When a user session or workspace expires permanently, ensure your controller first unbinds any active mounts across all nodes before executing asynchronous background tasks to delete the tenant's persistent directories from the backing volume.
For a complete reference implementation showing dynamic finalizer management, multi-tenant isolation, and snapshot restoration workflows, see the GKE Sandbox late-binding storage example on GitHub.
What's next
- Explore static Agent Sandbox integration.
- Deploy self-managed GKE workloads.
- Learn how to create and manage volume pools.