Storage developer guide
What this is
Section titled “What this is”Developer-focused information about driving block storage from code: which call to make, what state a volume must be in before it will accept it, what to retry and what never to retry, and the guest-side half that no API can do for you.
Concepts are in the user guide; every parameter is in the API reference.
Endpoint
Section titled “Endpoint”Volume actions are EC2 actions and go to the compute endpoint, not to a storage one:
https://ec2.shelfcs.com region hel1The same volumes are reachable through the block-storage service’s own API on its own host — see Storage developer guide. Both views are of one object; only the identifier differs. Manage a volume through the API you created it with.
The state machine
Section titled “The state machine”CreateVolume → creating → available ─ AttachVolume → in-use ↖ DetachVolume ↙ available ─ DeleteVolume → deleting → goneEvery call has a state precondition, and violating it is an error rather than a wait:
| Call | Volume must be | Instance must be |
|---|---|---|
AttachVolume | available | running or stopped, same zone |
DetachVolume | in-use | — |
ModifyVolume | available or in-use | — |
DeleteVolume | available | — |
So create → attach immediately fails: the volume is creating for a moment
first. Poll DescribeVolumes until status is available, or use a waiter.
ec2.get_waiter("volume_available").wait(VolumeIds=[vol])ec2.attach_volume(VolumeId=vol, InstanceId=inst, Device="/dev/sdf")ec2.get_waiter("volume_in_use").wait(VolumeIds=[vol])The retry trap
Section titled “The retry trap”[!warning]
CreateVolumeis not idempotent and cannot be made so. The Amazon EC2 model declares noClientTokenfor it, so no SDK will send one. A retry after a lost response creates a second volume, which occupies the pool and — once storage is billed — bills for it.
Do not wrap CreateVolume in a blind retry. Instead:
- Tag at creation:
--tag-specifications 'ResourceType=volume,Tags=[{Key=Name,Value=data-01}]' - On retry,
DescribeVolumes --filters Name=tag:Name,Values=data-01first. - Create only if nothing came back.
Terraform does this for you — aws_ebs_volume records the id in state before it
retries anything.
Everything else is safe: AttachVolume, DetachVolume and DeleteVolume are
naturally idempotent, and a delete of an already-deleted volume returns
InvalidVolume.NotFound, which every client treats as success.
Parameters that are refused, not ignored
Section titled “Parameters that are refused, not ignored”| Parameter | Result |
|---|---|
VolumeType other than standard | InvalidParameterValue |
Iops, Throughput | InvalidParameterValue |
Encrypted, KmsKeyId | InvalidParameterValue |
MultiAttachEnabled | InvalidParameterValue |
Size outside 1–1000 | InvalidParameterValue |
The AWS provider defaults type to gp2, so omitting type fails at apply.
Write type = "standard" explicitly.
Refusing rather than ignoring is deliberate: a module that sets
encrypted = true by policy fails loudly instead of shipping unencrypted
storage that reports success.
Growing a volume
Section titled “Growing a volume”ModifyVolume changes the volume. It does not change the filesystem.
sc ec2 modify-volume --volume-id vol-… --size 40DescribeVolumesModifications is not implemented — poll DescribeVolumes
for the new size instead. A client that waits on the modifications call gets
InvalidAction.
Then, inside the guest:
# an attached volume needs a rescan before the kernel sees the new sizeecho 1 > /sys/class/block/vdb/device/rescangrowpart /dev/vdb 1resize2fs /dev/vdb1 # or xfs_growfs /dataUntil you do that, you are paying for space the guest cannot see. And a Size
at or below the current one is an error, not a no-op — a silent no-op would make
a shrink attempt look like it worked.
The guest-side half
Section titled “The guest-side half”No API call formats or mounts anything. The full cycle:
lsblk # find it — the device name may not be what you asked formkfs.ext4 /dev/vdbmkdir -p /datablkid /dev/vdb # take the UUIDecho 'UUID=<uuid> /data ext4 defaults,nofail 0 2' >> /etc/fstabmount -aAutomate it in user data, and make it re-runnable — check for a filesystem before making one, or a re-run wipes the disk:
#cloud-configruncmd: - [ bash, -c, 'blkid /dev/vdb || mkfs.ext4 /dev/vdb' ] - [ bash, -c, 'grep -q /data /etc/fstab || echo "UUID=$(blkid -s UUID -o value /dev/vdb) /data ext4 defaults,nofail 0 2" >> /etc/fstab' ] - [ mount, -a ]Remember that runcmd swallows failures — see
Instance metadata and user data.
Detaching safely
Section titled “Detaching safely”umount /data # inside the guest, FIRSTsc ec2 detach-volume --volume-id vol-…--force exists for an unresponsive machine and risks filesystem corruption. It
does not stop the instance first.
Detaching does not stop billing. The volume exists until you delete it.
Errors
Section titled “Errors”| Code | Cause | Do |
|---|---|---|
VolumeInUse | Attached already, or you tried to delete an attached volume | Detach first |
IncorrectState | Wrong state for the call | Wait, then retry |
InvalidVolume.ZoneMismatch | Volume and instance in different zones | There is one zone — check what you passed |
VolumeLimitExceeded | Your quota | Ask for more |
InsufficientVolumeCapacity | The pool is full, not your quota | Nothing you can change |
InvalidVolume.NotFound | No such volume, or another account’s | Treat as not-found |
InsufficientVolumeCapacity is the one to handle differently: retrying, or
asking for quota, will not help.
Terraform
Section titled “Terraform”resource "aws_ebs_volume" "data" { availability_zone = "hel1-a" size = 20 type = "standard" # required; the gp2 default is rejected 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 skip_destroy = true # when the volume outlives the instance force_detach = false # leave it}aws_ebs_volume with size reduced is a replace, not a shrink — Terraform
will destroy the volume and its data. Guard anything you care about with
lifecycle { prevent_destroy = true }.