Unit 1.2: Terraform Provider and Core Patterns
Introduction
In Unit 1.1 you provisioned a server three ways: raw API with curl, the Python SDK, and ionosctl. All three are imperative. You issue a POST, poll /requests/{id}/status until DONE, then issue the next call. That works, but it does not give you a reproducible, version-controlled description of your infrastructure. For TaskBoard, the task management API and frontend you are building across this course, you need infrastructure that lives in Git, reviews like code, and tears down cleanly so test environments never silently bill you.
This unit moves you to declarative provisioning with the IONOS Terraform provider. You will write the HCL that builds TaskBoard's base VDC, watch what Terraform does under the hood with the async IONOS API, store state remotely in Object Storage, and adopt the ephemeral-environment pattern that keeps cost under control. The async polling you handled manually in 1.1 is now the provider's job.
1. Provider Setup and Authentication
The IONOS provider for Terraform interacts with IONOS Cloud compute, storage, and networking resources. It currently supports the latest V5 and V6 API offerings, and is published on the Terraform Registry under ionos-cloud/ionoscloud. Before anything works, you must have an IONOS account whose credentials authenticate against the Cloud API, exactly as in Unit 1.1.
1.1 Declaring and Pinning the Provider
Always pin the provider version. Terraform's required_providers block locks you to a known-good release so a terraform init next month does not pull a breaking change into your pipeline.
# versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
ionoscloud = {
source = "ionos-cloud/ionoscloud"
version = "~> 6.4"
}
}
}
The ~> 6.4 constraint allows patch and minor updates within the 6.x line but blocks a jump to 7.0. Run terraform init to download the pinned provider into .terraform/. Commit the generated .terraform.lock.hcl file so every teammate and CI runner resolves the identical provider build.
1.2 Authentication: Environment Variable vs Provider Block
The cleanest pattern is to authenticate with the bearer token from Token Manager (covered in Unit 1.1) through the IONOS_TOKEN environment variable. Nothing secret then touches your HCL or your Git history.
export IONOS_TOKEN="eyJ0eXAiOiJKV1QiLCJraWQiO..."
terraform plan
With the variable set, the provider block can be empty:
# provider.tf
provider "ionoscloud" {}
You can also pass credentials explicitly in the provider block, which is useful when a single configuration manages resources across more than one account. Username and password (Basic auth) are supported as an alternative to the token.
provider "ionoscloud" {
token = var.ionos_token # never hardcode; pass via TF_VAR_ionos_token
}
Prefer the IONOS_TOKEN environment variable over a token committed to a .tfvars file. If you must use a variable, mark it sensitive = true and supply it through TF_VAR_ionos_token in your shell or CI secret store.
2. Core Resources: Building a VDC
A TaskBoard base environment needs a virtual data center, a LAN, a server, a boot volume, and a NIC connecting the server to the LAN. Every IONOS Terraform resource is prefixed with ionoscloud_, which keeps them unambiguous when a configuration mixes providers.
2.1 Datacenter, LAN, Server, Volume, NIC
The resource names below (ionoscloud_datacenter, ionoscloud_lan, ionoscloud_server, ionoscloud_nic) and their attribute schemas follow the IONOS Terraform provider documentation. The location value and image selection are grounded in the data-source sections of this unit.
# main.tf
resource "ionoscloud_datacenter" "taskboard" {
name = "taskboard-base"
location = "de/fra"
description = "TaskBoard base VDC, managed by Terraform"
}
resource "ionoscloud_lan" "app" {
datacenter_id = ionoscloud_datacenter.taskboard.id
name = "taskboard-app-lan"
public = true
}
resource "ionoscloud_server" "api" {
name = "taskboard-api"
datacenter_id = ionoscloud_datacenter.taskboard.id
cores = 2
ram = 4096
cpu_family = "INTEL_SKYLAKE"
availability_zone = "AUTO"
volume {
name = "taskboard-api-boot"
size = 20
disk_type = "SSD"
image_name = data.ionoscloud_image.ubuntu.id
image_password = var.server_password
}
nic {
lan = ionoscloud_lan.app.id
dhcp = true
}
}
Notice that the inline volume and nic blocks let you express the boot disk and network attachment as part of the server. For a data volume you attach after boot, or to manage a disk's lifecycle independently, declare a standalone ionoscloud_volume instead.
2.2 Standalone Volume and NIC
resource "ionoscloud_volume" "data" {
datacenter_id = ionoscloud_datacenter.taskboard.id
server_id = ionoscloud_server.api.id
name = "taskboard-data"
size = 50
disk_type = "SSD"
licence_type = "OTHER"
}
resource "ionoscloud_nic" "extra" {
datacenter_id = ionoscloud_datacenter.taskboard.id
server_id = ionoscloud_server.api.id
lan = ionoscloud_lan.app.id
dhcp = true
}
The references like ionoscloud_datacenter.taskboard.id and ionoscloud_server.api.id are what give Terraform its ordering. Because the volume's server_id reads from the server resource, Terraform knows the server must exist first. You never write explicit "wait for the datacenter" logic. That dependency graph is the whole point of moving off the imperative scripts from Unit 1.1.
3. The plan/apply/destroy Lifecycle
The three commands you will run constantly are terraform plan, terraform apply, and terraform destroy. Understanding what each does against the async IONOS API saves hours of confusion.
3.1 What Happens Under the Hood
terraform plan reads your HCL, reads current state, queries the IONOS API for the live state of each managed resource, and prints the diff. It changes nothing. terraform apply executes that diff by issuing the same POST, PUT, and DELETE calls you made by hand in 1.1.
Here is the key point: IONOS API operations are asynchronous. A POST returns a request ID and the resource is not ready yet. In Unit 1.1 you polled /requests/{id}/status until DONE before the next call. The IONOS Terraform provider does that polling internally for every resource. When apply shows a resource as still creating, it is waiting on exactly that request status before proceeding to dependent resources.
terraform plan -out=taskboard.tfplan
terraform apply taskboard.tfplan
Capturing the plan to a file with -out and applying that exact file guarantees apply does precisely what you reviewed, with no drift between plan and apply. This is the safe pattern for CI/CD.
3.2 Destroy and Targeted Operations
# Preview what will be torn down
terraform plan -destroy
# Tear it all down
terraform destroy
# Operate on a single resource (use sparingly)
terraform apply -target=ionoscloud_server.api
terraform destroy walks the dependency graph in reverse: NICs and volumes before the server, the server before the LAN, the LAN before the datacenter. This reverse ordering is why you should let Terraform manage the full stack rather than deleting resources manually in the DCD, which can leave Terraform state out of sync with reality.
4. Remote State on Object Storage
By default Terraform writes state to a local terraform.tfstate file. That does not work for a team or a pipeline. State must live somewhere shared, and IONOS Cloud Object Storage is S3-compatible, so it serves as a Terraform backend.
4.1 Configuring the S3 Backend
IONOS Object Storage authenticates with an Access Key and a Secret Key, not bearer tokens. These are the same S3 credentials you generate in Object Storage Key management. Object Storage is available in several regions, each with its own endpoint, including Frankfurt (de) at s3.eu-central-1.ionoscloud.com, Berlin (eu-central-2) at s3.eu-central-2.ionoscloud.com, and Logroño (eu-south-2) at s3.eu-south-2.ionoscloud.com.
# backend.tf
terraform {
backend "s3" {
bucket = "taskboard-tfstate"
key = "base/terraform.tfstate"
region = "eu-central-1"
endpoints = {
s3 = "https://s3.eu-central-1.ionoscloud.com"
}
skip_credentials_validation = true
skip_region_validation = true
skip_requesting_account_id = true
skip_s3_checksum = true
}
}
The skip_* flags are required because this is an external (non-AWS) S3 provider. Without them, the backend tries to call AWS-specific endpoints like the Security Token Service and fails. Supply the credentials through the standard S3 environment variables so they never enter your HCL:
export AWS_ACCESS_KEY_ID="your-object-storage-access-key"
export AWS_SECRET_ACCESS_KEY="your-object-storage-secret-key"
terraform init # migrates local state into the bucket
4.2 State Locking Considerations
State locking prevents two apply runs from corrupting state simultaneously. AWS S3 backends traditionally rely on a DynamoDB table for locks, which IONOS Object Storage does not provide. Treat the IONOS S3 backend as unlocked: serialize applies through a single CI/CD pipeline, never run concurrent applies against the same state key, and use a separate state key per environment (for example base/, staging/, prod/) so unrelated stacks never contend.
5. Data Sources, Imports, and Cost Control
Hardcoding image IDs and region strings makes configurations brittle. Data sources query the IONOS API at plan time so your HCL stays portable. Imports bring resources created outside Terraform under management. And disciplined destroy keeps your bill honest.
5.1 Image and Location Data Sources
data "ionoscloud_location" "frankfurt" {
name = "fra"
feature = "SSD"
}
data "ionoscloud_image" "ubuntu" {
type = "HDD"
location = "de/fra"
cloud_init = "V1"
image_alias = "ubuntu:latest"
}
The ionoscloud_image data source resolves a current image so you are not pinning a stale image UUID that may be retired. The ionoscloud_location data source confirms a region and its feature availability before you provision into it. Reference the result with data.ionoscloud_image.ubuntu.id in the server's volume block, as shown in Section 2.
The following table summarizes the four ways to drive IONOS provisioning that you have now seen across Units 1.1 and 1.2.
| Approach | Best For | Interface | State Tracked |
|---|---|---|---|
| API Direct | Full control, one-off calls | curl / HTTP | None (manual) |
| SDK | Application code | Python / Go / JS | None (manual) |
| Terraform | Reproducible infrastructure | HCL | Yes (state file) |
| ionosctl | Quick tasks, scripting | CLI | None (manual) |
Use Terraform when the infrastructure must be reproducible and reviewed. Reach for the SDK or ionosctl for runtime operations and quick checks that do not belong in your declarative stack.
5.2 Importing Existing Resources
If a datacenter or server already exists, perhaps created in the DCD or by an earlier curl script, import it rather than recreating it. Write a matching resource block first, then import. IONOS imports use a compound ID joining the datacenter ID and the resource ID.
# Import an existing server: format is datacenter_id/server_id
terraform import ionoscloud_server.api \
3fa85f64-5717-4562-b3fc-2c963f66afa6/9bc92f81-1234-4cde-8901-abcdef012345
# Import a datacenter (single ID)
terraform import ionoscloud_datacenter.taskboard 3fa85f64-5717-4562-b3fc-2c963f66afa6
After import, run terraform plan. If the plan shows changes, your HCL does not yet match reality. Reconcile the resource block's attributes to the live configuration until plan reports no changes. Only then is the resource safely under management.
5.3 Cost Awareness and Ephemeral Environments
Every resource Terraform provisions on apply incurs charges from the moment it exists. The disciplined workflow is: always plan before apply, always destroy what you spin up for testing. The ephemeral-environment pattern creates a full stack for a test run, then tears it down.
# Spin up a throwaway environment for a feature branch
terraform workspace new pr-142
terraform apply -auto-approve
# ... run integration tests against the live infrastructure ...
# Tear it all down so it stops billing
terraform destroy -auto-approve
terraform workspace delete pr-142
Wiring terraform destroy into the teardown step of a CI job guarantees a forgotten branch environment cannot quietly accumulate cost for weeks.
API Reference Quick Card
The IONOS Terraform provider calls these Cloud API endpoints under the hood. Knowing them helps you debug a stuck apply.
| Method | Endpoint | Description |
|---|---|---|
POST |
/datacenters |
Create a VDC (backs ionoscloud_datacenter) |
POST |
/datacenters/{id}/servers |
Create a server (backs ionoscloud_server) |
POST |
/datacenters/{id}/volumes |
Create a volume (backs ionoscloud_volume) |
POST |
/datacenters/{id}/lans |
Create a LAN (backs ionoscloud_lan) |
GET |
/requests/{id}/status |
Async request status the provider polls |
Base URL: https://api.ionos.com/cloudapi/v6
Authentication: Authorization: Bearer <token> (provider reads IONOS_TOKEN)
Code Lab
Objective: Build TaskBoard's base VDC with Terraform: a datacenter, a LAN, and a server with an attached volume. Plan, apply, verify, and destroy.
Prerequisites:
- IONOS Cloud account with an API token exported as
IONOS_TOKEN - Terraform 1.5 or later installed locally
ionosctlconfigured (from Unit 1.1) for verification
Step 1: Scaffold the configuration
mkdir taskboard-base && cd taskboard-base
touch versions.tf provider.tf main.tf
Expected output:
(no output; three empty files created)
Step 2: Add the provider and pin the version
Put the versions.tf content from Section 1.1 and provider "ionoscloud" {} from Section 1.2 into your files, then initialize.
terraform init
Expected output:
Initializing provider plugins...
- Installing ionos-cloud/ionoscloud v6.4.x...
Terraform has been successfully initialized!
Step 3: Write the VDC resources
Add the ionoscloud_datacenter, ionoscloud_lan, and ionoscloud_server blocks from Section 2.1 plus the ionoscloud_image data source from Section 5.1 to main.tf.
Step 4: Plan and review
export TF_VAR_server_password="ChangeMe-Strong-Pw-1"
terraform plan -out=taskboard.tfplan
Expected output:
Plan: 3 to add, 0 to change, 0 to destroy.
Step 5: Apply
terraform apply taskboard.tfplan
Expected output:
ionoscloud_datacenter.taskboard: Creation complete after 25s [id=3fa85f64-...]
ionoscloud_server.api: Still creating... [1m20s elapsed]
ionoscloud_server.api: Creation complete after 2m10s [id=9bc92f81-...]
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
Step 6: Verify with ionosctl
terraform output -raw datacenter_id 2>/dev/null || true
ionosctl server list --datacenter-id $(terraform state show ionoscloud_datacenter.taskboard | grep -m1 'id ' | awk '{print $3}' | tr -d '"')
Expected output:
ServerId Name State Cores Ram
9bc92f81 taskboard-api AVAILABLE 2 4096
Step 7: Inspect state
terraform state list
Expected output:
data.ionoscloud_image.ubuntu
ionoscloud_datacenter.taskboard
ionoscloud_lan.app
ionoscloud_server.api
Validation Checklist:
- [ ]
terraform applycompleted with 3 resources added - [ ]
ionosctl server listshows the server asAVAILABLE - [ ]
terraform state listshows all managed resources
Cleanup:
terraform destroy -auto-approve
Common Pitfalls
-
Deleting resources in the DCD that Terraform manages
- Problem: You delete the server manually in the web UI, then
terraform applyerrors or tries to recreate unrelated resources. - Why it happens: Terraform state still records the deleted server. The live API no longer has it, so state and reality diverge.
- Fix: Remove the stale entry from state, then let Terraform reconcile:
terraform state rm ionoscloud_server.api terraform plan # now matches reality - Problem: You delete the server manually in the web UI, then
-
Forgetting the S3 backend skip flags
- Problem:
terraform initagainst the Object Storage backend hangs or fails with an STS or account-ID error. - Why it happens: The S3 backend assumes AWS and tries AWS-only calls that IONOS Object Storage does not implement.
- Fix: Add the skip flags and the explicit endpoint to the backend block:
skip_credentials_validation = true skip_region_validation = true skip_requesting_account_id = true endpoints = { s3 = "https://s3.eu-central-1.ionoscloud.com" } - Problem:
-
Pinning a stale image UUID instead of using a data source
- Problem:
terraform applyfails with an image-not-found error months after the configuration last worked. - Why it happens: A hardcoded image UUID was retired upstream. The async server creation aborts because its boot volume references a missing image.
- Fix: Resolve the image at plan time with the data source and reference it:
data "ionoscloud_image" "ubuntu" { type = "HDD" location = "de/fra" image_alias = "ubuntu:latest" } # volume { image_name = data.ionoscloud_image.ubuntu.id } - Problem:
Summary
You can now describe IONOS infrastructure declaratively and manage its full lifecycle from code. You configured and version-pinned the IONOS Terraform provider, authenticated with IONOS_TOKEN, and built TaskBoard's base VDC from ionoscloud_datacenter, ionoscloud_lan, ionoscloud_server, ionoscloud_volume, and ionoscloud_nic resources. You moved state into an S3-compatible Object Storage backend, used data sources to resolve images and locations dynamically, imported existing resources, and adopted the ephemeral-environment pattern so test infrastructure tears down cleanly.
The async provisioning model you handled manually in Unit 1.1 is now the provider's internal concern: Terraform polls request status for you and orders operations through its dependency graph. From here, every infrastructure unit in this course builds on this same Terraform foundation.
Key Points:
- The IONOS provider supports V5 and V6 API offerings; always pin the version with
required_providersand commit the lock file - Authenticate with
IONOS_TOKENso no secret enters your HCL; all resources are prefixedionoscloud_ - Terraform polls the async
/requests/{id}/statusendpoint internally and orders operations via the resource dependency graph - The Object Storage S3 backend needs the
skip_*flags and an explicit IONOS endpoint, and authenticates with Access Key plus Secret Key, not a bearer token - Always
planbeforeapplyanddestroyephemeral environments to control cost
Important Terminology:
- Provider: The plugin that translates HCL resources into IONOS Cloud API calls, published as
ionos-cloud/ionoscloud - State: Terraform's record of which real IONOS resources it manages, stored locally or in an Object Storage backend
- Data source: A read-only query against the IONOS API at plan time, such as
ionoscloud_image, used to avoid hardcoding IDs - Import: Bringing an existing IONOS resource under Terraform management using a compound
datacenter_id/resource_id - Ephemeral environment: A full stack created for a test run and torn down with
terraform destroyto avoid lingering charges
Next Steps
Continue Learning: Unit 1.3: Knowledge Check - Programmatic Foundations
Related Topics: