Skip to content

Manage databases

Three provider objects cover databases:

Object Kind What it manages
kupe_database Resource The database: version, per-instance size, HA, deletion protection
kupe_database_binding Resource One role of one database in namespaces of one cluster
kupe_database Data source Reads an existing database by name

Credentials never pass through Terraform. The API does not return them, so they are not in state; they reach your cluster in the binding’s Secret.

This creates a database, binds the owner role into a namespace for migrations, the readwrite role into the application’s two namespaces and the readonly role into a namespace for reports, and outputs what the application needs:

resource "kupe_database" "orders" {
name = "orders-db"
version = "18"
resources = {
cpu = "500m"
memory = "1Gi"
storage = "10Gi"
}
high_availability = true
lifecycle {
prevent_destroy = true
}
}
resource "kupe_database_binding" "migrations" {
database = kupe_database.orders.name
cluster = kupe_cluster.prod.name
namespaces = ["orders-migrate"]
role = "owner"
}
resource "kupe_database_binding" "api" {
database = kupe_database.orders.name
cluster = kupe_cluster.prod.name
namespaces = ["orders-api", "orders-worker"]
role = "readwrite"
}
resource "kupe_database_binding" "reports" {
database = kupe_database.orders.name
cluster = kupe_cluster.prod.name
namespaces = ["orders-reports"]
role = "readonly"
}
output "orders_db_hosts" {
description = "In-cluster DNS name each namespace of the api binding connects to."
value = { for ns in kupe_database_binding.api.namespace_status : ns.name => ns.host }
}
output "orders_db_secret" {
description = "Credentials Secret in the orders-api and orders-worker namespaces."
value = kupe_database_binding.api.secret_name
}

terraform apply waits, up to each resource’s timeout, for the database to reach Running and for each binding to reach Ready in every namespace it lists. The cluster does not have to exist yet: a binding into a cluster that is still being created waits for it.

Operation Waits until Default timeout
kupe_database create, update Running, with the change in effect on every instance 30m
kupe_database delete the database is gone 15m
kupe_database_binding create, update Ready in every listed namespace, and every removed one withdrawn 5m
kupe_database_binding delete the binding is gone 5m

A database create or update is done when all three of these hold:

  • status.observedGeneration has reached the database’s generation, so the platform has applied the change.
  • The phase is Running.
  • The Ready condition is True, so every instance runs the change. The phase stays Running while the instances roll onto it.

A database with one instance is Degraded while that instance restarts for a CPU or memory change, so the provider rides out Degraded for 5 minutes, on create as well as update, and fails the apply if it lasts longer. Degraded counts only once the platform has applied the change, so a status read from before the change never fails the apply. A database that starts being deleted (Terminating) fails the apply at once.

A change to deletion_protection alone is in effect as soon as the API accepts it, because a delete reads the setting when it arrives, so the provider does not wait for it.

Change the timeouts with the timeouts attribute:

resource "kupe_database" "orders" {
timeouts = {
create = "60m"
}
}

When a database create or update runs out of time:

  • Degraded after the platform applied the change: the apply fails.
  • An update the platform has not applied, with the Ready condition saying it failed to and is retrying: the apply fails.
  • Anything else, such as instances still rolling: the apply ends with a warning. The platform carries on, and a later terraform apply records the result.

A binding still Pending when its timeout runs out, because its database or cluster is not running, also ends with a warning.

A database create that fails leaves the database in state, marked tainted, so the next terraform apply plans to replace it. Run terraform untaint on it to keep it instead. An update that fails is already recorded in state, so applying again plans no change and does not send it again. To back the change out, set the previous value in the configuration and apply.

A binding with a Failed namespace stops the create or update at once with an error carrying the binding’s status message. When the cause is a name clash (the namespace already has a Service named after the database, or a Secret named secret_name, that the platform did not create), the error also names the namespace and both objects and says what to change. The other namespaces keep working.

  • After a failed create, Terraform keeps the binding in state, marked tainted. Rename or remove that object, or set another secret_name, and run terraform apply again to replace it.
  • After a failed update, the new list is in state. Rename or remove the object and the platform delivers into the namespace on its next try, or take the namespace out of namespaces and apply.
Argument On change
kupe_database.resources.cpu, .memory In place. Instances restart one at a time.
kupe_database.resources.storage In place, larger only. A smaller value fails the plan.
kupe_database.high_availability In place, both ways. Removing it keeps the current setting.
kupe_database.deletion_protection In place.
kupe_database.version Fails the plan. There are no major version upgrades.
kupe_database.engine Fails the plan.
kupe_database.name Replaces the database.
kupe_database_binding.namespaces In place. Namespaces kept work throughout.
any other argument of kupe_database_binding except timeouts Replaces the binding.

When version is unset, the platform default is used and recorded in state. current_version shows the release the database is running.

Mount the binding Secret at /bindings/<secret_name> and set SERVICE_BINDING_ROOT=/bindings. The Secret’s uri key only works at that path, because its sslrootcert points at the mounted ca.crt. If you manage workloads with the hashicorp/kubernetes provider, reference the binding’s attributes:

resource "kubernetes_deployment_v1" "api" {
metadata {
name = "api"
namespace = "orders-api"
}
spec {
selector {
match_labels = { app = "api" }
}
template {
metadata {
labels = { app = "api" }
}
spec {
container {
name = "api"
image = "ghcr.io/example/api:1.0.0"
env {
name = "SERVICE_BINDING_ROOT"
value = "/bindings"
}
volume_mount {
name = "database"
mount_path = "/bindings/${kupe_database_binding.api.secret_name}"
read_only = true
}
}
volume {
name = "database"
secret {
secret_name = kupe_database_binding.api.secret_name
}
}
}
}
}
}

Have workloads read the Secret themselves. Reading it with a kubernetes_secret_v1 data source puts the password in Terraform state.

Deletion takes an apply and then a destroy, on purpose:

  1. Remove the lifecycle { prevent_destroy = true } block and set deletion_protection = false.
  2. Run terraform apply.
  3. Run terraform destroy, or remove the resource and apply.

Skipping step 2 does not stop the destroy at the start. Terraform deletes the bindings first, because they depend on the database, and only then fails on the database with a deletion protection error. The database survives without its bindings until you apply them again.

Deleting a database removes it immediately. Its storage is kept for 7 days for support-assisted recovery, then permanently deleted. There is no self-service restore.

Removing a kupe_database_binding, or a namespace from its namespaces, removes the Secret from that namespace, and the Service and the network path unless another binding of the same database lists it. It does not remove knowledge of the password. Rotate the role’s credentials afterwards if that matters; the provider has no rotation resource, so use the console, kupe database rotate-credentials or the API.

Terminal window
terraform import kupe_database.orders orders-db
terraform import kupe_database_binding.api orders-db/orders-db-prod-readwrite

With high_availability unset in the configuration, an imported database keeps its current setting, so importing a highly available database does not plan turning it off. Set high_availability = false to turn it off.

deletion_protection defaults to true for an imported database too. Importing an unprotected database without the attribute in the configuration plans turning protection on; set deletion_protection = false to keep it off.

data "kupe_database" "orders" {
name = "orders-db"
}
output "orders_db_running_version" {
value = data.kupe_database.orders.current_version
}

The data source returns the engine, requested version, current_version, resources, high_availability, deletion_protection, phase and created_at. It never returns credentials.