git:20260612.b890dfa to git:20260618.1b866b6

107 added, 262 removed. Audit A to A.

---
name: stateful-storage
description: "Creates persistent storage for stateful workloads on Control Plane. Use when the user asks about volumes, volume sets, disks, mounting storage, snapshots, volume expansion, filesystems, shared storage, or backups."
---
# Stateful Storage & VolumeSets
- > **Tool availability:** some MCP tools named here live in the `full` toolset profile — if one is not advertised on this connection, tell the user to reconnect the MCP server with `?toolsets=full` (or use the `cpln` CLI fallback). Reads and deletes work on every profile via the generic `list_resources` / `get_resource` / `delete_resource` tools.
-
- The `workload` skill covers persistent-storage basics (stateful type, filesystem/perf-class immutability, reserved mount paths, the 15-volume limit, snapshot-before-destructive, the create→verify flow). This skill is the full volume-set detail. MCP-first throughout; CLI is the fallback when the MCP server is unavailable or in CI/CD driven by a service-account `CPLN_TOKEN`.
+ > **Tool availability:** the snapshot tools (`create_volumeset_snapshot`, `list_volumeset_snapshots`, `restore_volumeset_snapshot`, `delete_volumeset_snapshot`), `shrink_volumeset`, and `delete_volumeset_volume` are in the `full` MCP toolset; `create_volumeset`, `update_volumeset`, `mount_volumeset_to_workload`, `expand_volumeset`, and the generic `list_resources`/`get_resource`/`delete_resource` reads are in `core`. If a `full` tool is not advertised, reconnect the MCP server with `?toolsets=full` or use the `cpln` CLI fallback.
- ## VolumeSet Overview
+ A **VolumeSet** is GVC-scoped persistent storage for workloads. The `workload` skill covers the basics (stateful type, reserved mount paths, the 15-volume limit, create-then-verify); this skill is the full volume-set detail. The one trap that drives most rework: **`fileSystemType` and `performanceClass` are immutable** (a PATCH that changes either returns HTTP 400) — to change either you must create a new volumeset, and the old data does not carry over. Choose both at creation.
- A **VolumeSet** provides persistent block or shared storage for workloads within a GVC (GVC-scoped). Three filesystem types:
+ ## Filesystem types and performance classes
- | Filesystem | Workloads | Volumes Created | Snapshots | Best For |
+ | Filesystem | Access | Workloads | Volumes provisioned | Snapshots / shrink / delete-volume |
|---|---|---|---|---|
- | **ext4** | One workload only | One per replica per location | Yes | General-purpose databases |
- | **xfs** | One workload only | One per replica per location | Yes | Large files, high throughput |
- | **shared** | Multiple workloads | One per location | No | Shared file storage across workloads |
-
- Filesystem type is **immutable** after creation. ext4/xfs lock to a single workload. Data is **per-location** — volumes do not replicate across locations.
-
- ## Performance Classes
+ | `ext4` | read-write-once | one stateful/vm workload | one per replica, per location | yes |
+ | `xfs` | read-write-once | one stateful/vm workload | one per replica, per location | yes |
+ | `shared` | read-write-many | any workload type, many at once | one per location (shared by all replicas) | no — expand only |
- | Class | Min Size | Max Size | Filesystem |
+ | Performance class | Min | Max | Filesystems |
|---|---|---|---|
- | `general-purpose-ssd` | 10 GB | 65,536 GB | ext4, xfs |
- | `high-throughput-ssd` | 200 GB | 65,536 GB | ext4, xfs |
- | `shared` | 10 GB | 65,536 GB | shared only (auto-set) |
-
- Performance class is **immutable** after creation; throughput/IOPS vary by cloud provider. When `fileSystemType` is `shared`, performance class is **automatically set to `shared`** — do not specify another class.
+ | `general-purpose-ssd` | 10 GB | 65536 GB | ext4, xfs |
+ | `high-throughput-ssd` | 200 GB | 65536 GB | ext4, xfs |
+ | `shared` | 10 GB | 65536 GB | shared (auto-set) |
- ## VolumeSet lifecycle
+ When `fileSystemType: shared`, `performanceClass` is **auto-set to `shared`** — do not specify another. Data is **per-location** and never replicated across locations; for cross-location redundancy, replicate at the application layer (e.g. WAL streaming).
- Prefer the MCP tools: `mcp__cpln__create_volumeset` (performance class, filesystem, initial capacity, snapshot policy, autoscaling), `mcp__cpln__get_resource` (kind="volumeset"), `mcp__cpln__list_resources` (kind="volumeset"), `mcp__cpln__update_volumeset` (mutable fields only — filesystem and performance class are immutable). CLI fallback: `cpln volumeset` / `cpln apply`.
+ ## Create a volumeset
- ### YAML Manifest (fallback / IaC)
+ Use `mcp__cpln__create_volumeset` (MCP create/mount tools default `fileSystemType` to **xfs** and `performanceClass` to `general-purpose-ssd`; the raw API/`cpln apply` default is **ext4**). YAML for IaC / CLI fallback:
```yaml
kind: volumeset
- name: my-data
- gvc: my-gvc
+ name: pg-data
+ gvc: GVC
spec:
fileSystemType: ext4
- initialCapacity: 20
performanceClass: general-purpose-ssd
+ initialCapacity: 20 # GB; within the class min/max and <= autoscaling.maxCapacity
autoscaling:
maxCapacity: 100
- minFreePercentage: 20
- scalingFactor: 1.5
+ minFreePercentage: 20 # 1-100
+ scalingFactor: 1.5 # >= 1.1
snapshots:
- createFinalSnapshot: true
- retentionDuration: 7d
- schedule: "0 2 * * *"
+ schedule: "0 2 * * *" # cron; no more than once per hour
+ retentionDuration: 7d # float + d/h/m; tool default 7d
```
- Apply with `cpln apply -f volumeset.yaml --gvc my-gvc`. CLI flags mirror these fields (`--file-system-type`, `--performance-class`, `--initial-capacity`, `--enable-autoscaling`, `--max-capacity`, `--min-free-percentage`, `--scaling-factor`, `--retention-duration`, `--schedule`).
+ Apply with `cpln apply -f volumeset.yaml --gvc GVC`. Update mutable fields (capacity, autoscaling, snapshot policy, tags) with `mcp__cpln__update_volumeset`.
### Autoscaling
- **Reactive scaling** keeps at least `minFreePercentage` free, growing the volume when free space falls below that target:
-
- ```
- newCapacity = currentCapacity × scalingFactor
- ```
-
- Capped at `maxCapacity`. `scalingFactor` minimum is **1.1**.
-
- **Predictive scaling** proactively expands before space runs low. Requires `minFreePercentage > 0` and `scalingFactor >= 1.1`:
-
- ```yaml
- autoscaling:
- maxCapacity: 200
- minFreePercentage: 20
- scalingFactor: 1.5
- predictive:
- enabled: true
- lookbackHours: 24 # 1-168
- projectionHours: 6 # 1-72
- minDataPoints: 10 # 2-100
- minGrowthRateGBPerHour: 0.01
- scalingFactor: 1.2 # >= 1.1, defaults to parent scalingFactor
- ```
-
- The system uses whichever target is larger (reactive vs. predictive).
-
- ## Mounting Volumes to Workloads
-
- | Filesystem | Required Workload Type | Volumes per container |
- |---|---|---|
- | ext4 / xfs | **Stateful or VM** | Up to 15 |
- | shared | Any type | Up to 15 |
-
- Volume URI: `cpln://volumeset/VOLUMESET_NAME`. **Reserved mount paths** (rejected): `/dev`, `/dev/log`, `/tmp`, `/var`, `/var/log`.
-
- **Recovery policy** on mount: `retain` (default) keeps existing volume data when a new replica is created; `recycle` starts fresh.
-
- Use `mcp__cpln__mount_volumeset_to_workload` to attach — it mounts into the **first container** and creates the volumeset if missing (defaults: mount path `/mnt/{volumesetName}`, filesystem `xfs`, class `general-purpose-ssd`; size/filesystem/class are create-only and ignored when the volumeset already exists). ext4/xfs (read-write-once) require a **stateful or vm** workload; shared mounts on any type. Workload type is immutable — switching requires delete + recreate (see migration sequence below).
-
- ### Stateful Workload Features
-
- - **Stable replica identities:** `{workloadName}-{replicaIndex}` (e.g., `my-database-0`)
- - **Stable hostnames:** `{replicaIdentity}.{workloadName}`
- - **Replica-direct endpoints:** enable `spec.loadBalancer.replicaDirect: true` via `mcp__cpln__configure_workload_load_balancer`
- - **No Capacity AI** — use `minCpu`/`minMemory` instead
-
- ### Migrating an existing workload to stateful (destructive — confirm first)
-
- Workload type is immutable, so adding an ext4/xfs volume to a serverless/standard workload requires **delete + recreate as stateful** (vm workloads can also mount ext4/xfs volumesets, but a service migrates to stateful). This is destructive — the workload is removed, traffic 5xx's during the recreate window, and any in-memory/non-persistent state is lost. Governed by the destructive-ops guardrail in `rules/cpln-guardrails.md`: **stop and get explicit confirmation before any delete, regardless of permission mode.**
-
- Confirm with the user before step 4:
- - The exact public URL that will return errors during the cutover window
- - Whether any other workload calls this one via internal DNS — those callers fail until step 5 completes
- - Whether the workload has runtime state worth draining first (in-flight requests, in-memory caches absent on the new instance)
- - That losing whatever's in the (non-persistent) container at delete time is acceptable
-
- Safe sequence once confirmed:
-
- ```bash
- # 1. Capture the current spec — your roll-back artifact
- cpln workload get <workload> --gvc <gvc> -o yaml-slim > <workload>.bak.yaml
-
- # 2. Build the new manifest from the backup:
- # - Change spec.type from "serverless" / "standard" to "stateful"
- # - Add the volume mount under spec.containers[<container>].volumes
- # - Keep the SAME name (preserves public URL, internal DNS, domain routes,
- # policy targetLinks, identity bindings, external consumers)
- # - Keep all other spec fields (image, identityLink, env, ports, etc.) intact
-
- # 3. Apply the volumeset first (it must exist before the workload references it)
- cpln apply --file <volumeset>.yaml --gvc <gvc>
-
- # 4. Delete the old workload (destructive — already confirmed above)
- cpln workload delete <workload> --gvc <gvc>
-
- # 5. Apply the new stateful manifest. Tell the user upfront this typically takes
- # 2–5 min (volumeset provision + container schedule).
- #
- # Use `--ready` with the safety-net wrapper from rules/cpln-guardrails.md:
- # --ready handles the success path; the watcher kicks in only past the
- # expected window AND only on confirmed terminal container errors. Plain
- # `--ready` alone is wrong for a fresh recreate — if the manifest is
- # misconfigured (wrong DSN, bad image tag, missing secret), --ready sits
- # through its full timeout while the container is already dead.
- cpln apply --file <workload>.yaml --gvc <gvc> --ready &
- APPLY_PID=$!
-
- PATIENCE=180 # 3 min — typical stateful first-deploy is 2–5 min
- sleep $PATIENCE
-
- while kill -0 $APPLY_PID 2>/dev/null; do
- MSG=$(cpln workload get <workload> --gvc <gvc> -o json 2>/dev/null \
- | jq -r "[.status.versions[-1].containers[]?.message // empty] | join(\" | \")")
- if echo "$MSG" | grep -qiE "exitcode: [^0]|fatal|startup failed|crashloop|imagepullbackoff|imagepullerror|errimagepull"; then
- echo "FAILED: $MSG"; kill $APPLY_PID 2>/dev/null; break
- fi
- sleep 30
- done
- wait $APPLY_PID
-
- # 6. ONE conclusive sanity check — not a polling loop. Run ONLY after the wait
- # above completed successfully. If the watcher killed the apply or --ready
- # exited non-zero, skip to diagnosis — do not wait again.
- cpln workload get <workload> --gvc <gvc>
- ```
-
- If the watcher killed the apply, `--ready` exited non-zero, or step 6 shows an unhealthy state, **diagnose** — `mcp__cpln__list_deployments` (failed deployment + exact error; CLI `cpln workload get-deployments <workload> --gvc <gvc>`), `mcp__cpln__get_workload_logs` for stderr where most startup failures land (CLI `cpln logs '{gvc="<gvc>", workload="<workload>"}' --org <org>`), and re-read the manifest for the culprit the error points at (DSN, secret refs, port, image tag, env). **Then fix and re-apply with the safety net wrapped again** — re-applying a broken manifest plain costs more wall-clock and obscures the issue. Restore from `<workload>.bak.yaml` if unrecoverable.
-
- For waits on operations that lack a `--ready` flag (e.g. after `cpln workload force-redeployment`), use a bounded shell wait, never an AI polling loop:
-
- ```bash
- # Wait up to 5 minutes for the workload to be healthy. One tool call, bounded.
- timeout 300 bash -c 'until cpln workload get <name> --gvc <gvc> -o json | jq -e ".status.healthCheck.status == true" >/dev/null 2>&1; do sleep 10; done' && echo "ready" || echo "timeout"
- ```
-
- App-layer verification (HTTP endpoint reachable):
-
- ```bash
- curl --retry 30 --retry-delay 5 --retry-connrefused -fsS https://<workload>.<gvc>.cpln.app/healthz
- ```
-
- ## Snapshot Management
+ **Reactive**: a background job checks volumes about once a minute; when free space falls below `minFreePercentage` it resizes the volume to hold current usage at that margin, scaled up: `new_capacity = ceil(usedGB / (1 - minFreePercentage/100) * scalingFactor)`, capped at `maxCapacity`. Both fields are required, or autoscaling does nothing.
- Snapshots are available for **ext4 and xfs only** — never shared.
+ **Predictive** runs the same formula on *projected* usage (from the recent growth rate) to expand ahead of demand; the larger of the reactive and predictive targets wins. Requires `minFreePercentage > 0` and `scalingFactor >= 1.1`:
- **Automatic** (volumeset spec):
```yaml
- snapshots:
- createFinalSnapshot: true
- retentionDuration: 7d # Nd / Nh / Nm (days/hours/minutes)
- schedule: "0 */6 * * *" # cron, minimum once per hour
+ autoscaling:
+ maxCapacity: 200
+ minFreePercentage: 20
+ scalingFactor: 1.5
+ predictive:
+ enabled: true # default false
+ lookbackHours: 24 # 1-168
+ projectionHours: 6 # 1-72
+ minDataPoints: 10 # 2-100
+ minGrowthRateGBPerHour: 0.01
+ scalingFactor: 1.2 # >= 1.1; defaults to the parent scalingFactor
```
- **Manual** — MCP tools:
- - `mcp__cpln__create_volumeset_snapshot` — point-in-time snapshot (the safety net before any shrink/restore/volume-delete; `--tag key=value` on the CLI to organize)
- - `mcp__cpln__list_volumeset_snapshots` — find a snapshot before restoring (filter by location, volumeIndex, snapshotName)
- - `mcp__cpln__restore_volumeset_snapshot` — restore a volume (destructive)
- - `mcp__cpln__delete_volumeset_snapshot` — delete a snapshot (destructive — removes a recovery path)
-
- CLI fallback (`cpln volumeset snapshot create|get|restore|delete my-data --gvc my-gvc --snapshot-name NAME --location aws-us-east-2 --volume-index 0`).
-
- ## Volume Expansion & Shrink
-
- **Expand** — live, no downtime; all filesystem types. Throttled to **4 expansions per volume per rolling 24 hours** — an HTTP 429 means the window is exhausted, and waiting briefly will NOT help (the oldest expansion must age out). Prefer `mcp__cpln__expand_volumeset`; CLI `cpln volumeset expand my-data --gvc my-gvc --new-size 50 --location aws-us-east-2 --volume-index 0` (`--timeout-seconds` overrides the default 600s wait).
-
- **Shrink** — **ext4/xfs only**; data is preserved via an online presync + final delta sync onto the new smaller volume, but if **used bytes exceed the new capacity the data cannot fit and is lost**. Floor is class-dependent: 10 GB `general-purpose-ssd` / 200 GB `high-throughput-ssd`. Snapshot first with `mcp__cpln__create_volumeset_snapshot`, then `mcp__cpln__shrink_volumeset`; present the blast radius and get explicit confirmation first (`rules/cpln-guardrails.md`). CLI `cpln volumeset shrink my-data --gvc my-gvc --new-size 10 --location aws-us-east-2 --volume-index 0`.
-
- ## Volume Management
-
- Inspect via `mcp__cpln__get_resource` (kind="volumeset") — status reports per-location volume counts, bound workload, snapshot counts. To delete a single volume (**permanent data loss, ext4/xfs only**), snapshot first, then `mcp__cpln__delete_volumeset_volume` — destructive, confirm the blast radius first. CLI `cpln volumeset volume get|delete my-data --gvc my-gvc [--location aws-us-east-2] [--volume-index 0]`.
-
- ## Common Patterns
+ ## Mount to a workload
- ### PostgreSQL with ext4
+ Mount with `mcp__cpln__mount_volumeset_to_workload` — it attaches to the **first container** and creates the volumeset if missing (create-only defaults, ignored when the volumeset already exists: path `/mnt/{volumesetName}`, filesystem `xfs`, class `general-purpose-ssd`). Volume URI is `cpln://volumeset/VOLUMESET`.
- 1. Create the volumeset with `mcp__cpln__create_volumeset` (ext4, initial capacity, autoscaling, snapshot policy).
- 2. Mount it to a stateful workload with `mcp__cpln__mount_volumeset_to_workload` (path `/var/lib/postgresql/data`).
+ - **ext4/xfs require a `stateful` or `vm` workload** (mounting on serverless/standard returns HTTP 400); `shared` mounts on any type. Workload type is immutable — see "Migrating to stateful" below.
+ - Up to **15 volumes** per container. **Reserved mount paths** (rejected): `/dev`, `/dev/log`, `/tmp`, `/var`, `/var/log`.
+ - `recoveryPolicy`: `retain` (default — reuse an existing volume's data on a new replica) or `recycle` (start fresh).
+ - `path` is required for non-vm workloads and rejected for `vm` (VM disks use `name`/`bus`/`bootOrder` instead).
+ - Stateful workloads give each replica a stable index and its own volume; `spec.loadBalancer.replicaDirect` (stateful-only) exposes per-replica endpoints — see the `workload` skill.
```yaml
kind: workload
- name: my-database
- gvc: my-gvc
+ name: pg
+ gvc: GVC
spec:
type: stateful
containers:
- name: postgres
image: //image/postgres:16
ports:
- - protocol: http
- number: 5432
- resources:
- cpu: 500m
- memory: 1Gi
+ - number: 5432
+ protocol: tcp # http | http2 | grpc | tcp — a DB is tcp, not http
volumes:
- uri: cpln://volumeset/pg-data
path: /var/lib/postgresql/data
- defaultOptions:
- autoscaling:
- maxScale: 3
- minScale: 1
```
- ### Shared File Storage
+ ## Snapshots
- ```yaml
- kind: volumeset
- name: shared-uploads
- gvc: my-gvc
- spec:
- fileSystemType: shared
- initialCapacity: 50
- # performanceClass auto-set to "shared" — do not specify another class
- ```
+ Snapshots are **ext4/xfs only — never `shared`**. Automatic policy lives in `spec.snapshots`: `createFinalSnapshot` (default `true` — a snapshot is taken before any volume in the set is deleted), `retentionDuration`, and `schedule` (cron whose minute field must be a single concrete value, so no more than once per hour). Manual: `mcp__cpln__create_volumeset_snapshot`, `list_volumeset_snapshots`, `restore_volumeset_snapshot`, `delete_volumeset_snapshot`. A restore creates a **new volume** and discards everything written since the snapshot.
- Multiple workloads can mount `cpln://volumeset/shared-uploads`. Shared volumes only support `expand` — no shrink, no volume delete, no snapshots.
+ ## Resize and delete volumes
- Shared volumes consume CPU/memory per mount point — tune via `mountOptions.resources`:
+ - **Expand** — live, no downtime, all filesystems. Throttled to **4 expansions per volume per rolling 24 hours**; the 5th returns **HTTP 429** and a brief wait does not help (the oldest expansion must age out of the window). `mcp__cpln__expand_volumeset`.
+ - **Shrink** (ext4/xfs only) — data is migrated to the new smaller volume via an online presync + final delta sync, and the replica restarts during the swap. The platform **rejects the shrink with HTTP 400 when known used bytes (+5% metadata headroom) would not fit**; data is only lost if used bytes genuinely exceed the new capacity. Floor is the class minimum (10 / 200 GB). `mcp__cpln__shrink_volumeset`.
+ - **Delete a volume** (ext4/xfs only) — permanent loss of that volume's data. `mcp__cpln__delete_volumeset_volume`.
+ Shrink, volume-delete, snapshot-delete, and restore are destructive: **snapshot first** as the recovery net, then confirm the blast radius (the destructive-ops guardrail returns an impact preview before executing).
+
+ ## Shared filesystem
+
+ A `shared` volumeset is mounted read-write by many workloads at once but supports only expand — no snapshots, shrink, or volume-delete. Each mount point is provisioned its own CPU/memory; tune with `mountOptions.resources` (defaults `minCpu 500m`, `maxCpu 2000m`, `minMemory 1Gi`, `maxMemory 2Gi`; max/min at most 4000m and 4096Mi apart, ratio at most 4:1):
+
```yaml
spec:
- fileSystemType: shared
+ fileSystemType: shared # performanceClass auto-set to "shared"
initialCapacity: 50
mountOptions:
- resources:
- minCpu: 50m
- maxCpu: 200m
- minMemory: 64Mi
- maxMemory: 256Mi
+ resources: { minCpu: 500m, maxCpu: 2000m, minMemory: 1Gi, maxMemory: 2Gi }
```
- Constraints: `maxCpu`/`minCpu` at most 4000m apart, ratio >= 1:4; memory at most 4096Mi apart, ratio >= 1:4.
-
- ### Backup Workflow
-
- 1. Before maintenance, take a named snapshot with `mcp__cpln__create_volumeset_snapshot`.
- 2. Perform the maintenance / migration.
- 3. If rollback is needed, find the snapshot with `mcp__cpln__list_volumeset_snapshots` and restore with `mcp__cpln__restore_volumeset_snapshot` (destructive — discards everything written since the snapshot; confirm first).
-
- ## Custom Encryption (AWS Only)
+ ## Custom encryption (AWS only)
- Encrypt ext4/xfs volumes with AWS KMS:
+ Volumes are encrypted by default. To use your own AWS KMS keys on ext4/xfs volumes (not `shared`, not BYOK):
```yaml
spec:
- fileSystemType: ext4
customEncryption:
regions:
- aws-us-east-1:
+ aws-us-east-1: # format: {cloud-provider}-{region}
keyId: "arn:aws:kms:us-east-1:123456789:key/KEY_ID"
```
- - Region format: `aws-{aws-region-name}`
- - Not available for shared filesystem or BYOK clusters
- - KMS key is immutable once a volume is created
+ The `keyId` is injected as the EBS storage-class `kmsKeyId`. The KMS key policy **must grant Control Plane's AWS account `arn:aws:iam::957753459089:root`** the volume-encryption permissions (`Decrypt`, `Encrypt`, `GenerateDataKey`, `CreateGrant`, etc.); the key is immutable once a volume exists.
- ## BYOK Considerations
+ ## BYOK storage classes
- For Bring Your Own Kubernetes clusters:
- - Requires a **CSI-compatible storage driver** on the cluster
- - Storage classes follow `{performanceClass}-{fileSystemType}` (e.g. `general-purpose-ssd-ext4`, `high-throughput-ssd-xfs`)
- - `spec.storageClassSuffix` selects alternatives — constructed as `{performanceClass}-{fileSystemType}-{suffix}`, falling back to the class without suffix if not found
+ On self-hosted clusters, volumes use the storage class `{performanceClass}-{fileSystemType}` (e.g. `general-purpose-ssd-ext4`) and the cluster needs a CSI-compatible driver. `spec.storageClassSuffix` selects an alternative `{performanceClass}-{fileSystemType}-{suffix}`, falling back to the unsuffixed class if it is not found.
- ## Gotchas
+ ## Migrating a workload to stateful
- - **Filesystem and performance class are immutable** — choose carefully at creation
- - **ext4/xfs lock to one workload** — plan the volumeset-to-workload mapping
- - **Data is per-location** — no automatic cross-location replication
- - **Shared has no snapshots** — implement application-level backups
- - **Volume-delete destroys data; shrink loses data when used bytes exceed the new size** — always snapshot first
- - **Expand throttled** — 4 expansions per volume per rolling 24 hours (HTTP 429 = window exhausted; brief waits do not help)
- - **Snapshot restore creates a new volume** — unsaved data since the snapshot is lost
- - **Workload type is immutable** — ext4/xfs volumes need a stateful or vm workload, so switching type (e.g. serverless → stateful to add a volume) requires destructive delete + recreate; confirm the blast radius first (see the migration sequence above)
+ Workload type is immutable, so adding an ext4/xfs volume to a serverless/standard workload means **delete + recreate as `stateful`** — destructive. Before deleting, confirm with the user: the public URL that 5xx's during the cutover, any internal callers that fail until recreate, runtime/in-memory state lost at delete, and that the recreate typically takes 2-5 min. Sequence: capture the spec (`cpln workload get WORKLOAD --gvc GVC -o yaml-slim > bak.yaml`) as a rollback artifact; apply the volumeset; delete the old workload; apply the new manifest with `spec.type: stateful` + the volume mount, **keeping the same name** to preserve URL/DNS/policy/identity links. For the deploy-wait pattern, see the `workload` skill.
- ## Quick Reference — MCP Tools
+ ## Verify
- | Tool | Purpose |
- |---|---|
- | `mcp__cpln__create_volumeset` | Create a volumeset (filesystem, perf class, capacity, snapshot policy) |
- | `mcp__cpln__mount_volumeset_to_workload` | Mount volumeset to workload (creates volumeset if needed) |
- | `mcp__cpln__update_volumeset` | Update mutable fields (capacity, snapshot policy, autoscaling, tags) |
- | `mcp__cpln__delete_resource` (kind="volumeset") | Delete a volumeset |
- | `mcp__cpln__list_resources` (kind="volumeset") | List volumesets in a GVC |
- | `mcp__cpln__get_resource` (kind="volumeset") | Get volumeset details (spec + per-location status) |
- | `mcp__cpln__expand_volumeset` | Increase volume capacity |
- | `mcp__cpln__shrink_volumeset` | Decrease volume capacity (data loss) |
- | `mcp__cpln__delete_volumeset_volume` | Delete a specific volume (data loss) |
- | `mcp__cpln__create_volumeset_snapshot` | Create a point-in-time snapshot |
- | `mcp__cpln__list_volumeset_snapshots` | List snapshots across a volumeset's volumes |
- | `mcp__cpln__delete_volumeset_snapshot` | Delete a snapshot |
- | `mcp__cpln__restore_volumeset_snapshot` | Restore volume from snapshot |
+ - `mcp__cpln__get_resource` (kind `volumeset`): `status.locations[].volumes[]` show per-volume `currentSize`, `currentBytesUsed`, `lifecycle` (expect `bound`), and snapshot counts; `status.usedByWorkload` names the bound workload.
+ - After mounting, poll `mcp__cpln__list_deployments` until ready and confirm the container's volume is mounted at the expected path.
- CLI fallback mirrors these: `cpln volumeset create|get|update|delete|expand|shrink`, `cpln volumeset volume get|delete`, `cpln volumeset snapshot create|get|restore|delete`.
+ ## Troubleshooting
- ## Cross-References
+ | Symptom | Cause | Fix |
+ |---|---|---|
+ | 400 mounting a volumeset | ext4/xfs on a serverless/standard workload | Use a `stateful` or `vm` workload (recreate to change type) |
+ | 400 "performanceClass / fileSystemType is immutable" | Tried to change either on update | Create a new volumeset; migrate data via snapshot/restore |
+ | 400 on mount with a path | Path is reserved (`/dev`, `/tmp`, `/var`, ...) | Mount elsewhere (e.g. `/data`, `/mnt/...`) |
+ | HTTP 429 on expand | 4 expansions on that volume in the last 24 h | Wait for the oldest to age out; plan larger steps |
+ | 400 on shrink | New size cannot hold used bytes (+5%) | Shrink less, or free space / snapshot then rebuild |
+ | Snapshot fields rejected | Volumeset is `shared` | Snapshots need ext4/xfs |
+ | Deployment stuck after mount | Volume provisioning (2-5 min on first deploy) | Poll `list_deployments`; check `get_workload_logs` if it stays unready |
- - **Workload skill** — Start here: the primary workload skill (types, defaults, spec shape) that routes here for storage detail.
- - **Template Catalog skill** — database templates use volumesets for persistent storage
- - **Firewall skill** — cloud storage volumes (S3, GCS, Azure Blob) require outbound firewall rules
+ ## MCP tools quick reference
+ | Tool | Purpose | Tier |
+ |---|---|---|
+ | `mcp__cpln__create_volumeset` | Create a volumeset | core |
+ | `mcp__cpln__update_volumeset` | Update mutable fields (capacity, autoscaling, snapshots, tags) | core |
+ | `mcp__cpln__mount_volumeset_to_workload` | Mount to a workload (creates the volumeset if missing) | core |
+ | `mcp__cpln__expand_volumeset` | Grow a volume (4 / 24 h limit) | core |
+ | `mcp__cpln__shrink_volumeset` | Shrink a volume (ext4/xfs) | full |
+ | `mcp__cpln__delete_volumeset_volume` | Delete one volume (ext4/xfs) | full |
+ | `mcp__cpln__create_volumeset_snapshot` | Point-in-time snapshot | full |
+ | `mcp__cpln__list_volumeset_snapshots` | List snapshots | full |
+ | `mcp__cpln__restore_volumeset_snapshot` | Restore a snapshot to a new volume | full |
+ | `mcp__cpln__delete_volumeset_snapshot` | Delete a snapshot | full |
+ | `mcp__cpln__get_resource` / `list_resources` / `delete_resource` (kind `volumeset`) | Read / list / delete a volumeset | core |
+
+ CLI fallback (CI/CD via a service-account `CPLN_TOKEN`): `cpln volumeset create|get|update|delete|expand|shrink`, `cpln volumeset snapshot create|get|restore|delete`, `cpln volumeset volume get|delete`; `expand`/`shrink` need `--new-size` (`--location`/`--volume-index` optional), and `cpln apply -f` for YAML.
+
+ ## Related skills
+
+ | Skill | For |
+ |---|---|
+ | `workload` | Workload types, the deploy-and-verify flow, load-balancer/`replicaDirect` config |
+ | `template-catalog` | Postgres, Redis, and other databases that provision volumesets for you |
+ | `firewall-networking` | Outbound rules for cloud-bucket volumes (`s3://`, `gs://`, `azureblob://`) |
+
## Documentation
- [Volume Set Reference](https://docs.controlplane.com/reference/volumeset.md)
- - [Workload Volumes Reference](https://docs.controlplane.com/reference/workload/volumes.md)
+ - [Workload Volumes](https://docs.controlplane.com/reference/workload/volumes.md)
- [CLI volumeset Commands](https://docs.controlplane.com/cli-reference/commands/volumeset.md)
- - [Workload Types Reference](https://docs.controlplane.com/reference/workload/types.md)