Substrate integrates with the Container Storage Interface (CSI) to provide dynamically provisioned, per-actor external volumes that seamlessly attach and detach as actors transition through their lifecycle.
In Kubernetes, volumes are reconciled asynchronously via standard Kubernetes objects (e.g. PersistentVolumeClaim, PersistentVolume). Agent Substrate takes a different approach tailored for actor lifecycle operations:
- No PV or PVC Objects: External volumes are declaratively defined in the
ActorTemplateviaexternalVolumeTemplateand provisioned dynamically for each actor instance. Volume operations are coupled directly with the actor lifecycle. - Direct Network-Based CSI Controller: The Substrate control plane (
ateapi) communicates directly with the CSI Controller gRPC service over the network (via TCP or DNS endpoints, optionally secured with TLS/mTLS).
To discover and communicate with CSI drivers, Substrate uses dynamic discovery driven by the cluster-scoped CSIDriverConfig Custom Resource Definition (CRD).
CSIDriverConfig defines the gRPC connection parameters for a specific CSI driver. It bridges the Kubernetes StorageClass (referenced in the ActorTemplate) to the network endpoint of the CSI Controller service and the local socket path of the CSI Node plugin.
apiVersion: ate.dev/v1alpha1
kind: CSIDriverConfig
metadata:
name: nfs.csi.k8s.io
spec:
driverName: nfs.csi.k8s.io
controllerEndpoint: tcp://csi-nfs-controller.kube-system.svc.cluster.local:50052
nodeSocketOverride: unix:///var/lib/kubelet/plugins/csi-nfsplugin/csi.sock
tls:
enabled: true
usePodIdentity: true
serverName: csi-nfs-controller.kube-system.svc.cluster.local| Field | Type | Description |
|---|---|---|
driverName |
string |
Required. The standard CSI driver name (e.g. nfs.csi.k8s.io, hostpath.csi.k8s.io, pd.csi.storage.gke.io). Matches the provisioner field on the referenced Kubernetes StorageClass. |
controllerEndpoint |
string |
Required. The gRPC endpoint for the CSI Controller service. Must be a valid URI starting with tcp://, dns:///, or unix:// (e.g., tcp://csi-controller.kube-system.svc:50051 or dns:///csi-svc.default.svc:9000). |
nodeSocketOverride |
string |
Optional. Override for the CSI Node service Unix domain socket on worker nodes. Must begin with unix://. If omitted, Substrate defaults to unix:///var/lib/kubelet/plugins/<driverName>/csi.sock. |
tls |
*CSIDriverTLSConfig |
Optional. Configures TLS or mTLS for the gRPC connection to the controllerEndpoint. |
| Field | Type | Description |
|---|---|---|
enabled |
bool |
Required. Enables TLS/mTLS for the gRPC connection. |
usePodIdentity |
bool |
Optional. When true, reuses Substrate's SPIFFE Pod Identity certificates for mutual TLS (mTLS) with dynamic CA trust bundle verification and rotation. Must be true when enabled is true. |
serverName |
string |
Optional. Server name override for TLS certificate verification. |
Note
For details on exposing CSI controller endpoints over the network and configuring CSI node DaemonSets with required mount propagations, see the CSI Driver Deployment Guide.
External volumes are declared on the ActorTemplate resource. For complete details on actor templates, see the ActorTemplate: The Workload Blueprint section in the Substrate API Guide.
To attach a CSI volume to an actor:
- Define the volume under
volumeswith anexternalVolumeTemplate. - Mount the volume inside one or more containers under
containers[].volumeMounts.
volumes:
- name: my-data-volume
externalVolumeTemplate:
capacity: 10Gi
storageClassName: standard-rwxname: Unique DNS-label-compliant volume name.externalVolumeTemplate.capacity: Quantity string representing the requested volume size (e.g.1Gi,50Gi).externalVolumeTemplate.storageClassName: Name of a KubernetesStorageClasspresent in the cluster whoseprovisionermatches a registeredCSIDriverConfig.
volumeMounts:
- name: my-data-volume
mountPath: /var/dataname: Must match the declaredvolumes[].name.mountPath: Unix path inside the container sandbox where the volume will be mounted.
Note
All declared volumes in volumes must be mounted by at least one container.
The following example demonstrates setting up an NFS CSI driver with Substrate and deploying an ActorTemplate that mounts an external NFS volume.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: csi-nfs-sc
provisioner: nfs.csi.k8s.io
parameters:
server: nfs-server.default.svc.cluster.local
share: /
reclaimPolicy: Delete
volumeBindingMode: Immediate
mountOptions:
- nfsvers=4.1apiVersion: ate.dev/v1alpha1
kind: CSIDriverConfig
metadata:
name: nfs.csi.k8s.io
spec:
driverName: nfs.csi.k8s.io
controllerEndpoint: tcp://csi-nfs-controller.kube-system.svc.cluster.local:50052
nodeSocketOverride: unix:///var/lib/kubelet/plugins/csi-nfsplugin/csi.sock
tls:
enabled: true
usePodIdentity: true
serverName: csi-nfs-controller.kube-system.svc.cluster.localRefer to ActorTemplate: The Workload Blueprint for general template options.
The WorkerPool is a Kubernetes resource, applied with kubectl apply:
apiVersion: ate.dev/v1alpha1
kind: WorkerPool
metadata:
name: agent-pool
namespace: ate-demo
labels:
workload: stateful-agent
spec:
replicas: 5
workerImage: ko://github.com/agent-substrate/substrate/cmd/ateom-gvisorThe ActorTemplate is a protojson-shaped ateapipb.ActorTemplate, created
through the ate API with kubectl ate create actor-template -f - (the
ate-demo atespace must exist):
metadata:
atespace: ate-demo
name: stateful-agent-template
workerSelector:
matchLabels:
workload: stateful-agent
containers:
- name: agent
image: gcr.io/my-project/agent-app@sha256:7f28ab0...
volumeMounts:
- name: shared-storage
mountPath: /mnt/shared
readyz:
httpGet:
path: /readyz
port: 8080
sandboxConfig:
sandboxClass: SANDBOX_CLASS_GVISOR
configName: gvisor-default
snapshotsConfig:
storageLocation: gs://my-snapshots-bucket/stateful-agent
volumes:
- name: shared-storage
externalVolumeTemplate:
capacity: 5Gi
storageClassName: csi-nfs-sc