A prod-orm database, and everything that hangs off it, can be declared in
Terraform: the database itself, data stores beside it, connection pools, api
keys, engine users, network access, access rules, database roles and catalog
policies — owned by you or by a team, and grouped in a team project.There is no prod-orm provider. The control plane is a plain JSON REST API, so
the module drives it with Mastercard/restapi,
a maintained generic REST provider, and reads connection details with
hashicorp/http.
It is published in the GitLab Terraform Module Registry as:source = "gitlab.com/prod-orm/prorm/restapi"
version = "~> 1.0"
The address is gitlab.com/<namespace>/<module>/<system>: the project's
top-level group (prod-orm), the module (prorm), and the provider it drives
(restapi). The module's own README — every input, every output, and what
each resource does on change and on destroy — is in the repository at
terraform/modules/prorm.Set up#
Get a token#
Every route the module calls takes Authorization: Bearer <token>. Use a
personal token for a machine with no browser: in the console, Security →
Sessions → A token for a machine with no browser (the same
POST /security/sessions/cli with purpose: "token" that PRORM_TOKEN is,
thirty days at most). An api key (prk_…) will not do: it belongs to one
database and is accepted only by the data plane.$ export TF_VAR_prorm_token=…
$ export API_DATA_IS_SENSITIVE=true
The second line tells the restapi provider to treat what it keeps about each
object as sensitive, so a plan that replaces an api key never prints the old
one.The project is private, so its module registry is too. Terraform needs a
GitLab token that can read the project's packages for the host gitlab.com:| Token | Scope | Where it fits |
|---|
| Personal, project or group access token | read_api | A laptop |
| Deploy token | read_package_registry | A shared runner or another company's CI |
CI/CD job token (CI_JOB_TOKEN) | — | A GitLab pipeline in a project allowed to reach this one |
Put it in the environment, TF_TOKEN_ plus the host with its dots as
underscores:$ export TF_TOKEN_gitlab_com=glpat-…
or in a credentials "gitlab.com" { token = "…" } block in ~/.terraformrc.The registry is namespaced by the top-level group, which is why the source is
gitlab.com/prod-orm/prorm/restapi and not the project's path. To pin one
project's registry instead, the project-level address works too:module "orders_db" {
source = "https://gitlab.com/api/v4/projects/prod-orm%2Fprod-orm/packages/terraform/modules/prorm/restapi/1.0.0"
# …
}
The published versions, and each one's README, are under the project's
Operate › Terraform modules.The module leaves the provider to you, so it can be used with count,
for_each and depends_on. create_returns_object is not optional: the
answer to a create is the only place a new object's id — and a key's secret —
ever appears.terraform {
required_providers {
restapi = { source = "Mastercard/restapi", version = "~> 3.0" }
http = { source = "hashicorp/http", version = "~> 3.4" }
}
}
variable "prorm_token" {
type = string
sensitive = true
}
provider "restapi" {
uri = "https://api.prod-orm.io"
bearer_token = var.prorm_token
create_returns_object = true
}
Declare a database#
module "orders_db" {
source = "gitlab.com/prod-orm/prorm/restapi"
version = "~> 1.0"
name = "orders"
engine = "postgres"
size = 2048 # MB
api_token = var.prorm_token # read GET …/connect for host, port and DSN
api_keys = { "orders-api" = { privilege = "readwrite", expires_in_days = 90 } }
engine_users = { analyst = { privilege = "readonly" } }
network_access = {
mode = "cloud"
allowed_ips = ["203.0.113.0/24"]
}
policies = { "auth.failed.standard.rotate" = {} }
}
$ terraform init
$ terraform apply
$ terraform output -json | jq '.'
A team's database in a project#
With team_id, the database and its stores are made in the team's context:
the team owns them, pays for them, and keeps them when you leave. project
either names an existing project by id or has the module create one.module "billing_db" {
source = "gitlab.com/prod-orm/prorm/restapi"
version = "~> 1.0"
name = "billing"
engine = "postgres"
size = 4096
team_id = 42
project = { name = "billing", environment = "production" }
stores = { "billing-cache" = { type = "redis", size = 256, part = "cache" } }
pools = { "billing-api" = { max_connections = 25 } }
roles = {
reporting = { ops = ["read"], tables = ["invoices", "payments"] }
}
api_keys = {
"billing-api" = { privilege = "readwrite" }
reporting = { role = "reporting", expires_in_days = 30 }
}
access_rules = {
"no-sanctioned" = { effect = "block", dimension = "country", value = "KP", mode = "enforce" }
}
}
Your role in the team must carry resource:provision and project:write.Secrets#
Api keys and engine-user passwords are shown by the API once, in the answer
to the create. The module keeps that answer and exposes the secrets only as
sensitive outputs — api_key_secrets, engine_user_passwords and
connection_strings (the DSN from /connect with each user's password in
it):$ terraform output -json api_key_secrets | jq -r '."orders-api"'
prk_99cbd6a2.sGCbE…
They are in the state as well, as every Terraform-made secret is: keep state
in an encrypted backend with restricted access. A key or a user cannot be
edited, so changing one replaces it — the old one is revoked and a new secret
issued. To rotate on purpose:$ terraform apply -replace='module.orders_db.restapi_object.api_key["orders-api"]'
[!WARNING]
The token in TF_VAR_prorm_token acts as you until it expires or is
revoked. Keep it in your CI system's secret store and revoke it
(prorm sessions revoke <id>) when the pipeline no longer needs it.
What it does not do#
A generic REST provider and this API do not meet everywhere, and the module
says where rather than pretending:Drift. A read answers the object inside a wrapper ({ "database": … }),
so Terraform keeps what you declared and puts it back only when the
declaration changes. An edit made in the console is not reverted.
Read-once fields. size, cpu, memory and team_id are read at
create. Resize with resize_to, which asks POST …/resize.
Project entries are not taken out again on destroy: the attach does not
answer with the entry's id. A project the module created is deleted with them.
Network access has no DELETE; removing it leaves the last record.
The last open pool of a database cannot be closed; terraform state rm
it before destroying.
Access rules that would lock out the runner applying them are refused
(409 would_lock_you_out); the module never sends force.
Not modelled: passwordless grants (use prorm roles grant), ssh keys,
backups, replicas, pause and resume, and teams themselves.
See the module README for the full list, and the CLI for the
operations Terraform does not cover.Publishing a version#
The module lives in this repository at terraform/modules/prorm, and CI checks
it with the rest of terraform/ (tf:fmt, tf:validate, tf:tfsec). To
release, add a ## x.y.z entry to its CHANGELOG.md, push a tag
terraform-prorm-vx.y.z, and run the manual publish:terraform-module job;
it uploads the module to PUT /projects/:id/packages/terraform/modules/prorm/restapi/x.y.z/file
with the job's token. The registry refuses a version that already exists. Modified at 2026-10-04 00:40:16