16 min read

Learning Objectives

By the end of this module, you will be able to:

  • Provision Block Storage volumes with the `ionoscloud_volume` resource, selecting the correct storage type and availability zone, and attach them to servers
  • Create Object Storage buckets and S3 access credentials with `ionoscloud_s3_bucket` and `ionoscloud_s3_key`, exposing them as sensitive Terraform outputs
  • Configure Object Storage as an S3-compatible remote-state backend for Terraform
  • Provision shared NFS storage using `ionoscloud_nfs_cluster` and `ionoscloud_nfs_share`, and mount it from a Linux server
  • Choose between Block Storage, Object Storage, and NFS in code based on access pattern, attachment model, and authentication requirements

Unit 2.4: Storage Provisioning as Code

Introduction

You are building out TaskBoard's storage tier, and each piece of state lands in a different place. The API server needs a persistent data volume that survives reboots and behaves like a local disk. User-uploaded file attachments belong in an object store reached over HTTP with S3 credentials, not bolted onto a single VM. And if you later run several workers that all read the same set of files, you want a shared filesystem rather than copies on every node.

All three are provisioned the same way: declarative Terraform resources against the IONOS API, with the same async create-and-wait model you have used since Unit 1.1. The differences that bite you are the constraints. Block Storage and Compute do not share the same availability zones. Object Storage does not use your bearer token at all. NFS speaks only one protocol version. This unit walks each storage type at the code level and shows where these constraints change what you write.

1. Block Storage Volumes with Terraform

Block Storage is provisioned as ionoscloud_volume and behaves as an iSCSI block device attached to a server. You declare the size, the storage type, and the availability zone, and Terraform handles the async provisioning and attachment. A volume is created inside a datacenter and bound to a single server through the server_id attribute.

The minimum volume size is 1 GiB and the maximum is 4096 GiB (4 TiB) for every Block Storage type. Larger volumes can be requested through IONOS Cloud Support. The storage type cannot be changed after provisioning, so pick it correctly at create time rather than planning to convert later.

1.1 Declaring and attaching a volume

The following configuration creates an SSD Premium data volume and attaches it to the TaskBoard API server. The image_name and image_password (or ssh_key_path) are required when the volume is bootable; for a pure data disk you provide a licence_type instead and skip the image.

resource "ionoscloud_datacenter" "taskboard" {
  name     = "taskboard"
  location = "de/txl"
}

resource "ionoscloud_volume" "api_data" {
  datacenter_id = ionoscloud_datacenter.taskboard.id
  server_id     = ionoscloud_server.api.id
  name          = "taskboard-api-data"
  size          = 50
  disk_type     = "SSD Premium"
  licence_type  = "LINUX"
  availability_zone = "AUTO"
}

The disk_type accepts the storage technology variants exposed by Block Storage: HDD, SSD Premium, and SSD Standard. SSD Premium delivers the highest IOPS per volume, SSD Standard trades performance for cost, and HDD is the lowest-cost spinning option. All three cap at 4 TiB (4096 GiB) per volume with a 1 GiB floor.

1.2 The availability zone trap

Block Storage availability zones are not the same set as Compute availability zones, and this is the single most common provisioning error in the storage tier. Block Storage supports Zone 1, Zone 2, Zone 3, and Auto. Compute servers support only Zones 1, 2, and Auto. Zone 3 exists for storage but there is no Zone 3 for compute.

This matters because a volume and the server it attaches to live in the same datacenter but are placed by independent zone settings. If you pin a volume to a zone that does not align with your server placement strategy, you constrain scheduling for no benefit. The safe default is AUTO on both server and volume unless you have a specific zone-pinning requirement.

// Safe default: let the platform place both
resource "ionoscloud_server" "api" {
  datacenter_id     = ionoscloud_datacenter.taskboard.id
  name              = "taskboard-api"
  cores             = 4
  ram               = 8192
  availability_zone = "AUTO" // Zone 1, 2, or AUTO only - never Zone 3
}

