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_TOKENexported) - Terraform 1.5+ with the
ionos-cloud/ionoscloudprovider - 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 completeand is attached to the API server - [ ]
terraform outputreturns 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:
-
Pinning a volume to Zone 3 while the server is in Zone 1 or 2
- Problem: A volume created in
ZONE_3cannot 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 supportsZone 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 setZONE_3on a server.
- Problem: A volume created in
-
Sending the IONOS bearer token to Object Storage
- Problem: S3 requests return
403 SignatureDoesNotMatchorAccessDeniedeven 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.comis not accepted by the S3 endpoint. - Fix: Generate credentials with
ionoscloud_s3_key, setAWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEY, and always pass the explicit IONOS S3 endpoint URL.
- Problem: S3 requests return
-
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/gidwith the application user, since root squash is in effect:
sudo mount -t nfs 10.7.222.100:/<share-uuid> /mnt/uploads - Problem:
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_volumeprovisions 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
s3backend type. - Root squash: NFS behavior that maps client root to a non-privileged identity; requires aligning share
uid/gidwith the application user.
Next Steps
Continue Learning: Unit 2.5: Database and Streaming Service Provisioning
Related Topics: