15 min read

Learning Objectives

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

  • Provision a PostgreSQL cluster with `ionoscloud_pg_cluster`, configuring version, instances, storage, maintenance window, and backup in Terraform
  • Provision an In-Memory DB replica set with `ionoscloud_inmemorydb_replicaset` for caching and session storage
  • Provision Kafka clusters and topics with `ionoscloud_kafka_cluster` and `ionoscloud_kafka_cluster_topic`, and adapt the same pattern to MongoDB and MariaDB
  • Extract connection strings, credentials, and TLS material from Terraform state as `sensitive` outputs without leaking them to logs
  • Apply the single-primary provisioning model correctly: provision one writable cluster and plan read scaling at the caching layer

Unit 2.5: Database and Streaming Service Provisioning

Introduction

TaskBoard now has compute, networking, and storage in code. The application still needs a place to keep state. Tasks, boards, and users belong in a relational store with TLS and automated backups. Session data and hot read paths belong in an in-memory cache. Task-change events that the worker service consumes belong on a stream.

This unit provisions all three as Terraform resources. You define the PostgreSQL cluster (transactional storage) and the In-Memory DB replica set (session and read cache) that TaskBoard depends on, then see the same provisioning pattern applied to MongoDB, MariaDB, and Kafka. The hard rule running through every example: IONOS managed databases run as a single writable primary. There are no readable read replicas to scale reads against. You provision one cluster and you scale reads with the cache, not with the database. Every example marks credentials sensitive so connection material never lands in plan output or CI logs.

1. Provisioning PostgreSQL with Terraform

PostgreSQL is TaskBoard's system of record. The ionoscloud_pg_cluster resource creates a managed cluster: you specify the engine version, the number of instances, the resource size, storage, the LAN connection, the maintenance window, and the initial credentials. Provisioning is asynchronous, and a new cluster can take 20 to 30 minutes to reach AVAILABLE because the platform provisions fresh nodes for every requested instance.

The cluster attaches to a private LAN (the database tier from Unit 2.2), so it is never directly exposed to the internet. The default port is 5432 and it is not configurable.

1.1 The pg_cluster resource

Supported PostgreSQL versions are 14, 15, and 16. Storage types are SSD Premium, SSD, and HDD. The connections block binds the cluster to an existing LAN and assigns it a CIDR within that LAN.

resource "ionoscloud_pg_cluster" "taskboard" {
  postgres_version = "16"
  instances        = 1
  cores            = 4
  ram              = 8192
  storage_size     = 50
  storage_type     = "SSD Premium"
  location         = ionoscloud_datacenter.taskboard.location
  display_name     = "taskboard-pg"

  connections {
    datacenter_id = ionoscloud_datacenter.taskboard.id
    lan_id        = ionoscloud_lan.db.id
    cidr          = "10.20.30.4/24"
  }

  maintenance_window {
    day_of_the_week = "Sunday"
    time            = "03:00:00"
  }

  credentials {
    username = "taskboard_admin"
    password = var.pg_admin_password
  }

  synchronization_mode = "ASYNCHRONOUS"
}

The instances value is the total node count, not a read-replica count. The supported range is 1 to 5 instances per cluster. Additional instances are HA standbys, not endpoints your application reads from. PostgreSQL supports two replication modes: ASYNCHRONOUS (the default) and STRICTLY_SYNCHRONOUS. Strictly synchronous mode requires a minimum of 3 instances. For TaskBoard you provision a single primary (instances = 1) and scale reads with In-Memory DB in Section 2.

1.2 Backups, maintenance, and version pinning

Managed PostgreSQL keeps automated backups with a 7-day retention by default and supports point-in-time recovery within that window, but on the v2 API retention is configurable from 1 to 365 days via backup.retentionDays at cluster create or update time. Note that the general Backup Service does not back up managed databases, so do not plan to point it at the cluster. Recovery is PITR plus your own scheduled dumps, covered in Module 4.

Pin the postgres_version explicitly rather than letting it float. Major upgrades change behaviour and are not something you want a plan diff to trigger silently.

variable "pg_admin_password" {
  type      = string
  sensitive = true
}

