Storage resources
Full YAML field reference for StorageClass, Volume, Snapshot, and the service-spec volumes[] block.
This page is the schema reference for Rune's storage resources. For the big-picture model and the lifecycle, see the storage concept.
StorageClass
Cluster-scoped. Names a driver and default parameters.
storageClass:
name: do-nyc3
driver: do-volume
parameters:
region: nyc3
fsType: ext4
apiTokenSecretRef: do/api-token
reclaimPolicy: retain
default: false
allowedTopologies:
- matchLabels:
rune.io/region: nyc3
- matchExpressions:
- key: rune.io/zone
operator: In
values: [nyc3a, nyc3b]
labels:
tier: cloud
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | DNS-1123. Cluster-unique. |
driver | string | yes | Registered driver name (e.g. local, local-host, do-volume, hcloud-volume, aws-ebs, gce-pd). |
parameters | map[string]string | no | Driver-specific. See driver tables below. |
reclaimPolicy | enum | no | retain (default) or delete. Per-volume override allowed. |
default | bool | no | At most one class may be true. API server enforces uniqueness. |
allowedTopologies | []TopologySelector | no | Optional placement constraints; matched against node labels. |
labels | map[string]string | no | Free-form labels. |
TopologySelector is a list of matchLabels and/or matchExpressions
(operators: In, NotIn, Exists, DoesNotExist).
Secret references in parameters
Any value in a StorageClass or Volume parameters map may be a
secret: reference instead of a literal. The controller resolves
the reference against the secrets store before the driver sees it,
so drivers always receive plaintext.
storageClass:
name: do-nyc3
driver: do-volume
parameters:
region: nyc3
# FQDN: pinned to a specific namespace — recommended for
# cluster-scoped StorageClasses since one shared secret serves
# every namespace's volumes.
apiToken: secret:do-api-token.shared.rune/token
Form: secret:<name>[.<namespace>.rune]/<key>. Values without the
secret: prefix pass through verbatim. A missing secret or key fails
the operation with InvalidParameters.
Shorthand vs FQDN — important for StorageClass
The shorthand form secret:<name>/<key> (no namespace) is resolved
against the consuming Volume's namespace, not the StorageClass's.
Since StorageClass is cluster-scoped, a single class with a shorthand
ref will look up the secret in whichever namespace happens to be
using the class at that moment — so every namespace would need its
own copy of the secret. For shared infrastructure tokens (DO API
token, S3 credentials, etc.) use the FQDN form to pin the
lookup to one namespace regardless of which Volume triggered the
operation. Shorthand is fine on Volume.parameters overrides (the
Volume already has its own namespace).
Volume
Namespaced. A unit of durable storage.
volume:
name: pgdata-postgres-0
namespace: prod
storageClassName: local
size: 10Gi
accessMode: ReadWriteOnce
reclaimPolicy: retain
parameters: {}
labels:
app: postgres
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | DNS-1123. Unique within the namespace. |
namespace | string | no | Default default. |
storageClassName | string | conditional | Required unless a default class is set. Falls back to runefile.[storage].defaultStorageClass. |
size | quantity | yes | E.g. 10Gi, 500Mi, 1G. Kubernetes Quantity syntax: binary suffixes (Ki/Mi/Gi/...) are powers of two, SI suffixes (K/M/G/...) are decimal. Unitless integers are bytes, not gigabytes — write 10Gi, not 10. Driver may treat as informational (e.g. hostPath). |
accessMode | enum | yes | ReadWriteOnce, ReadOnlyMany, ReadWriteMany. Driver-gated. |
reclaimPolicy | enum | no | retain or delete. Defaults to the class's policy. |
parameters | map[string]string | no | Per-volume overrides merged on top of class parameters. |
labels | map[string]string | no | Free-form. |
Driver-specific parameters
local
| Key | Notes |
|---|---|
| (none required) | Rune manages the directory under runefile.[storage].localVolumeRoot. |
local-host
| Key | Notes |
|---|---|
hostPath | Required. Absolute path; must sit under runefile.[storage].hostPathAllowlist. |
createIfMissing | "true" to create the directory if missing. Honoured only when [storage] allowCreateMissing = true. |
do-volume
| Key | Notes |
|---|---|
region | Required. DigitalOcean region (e.g. nyc3). Block-storage volumes are region-pinned — see Region pinning below. |
fsType | ext4 (default), xfs. |
apiToken | DO API token. Accepts a literal value or a secret reference like secret:do-api-token/token. |
apiTokenSecretRef | Legacy. <namespace>/<secret-name> form, kept for runefile back-compat. Prefer the generic secret: reference on apiToken above. |
Required DigitalOcean token scopes
A Full Access token works, but the driver only needs the
following custom scopes. These mirror exactly the eight HTTP endpoints
the driver calls (pkg/storage/driver/dovolume/client.go); anything
else is over-privileged.
| Resource | Operations | DO endpoint(s) the driver hits | What fails without it |
|---|---|---|---|
block_storage | create, read, delete | POST/GET/DELETE /v2/volumes[/{id}] | Provision (create), reconcile/observe (read), reclaim (delete). |
block_storage_action | create | POST /v2/volumes/{id}/actions | Attach, detach, and online-resize. This is the one most operators miss — provisioning succeeds and then attach silently 401s, leaving the volume stuck Available with the instance pending. |
actions | read | GET /v2/actions/{id} | Polling the async action to completion. Without it, attach/detach return an action ID the driver can't observe and times out as Stalled. |
droplet | read | GET /v2/droplets?name=<node> | Translating the Rune node ID into a DO droplet ID for the attach call. |
block_storage_snapshot | create, read, delete | POST /v2/volumes/{id}/snapshots, DELETE /v2/snapshots/{id} | Only required if you use rune snapshot create / restore against volumes on this class. Omit if you don't use snapshots. |
The driver does not call /v2/regions, /v2/sizes, or update
the volume metadata, so regions:read, sizes:read, and
block_storage:update are not needed despite what the DO console's
default scope hints sometimes suggest.
Region pinning
DO block-storage volumes belong to a single region. The driver provisions in the region named on the StorageClass, and DO refuses to attach a volume to a droplet in a different region. For a multi-region cluster, create one StorageClass per region:
storageClass:
name: do-volumes-nyc3
driver: do-volume
parameters: { region: nyc3, fsType: ext4, apiTokenSecretRef: shared/do-api-token }
---
storageClass:
name: do-volumes-fra1
driver: do-volume
parameters: { region: fra1, fsType: ext4, apiTokenSecretRef: shared/do-api-token }
Use allowedTopologies (RUNE-072) or the namespace scoping of your
StorageClass references to keep claims targeted at the right region.
hcloud-volume
| Key | Notes |
|---|---|
location | Required. Hetzner Cloud location (e.g. nbg1, fsn1, hel1, ash). Volumes are location-pinned — see Region pinning (the same rule applies). region is accepted as an alias for cross-provider topology key compatibility. |
fsType | ext4 (default), xfs. |
apiToken | Hetzner Cloud API token. Accepts a literal value or a secret reference like secret:hcloud-api-token.shared.rune/token. |
The driver calls the following Hetzner Cloud API endpoints:
| Operation | Endpoint |
|---|---|
| Provision | POST /v1/volumes |
| Observe / reconcile | GET /v1/volumes/{id} |
| Delete | DELETE /v1/volumes/{id} |
| Attach | POST /v1/volumes/{id}/actions/attach |
| Detach | POST /v1/volumes/{id}/actions/detach |
| Resize (offline) | POST /v1/volumes/{id}/actions/resize |
| Action polling | GET /v1/actions/{id} |
| Server lookup | GET /v1/servers?name=<node-hostname> |
Capabilities:
Snapshots: false— Hetzner Cloud has no volume-snapshot API. Writes toSnapshotagainst anhcloud-volumeclass are rejected at cast time.Expand: true,OnlineExpand: false—rune volume resizeworks but the volume must be unbound first.BlockDevice: true— the driver formats the volume on first mount and exposes the underlying block device at/dev/disk/by-id/scsi-0HC_Volume_<id>.
aws-ebs
| Key | Notes |
|---|---|
region | Required. AWS region for the EC2 API client (e.g. eu-west-2). |
availabilityZone | Required (or supply a rune.io/zone topology label). EBS volumes are AZ-pinned and only attach to instances in the same AZ — e.g. eu-west-2a. |
volumeType | gp3 (default), gp2, io2, sc1, st1, … |
iops | Optional. Provisioned IOPS for gp3/io2. Omitted → AWS default. |
throughput | Optional. Provisioned throughput (MiB/s) for gp3. Omitted → AWS default. |
encrypted | true (default) / false. Encrypts the volume at rest. |
kmsKeyId | Optional KMS key ARN/ID for encryption. Omitted → the account default EBS key. |
fsType | ext4 (default), xfs. |
accessKeyId / secretAccessKey / sessionToken | Optional static credentials for off-instance / cross-account controllers. Accept literals or secret references. Omit on an EC2 node with an instance profile — the driver uses the instance role via the standard AWS credential chain. |
The driver reads AWS credentials from the standard chain (instance
role first), so the recommended setup attaches an instance profile to
the node and configures no credentials on the StorageClass. The
terraform-aws-rune module does this by
default.
Required IAM permissions
The instance role (or supplied credentials) needs these EC2 actions — one row per Driver method:
| Action | Used by |
|---|---|
ec2:CreateVolume | Provision |
ec2:DeleteVolume | reclaim Delete |
ec2:DescribeVolumes | Provision (adopt-by-tag), Attach/Detach, Expand, observe |
ec2:AttachVolume | Attach |
ec2:DetachVolume | Detach |
ec2:ModifyVolume | Expand (online) |
ec2:CreateSnapshot | Snapshot — omit if you don't use rune snapshot |
ec2:DeleteSnapshot | Snapshot delete |
ec2:DescribeSnapshots | snapshot observe |
ec2:DescribeInstances | Attach/Detach — resolve the node's hostname to an EC2 instance |
ec2:CreateTags | tag created volumes/snapshots (rune.io/volume-id, Name) |
The managed policy AmazonEBSCSIDriverPolicy is a convenient superset
of the above (it scopes some actions with conditions on the
ebs.csi.aws.com/cluster tag — fine since the driver tags its
volumes). terraform-aws-rune's enable_ebs_volume_access attaches a
scoped inline policy instead.
Capabilities:
Snapshots: true— EBS snapshot API.Expand: true,OnlineExpand: true—rune volume resizegrows the volume in place viaModifyVolumewhile it stays attached. The filesystem is grown on the next mount.BlockDevice: true— the driver formats on first mount and resolves the device via the stable by-id symlink/dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_<vol-id-no-dash>(Nitro instances).
Node → instance resolution
Attach/Detach map a Rune node onto an EC2 instance by the node's OS
hostname, tried in order: an i-… instance ID, the private-dns-name
(EC2 default hostname, matched exactly and as <name>.*), then the
Name tag. An ambiguous match (two instances) is an error — keep
hostnames / Name tags unique.
gce-pd
| Key | Notes |
|---|---|
zone | Required (or supply a rune.io/zone topology label). Persistent Disks are zonal and only attach to instances in the same zone — e.g. europe-west2-a. |
project | GCP project ID. Optional on a GCE node (read from the metadata server); required for off-instance controllers. |
diskType | pd-balanced (default), pd-ssd, pd-standard, pd-extreme. |
fsType | ext4 (default), xfs. |
credentialsJSON | Optional service-account key (JSON) for off-instance / cross-project controllers. Accepts a literal or a secret reference. Omit on a GCE node with a service account — the driver uses Application Default Credentials. |
The driver reads credentials from Application Default Credentials
(the instance service account first), so the recommended setup attaches
a service account to the node and configures no credentials on the
StorageClass. The terraform-google-rune
module does this when enable_pd_csi_access = true.
Required IAM permissions
The service account (or supplied key) needs the Compute permissions the
driver calls — bundled by roles/compute.storageAdmin, or a custom role
with:
| Permission group | Used by |
|---|---|
compute.disks.create / .delete / .get | Provision, Delete, observe |
compute.disks.resize | Expand (online) |
compute.disks.use / compute.instances.attachDisk / .detachDisk | Attach / Detach |
compute.disks.createSnapshot / compute.snapshots.create / .delete / .get | Snapshot — omit if you don't use rune snapshot |
compute.instances.get | Attach / Detach — resolve the node's hostname to an instance |
compute.zoneOperations.get / compute.globalOperations.get | wait for the disk/snapshot operations to complete |
Capabilities:
Snapshots: true— GCE disk snapshots (global resources).Expand: true,OnlineExpand: true—rune volume resizegrows the disk in place viadisks.resizewhile attached. The filesystem is grown on the next mount.BlockDevice: true— the driver formats on first mount and resolves the device via the stable by-id symlink/dev/disk/by-id/google-<diskName>(the disk is attached withDeviceName == <diskName>).
Node → instance resolution
Attach/Detach map a Rune node onto a GCE instance by the node's OS
hostname, which on GCE equals the instance name (compute.instances.get
in the StorageClass zone). A name mismatch surfaces as
no GCE instance "<host>" in zone <zone> on the first Attach.
Status fields (read-only)
| Field | Notes |
|---|---|
status | Pending, Provisioning, Available, Bound, Released, Failed, Stalled. |
handle | Driver-specific identifier (path, volume ID, …). |
boundClaim | The instance/claim currently bound to this volume. |
boundNode | Node where the volume is currently attached. |
ownerService | Set when the volume was created from a service claimTemplate. |
failureMessage | Last driver/controller error if Failed/Stalled. |
attempts | Provision retry count. |
driverParameters | Controller-captured snapshot of the merged StorageClass + Volume parameters (post-secret-resolution-source) at successful Provision time. Used by reclaim Delete / Detach / Unmount when the class has been deleted before its volumes, so orphan cleanup still has the parameters the driver needs. Read-only. |
Snapshot
Namespaced. Point-in-time copy of one volume.
snapshot:
name: pgdata-2025-11-15
namespace: prod
source:
volume: pgdata-postgres-0
labels:
app: postgres
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | DNS-1123. Unique within the namespace. |
namespace | string | no | Default default. |
source.volume | string | yes | Source volume name in the same namespace. |
labels | map | no | Free-form. |
The source volume's driver must advertise Capabilities.Snapshots = true —
the API server rejects writes against drivers that don't (e.g. local-host).
Status fields (read-only)
| Field | Notes |
|---|---|
status | Pending, Creating, Ready, Deleting, Failed. |
handle | Driver-specific snapshot identifier. |
failureMessage | Last driver/controller error if Failed. |
Service volumes[]
Top-level field on the service spec. See also the service spec reference.
service:
name: postgres
scale: 3
volumes:
- name: pgdata
mountPath: /var/lib/postgresql/data
readOnly: false
subPath: ""
claimTemplate:
storageClassName: local
size: 10Gi
accessMode: ReadWriteOnce
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Mount identifier. Unique within the service. |
mountPath | string | yes | Absolute path inside the container/process. Blocklist: /, /etc, /proc, /sys, /var/run/docker.sock. |
readOnly | bool | no | Mount read-only. Default false. |
subPath | string | no | Mount a sub-directory of the volume. |
claim | object | conditional | Exactly one of claim or claimTemplate. |
claimTemplate | object | conditional | Exactly one of claim or claimTemplate. |
claim
claim:
name: web-data # bare name → same namespace as the service
# name: shared.common.rune # FQDN form → cross-namespace
Cast-time error: an RWO claim mount on a service with scale > 1. Use
claimTemplate for stateful sets.
claimTemplate
claimTemplate:
storageClassName: local # optional; defaults to the cluster default
size: 10Gi # required
accessMode: ReadWriteOnce # required
parameters: {} # optional driver-specific overrides
reclaimPolicy: retain # optional override
Per-replica volumes are auto-provisioned with stable names of the form
<volume-name>-<service-name>-<ordinal>, e.g. pgdata-postgres-0.
Validation
All of the following are checked at cast time and on every API write:
- Exactly one of
claim/claimTemplateper mount entry. mountPathis absolute, unique within the service, doesn't overlap anysecretMounts/configmapMountspath, and isn't in the blocklist.- For RWO
claimmounts:service.scale == 1. claimTemplate.accessModeis in the chosen driver'sCapabilities.AccessModes.Snapshotwrites against drivers withoutCapabilities.Snapshotsare rejected.local-hosthostPathis absolute, has no.., sits underrunefile.[storage].hostPathAllowlist.- Process-runtime services may use
local-hostonly; block-device drivers (do-volume,hcloud-volume,aws-ebs,gce-pd) are rejected at cast time. - A
VolumewhosereclaimPolicy: deletetargets thelocal-hostdriver is rejected.