resource "ionoscloud_volume" "api_data" {
  # ...
  availability_zone = "AUTO"   # Zone 1, 2, 3, or AUTO permitted for storage
}

Volumes can be mixed across types on a single VM. A server may carry both SSD and HDD volumes at once. The per-VM volume limit depends on type: SSD Premium allows up to 4 volumes attached to one VM, while HDD and SSD Standard allow up to 24. Plan multi-volume layouts against the limit of the type you choose.

1.3 Encryption and durability context

Block Storage provides double-redundant storage: data is written to two storage servers, each protected by RAID, for redundancy at the platform level. Encryption at rest for logical volumes uses the AES-XTS algorithm. You do not configure either in the volume resource; both are properties of the platform storage layer, so your Terraform stays focused on size, type, and placement.

2. Object Storage Buckets and Access Keys

Object Storage is provisioned with ionoscloud_s3_bucket, and its access credentials are minted with ionoscloud_s3_key. The critical difference from every other resource in this course: Object Storage does not authenticate with your IONOS bearer token. It implements the AWS S3 API and authenticates with an Access Key and a Secret Key. Your application, your CI pipeline, and any S3 client use those keys, never the cloud API token.

resource "ionoscloud_s3_key" "taskboard" {
  user_id = var.user_id
}

resource "ionoscloud_s3_bucket" "attachments" {
  name = "taskboard-attachments-prod"
}

Bucket names must be globally unique across all Object Storage tenants and must be between 3 and 63 characters. Treat the name like a DNS label, lowercase with hyphens, and assume the obvious names are already taken. A name collision surfaces as a creation failure at apply time, not at plan time.

2.1 Outputting credentials as sensitive values

The ionoscloud_s3_key resource produces an access key and a secret key. The secret key must never appear in logs or plain output. Mark the outputs sensitive = true so Terraform redacts them from CLI output and from CI logs; the values still live in state, so protect the state backend accordingly.

output "s3_access_key" {
  value     = ionoscloud_s3_key.taskboard.id
  sensitive = true
}

output "s3_secret_key" {
  value     = ionoscloud_s3_key.taskboard.secret_key
  sensitive = true
}

output "s3_bucket_name" {
  value = ionoscloud_s3_bucket.attachments.name
}

An access key is 92 characters and a secret key is 64 characters. Each user can hold up to 5 access keys, which is enough to rotate without downtime: create the new key, roll your application config, then destroy the old key. The credentials are not tied to a specific region or bucket; one key pair works across all buckets the user can reach.

2.2 Endpoints and regions

Object Storage exposes the S3 API v2 standard, and you address it through a region-specific endpoint. Unlike AWS, you must set the endpoint explicitly in every client because the default AWS endpoints will not resolve. The following table lists the service endpoints your code and Terraform backend will reference.

Location Region S3 Endpoint
Frankfurt, Germany eu-central-4 s3.eu-central-4.ionoscloud.com
Berlin, Germany eu-central-2 s3.eu-central-2.ionoscloud.com
Logroño, Spain eu-south-2 s3.eu-south-2.ionoscloud.com

Pick the endpoint matching the region where you create the bucket and reuse it everywhere: in boto3, in the S3 backend block below, and in any presigned-URL generation. Connections use TLS, with TLS 1.2 and 1.3 supported. The maximum object size is 5 TB. Object Storage offers a single storage class, STANDARD, and a bucket supports up to 1000 lifecycle rules for object expiration; lifecycle rules cannot transition objects to another storage class (there is no cold or archive tier).

3. Object Storage as a Terraform State Backend

Because Object Storage is S3-compatible, it doubles as a remote-state backend for Terraform. This solves the problem you met in Unit 1.2: local state does not survive across a team or a CI runner. Storing state in a bucket gives every pipeline run a shared, durable source of truth.

3.1 Backend configuration

The s3 backend needs the IONOS endpoint and the same skip flags you use for any non-AWS S3 implementation, because the backend otherwise tries to validate against AWS account and region rules that do not apply.

terraform {
  backend "s3" {
    bucket   = "taskboard-tfstate"
    key      = "infrastructure/terraform.tfstate"
    region   = "eu-central-4"
    endpoints = {
      s3 = "https://s3.eu-central-4.ionoscloud.com"
    }
    skip_credentials_validation = true
    skip_requesting_account_id  = true
    skip_region_validation      = true
    skip_s3_checksum            = true
  }
}

Supply the access key and secret key to the backend through environment variables (AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY) rather than hardcoding them. The state bucket must already exist before terraform init, so provision it once with a small bootstrap configuration or with ionosctl, then point your main stack's backend at it.

3.2 Why this matters for the pipeline

The state bucket holds your S3 secret keys and database connection strings inside the state file. Restrict who can read it. A practical pattern is one bucket per environment, with separate key prefixes per stack, so a dev pipeline cannot read prod state. This carries directly into the CI/CD work in Module 3, where the same credentials become pipeline secrets.

4. NFS Shared Storage

When several servers need to read and write the same files, neither a single-attach Block Storage volume nor an object store fits cleanly. NFS gives you a POSIX filesystem mounted concurrently across Linux clients. It is provisioned in two resources: ionoscloud_nfs_cluster defines the cluster, and ionoscloud_nfs_share defines a share within it. The same async lifecycle and terraform destroy cleanup apply.

resource "ionoscloud_nfs_cluster" "shared" {
  name     = "taskboard-shared"
  location = "de/txl"
  size     = 2

  nfs {
    min_version = "4.2"
  }

  connections {
    datacenter_id = ionoscloud_datacenter.taskboard.id
    ip_address    = "10.7.222.100/24"
    lan           = ionoscloud_lan.app.id
  }
}

resource "ionoscloud_nfs_share" "uploads" {
  cluster_id = ionoscloud_nfs_cluster.shared.id
  location   = ionoscloud_nfs_cluster.shared.location
  name       = "uploads"
  quota      = 1024
  gid        = 1000
  uid        = 1000
}

4.1 Protocol and capacity constraints

NFS on IONOS speaks NFSv4.2 only. NFSv3 is not supported, so any client tooling or fstab entry pinned to v3 will fail to mount. Cluster size runs from a minimum of 2 TiB to a maximum of 42 TiB, with all provisioned capacity fully usable. Share quotas are expressed in MiB.

Encryption at rest is provided by the platform. Encryption in transit is not documented for NFS, so for sensitive data keep the share on a private LAN and rely on network isolation rather than expecting TLS on the mount. The cluster runs in an Active-Passive high-availability mode and is reached over a private IP you assign in the connections block.

4.2 Mounting the share

After apply, the share is mounted from a Linux client using the cluster IP and the share UUID returned in the resource's nfs_path output. Linux is the supported client OS.

sudo mount -t nfs 10.7.222.100:/<share-uuid> /mnt/uploads

Root squash is supported, so map UID and GID on the share to match the application user that reads and writes the files, as shown by the uid and gid arguments above. Mismatched ownership is the usual cause of permission-denied errors right after a successful mount.

5. Choosing the Right Storage in Code

Each storage type maps to a different access pattern, and choosing wrong shows up as either a constraint you fight or a bill you did not expect. Block Storage attaches to exactly one server and behaves like a local disk: use it for databases, OS volumes, and any single-writer workload. Object Storage is reached over HTTP with S3 credentials and scales independently of any VM: use it for user uploads, backups, static assets, and Terraform state. NFS gives concurrent POSIX access across many Linux clients: use it only when multiple servers genuinely must share a filesystem.

The following table summarizes the decision at the level you need it while writing Terraform.

Need Resource Attachment Auth
Single-server persistent disk ionoscloud_volume One server, iSCSI IONOS API token (provisioning)
HTTP-accessible object store ionoscloud_s3_bucket + ionoscloud_s3_key None (S3 API) Access Key + Secret Key
Shared filesystem, many clients ionoscloud_nfs_cluster + ionoscloud_nfs_share Many Linux clients, NFSv4.2 Private LAN mount