# Pass at apply time or via TF_VAR_pg_admin_password, never hardcode:
#   terraform apply -var="pg_admin_password=$(openssl rand -base64 24)"

The maintenance_window controls when the platform applies patches. Each window is up to 4 hours long. Pick a low-traffic slot so HA failover during patching lands off-peak.

2. Provisioning In-Memory DB for caching

TaskBoard uses In-Memory DB for session storage and for caching task lists. Because PostgreSQL has no readable replicas, the cache is the read-scaling layer, not an optional optimization. The ionoscloud_inmemorydb_replicaset resource provisions a Redis-compatible replica set. The engine is Redis OSS stable version 7.2 and the default port is 6379.

In-Memory DB now exposes two API versions. The v2 API (version 2.0.0), whose primary resource is the Cluster, is GA and is the recommended API for production. The v1 API (version 1.0.0), whose primary resource is the ReplicaSet, is the legacy implementation and has a deprecation announced for an upcoming release; migration from v1 to v2 is customer-initiated, not automatic, so existing v1 clusters must be migrated to the v2 Cluster resource manually before v1 is removed. The Terraform examples below provision the In-Memory DB replica set, and the connection model they produce (engine, port, TLS, fixed connection ceiling) is the same regardless of which API version backs the resource. For new direct API or SDK development, target the v2 Cluster endpoints.

The replica set is one active node and n-1 passive nodes. Passive nodes provide failover, not extra read throughput from the client's perspective, since clients connect through the active endpoint.

2.1 The inmemorydb_replicaset resource

A critical constraint: credentials are set only at creation. You cannot change the password through a later update, so generate it once and store it where your application can read it. Minimum storage is 10 GB per node. The default eviction policy is allkeys-lru. Persistence mode defaults to None; the API accepts None, AOF, RDB, and RDB_AOF.

resource "ionoscloud_inmemorydb_replicaset" "taskboard_cache" {
  display_name    = "taskboard-cache"
  location        = ionoscloud_datacenter.taskboard.location
  version         = "7.2"
  replicas        = 2
  persistence_mode = "RDB"
  eviction_policy  = "allkeys-lru"

  resources {
    cores = 2
    ram   = 4
  }

  connections {
    datacenter_id = ionoscloud_datacenter.taskboard.id
    lan_id        = ionoscloud_lan.db.id
    cidr          = "10.20.30.5/24"
  }

  credentials {
    username = "default"
    plain_text_password = var.cache_password
  }

  maintenance_window {
    day_of_the_week = "Sunday"
    time            = "04:00:00"
  }
}

2.2 TLS for the cache connection

The cache endpoint supports TLS, but it is encrypted only if you enable TLS on the instance at creation; it is not on by default. Once enabled, the certificate authority is Let's Encrypt, so a standard system CA bundle validates the connection without you having to ship a custom root. Your Redis client connects with TLS enabled against the active endpoint:

import redis

# host comes from the Terraform output (Section 4); password set at creation
r = redis.Redis(
    host=cache_host,
    port=6379,
    username="default",
    password=cache_password,
    ssl=True,
    ssl_cert_reqs="required",
)
r.set("session:abc123", "user-42", ex=3600)

Because the password cannot be rotated in place, treat a rotation as a replace: provision a new replica set, cut traffic over, then destroy the old one.

3. Provisioning Kafka, MongoDB, and MariaDB (same pattern)

PostgreSQL and In-Memory DB cover TaskBoard's needs, but the provisioning pattern generalizes. Kafka, MongoDB, and MariaDB all follow the same shape: a cluster resource, a connections block binding to a LAN, a maintenance window, and credentials, all asynchronous. Below are the resource names and the differences that matter when you reach for them.

3.1 Kafka clusters and topics

TaskBoard's event stream (task-change events consumed by the worker) runs on Kafka. Provisioning is two resources: the cluster, then one resource per topic. The supported Kafka version is 4.0.0. Versions 3.9.0 and 3.9.1 are deprecated; existing clusters on them keep running, but new clusters should use 4.0.0. A cluster runs 3 brokers, and IONOS recommends (though does not default to) a replication factor of 3 for topics, matching the 3-broker layout. Authentication is mTLS for the data plane and a Bearer token for the management API. Encryption in transit is TLS.

