Skip to content

RDS for PostgreSQL

Runway can provision an Amazon RDS for PostgreSQL instance in the AWS account of every Dedicated tenant that enables your module, and wire it into your Helm chart through the Fairway infrastructure.postgresql contract. Runway owns the instance and its credentials: runwayctl dedicated provision creates the instance and generates the master password, and runwayctl dedicated deploy gives your workload the connection details. You don’t create or manage the secret yourself.

This page assumes your module is set up for Dedicated. If it isn’t, start with Getting Started.

An instance is configured in three places, and each setting comes from the first one that sets it:

  1. The tenant’s override in the Tenant Model, under modular_features.modules.<module-name>.values_override.infrastructure.rds.<name>.
  2. Your default in the module manifest, under spec.infrastructure.rds.<name>.defaultConfig, which catalog create copies into the Module Catalog entry.
  3. The built-in default in the field reference.

<name> is the instance’s logical name, a name of your choice that the module manifest and the Tenant Model share. Entries are merged by logical name and then field by field, so a tenant only sets what differs. Whether your module needs PostgreSQL at all is its postgresql presence in the Fairway manifest.

Step 1: Declare the dependency in your Fairway manifest

Section titled “Step 1: Declare the dependency in your Fairway manifest”
.runway/fairway.yaml
spec:
infrastructure:
postgresql:
presence: REQUIRED
Presence Module manifest Tenant Model, after the merge
REQUIRED Needs at least one enabled default. Needs exactly one enabled instance.
OPTIONAL Any defaults, enabled or not, or none. At most one enabled instance.
UNUSED Must have no defaults, not even disabled ones. Must have no enabled instance.

catalog create checks the module manifest against the presence when you release the module. provision and deploy check the merged result on every tenant.

Don’t set spec.values.infrastructure.postgresql.secretRef yourself. Runway sets it, and overwrites anything you put there with a warning. Other postgresql settings, such as clientName or pool sizes, are yours to set and are kept.

Step 2: Set the defaults in your module manifest

Section titled “Step 2: Set the defaults in your module manifest”
.runway/module.yaml
apiVersion: runway/v3
kind: RunwayManifest
metadata:
name: example-service-dedicated
spec:
infrastructure:
rds:
main:
defaultConfig:
databaseName: example
instanceType: small
diskSizeGb: 50

A defaultConfig must be valid on its own, without any override, including when it is disabled. databaseName has no built-in default, so every RDS defaultConfig sets it. Field names can be in camelCase or snake_case.

A tenant that is happy with your defaults needs no infrastructure block. To change a setting for one tenant, set only that field:

Tenant Model
{
"modular_features": {
"modules": {
"example-service-dedicated": {
"enabled": true,
"version": "v1.26.0",
"values_override": {
"infrastructure": {
"rds": {
"main": { "instance_type": "large", "max_disk_size_gb": 0 }
}
}
}
}
}
}
}

For this tenant, the instance is large with storage autoscaling turned off, from the Tenant Model; has the database example and 50 GB of storage, from the module manifest; and runs PostgreSQL 18 in two availability zones, from the built-in defaults. An explicit 0 or false overrides a default, so a tenant can turn off any setting that is on by default.

The override is partial: it only has to parse, with the right types and no unknown fields. The merged instance is validated as a complete configuration, where an unset field stands for its built-in default. An override that is valid on its own can still give an invalid result: max_disk_size_gb: 30 fails with the default disk_size_gb: 50 above. A tenant can also add an instance that has no default, which must then be complete on its own, including database_name.

InfrastructureConfigRDS in runwayctl’s rds.proto. Unknown fields are rejected.