For TaskBoard, this resolves cleanly. The API server's data volume is Block Storage, attached to one server, sized for the working set. File attachments go to Object Storage because the browser uploads directly with presigned URLs and the API never proxies the bytes. NFS is not used in the base TaskBoard build; it is the pattern you reach for later if you add a fleet of workers that must share a content directory.

API Reference Quick Card

Key API endpoints for storage provisioning:

Method Endpoint Description
GET /datacenters/{dcId}/volumes List Block Storage volumes
POST /datacenters/{dcId}/volumes Create a Block Storage volume
POST /datacenters/{dcId}/servers/{serverId}/volumes Attach a volume to a server
DELETE /datacenters/{dcId}/volumes/{id} Delete a volume
GET /um/users/{userId}/s3keys List S3 access keys for a user
POST /um/users/{userId}/s3keys Create an S3 access key

Cloud API Base URL: https://api.ionos.com/cloudapi/v6 Object Storage S3 Endpoint: https://s3.eu-central-4.ionoscloud.com (region-specific) NFS API Base URL: https://nfs.{region}.ionos.com Authentication: Cloud API uses Authorization: Bearer <token>; Object Storage uses Access Key + Secret Key (AWS Signature)

Code Lab

Objective: Deploy TaskBoard's storage tier with Terraform: a Block Storage data volume attached to the API server and an Object Storage bucket with access credentials output as sensitive values.

Prerequisites:

  • IONOS Cloud account with API token (IONOS_TOKEN exported)
  • Terraform 1.5+ with the ionos-cloud/ionoscloud provider
  • An existing datacenter and API server (from the Unit 2.1 lab) or create them inline

Step 1: Pin the provider

terraform {
  required_providers {
    ionoscloud = {
      source  = "ionos-cloud/ionoscloud"
      version = "~> 6.4"
    }
  }
}

provider "ionoscloud" {}

Expected output:

(no output; init resolves the provider in Step 4)

Step 2: Declare the Block Storage data volume

variable "user_id" { type = string }

resource "ionoscloud_volume" "api_data" {
  datacenter_id     = var.datacenter_id
  server_id         = var.server_id
  name              = "taskboard-api-data"
  size              = 50
  disk_type         = "SSD Premium"
  licence_type      = "LINUX"
  availability_zone = "AUTO"
}

Step 3: Declare the Object Storage bucket and key

resource "ionoscloud_s3_key" "taskboard" {
  user_id = var.user_id
}

resource "ionoscloud_s3_bucket" "attachments" {
  name = "taskboard-attachments-${var.user_id}"
}

output "s3_access_key" {
  value     = ionoscloud_s3_key.taskboard.id
  sensitive = true
}

output "s3_secret_key" {
  value     = ionoscloud_s3_key.taskboard.secret_key
  sensitive = true
}

Step 4: Initialize and plan

terraform init
terraform plan -out=storage.plan

Expected output:

Plan: 3 to add, 0 to change, 0 to destroy.
Changes to Outputs:
  + s3_access_key = (sensitive value)
  + s3_secret_key = (sensitive value)

Step 5: Apply

terraform apply storage.plan

Expected output:

ionoscloud_s3_key.taskboard: Creation complete
ionoscloud_s3_bucket.attachments: Creation complete
ionoscloud_volume.api_data: Creation complete after 1m12s
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Step 6: Read the sensitive credentials

terraform output -raw s3_access_key
terraform output -raw s3_secret_key

Expected output:

(a 92-character access key, then a 64-character secret key)

Step 7: Verify the bucket with an S3 client

export AWS_ACCESS_KEY_ID=$(terraform output -raw s3_access_key)
export AWS_SECRET_ACCESS_KEY=$(terraform output -raw s3_secret_key)

aws --endpoint-url https://s3.eu-central-4.ionoscloud.com s3 ls

Expected output:

2026-06-05 12:01:33 taskboard-attachments-<user_id>