resource "ionoscloud_kafka_cluster" "events" {
  name    = "taskboard-events"
  version = "4.0.0"
  size    = "S"
  location = ionoscloud_datacenter.taskboard.location

  connections {
    datacenter_id   = ionoscloud_datacenter.taskboard.id
    lan_id          = ionoscloud_lan.app.id
    broker_addresses = ["10.20.20.10/24", "10.20.20.11/24", "10.20.20.12/24"]
  }
}

resource "ionoscloud_kafka_cluster_topic" "task_changes" {
  cluster_id          = ionoscloud_kafka_cluster.events.id
  location            = ionoscloud_kafka_cluster.events.location
  name                = "task-changes"
  number_of_partitions = 6
  replication_factor   = 3
  retention_time       = 604800000
}

Partition count is the parallelism ceiling for consumers, so size it for your expected consumer fan-out (Module 4 covers consumer scaling). The replication factor of 3 matches the 3-broker layout. Client certificates issued for mTLS are valid for 365 days, so plan rotation before they expire.

3.2 MongoDB and MariaDB

MongoDB uses ionoscloud_mongo_cluster. Supported versions are 6.0 and 7.0. The connection string follows the format mongodb+srv://m-<id>.mongodb.<region>.ionos.com, so your driver gets an SRV record rather than a raw host list.

MariaDB uses ionoscloud_mariadb_cluster. It supports LTS versions only, starting from 10.6 (for example 10.6, 10.11), the default port is 3306, and replication is asynchronous only. The certificate authority is Let's Encrypt.

resource "ionoscloud_mariadb_cluster" "reporting" {
  mariadb_version = "10.11"
  instances       = 1
  cores           = 2
  ram             = 4
  storage_size    = 20
  display_name    = "taskboard-reporting"
  location        = ionoscloud_datacenter.taskboard.location

  connections {
    datacenter_id = ionoscloud_datacenter.taskboard.id
    lan_id        = ionoscloud_lan.db.id
    cidr          = "10.20.30.6/24"
  }

  maintenance_window {
    day_of_the_week = "Sunday"
    time            = "05:00:00"
  }

  credentials {
    username = "reporting_admin"
    password = var.maria_password
  }
}

The decision table for which engine to provision:

Resource Engine Default Port Versions Replication
ionoscloud_pg_cluster PostgreSQL 5432 14, 15, 16 Async (default), Strictly Synchronous
ionoscloud_mariadb_cluster MariaDB 3306 LTS from 10.6 Asynchronous only
ionoscloud_mongo_cluster MongoDB n/a (SRV) 6.0, 7.0 Replica set
ionoscloud_inmemorydb_replicaset Redis 7.2 6379 7.2 Async (default), Semi-Synchronous
ionoscloud_kafka_cluster Kafka 4.0.0 mTLS data plane 4.0.0 Replication factor 3

Pick PostgreSQL or MariaDB for relational transactional data, MongoDB for document data, In-Memory DB for caching, and Kafka for event streams. None of the relational engines give you readable replicas, so the read-scaling answer is always the cache.

4. Connection outputs and the single-primary constraint

Provisioning is only useful if the application can connect. Terraform knows the cluster's DNS name and credentials after apply, so expose them as outputs, every one of them marked sensitive so they do not appear in plan output, CI logs, or terraform output without -raw.

4.1 Sensitive outputs

The PostgreSQL cluster exposes a stable DNS hostname, for example pg-010203.postgresql.de-fra.ionos.com, which your application uses instead of an IP. Build the connection string from the cluster's DNS name plus the credentials you supplied.

output "pg_connection_string" {
  value     = "postgresql://${ionoscloud_pg_cluster.taskboard.credentials[0].username}@${ionoscloud_pg_cluster.taskboard.dns_name}:5432/postgres?sslmode=require"
  sensitive = true
}

output "cache_host" {
  value     = ionoscloud_inmemorydb_replicaset.taskboard_cache.dns_name
  sensitive = true
}

output "kafka_broker_addresses" {
  value     = ionoscloud_kafka_cluster.events.connections[0].broker_addresses
  sensitive = true
}