Field Type Default Description
enabled boolean true Provisions the instance. See Enabling and disabling.
database_name string none Required. Name of the database created on the instance. Must be a valid unquoted PostgreSQL identifier: ^[a-z_][a-z0-9_]{0,62}$.
instance_type string xsmall Instance type, as a load category.
disk_size_gb integer 20 Allocated storage in GB (gp3). At least 20.
max_disk_size_gb integer twice disk_size_gb Upper bound for storage autoscaling, in GB. Must be greater than disk_size_gb, or 0 to disable autoscaling.
engine_version string "18" PostgreSQL major version, digits only. RDS applies minor upgrades automatically.
multi_az boolean true Deploys a synchronous standby in a second availability zone.
prevent_db_deletion boolean true Enables deletion protection. See Removing an instance.
password_version integer 1 Version of the master password. At least 1. See Rotating the password.

Logical names are lowercase letters, digits, and hyphens, starting and ending with a letter or digit, at most 45 characters.

Backup retention isn’t configurable per instance. Every instance keeps automated backups for the tenant backup retention, the Tenant Model’s top-level backup_retention_days (default 30), capped at RDS’s maximum of 35 days. The tenant’s AWS Backup plan owns the backups.

instance_type takes a load category, which maps to an RDS Graviton instance class:

instance_type RDS instance class
2xsmall db.t4g.medium
xsmall db.m7g.large
small db.m7g.xlarge
medium db.m7g.2xlarge
large db.m7g.4xlarge
xlarge db.m7g.8xlarge
2xlarge db.m7g.16xlarge

Every instance has an enabled field. Unset means enabled.

  • Off by default: set enabled: false in the defaultConfig. The defaults are still checked, so that they are sane when a tenant turns the instance on with enabled: true, without repeating the other settings. A REQUIRED PostgreSQL dependency can’t be off by default.
  • Off for one tenant: the tenant sets enabled: false. Runway doesn’t provision the instance, deploys no Secret and no Helm values for it, and doesn’t check its other settings. For a REQUIRED dependency, this is an error.

Disabling an instance that was provisioned before has the same effect as removing it from the Tenant Model: the next provision destroys it. Deletion protection (prevent_db_deletion, on by default) makes RDS refuse, so removing an instance takes two steps:

  1. Set prevent_db_deletion: false for the instance in the Tenant Model, and provision the module.
  2. Set enabled: false, or remove the entry if it has no default, and provision the module again.

provision creates, in the tenant’s AWS account and region:

  • The RDS instance <tenant-id>-<module-name>-<name>-<digest>. The digest is the first 8 hex characters of the SHA-256 of <account>/<tenant-id>/<module-name>/<name>, so two tenants or modules never collide. If the identifier exceeds 63 characters, <module-name>-<name> is shortened, keeping the tenant prefix and the digest. The instance is placed in the tenant’s database subnets and accepts connections only from the tenant’s <tenant-id>-internal-networking security group. It is encrypted with the tenant’s customer-managed RDS key from the Tenant Model, or with a key per instance if it has none.

  • The database database_name, owned by the master user runway.

  • A generated master password, applied to the instance through a write-only attribute, so that it is never stored in the Terraform state.

  • Two secrets in AWS Secrets Manager, encrypted with alias/<tenant-id>-tenant-secrets-kms-key:

    Secret Keys
    runway/<module-name>/postgresql/<name> host, port, dbname, username
    runway/<module-name>/postgresql/<name>/password password

deploy then:

  • writes the Kubernetes Secret rds-<name> in the module’s namespace, with the keys host, port, dbname, username, and password;
  • points infrastructure.postgresql.secretRef in your Helm values at it.

Your service reads the connection details through the Fairway contract, for example with LabKit’s v2/postgres package, the same way as on EKS.

  1. Increase password_version in the Tenant Model, and provision the module. Runway generates a new password and applies it to the instance and to AWS Secrets Manager in the same apply.
  2. Deploy the module, which updates the Kubernetes Secret.
  3. Restart the module’s workloads, so that they read the new password.

Only ever increase the version. Connections that are already open keep working, but new connections fail between the provision and the restart.