Validation Checklist:

  • [ ] Volume shows Creation complete and is attached to the API server
  • [ ] terraform output returns redacted (sensitive value) entries without -raw
  • [ ] The S3 client lists the bucket using Access Key + Secret Key, not the bearer token

Cleanup:

terraform destroy -auto-approve

Common Pitfalls

Developer mistakes to avoid when provisioning storage on IONOS Cloud:

  1. Pinning a volume to Zone 3 while the server is in Zone 1 or 2

    • Problem: A volume created in ZONE_3 cannot be co-located with a compute server, because Compute has no Zone 3.
    • Why it happens: Block Storage availability zones are Zone 1, Zone 2, Zone 3, Auto, but Compute only supports Zone 1, Zone 2, Auto. Developers assume the zone sets match.
    • Fix: Use availability_zone = "AUTO" on both server and volume unless you have a deliberate zone-pinning plan, and never set ZONE_3 on a server.
  2. Sending the IONOS bearer token to Object Storage

    • Problem: S3 requests return 403 SignatureDoesNotMatch or AccessDenied even though your cloud API token is valid.
    • Why it happens: Object Storage implements the AWS S3 API and authenticates with an Access Key and Secret Key. The bearer token used for api.ionos.com is not accepted by the S3 endpoint.
    • Fix: Generate credentials with ionoscloud_s3_key, set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, and always pass the explicit IONOS S3 endpoint URL.
  3. Mounting an NFS share with NFSv3

    • Problem: mount -t nfs -o vers=3 ... hangs or fails; the share refuses the connection.
    • Why it happens: IONOS NFS supports NFSv4.2 only. NFSv3 is not available, so any v3-pinned mount option or legacy fstab entry will not negotiate.
    • Fix: Mount without a v3 option (NFSv4.2 negotiates by default) and align the share uid/gid with the application user, since root squash is in effect:
    sudo mount -t nfs 10.7.222.100:/<share-uuid> /mnt/uploads
    

Summary

You can now provision all three IONOS storage tiers entirely in Terraform and wire them into an application. Block Storage volumes attach to a single server with ionoscloud_volume, sized between 1 GiB and 4 TiB, with the storage type and zone fixed at create time. Object Storage buckets and S3 keys are declared with ionoscloud_s3_bucket and ionoscloud_s3_key, authenticated with Access Key and Secret Key rather than your bearer token, and the same buckets back your Terraform remote state. NFS clusters and shares deliver concurrent NFSv4.2 access for the cases where many Linux clients must share a filesystem. The recurring lesson is that the constraints, not the resource syntax, decide your design: zone mismatches, the S3 auth model, and the NFS protocol version are where production time gets lost.

Key Points:

  • ionoscloud_volume provisions Block Storage from 1 GiB to 4 TiB; storage type and zone are immutable after creation
  • Block Storage supports Zone 1, 2, 3, and Auto, but Compute has no Zone 3; default both to AUTO
  • Object Storage authenticates with Access Key + Secret Key over a region-specific S3 endpoint, never the IONOS bearer token
  • Object Storage buckets work as an S3-compatible Terraform remote-state backend with the skip-validation flags set
  • NFS provides shared NFSv4.2 storage from 2 TiB to 42 TiB; NFSv3 is not supported

Important Terminology:

  • ionoscloud_volume: Terraform resource for a Block Storage iSCSI volume attached to one server.
  • ionoscloud_s3_key: Terraform resource that mints an Access Key and Secret Key pair for Object Storage; output as sensitive.
  • S3 endpoint: Region-specific URL (for example s3.eu-central-1.ionoscloud.com) that all Object Storage clients must set explicitly.
  • Remote-state backend: A shared, durable store for Terraform state; an Object Storage bucket configured with the s3 backend type.
  • Root squash: NFS behavior that maps client root to a non-privileged identity; requires aligning share uid/gid with the application user.

Next Steps

Continue Learning: Unit 2.5: Database and Streaming Service Provisioning

Related Topics: