# How to add a block volume to a template

Source: https://docs.quake.ai/docs/automation/how-to/add-volume-to-template
Markdown: https://docs.quake.ai/docs/automation/how-to/add-volume-to-template.md

---

# How to add a block volume to a template

Attach a block volume to an instance that a template provisions, format it on first boot, and record the mount in `/etc/fstab` so the filesystem mounts after a reboot. This guide extends templates without persistent data disks, such as [simple-vm](/resources/iac-templates/simple-vm).

These templates provide reference implementations:

- [wordpress-mysql](/resources/iac-templates/wordpress-mysql): mounts `/dev/sdb` at `/var/lib/mysql` for MySQL data
- [self-managed-postgres](/resources/iac-templates/self-managed-postgres): mounts `/dev/sdb` at `/var/lib/postgresql` for PostgreSQL data
- [dev-environment](/resources/iac-templates/dev-environment): uses a block volume to back a shared NFS export

<PrerequisiteBlock methods={["console", "cli", "terraform"]}>

- You deployed a template from `iac/templates/` and ran `tofu init` in that directory
- OpenTofu or Terraform installed with Quake AI credentials configured
- Enough block-storage quota for the new volume (`openstack limits show --absolute -c name -c value | grep volumes`)

</PrerequisiteBlock>

## Reference pattern from existing templates

The [wordpress-mysql](/resources/iac-templates/wordpress-mysql) template declares a block volume, an attachment to the MySQL instance, and a `cloud-init` script that formats and mounts the disk:

```hcl
resource "openstack_blockstorage_volume_v3" "mysql_data" {
  name = "${local.name_prefix}-mysql-data"
  size = var.db_volume_size
}

resource "openstack_compute_volume_attach_v2" "mysql_data" {
  instance_id = openstack_compute_instance_v2.mysql.id
  volume_id   = openstack_blockstorage_volume_v3.mysql_data.id
}
```

The mount logic lives in `cloud-init/mysql.yaml`:

```yaml
#cloud-config
runcmd:
  - |
    set -e
    DEV=/dev/sdb
    for i in $(seq 1 30); do [ -b "$DEV" ] && break; sleep 5; done
    if ! blkid "$DEV" >/dev/null 2>&1; then mkfs.ext4 -F "$DEV"; fi
    mkdir -p /var/lib/mysql
    mount "$DEV" /var/lib/mysql
    grep -q "$DEV" /etc/fstab || echo "$DEV /var/lib/mysql ext4 defaults,nofail 0 2" >> /etc/fstab
```

On Quake AI, the boot volume occupies the first SCSI device. The first attached data volume appears as `/dev/sdb`, which is the path used by the templates in this repository.

## Add volume resources to your template

The following example extends [simple-vm](/resources/iac-templates/simple-vm). Adjust resource names and the mount path for your workload.

### 1. Declare variables

Add to `variables.tf`:

```hcl
variable "data_volume_size" {
  description = "Size in GB for the attached data volume"
  type        = number
  default     = 20
}

variable "data_mount_path" {
  description = "Filesystem path where the data volume is mounted"
  type        = string
  default     = "/mnt/data"
}
```

### 2. Create the volume and attachment

Add to `main.tf` (or a new `storage.tf`):

```hcl
resource "openstack_blockstorage_volume_v3" "data" {
  name = "${var.instance_name}-data"
  size = var.data_volume_size
}

resource "openstack_compute_volume_attach_v2" "data" {
  instance_id = openstack_compute_instance_v2.vm.id
  volume_id   = openstack_blockstorage_volume_v3.data.id
}
```

Place the attachment after the instance resource so OpenTofu can resolve `openstack_compute_instance_v2.vm.id`.

### 3. Mount the volume on first boot

Create `cloud-init/data-volume.yaml`:

```yaml
#cloud-config
runcmd:
  - |
    set -e
    DEV=/dev/sdb
    MOUNT=${mount_path}
    for i in $(seq 1 30); do [ -b "$DEV" ] && break; sleep 5; done
    if ! blkid "$DEV" >/dev/null 2>&1; then mkfs.ext4 -F "$DEV"; fi
    mkdir -p "$MOUNT"
    mount "$DEV" "$MOUNT"
    grep -q "$DEV" /etc/fstab || echo "$DEV $MOUNT ext4 defaults,nofail 0 2" >> /etc/fstab
```

