TechEarl

How to Add an EBS Volume to an EC2 Instance

Create an EBS volume, attach it to a running EC2 instance, format and mount it, and survive reboots with a UUID-based fstab entry. Console, AWS CLI, and Terraform walkthroughs plus the Nitro device-naming gotcha that trips everyone.

Ishan KarunaratneIshan Karunaratne⏱️ 18 min readUpdated
Create an EBS volume with aws ec2 create-volume, attach it to a running EC2 instance, format with mkfs.ext4 or mkfs.xfs, mount it, and persist across reboots with a UUID-based /etc/fstab entry. Console, AWS CLI, and Terraform walkthroughs.

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.

Try it with your own values

Jump to:

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:

bash
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:

bash
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:

bash
aws ec2 wait volume-available --region :region --volume-ids :volume_id

To restore from a snapshot instead, swap --size for --snapshot-id:

bash
aws ec2 create-volume \
  --region :region \
  --snapshot-id snap-0abc123 \
  --volume-type :volume_type \
  --availability-zone :availability_zone

The 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

bash
aws ec2 attach-volume \
  --region :region \
  --volume-id :volume_id \
  --instance-id :instance_id \
  --device :device

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

code
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 argumentXen instance seesNitro 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:

bash
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):

bash
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):

bash
sudo mkfs.xfs -L data /dev/nvme1n1

This is destructive. Double-check the device path on instances with multiple unformatted volumes.

Mount point, mount, permissions:

bash
sudo mkdir -p /mnt/data
sudo mount /dev/nvme1n1 /mnt/data
sudo chown ec2-user:ec2-user /mnt/data

Confirm 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:

bash
sudo mount -o discard /dev/nvme1n1 /mnt/data

EBS 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):

code
/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):

bash
sudo blkid /dev/nvme1n1

Output:

code
/dev/nvme1n1: LABEL="data" UUID="3f7e1c2a-9b4d-4a8e-bc6f-2d1a5e9b7c8f" TYPE="ext4"

Add it to fstab:

bash
echo "UUID=3f7e1c2a-9b4d-4a8e-bc6f-2d1a5e9b7c8f /mnt/data ext4 defaults,nofail 0 2" \
  | sudo tee -a /etc/fstab

nofail 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:

bash
sudo umount /mnt/data
sudo mount -a
df -h /mnt/data

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

TypeMediaMin sizeMax sizeMax IOPSMax throughputBest for
gp3SSD1 GiB16 TiB16,0001,000 MiB/sDefault for everything
gp2SSD1 GiB16 TiB16,000250 MiB/sLegacy, no reason to pick today
io2 Block ExpressSSD4 GiB64 TiB256,0004,000 MiB/sHigh-end databases, latency-critical
io1SSD4 GiB16 TiB64,0001,000 MiB/sLegacy provisioned IOPS
st1HDD125 GiB16 TiB500500 MiB/sLarge sequential workloads, logs
sc1HDD125 GiB16 TiB250250 MiB/sCold 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.

ApproachBest forCommand / action
EC2 consoleOne-off volumes, exploringVolumes, Create volume, then Actions, Attach volume
AWS CLIScripted, batchedaws ec2 create-volume plus attach-volume
TerraformInfra-as-code, audit trailsaws_ebs_volume plus aws_volume_attachment
AWS CDKProgrammatic, app-alignedec2.Volume plus instance.attachVolume()

The Terraform snippet:

hcl
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:

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:

bash
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
done

Multi-Attach and other caveats

Multi-Attach lets one EBS volume attach to up to 16 EC2 instances simultaneously, all read-write. Significant caveats:

  • io1 and io2 only. 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:

bash
sudo reboot

Wait for the instance to come back, SSH in, confirm the mount is present without manual intervention:

bash
df -h /mnt/data
mount | grep /mnt/data

If 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

FAQ

TagsAWSEC2EBSLinuxfstabmkfsTerraformgp3io2Nitro
Share
Ishan Karunaratne

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years across software, Linux systems, DevOps, and infrastructure — and a more recent focus on AI. Currently Chief Technology Officer at a tech startup in the healthcare space.

Keep reading

Related posts

Grow an AWS EBS volume with zero downtime: aws ec2 modify-volume to enlarge, wait for the optimizing state, then sudo growpart to extend the partition and sudo resize2fs (ext4) or sudo xfs_growfs (XFS) to stretch the filesystem. No detach, no reboot, on a live EC2 instance.

How to Extend an AWS EBS Volume Without a Restart

Grow an EBS volume on a running EC2 instance in four steps. Modify the volume, wait for the optimizing state, expand the partition with growpart, then stretch the filesystem with resize2fs or xfs_growfs. No detach, no reboot.

Connect to an AWS EC2 instance using plain SSH with a key pair, EC2 Instance Connect, AWS Systems Manager Session Manager, or an EC2 Instance Connect Endpoint for private instances. Default usernames, security group rules, and troubleshooting Permission denied and Connection timed out.

How to SSH into an AWS EC2 Instance

Connect to an EC2 instance four ways: plain SSH with a key pair, EC2 Instance Connect, Session Manager, and EC2 Instance Connect Endpoint. Default usernames, security group rules, and the troubleshooting matrix that fixes Permission denied and Connection timed out.

Macro photograph of a printer's typecase drawer with brass-and-wood compartments, one new compartment freshly added at the end, single warm side light

How to Add a Column to a MySQL Table

Add a column to a MySQL table with ALTER TABLE ADD COLUMN. Covers DEFAULT values, NOT NULL on existing rows, AFTER positioning, and ALGORITHM=INSTANT on MySQL 8.0.12+.