The default SSL mode for PostgreSQL is prefer and it cannot be disabled by the client, so always set sslmode=require (or stricter) on the application side. The TLS root certificate for PostgreSQL is ISRG Root X1, which is in any modern system CA bundle.

4.2 Why there is no read replica to point at

This is the constraint that breaks naive scaling plans. There is no read_endpoint output to create, because there is no read replica. In-Memory DB's replica set is one active and n-1 passive nodes, and the passives are failover targets, not read endpoints. PostgreSQL instances > 1 gives you HA standbys, not query targets.

# WRONG: there is no separate read endpoint to expose
# output "pg_read_endpoint" { value = ... }  # does not exist

# RIGHT: reads scale through the cache, writes go to the single primary
output "pg_primary_dns" {
  value     = ionoscloud_pg_cluster.taskboard.dns_name
  sensitive = true
}

Wire the application so that every write and every consistent read goes to the primary, and every hot read goes through In-Memory DB with a cache-aside pattern. That integration is the subject of Module 4, but the provisioning decision is made here: one primary, one cache.

API Reference Quick Card

Key API endpoints for database and streaming provisioning:

Method Endpoint Description
POST https://api.ionos.com/databases/postgresql/clusters Create a PostgreSQL cluster
GET https://api.ionos.com/databases/postgresql/clusters/{clusterId} Get cluster status (poll until AVAILABLE)
POST https://in-memory-db.{region}.ionos.com/clusters Create an In-Memory DB cluster (v2 API, recommended)
POST https://in-memory-db.{region}.ionos.com/replicasets Create an In-Memory DB replica set (v1 API, deprecation announced)
POST /clusters (Kafka regional host) Create a Kafka cluster
POST /clusters/{clusterId}/topics Create a Kafka topic

Base URL (Cloud DBaaS): https://api.ionos.com/databases/postgresql In-Memory DB / Kafka: region-specific hosts (for example https://in-memory-db.de-fra.ionos.com) Authentication: Authorization: Bearer <token> for management APIs; mTLS for the Kafka data plane

Code Lab

Objective: Deploy TaskBoard's database tier with Terraform: a single-primary PostgreSQL cluster and an In-Memory DB replica set, then output their connection details as sensitive values.

Prerequisites:

  • IONOS Cloud account with API token (IONOS_TOKEN exported)
  • Terraform >= 1.5 with the ionoscloud provider
  • An existing datacenter and database LAN from Units 2.1 and 2.2

Step 1: Declare sensitive variables

variable "pg_admin_password" {
  type      = string
  sensitive = true
}
variable "cache_password" {
  type      = string
  sensitive = true
}

Step 2: Add the PostgreSQL cluster (from Section 1.1), then validate:

terraform validate

Expected output:

Success! The configuration is valid.

Step 3: Add the In-Memory DB replica set (from Section 2.1) and the sensitive outputs (from Section 4.1). Then plan:

terraform plan \
  -var="pg_admin_password=$(openssl rand -base64 24)" \
  -var="cache_password=$(openssl rand -base64 24)"

Expected output:

Plan: 2 to add, 0 to change, 0 to destroy.
Changes to Outputs:
  + cache_host           = (sensitive value)
  + pg_connection_string = (sensitive value)

Step 4: Apply (provisioning is asynchronous and can take 20 to 30 minutes for PostgreSQL):

terraform apply -auto-approve \
  -var="pg_admin_password=$PG_PW" \
  -var="cache_password=$CACHE_PW"

Expected output:

ionoscloud_pg_cluster.taskboard: Creation complete after 24m12s
ionoscloud_inmemorydb_replicaset.taskboard_cache: Creation complete after 6m41s
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Step 5: Read the sensitive connection string (note -raw, required because it is sensitive):

terraform output -raw pg_connection_string

Expected output:

postgresql://taskboard_admin@pg-010203.postgresql.de-fra.ionos.com:5432/postgres?sslmode=require

Step 6: Verify the PostgreSQL connection over TLS from a host on the database LAN:

psql "$(terraform output -raw pg_connection_string)" -c "SELECT version();"

Expected output:

PostgreSQL 16.x ...