Wire it into the instance. If the instance has no existing `user_data`, add:

```hcl
resource "openstack_compute_instance_v2" "vm" {
  # ... existing arguments ...

  user_data = templatefile("${path.module}/cloud-init/data-volume.yaml", {
    mount_path = var.data_mount_path
  })
}
```



Adding or replacing `user_data` on a running instance forces OpenTofu to recreate the instance. Plan for brief downtime. If the template already passes cloud-init content, merge the mount script into the existing file instead of replacing it.



## Plan and apply

From the template directory:

```bash
tofu plan
tofu apply
```

The plan shows one new `openstack_blockstorage_volume_v3` and one `openstack_compute_volume_attach_v2`. It also shows an instance replacement when you add `user_data` for the first time.

## Verify the attachment and mount

<MethodTabs>
<Method label="Console">

1. Open **Storage** > **Volumes** and confirm a volume named `INSTANCE_NAME-data` shows **In-use** with the instance listed as its attachment.
2. SSH to the instance and open a terminal session.
3. Run `sudo cloud-init status --wait` to wait for the mount command to finish.
4. Run `df -h /mnt/data` and `grep "/dev/sdb /mnt/data " /etc/fstab`. Replace `/mnt/data` with the mount path you configured. Confirm the filesystem size and the matching `/etc/fstab` entry.
5. Reboot the instance from **Compute** > **Instances** > **Soft Reboot**. After the instance accepts SSH connections again, run `df -h /mnt/data` and confirm the output includes `/mnt/data`.

</Method>
<Method label="CLI">

List volumes and confirm the attachment:

```bash
openstack volume list -f table -c Name -c Status -c Attached to
```

Show attachment details for one volume:

```bash
openstack volume show VOLUME_NAME -c attachments -f yaml
```

SSH to the instance, wait for `cloud-init`, and verify the mount and `/etc/fstab` entry. Replace `YOUR_USER` with the default SSH user for your image. See [default usernames by distribution](/docs/compute/how-to/create-password#default-usernames-by-distribution). Replace `/mnt/data` in the command if you configured a different mount path.

```bash
ssh -i ~/.ssh/YOUR_KEY YOUR_USER@FLOATING_IP \
  'sudo cloud-init status --wait && df -h /mnt/data && grep "/dev/sdb /mnt/data " /etc/fstab'
```

Reboot the instance. After its status returns to `ACTIVE` and it accepts SSH connections, verify that `/etc/fstab` restored the mount:

```bash
openstack server reboot --soft INSTANCE_NAME
openstack server show INSTANCE_NAME -f value -c status
ssh -i ~/.ssh/YOUR_KEY YOUR_USER@FLOATING_IP 'df -h /mnt/data'
```

</Method>
<Method label="Terraform">

Confirm OpenTofu recorded the new resources:

```bash
tofu state list | grep blockstorage
tofu state list | grep volume_attach
```

Optional output block in `outputs.tf`:

```hcl
output "data_volume_id" {
  value       = openstack_blockstorage_volume_v3.data.id
  description = "Block volume ID for the attached data disk"
}
```

Retrieve it after apply:

```bash
tofu output data_volume_id
```

</Method>
</MethodTabs>

## See also

- [Create and attach a block volume](/docs/quickstart/create-block-volume): step-by-step tutorial for volumes outside IaC
- [wordpress-mysql template](/resources/iac-templates/wordpress-mysql): production volume + MySQL mount pattern
- [self-managed-postgres template](/resources/iac-templates/self-managed-postgres): PostgreSQL data volume pattern
- [dev-environment template](/resources/iac-templates/dev-environment): shared volume with NFS export
- [How to customize a template's image and flavor](/docs/automation/how-to/customize-template-image-flavor): resize compute without adding storage
- [Block storage how-to guides](/docs/block/how-to): create, extend, and snapshot volumes through the Console and CLI
