Skip to content

Storage developer guide

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.

Volume actions are EC2 actions and go to the compute endpoint, not to a storage one:

https://ec2.shelfcs.com region hel1

The 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.

CreateVolume → creating → available ─ AttachVolume → in-use
↖ DetachVolume ↙
available ─ DeleteVolume → deleting → gone

Every call has a state precondition, and violating it is an error rather than a wait:

CallVolume must beInstance must be
AttachVolumeavailablerunning or stopped, same zone
DetachVolumein-use—
ModifyVolumeavailable or in-use—
DeleteVolumeavailable—

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])

[!warning]

CreateVolume is not idempotent and cannot be made so. The Amazon EC2 model declares no ClientToken for 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:

  1. Tag at creation: --tag-specifications 'ResourceType=volume,Tags=[{Key=Name,Value=data-01}]'
  2. On retry, DescribeVolumes --filters Name=tag:Name,Values=data-01 first.
  3. 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.

ParameterResult
VolumeType other than standardInvalidParameterValue
Iops, ThroughputInvalidParameterValue
Encrypted, KmsKeyIdInvalidParameterValue
MultiAttachEnabledInvalidParameterValue
Size outside 1–1000InvalidParameterValue

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.

ModifyVolume changes the volume. It does not change the filesystem.

Terminal window
sc ec2 modify-volume --volume-id vol-… --size 40

DescribeVolumesModifications is not implemented — poll DescribeVolumes for the new size instead. A client that waits on the modifications call gets InvalidAction.

Then, inside the guest:

Terminal window
# an attached volume needs a rescan before the kernel sees the new size
echo 1 > /sys/class/block/vdb/device/rescan
growpart /dev/vdb 1
resize2fs /dev/vdb1 # or xfs_growfs /data

Until 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.

No API call formats or mounts anything. The full cycle:

Terminal window
lsblk # find it — the device name may not be what you asked for
mkfs.ext4 /dev/vdb
mkdir -p /data
blkid /dev/vdb # take the UUID
echo 'UUID=<uuid> /data ext4 defaults,nofail 0 2' >> /etc/fstab
mount -a

Automate it in user data, and make it re-runnable — check for a filesystem before making one, or a re-run wipes the disk:

#cloud-config
runcmd:
- [ 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.

Terminal window
umount /data # inside the guest, FIRST
sc 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.

CodeCauseDo
VolumeInUseAttached already, or you tried to delete an attached volumeDetach first
IncorrectStateWrong state for the callWait, then retry
InvalidVolume.ZoneMismatchVolume and instance in different zonesThere is one zone — check what you passed
VolumeLimitExceededYour quotaAsk for more
InsufficientVolumeCapacityThe pool is full, not your quotaNothing you can change
InvalidVolume.NotFoundNo such volume, or another account’sTreat as not-found

InsufficientVolumeCapacity is the one to handle differently: retrying, or asking for quota, will not help.

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 }.