Adding an EBS volume to a running EC2 instance is three operations: create the volume, attach it, then format and mount it from inside the instance. AWS handles the first two at the control-plane layer in seconds. The third part runs on the instance itself, and is where every gotcha lives. The two things people get wrong: the AWS-vs-Linux device-naming mismatch (you attach as /dev/sdf and the OS sees it as /dev/xvdf on Xen or /dev/nvme1n1 on Nitro), and using the device path in /etc/fstab instead of the filesystem UUID, which guarantees a broken fstab the next time the kernel reorders devices. Below: the exact procedure for Console, AWS CLI, and Terraform; the EBS volume type comparison (gp3, gp2, io2 Block Express, st1, sc1); Multi-Attach caveats; and the verification commands that prove the mount survives a reboot.
How do I add an EBS volume to an EC2 instance?
Create the volume with aws ec2 create-volume --volume-type gp3 --size 100 --availability-zone us-east-1a, capturing the volume ID. Attach it to the running instance with aws ec2 attach-volume --volume-id vol-XXX --instance-id i-XXX --device /dev/sdf. The instance sees the new block device immediately (run lsblk to confirm). On Nitro instances the device appears as /dev/nvme1n1, not /dev/sdf; on older Xen instances it appears as /dev/xvdf. Format with sudo mkfs.ext4 -L data /dev/nvme1n1 (or mkfs.xfs), create a mount point, mount it, then add a /etc/fstab entry using UUID= (not the device path) with the nofail flag so a missing disk does not block boot. The instance stays running throughout. Snapshot the volume before mounting anything that already contains data.
Jump to:
- Before you start: prerequisites and snapshot
- Step 1: Create the EBS volume
- Step 2: Attach the volume to the instance
- The AWS-vs-Linux device naming gotcha
- Step 3: Format and mount inside the instance
- Step 4: Persist the mount with fstab and UUID
- EBS volume types compared
- Console vs CLI vs Terraform
- Multi-Attach and other caveats
- Verify the volume survives a reboot
- What to do next
- FAQ
Before you start: prerequisites and snapshot
You need an IAM principal with ec2:CreateVolume and ec2:AttachVolume (the AWS-managed AmazonEC2FullAccess policy covers both). The instance can be running. No reboot at any point.
The volume must be created in the same Availability Zone as the instance. This is the most common up-front mistake: launching an instance in us-east-1a and creating a volume in us-east-1b and being unable to attach. AZs are scoped per AWS account, so my us-east-1a is not the same physical zone as your us-east-1a, but for a single account a volume and instance must agree on the AZ string.
If the volume already contains data (snapshot restore, migration, import), snapshot first:
aws ec2 create-snapshot \
--region :region \
--volume-id :volume_id \
--description "pre-attach-$(date +%Y%m%d-%H%M)"Snapshots are incremental, cost cents, and give you a single-API-call rollback. Skip the snapshot only on a fresh empty volume.
Step 1: Create the EBS volume
A 100 GiB gp3 volume in the instance's AZ:
aws ec2 create-volume \
--region :region \
--volume-type :volume_type \
--size :volume_size \
--availability-zone :availability_zone \
--tag-specifications 'ResourceType=volume,Tags=[{Key=Name,Value=data-disk-01},{Key=role,Value=data}]'--size is in GiB. --volume-type accepts gp3, gp2, io1, io2, st1, sc1, or standard. The default for gp3 is 3,000 IOPS and 125 MiB/s throughput, both adjustable with --iops and --throughput. gp2 scales IOPS with size; gp3 decouples them and is cheaper for the same performance, so unless you need backward compatibility there is no reason to pick gp2 anymore.
The command returns the volume ID immediately. To wait for it to reach available:
aws ec2 wait volume-available --region :region --volume-ids :volume_idTo restore from a snapshot instead, swap --size for --snapshot-id:
aws ec2 create-volume \
--region :region \
--snapshot-id snap-0abc123 \
--volume-type :volume_type \
--availability-zone :availability_zoneThe size is inherited from the snapshot; pass --size larger than the source to create a bigger volume from the same snapshot.
Step 2: Attach the volume to the instance
aws ec2 attach-volume \
--region :region \
--volume-id :volume_id \
--instance-id :instance_id \
--device :deviceThe --device argument is the device name AWS will report in volume metadata. It is not necessarily the device path that appears on the Linux instance. See the gotcha section below.
Conventional AWS device names for additional data volumes: /dev/sdf through /dev/sdp. Root volumes are conventionally /dev/sda1 or /dev/xvda, which AWS reserves. Pick /dev/sdf for the first data volume on an instance, /dev/sdg for the second, and so on.
Attach completes in seconds. From inside the instance, confirm with lsblk:
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
nvme0n1 259:0 0 8G 0 disk
└─nvme0n1p1 259:1 0 8G 0 part /
nvme1n1 259:2 0 100G 0 disk
nvme1n1 (or xvdf on older instances) is the new volume. No partitions, no filesystem.
Console method: EC2 console, Volumes, select the volume, Actions, Attach volume. Same effect, slower for batches.
The AWS-vs-Linux device naming gotcha
This single mismatch trips everyone exactly once. AWS device names are advisory; Linux assigns its own.
| AWS device argument | Xen instance sees | Nitro instance sees |
|---|---|---|
/dev/sda1 (root) | /dev/xvda | /dev/nvme0n1 |
/dev/sdf | /dev/xvdf | /dev/nvme1n1 |
/dev/sdg | /dev/xvdg | /dev/nvme2n1 |
/dev/sdh | /dev/xvdh | /dev/nvme3n1 |
Nitro instances are the modern default (C5, M5, R5, T3, T3a, T4g, all Graviton families, and every C6/M6/R6 and later). The kernel sees EBS volumes through the NVMe driver, so devices appear as /dev/nvmeXn1 in order of attachment, not by the AWS device argument.
Xen instances (older C3, M3, R3, T2 and earlier) use the Xen block driver, so /dev/sdf from AWS becomes /dev/xvdf (a one-character translation: s to xv).
The reliable way to map an AWS device argument to a Linux device path on Nitro is the nvme id-ctrl command, which reads the device's identity controller block:
sudo nvme id-ctrl -v /dev/nvme1n1 | grep -i "sn\|vol"The serial number contains the EBS volume ID (with hyphens stripped). Compare against aws ec2 describe-volumes output to know which Linux device corresponds to which EBS volume.
For most workflows you can skip the mapping ritual and just use lsblk to find the unformatted device by size and TYPE. On an instance with one root disk and one new data disk, the latter is unmistakable.
Step 3: Format and mount inside the instance
Format with ext4 (the right default for most workloads):
sudo mkfs.ext4 -L data /dev/nvme1n1-L data labels the filesystem data, which lets you reference it later as /dev/disk/by-label/data. The label is convenient but the UUID is what goes in fstab.
For XFS (Amazon Linux 2023 boot disks use XFS by default; large-file workloads benefit from it):
sudo mkfs.xfs -L data /dev/nvme1n1This is destructive. Double-check the device path on instances with multiple unformatted volumes.
Mount point, mount, permissions:
sudo mkdir -p /mnt/data
sudo mount /dev/nvme1n1 /mnt/data
sudo chown ec2-user:ec2-user /mnt/dataConfirm with df -h /mnt/data. The filesystem should report close to the volume size (a small percentage goes to filesystem overhead and the journal).
For workloads that benefit from filesystem-level TRIM passthrough to EBS, mount with discard:
sudo mount -o discard /dev/nvme1n1 /mnt/dataEBS treats discards as a hint to release allocated blocks. The performance impact is small on modern volumes; the cost benefit is real on volumes that grow and shrink in real usage.
Step 4: Persist the mount with fstab and UUID
The mount is live but ephemeral. Reboot now and the disk is unmounted; you have to re-run the mount command manually.
The fix is /etc/fstab. Two ways to write the entry, only one is right.
Wrong (the version that bites people):
/dev/nvme1n1 /mnt/data ext4 defaults 0 2
Works until you attach another volume, detach and re-attach this one, or move to a different instance type with different NVMe enumeration. The kernel can re-letter the devices and /dev/nvme1n1 ends up being a different volume, or nothing at all.
Right (UUID, baked into the filesystem at format time):
sudo blkid /dev/nvme1n1Output:
/dev/nvme1n1: LABEL="data" UUID="3f7e1c2a-9b4d-4a8e-bc6f-2d1a5e9b7c8f" TYPE="ext4"
Add it to fstab:
echo "UUID=3f7e1c2a-9b4d-4a8e-bc6f-2d1a5e9b7c8f /mnt/data ext4 defaults,nofail 0 2" \
| sudo tee -a /etc/fstabnofail matters. Without it, a missing disk (detached, snapshotted out from under the instance, accidentally deleted) prevents the instance from booting and you have to recover via the EC2 Serial Console or by detaching the root volume. With nofail, boot continues and you can SSH in to diagnose.
Test without rebooting:
sudo umount /mnt/data
sudo mount -a
df -h /mnt/dataClean output from mount -a plus the disk in df -h means the fstab line is correct.
Critical: before you ever run aws ec2 detach-volume, remove or comment out the fstab line. Otherwise the next reboot pauses at the fstab stage waiting for a disk that no longer exists.
EBS volume types compared
AWS offers five active EBS volume types plus the legacy standard (magnetic) type. Pick by workload.
| Type | Media | Min size | Max size | Max IOPS | Max throughput | Best for |
|---|---|---|---|---|---|---|
gp3 | SSD | 1 GiB | 16 TiB | 16,000 | 1,000 MiB/s | Default for everything |
gp2 | SSD | 1 GiB | 16 TiB | 16,000 | 250 MiB/s | Legacy, no reason to pick today |
io2 Block Express | SSD | 4 GiB | 64 TiB | 256,000 | 4,000 MiB/s | High-end databases, latency-critical |
io1 | SSD | 4 GiB | 16 TiB | 64,000 | 1,000 MiB/s | Legacy provisioned IOPS |
st1 | HDD | 125 GiB | 16 TiB | 500 | 500 MiB/s | Large sequential workloads, logs |
sc1 | HDD | 125 GiB | 16 TiB | 250 | 250 MiB/s | Cold storage, infrequent access |
Default to gp3. It is cheaper than gp2 at the same baseline (3,000 IOPS, 125 MiB/s included) and IOPS plus throughput are tunable independent of size. Reach for io2 Block Express only when you have measured gp3 and need more IOPS than its 16,000 ceiling allows, or when sub-millisecond latency is a requirement.
st1 is the right answer for big-block sequential workloads: log aggregators, data warehouse staging, video processing pipelines. sc1 is for things you almost never read.
Performance on gp3 is set at create time, not derived from size, so a 100 GiB gp3 and a 10 TiB gp3 have the same default 3,000 IOPS. Bump IOPS with --iops 8000 at create time (or modify later with aws ec2 modify-volume). The first 3,000 IOPS and 125 MiB/s are free; you only pay for provisioned performance above that.
For the live-resize procedure when a volume fills up, see How to Extend an AWS EBS Volume Without a Restart.
Console vs CLI vs Terraform
Same operation, three interfaces. Pick by workflow.
| Approach | Best for | Command / action |
|---|---|---|
| EC2 console | One-off volumes, exploring | Volumes, Create volume, then Actions, Attach volume |
| AWS CLI | Scripted, batched | aws ec2 create-volume plus attach-volume |
| Terraform | Infra-as-code, audit trails | aws_ebs_volume plus aws_volume_attachment |
| AWS CDK | Programmatic, app-aligned | ec2.Volume plus instance.attachVolume() |
The Terraform snippet:
resource "aws_ebs_volume" "data" {
availability_zone = "us-east-1a"
size = 100
type = "gp3"
iops = 3000
throughput = 125
encrypted = true
tags = {
Name = "data-disk-01"
role = "data"
}
}
resource "aws_volume_attachment" "data" {
device_name = "/dev/sdf"
volume_id = aws_ebs_volume.data.id
instance_id = aws_instance.web.id
}
Two resources, not one. aws_ebs_volume creates the volume; aws_volume_attachment attaches without coupling the volume's lifecycle to the instance. If you put ebs_block_device inside the aws_instance resource instead, deleting the instance deletes the volume (almost never what you want for data disks).
The CDK equivalent in TypeScript:
const dataVolume = new ec2.Volume(this, "DataVolume", {
availabilityZone: "us-east-1a",
size: cdk.Size.gibibytes(100),
volumeType: ec2.EbsDeviceVolumeType.GP3,
encrypted: true,
});
new ec2.CfnVolumeAttachment(this, "DataVolumeAttachment", {
device: "/dev/sdf",
instanceId: instance.instanceId,
volumeId: dataVolume.volumeId,
});The format / mount / fstab steps still happen inside the instance. Use a user-data script, an Ansible playbook, or AWS Systems Manager Run Command. The fact that Terraform creates the volume and attaches it does not get you a formatted filesystem; the OS-side work is yours.
To attach volumes across a fleet, a Bash for loop over the instance list does it cleanly:
for instance in i-aaa i-bbb i-ccc; do
vol=$(aws ec2 create-volume --region :region --volume-type :volume_type --size :volume_size \
--availability-zone :availability_zone --query VolumeId --output text)
aws ec2 wait volume-available --region :region --volume-ids "$vol"
aws ec2 attach-volume --region :region --volume-id "$vol" --instance-id "$instance" --device :device
doneMulti-Attach and other caveats
Multi-Attach lets one EBS volume attach to up to 16 EC2 instances simultaneously, all read-write. Significant caveats:
io1andio2only. Not gp3, gp2, st1, or sc1.- All instances must be in the same AZ.
- Instances must be Nitro-based.
- ext4 and XFS corrupt themselves under Multi-Attach. You need a cluster-aware filesystem: GFS2, OCFS2, or a clustered application that handles its own concurrency.
- Snapshots are not consistent unless the filesystem itself coordinates.
For most use cases that ask "can I share a disk between instances?", the answer is EFS (NFS over the network, simple) or FSx (managed Lustre, Windows File Server, OpenZFS, or NetApp). Multi-Attach is the right answer only when you have a cluster-aware filesystem and a workload that genuinely benefits from shared block storage.
Encryption at rest is on by default in most accounts since 2021 (you set the default with aws ec2 enable-ebs-encryption-by-default). For customer-managed keys pass --kms-key-id to create-volume.
Per-instance attachment limits depend on instance type. The classic ceiling was 25 EBS volumes per instance; modern Nitro instances support up to 128 or more on certain types. The instance volume limits page in the EC2 user guide is the authoritative source.
Volumes attach across AZs is not supported. A volume in us-east-1a cannot be attached to an instance in us-east-1b. To migrate, create a snapshot, restore the snapshot to a new volume in the target AZ, attach.
Detach order matters. Always sudo umount /mnt/data from the instance before aws ec2 detach-volume. Detaching a mounted volume risks filesystem corruption.
Verify the volume survives a reboot
The only verification that proves the fstab entry works:
sudo rebootWait for the instance to come back, SSH in, confirm the mount is present without manual intervention:
df -h /mnt/data
mount | grep /mnt/dataIf the mount is missing, check dmesg | tail and journalctl -xb | grep -i fstab. The usual suspects: UUID typo from a copy-paste, filesystem type mismatch (ext4 declared but volume is xfs), missing nofail blocking boot at the fstab stage and dropping the instance into emergency mode.
For instances that hosted databases, the post-reboot ritual extends to a logical check: confirm MySQL, PostgreSQL, or Redis came back up cleanly. The MySQL backup procedure is the right pre-flight before any disk operation on a database instance.
What to do next
- The companion EC2 articles: How to SSH into an AWS EC2 Instance and How to Extend an AWS EBS Volume Without a Restart.
- The GCP equivalent: How to Add a Persistent Disk to a Google Cloud VM. Same procedure, different control plane and device-naming model.
- The canonical reference: AWS's Attach an Amazon EBS volume to an Amazon EC2 instance documents Console, CLI, and PowerShell flows.
- Make an Amazon EBS volume available for use is the upstream source for the mkfs / mount / fstab section.
- How to Export or Backup All MySQL Databases: the standard reason to give a database its own EBS volume with its own snapshot schedule.
- Bash For Loops and Bash While Loops: patterns for provisioning across many instances.
- SSH Cheat Sheet: the SSH reference for the operations on the instance side.





