Volume actions
Terms used below: the pool is the single storage pool every volume, and every instance root disk, is carved from. The record is the layer’s own map from a vol-, snap- or i- identifier to the underlying resource.
Assumption: RunInstances §9.4 Option A — the vol- ↔ the block-storage service’s own UUID binding lives in a store owned by the layer, because the block-storage service’s own metadata on a volume is customer-writable and therefore cannot hold an identifier the layer trusts. Under Option B (the block-storage service volume-image metadata property) every id resolution below becomes GET /volumes/detail?metadata={"vol-id":…}, one call per id, and a customer who edits that metadata orphans their own volume.
Nothing on this page is enforced that the catalog does not enforce. Where a figure is advertised but not yet limited on the hypervisor, it is marked NOT ENFORCED and named in section 9. That is the opposite of how the rest of this platform works, and it is a defect, not a design.
1. Conformance
Section titled “1. Conformance”Implemented with differences.
| # | AWS | This layer | Why | What breaks for a client assuming AWS |
|---|---|---|---|---|
| D1 | Volume types gp2, gp3, io1, io2, st1, sc1, standard. | One type. standard is accepted and returned; every other name is rejected with InvalidParameterValue. | The hardware has one class of disk — 7200 rpm SATA. standard is Amazon’s own name for magnetic storage, so the alias is honest; gp3 and io2 would be a promise of SSD latency this hardware cannot make. | Terraform aws_ebs_volume with type = "gp3" errors instead of silently getting a slower disk. This is deliberate — see section 9.1. |
| D2 | Iops and Throughput are settable on CreateVolume and ModifyVolume for the provisioned types. | Both parameters are rejected with InvalidParameterValue. The performance of a volume is fixed by its type and published in Volume types. | There is one type and it is not provisionable. Accepting the parameter and ignoring it would let a customer believe they had bought IOPS. | A module that always sets iops must set it conditionally on type, which is what the AWS provider already requires. |
| D3 | Size 1 GiB to 16 TiB depending on type. | 1 GiB to 1000 GiB (volume_types[0].min_gib, max_gib). Outside that: InvalidParameterValue. | The whole pool is 1400 GiB and it also holds every instance root disk. | A request for a 2 TiB volume errors rather than being silently truncated. |
| D4 | Multi-attach on io1/io2. | A volume attaches to one instance at a time. MultiAttachEnabled is rejected. | Not implemented, and not safe on a single-node LVM backend without a cluster filesystem. | aws_ebs_volume.multi_attach_enabled errors. |
| D5 | Encrypted, KmsKeyId. | Rejected with InvalidParameterValue. There is no key management service. | No KMS exists on this platform. Returning encrypted: false for a request that asked for true would be a lie a compliance audit would later find. | A module that sets encrypted = true by policy fails loudly at plan time rather than shipping unencrypted storage. |
| D6 | DescribeVolumes MaxResults 5–500. | 5–500, default 1000 when absent, with a nextToken when more exist. | Conventions, “Pagination”. | None. |
| D7 | A deleted volume “might appear” briefly. | DeleteVolume returns after the block-storage service accepts the delete; the volume then reports deleting until the block-storage service finishes, and afterwards InvalidVolume.NotFound. There is no synthesised deleted window. | the block-storage service’s delete is asynchronous and the object disappears at the end of it; unlike a terminated instance, no client polls a volume to a terminal state — the AWS provider’s aws_ebs_volume delete waits for not-found. | None. |
| D8 | Another account’s volume id behaves as not-found. | InvalidVolume.NotFound, byte-identical to an id that never existed. | Conventions, “Accounts and tenancy”, Visibility. | None. |
| D9 | Volume IOPS and throughput are enforced per volume. | NOT ENFORCED. The catalog declares 48 IOPS and 11 MB/s per volume; no a block-storage QoS specification or front-end throttle applies it today. A single busy volume can take the whole pool’s throughput. | bootstrap/roles/openstack/tasks/resources.yml creates the compute service flavors from the catalog and nothing else; there is no volume_type/qos_spec task. | A noisy neighbour. This breaks the platform’s own first rule — every capability figure is enforced, not advertised — and it blocks the first customer. Section 9.2. |
2. Endpoint and credentials
Section titled “2. Endpoint and credentials”Volume actions are EC2 actions. They go to the EC2 endpoint, not to the block-storage service:
https://ec2.shelfcs.comSigned with SigV4 using an access key pair, verified by the identity service’s the identity service’s signature-verification call. See Service endpoints for the full host list and Authentication for the signing rules.
The same volumes are reachable through the block-storage service’s own API at
https://volume.shelfcs.com — see
Storage developer guide. The two views
are of one object; a volume created through EC2 is visible, resizable and
deletable through the block-storage service and vice versa. Only the identifier differs (vol-…
against a UUID).
3. Permissions
Section titled “3. Permissions”| Action | Resource scope |
|---|---|
ec2:CreateVolume | volume/* |
ec2:DescribeVolumes | Requires "Resource": "*" |
ec2:DescribeVolumeStatus | Requires "Resource": "*" |
ec2:AttachVolume | Both volume/<id> and instance/<id> |
ec2:DetachVolume | Both volume/<id> and instance/<id> |
ec2:ModifyVolume | volume/<id> |
ec2:DeleteVolume | volume/<id> |
AttachVolume and DetachVolume evaluate against both resources. A policy
naming only the volume denies the call. This is Amazon’s rule and it is kept,
because a policy written for AWS must not become more permissive here.
Every action accepts DryRun. Permitted: DryRunOperation. Denied:
UnauthorizedOperation (HTTP 403).
4. CreateVolume
Section titled “4. CreateVolume”| Parameter | Model member | Type | Required | Constraints | Status |
|---|---|---|---|---|---|
AvailabilityZone | AvailabilityZone | string | Yes | hel1-a. No default. Unknown zone: InvalidParameterValue. | supported |
Size | Size | integer | Conditional | GiB, 1–1000. Required unless SnapshotId is given. | supported |
VolumeType | VolumeType | string | No | standard only. Defaults to standard. | supported |
SnapshotId | SnapshotId | string | No | snap-…. Restores from a snapshot. | supported |
TagSpecification.N | TagSpecifications | list | No | ResourceType must be volume. | supported |
DryRun | DryRun | boolean | No | supported | |
Iops, Throughput | integer | No | rejected with InvalidParameterValue (D2) | ||
Encrypted, KmsKeyId | No | rejected with InvalidParameterValue (D5) | |||
MultiAttachEnabled | boolean | No | rejected with InvalidParameterValue (D4) | ||
OutpostArn | string | No | rejected with InvalidParameterValue |
AvailabilityZone is required and has no default. A volume attaches only to an
instance in its own zone, and there is no cross-zone attach. There is one zone
today; ask DescribeAvailabilityZones for it rather than writing hel1-a into
your code, so that the day there are two you do not have to.
With SnapshotId, Size may be omitted to take the snapshot’s size, or given
to create a larger volume. It may never be smaller than the snapshot.
Mapping
Section titled “Mapping”POST /v3/{project_id}/volumes, body volume:
| EC2 | the block-storage service |
|---|---|
Size | size |
AvailabilityZone | availability_zone |
VolumeType (standard) | volume_type hdd |
SnapshotId (resolved) | snapshot_id |
TagSpecification.N | metadata, key-prefixed so customer metadata and tags cannot collide |
The vol- identifier is minted by the layer and written to the record against
the UUID the block-storage service returns, before the response is sent. A crash between the block-storage service’s
201 and that write leaves an orphaned the block-storage service volume that the EC2 API cannot see;
it is still billed. This is the same failure the missing ClientToken causes
below, and it is reconciled by the same sweep — section 9.3.
Response
Section titled “Response”volumeId, size, volumeType (standard), status (creating),
availabilityZone, createTime, snapshotId where applicable, tagSet,
encrypted (false), multiAttachEnabled (false).
volumeType is returned as standard, which is both the value accepted on
input and the value a client’s model expects. Our own name for it, hdd, is
never on the wire.
Errors
Section titled “Errors”| Code | Status | Cause |
|---|---|---|
InvalidParameterValue | 400 | Size out of 1–1000, unknown type, unknown zone, or a rejected parameter (D1, D2, D3, D4, D5) |
InvalidParameterCombination | 400 | Neither Size nor SnapshotId; or Size smaller than the snapshot |
InvalidSnapshot.NotFound | 400 | No such snapshot, or it belongs to another account |
VolumeLimitExceeded | 400 | The project’s the block-storage service quota would be exceeded |
InsufficientVolumeCapacity | 500 | The pool cannot fit it (section 6) |
Not idempotent
Section titled “Not idempotent”The Amazon EC2 model declares no ClientToken for CreateVolume, so none can
be sent — an SDK rejects the parameter before the request leaves the machine.
A retry after a lost response creates a second volume, and bills for it.
Tag volumes at creation and reconcile by tag if you retry automatically.
Terraform does this for you: aws_ebs_volume records the id in state before it
retries anything.
5. DescribeVolumes
Section titled “5. DescribeVolumes”| Parameter | Type | Constraints |
|---|---|---|
VolumeId.N | list | ^vol-([0-9a-f]{8}|[0-9a-f]{17})$. Anything else: InvalidVolumeID.Malformed |
Filter.N | list | See below |
MaxResults | integer | 5–500. Cannot be combined with VolumeId.N |
NextToken | string | Opaque. Cannot be combined with VolumeId.N |
DryRun | boolean |
Paginated. An empty page may still carry a NextToken — filters are applied
after the page is assembled (conventions, “Pagination”, Filters), so a page can
filter down to nothing and still not be the last.
Filters
Section titled “Filters”Supported — a filter is supported when the element it matches is emitted:
| Filter | Matched against |
|---|---|
status | creating, available, in-use, deleting, error |
attachment.instance-id | The instance a volume is attached to |
attachment.device | The requested device name |
attachment.status | attaching, attached, detaching |
attachment.delete-on-termination | |
availability-zone | |
size | GiB, exact |
snapshot-id | |
volume-type | Always standard |
create-time | Wildcards * and ? allowed |
volume-id | |
tag:<key>, tag-key |
Rejected with InvalidParameterValue: encrypted, multi-attach-enabled,
fast-restored, throughput, iops, outpost-arn. They describe elements
this layer never emits, and matching nothing silently is the failure the
pagination convention forbids.
Response
Section titled “Response”For each volume: volumeId, size, snapshotId, availabilityZone,
status, createTime, attachmentSet, volumeType, encrypted (false),
multiAttachEnabled (false), tagSet.
Each attachmentSet item: volumeId, instanceId, device, status,
attachTime, deleteOnTermination.
Volume state
Section titled “Volume state”Computed from the block-storage service status:
| the block-storage service | EC2 |
|---|---|
creating, downloading | creating |
available | available |
attaching, in-use, detaching | in-use |
deleting | deleting |
error, error_deleting, error_extending, error_restoring | error |
reserved | available — the block-storage service reserves a volume between the attach request and the attach; EC2 has no such state |
maintenance, backing-up, restoring-backup, retyping | NOT MAPPED — section 9.4 |
6. Capacity, and what “thin” means here
Section titled “6. Capacity, and what “thin” means here”Every volume and every instance root disk comes out of one pool:
| Figure | Value | Source |
|---|---|---|
| Pool | default | |
| Sellable | 1400 GiB | |
| Reserved for instance root disks | 1200 GiB thin LV nova-instances | |
| Volume GiB already sold | Tracked internally; free space plus that figure must still cover the catalog |
Root volumes are thin-provisioned, but the sum of what has been sold is
capped, so the pool cannot fill silently behind a customer who is within their
quota. A CreateVolume that would take the pool past that cap fails with
InsufficientVolumeCapacity rather than succeeding into an over-committed pool.
[!warning]
One box, one pool, one disk class. There is no replication of a volume across hosts, and as of today the pool is not mirrored across disks. See our internal readiness list gap 1. Treat a volume as durable against a process crash, not against a disk failure, and take snapshots.
7. AttachVolume
Section titled “7. AttachVolume”| Parameter | Type | Required | Notes |
|---|---|---|---|
VolumeId | string | Yes | Must be available |
InstanceId | string | Yes | Must be in the same zone, and running or stopped |
Device | string | Yes | The device name inside the guest |
DryRun | boolean | No |
Mapped to the compute service POST /servers/{server_id}/os-volume_attachments with
volumeId and device — the compute service, not the block-storage service, because the compute service owns the hypervisor
side of the attach and calling the block-storage service’s os-attach directly would leave the compute service’s
own view of the instance wrong.
The device name is a request, not a guarantee: the guest kernel decides what the
device is actually called, and /dev/sdf may arrive as /dev/vdb. Identify
volumes inside the instance by filesystem UUID or label — blkid, then
UUID=… in /etc/fstab — or a reboot that renumbers devices mounts the wrong
one.
Attaching does not partition, format or mount anything. That happens inside the instance, and we do not do it for you.
Errors
| Code | Status | Cause |
|---|---|---|
VolumeInUse | 400 | Already attached to an instance |
InvalidVolume.ZoneMismatch | 400 | Volume and instance are in different zones |
IncorrectState | 400 | The volume is not available, or the instance is not in a state that can take an attach |
InvalidParameterValue | 400 | The device name is already in use on that instance |
InvalidInstanceID.NotFound | 400 | No such instance, or another account’s |
AttachmentLimitExceeded | 400 | Per-instance attachment limit reached |
A volume attaches to one instance at a time. There is no multi-attach (D4).
deleteOnTermination is false for every volume attached by this action.
A volume attached after launch survives the instance. Only a root volume created
by RunInstances from a block device mapping carries deleteOnTermination: true.
8. DetachVolume
Section titled “8. DetachVolume”| Parameter | Type | Required | Notes |
|---|---|---|---|
VolumeId | string | Yes | |
InstanceId | string | No | Verified if given; mismatch is InvalidParameterValue |
Device | string | No | Verified if given |
Force | boolean | No | Detach without the guest releasing it |
DryRun | boolean | No |
Mapped to the compute service DELETE /servers/{server_id}/os-volume_attachments/{volume_id}.
With Force, to the block-storage service os-force_detach, which drops the attachment record
whether or not the hypervisor agreed.
[!warning]
Unmount the filesystem inside the instance before detaching. Detaching a mounted, written filesystem is the equivalent of pulling the disk out, and loses whatever was in flight.
Forcedetaches regardless. It is for an unresponsive instance, and it risks filesystem corruption. It does not stop the instance first.
Detaching does not stop billing. The volume still exists and is still charged until it is deleted (section 10).
9. ModifyVolume
Section titled “9. ModifyVolume”| Parameter | Type | Required | Notes |
|---|---|---|---|
VolumeId | string | Yes | |
Size | integer | Yes | Larger than the current size, at most 1000 |
DryRun | boolean | No | |
VolumeType, Iops, Throughput, MultiAttachEnabled | No | rejected with InvalidParameterValue |
Mapped to the block-storage service POST /volumes/{volume_id}/action, os-extend.
Volumes grow and never shrink. A Size at or below the current size is
InvalidParameterValue, not a no-op — a silent no-op would let a shrink attempt
look like it worked.
Response: volumeModification with volumeId, modificationState
(modifying, then optimizing, then completed), originalSize,
targetSize, startTime. DescribeVolumesModifications is not
implemented; poll DescribeVolumes for the new size instead. A client that
waits on DescribeVolumesModifications receives InvalidAction — section 12.
Growing the volume does not grow the filesystem on it. Inside the instance,
extend the partition and then the filesystem — growpart /dev/vdb 1, then
resize2fs or xfs_growfs. Until you do, the extra space is invisible to the
guest and you are paying for it.
Extending an attached volume is supported by the block-storage service for a volume in in-use,
but the guest only sees the new size after a rescan (echo 1 > /sys/class/block/vdb/device/rescan) or a reboot.
10. DeleteVolume
Section titled “10. DeleteVolume”| Parameter | Type | Required |
|---|---|---|
VolumeId | string | Yes |
DryRun | boolean | No |
The volume must be available, not attached. Deleting an attached volume is
VolumeInUse.
[!warning]
Deletion destroys the data. We hold no copy, and there is no recycle bin. Snapshots taken before deletion survive and are billed separately.
Deleting an already-deleted volume returns InvalidVolume.NotFound. Callers
retrying a delete treat that as success — the AWS provider does.
Billing: the charge ends when the volume is destroyed, not when it is detached. See How prices are set for what a volume-GiB-hour currently costs, which is a number that does not yet exist — attached-storage rating is not implemented, so volumes are today unbilled. That is a revenue defect, not a customer discount, and it will change.
11. DescribeVolumeStatus
Section titled “11. DescribeVolumeStatus”| Parameter | Type | Notes |
|---|---|---|
VolumeId.N | list | |
Filter.N | list | volume-status.status, availability-zone, volume-status.details-name, volume-status.details-status |
MaxResults | integer | 5–1000 |
NextToken | string |
Paginated.
Response: volumeId, availabilityZone, volumeStatus.status
(ok, impaired, insufficient-data), volumeStatus.details,
actionsSet (always empty), eventsSet (always empty).
volumeStatus.status is derived from the block-storage service status alone: error* states map
to impaired, everything else to ok. There is no per-volume health check
behind it, so ok means “the block-storage service has not recorded an error”, not “we have
verified this disk is healthy”. SMART monitoring runs at the host level
(smartd), not per volume, and does not feed this field.
eventsSet is always empty: there are no scheduled-maintenance events, because
there is no maintenance scheduling system. A client that watches this field for
retirement notices will never see one — that is a real gap, not a promise that
nothing will ever fail.
12. Not implemented
Section titled “12. Not implemented”Named so that their absence is deliberate. Each returns InvalidAction:
DescribeVolumesModifications, EnableVolumeIO, ModifyVolumeAttribute,
DescribeVolumeAttribute, CreateVolumePermission and the whole
*VolumeAttribute family, AttachVolume with MultiAttachEnabled, fast
snapshot restore, EBS direct APIs (ListChangedBlocks, GetSnapshotBlock),
Elastic Volumes’ online type change, and every provisioned-performance action.
13. Conformance evidence
Section titled “13. Conformance evidence”AWS CLI
Section titled “AWS CLI”aws --endpoint-url https://ec2.shelfcs.com --region hel1 \ ec2 create-volume --availability-zone hel1-a --size 20 --volume-type standard \ --tag-specifications 'ResourceType=volume,Tags=[{Key=Name,Value=data}]'
aws --endpoint-url https://ec2.shelfcs.com --region hel1 \ ec2 attach-volume --volume-id vol-… --instance-id i-… --device /dev/sdf
aws --endpoint-url https://ec2.shelfcs.com --region hel1 \ ec2 describe-volumes --filters Name=attachment.instance-id,Values=i-…--region hel1 must match the region in the credential scope, or every call
returns SignatureDoesNotMatch with no diagnosable cause.
Terraform
Section titled “Terraform”resource "aws_ebs_volume" "data" { availability_zone = "hel1-a" size = 20 type = "standard"
tags = { Name = "data" }}
resource "aws_volume_attachment" "data" { device_name = "/dev/sdf" volume_id = aws_ebs_volume.data.id instance_id = aws_instance.app.id}type = "standard" is required: the provider’s default is gp2, which this
layer rejects (D1). Omitting type therefore fails at apply, not at plan.
aws_volume_attachment sets force_detach = false by default; leave it there.
skip_destroy = true is the safe setting when the volume outlives the instance.
Inside the instance
Section titled “Inside the instance”lsblkmkfs.ext4 /dev/vdbblkid /dev/vdb # take the UUIDecho 'UUID=<uuid> /data ext4 defaults,nofail 0 2' >> /etc/fstabmount -anofail matters: without it an instance whose volume is detached fails to boot
into anything you can SSH to, and there is no serial console on the EC2 API —
you would need the web console.
14. Open decisions
Section titled “14. Open decisions”Each is marked OPEN (Michael) and none has been filled with a plausible value.
- Whether
gp3is accepted as an alias forstandard. Accepting it makes every stock Terraform module apply unchanged, which is the whole thesis of an AWS-shaped API; it also means a customer who asked for 3000 IOPS gets 48 and is told nothing. The current answer is to reject, and to lose the modules. - Volume QoS (D9). The catalog’s 48 IOPS / 11 MB/s is advertised and not
enforced. Enforcing it needs a block-storage QoS specification associated with the
hddvolume type and a task inresources.ymlto create both — today that file creates flavors only. Until then the platform’s own first rule is broken. - Reconciling orphaned the block-storage service volumes created when the layer crashed
between the block-storage service’s 201 and the record write (section 4). A periodic sweep of
the block-storage service volumes with no
vol-in the record, and what it does with them — delete, or adopt. - the block-storage service states with no EC2 mapping (section 5):
maintenance,backing-up,restoring-backup,retyping. An operator action can put a volume into one and the interim rule reportsin-use, which is a guess. - Whether volumes are billed, and at what rate. Section 10. The catalog’s
pricing rule prices the AWS-equivalent instance type only; there is no
storage price rule, so
cd-storage-4-16-1000was removed from the catalog for exactly this reason and attached volumes are free by accident. - Snapshot storage cost and retention. A snapshot survives its volume and nothing expires it.