Step 7: Verify the cache with a quick set/get over TLS:

redis-cli -h "$(terraform output -raw cache_host)" -p 6379 \
  --tls --user default -a "$CACHE_PW" PING

Expected output:

PONG

Validation Checklist:

  • [ ] PostgreSQL cluster reached AVAILABLE and accepts a TLS connection
  • [ ] In-Memory DB replica set responds to PING over TLS
  • [ ] All connection outputs are marked sensitive and require -raw to read
  • [ ] No password appears in plan output or apply logs

Cleanup:

terraform destroy -auto-approve \
  -var="pg_admin_password=$PG_PW" \
  -var="cache_password=$CACHE_PW"

Common Pitfalls

  1. Treating cluster instances as read replicas

    • Problem: You set instances = 3 on the PostgreSQL cluster and try to send read queries to a second node to offload the primary. Connections still all land on the same writable endpoint, and you never get read scaling.
    • Why it happens: IONOS managed databases run a single writable primary. Extra instances are HA standbys, not readable replicas, and there is no separate read endpoint.
    • Fix: Provision one primary and route hot reads through In-Memory DB:
    resource "ionoscloud_pg_cluster" "taskboard" {
      instances = 1   # single primary; cache handles read scaling
    }
    
  2. Leaking credentials through Terraform outputs

    • Problem: Your CI logs print the database password because an output was not marked sensitive, and terraform apply echoes it in the run.
    • Why it happens: Outputs default to visible. Any connection string built from credentials is plaintext unless flagged.
    • Fix: Mark every credential-bearing output sensitive = true and read with -raw only when needed:
    output "pg_connection_string" {
      value     = local.pg_conn
      sensitive = true
    }
    
  3. Trying to change the In-Memory DB password later

    • Problem: You run an update to rotate the cache password and the apply fails or the change is ignored.
    • Why it happens: In-Memory DB credentials are set only at creation and cannot be modified afterward.
    • Fix: Treat rotation as a replace. Provision a new replica set, migrate traffic, then destroy the old one. Generate the password once at creation:
    credentials {
      username            = "default"
      plain_text_password = var.cache_password  # set once, immutable
    }
    

Summary

You can now provision TaskBoard's entire data tier as code: a single-primary PostgreSQL cluster for transactions, an In-Memory DB replica set for sessions and read caching, and (using the same resource pattern) Kafka, MongoDB, or MariaDB when a workload needs them. You know how to bind each cluster to a private LAN, set a maintenance window, supply credentials safely, and expose connection details as sensitive Terraform outputs. Most importantly, you provision around the single-primary model: one writable cluster, with read scaling handled by the cache rather than by replicas that do not exist.

Key Points:

  • ionoscloud_pg_cluster provisions PostgreSQL (versions 14, 15, 16; port 5432); instances are HA nodes, not read replicas, and the range is 1 to 5
  • ionoscloud_inmemorydb_replicaset provisions Redis 7.2 on port 6379; credentials are set only at creation and cannot be changed in place
  • ionoscloud_kafka_cluster plus ionoscloud_kafka_cluster_topic provision Kafka 4.0.0 with 3 brokers and replication factor 3; the data plane authenticates with mTLS
  • Every connection output must be marked sensitive; the PostgreSQL default SSL mode is prefer and cannot be disabled, so the application sets sslmode=require
  • There are no readable read replicas on any managed engine; provision a single primary and scale reads through In-Memory DB

Important Terminology:

  • Single-primary model: The provisioning constraint that managed databases run one writable cluster with no readable replicas; reads scale at the cache layer.
  • Replica set (In-Memory DB): One active node plus n-1 passive failover nodes; clients connect through the active endpoint.
  • Maintenance window: A configurable weekly slot (up to 4 hours) when the platform applies patches and may trigger HA failover.
  • Sensitive output: A Terraform output flagged sensitive = true so it is hidden from plan, apply, and CI logs and requires -raw to read.
  • Replication factor (Kafka): The number of broker copies of each partition; defaults to 3, matching the 3-broker cluster.

Next Steps

Continue Learning: Unit 2.6: Knowledge Check - Infrastructure as Code

Related Topics: