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
FieldTypeRequiredNotes
namestringyesDNS-1123. Cluster-unique.
driverstringyesRegistered driver name (e.g. local, local-host, do-volume, hcloud-volume, aws-ebs, gce-pd).
parametersmap[string]stringnoDriver-specific. See driver tables below.
reclaimPolicyenumnoretain (default) or delete. Per-volume override allowed.
defaultboolnoAt most one class may be true. API server enforces uniqueness.
allowedTopologies[]TopologySelectornoOptional placement constraints; matched against node labels.
labelsmap[string]stringnoFree-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
FieldTypeRequiredNotes
namestringyesDNS-1123. Unique within the namespace.
namespacestringnoDefault default.
storageClassNamestringconditionalRequired unless a default class is set. Falls back to runefile.[storage].defaultStorageClass.
sizequantityyesE.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).
accessModeenumyesReadWriteOnce, ReadOnlyMany, ReadWriteMany. Driver-gated.
reclaimPolicyenumnoretain or delete. Defaults to the class's policy.
parametersmap[string]stringnoPer-volume overrides merged on top of class parameters.
labelsmap[string]stringnoFree-form.

Driver-specific parameters

local

KeyNotes
(none required)Rune manages the directory under runefile.[storage].localVolumeRoot.

local-host

KeyNotes
hostPathRequired. Absolute path; must sit under runefile.[storage].hostPathAllowlist.
createIfMissing"true" to create the directory if missing. Honoured only when [storage] allowCreateMissing = true.

do-volume

KeyNotes
regionRequired. DigitalOcean region (e.g. nyc3). Block-storage volumes are region-pinned — see Region pinning below.
fsTypeext4 (default), xfs.
apiTokenDO API token. Accepts a literal value or a secret reference like secret:do-api-token/token.
apiTokenSecretRefLegacy. <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.

ResourceOperationsDO endpoint(s) the driver hitsWhat fails without it
block_storagecreate, read, deletePOST/GET/DELETE /v2/volumes[/{id}]Provision (create), reconcile/observe (read), reclaim (delete).
block_storage_actioncreatePOST /v2/volumes/{id}/actionsAttach, 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.
actionsreadGET /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.
dropletreadGET /v2/droplets?name=<node>Translating the Rune node ID into a DO droplet ID for the attach call.
block_storage_snapshotcreate, read, deletePOST /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

KeyNotes
locationRequired. 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.
fsTypeext4 (default), xfs.
apiTokenHetzner 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:

OperationEndpoint
ProvisionPOST /v1/volumes
Observe / reconcileGET /v1/volumes/{id}
DeleteDELETE /v1/volumes/{id}
AttachPOST /v1/volumes/{id}/actions/attach
DetachPOST /v1/volumes/{id}/actions/detach
Resize (offline)POST /v1/volumes/{id}/actions/resize
Action pollingGET /v1/actions/{id}
Server lookupGET /v1/servers?name=<node-hostname>

Capabilities:

  • Snapshots: false — Hetzner Cloud has no volume-snapshot API. Writes to Snapshot against an hcloud-volume class are rejected at cast time.
  • Expand: true, OnlineExpand: falserune volume resize works 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

KeyNotes
regionRequired. AWS region for the EC2 API client (e.g. eu-west-2).
availabilityZoneRequired (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.
volumeTypegp3 (default), gp2, io2, sc1, st1, …
iopsOptional. Provisioned IOPS for gp3/io2. Omitted → AWS default.
throughputOptional. Provisioned throughput (MiB/s) for gp3. Omitted → AWS default.
encryptedtrue (default) / false. Encrypts the volume at rest.
kmsKeyIdOptional KMS key ARN/ID for encryption. Omitted → the account default EBS key.
fsTypeext4 (default), xfs.
accessKeyId / secretAccessKey / sessionTokenOptional 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:

ActionUsed by
ec2:CreateVolumeProvision
ec2:DeleteVolumereclaim Delete
ec2:DescribeVolumesProvision (adopt-by-tag), Attach/Detach, Expand, observe
ec2:AttachVolumeAttach
ec2:DetachVolumeDetach
ec2:ModifyVolumeExpand (online)
ec2:CreateSnapshotSnapshot — omit if you don't use rune snapshot
ec2:DeleteSnapshotSnapshot delete
ec2:DescribeSnapshotssnapshot observe
ec2:DescribeInstancesAttach/Detach — resolve the node's hostname to an EC2 instance
ec2:CreateTagstag 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: truerune volume resize grows the volume in place via ModifyVolume while 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

KeyNotes
zoneRequired (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.
projectGCP project ID. Optional on a GCE node (read from the metadata server); required for off-instance controllers.
diskTypepd-balanced (default), pd-ssd, pd-standard, pd-extreme.
fsTypeext4 (default), xfs.
credentialsJSONOptional 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 groupUsed by
compute.disks.create / .delete / .getProvision, Delete, observe
compute.disks.resizeExpand (online)
compute.disks.use / compute.instances.attachDisk / .detachDiskAttach / Detach
compute.disks.createSnapshot / compute.snapshots.create / .delete / .getSnapshot — omit if you don't use rune snapshot
compute.instances.getAttach / Detach — resolve the node's hostname to an instance
compute.zoneOperations.get / compute.globalOperations.getwait for the disk/snapshot operations to complete

Capabilities:

  • Snapshots: true — GCE disk snapshots (global resources).
  • Expand: true, OnlineExpand: truerune volume resize grows the disk in place via disks.resize while 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 with DeviceName == <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)

FieldNotes
statusPending, Provisioning, Available, Bound, Released, Failed, Stalled.
handleDriver-specific identifier (path, volume ID, …).
boundClaimThe instance/claim currently bound to this volume.
boundNodeNode where the volume is currently attached.
ownerServiceSet when the volume was created from a service claimTemplate.
failureMessageLast driver/controller error if Failed/Stalled.
attemptsProvision retry count.
driverParametersController-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
FieldTypeRequiredNotes
namestringyesDNS-1123. Unique within the namespace.
namespacestringnoDefault default.
source.volumestringyesSource volume name in the same namespace.
labelsmapnoFree-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)

FieldNotes
statusPending, Creating, Ready, Deleting, Failed.
handleDriver-specific snapshot identifier.
failureMessageLast 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
FieldTypeRequiredNotes
namestringyesMount identifier. Unique within the service.
mountPathstringyesAbsolute path inside the container/process. Blocklist: /, /etc, /proc, /sys, /var/run/docker.sock.
readOnlyboolnoMount read-only. Default false.
subPathstringnoMount a sub-directory of the volume.
claimobjectconditionalExactly one of claim or claimTemplate.
claimTemplateobjectconditionalExactly 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 / claimTemplate per mount entry.
  • mountPath is absolute, unique within the service, doesn't overlap any secretMounts/configmapMounts path, and isn't in the blocklist.
  • For RWO claim mounts: service.scale == 1.
  • claimTemplate.accessMode is in the chosen driver's Capabilities.AccessModes.
  • Snapshot writes against drivers without Capabilities.Snapshots are rejected.
  • local-host hostPath is absolute, has no .., sits under runefile.[storage].hostPathAllowlist.
  • Process-runtime services may use local-host only; block-device drivers (do-volume, hcloud-volume, aws-ebs, gce-pd) are rejected at cast time.
  • A Volume whose reclaimPolicy: delete targets the local-host driver is rejected.

See also