RDS for PostgreSQL
Runway can provision a managed Amazon RDS for PostgreSQL
instance for your EKS workload and wire the connection details into your service automatically.
Runway owns the lifecycle of the instance and its credentials: it creates the instance in the Runway
AWS accounts, generates the master password, publishes the connection details to Vault and AWS
Secrets Manager, and injects them into your Helm chart through the Fairway
infrastructure.postgresql contract. You do not create or manage the secret yourself.
This guide assumes you already have a working Runway for Kubernetes deployment. If you do not, start with Getting Started first.
Overview
Section titled “Overview”Provisioning an instance and wiring it into your deployment touches four files across two repositories:
- Define the instance in
config/runtimes/eks/rds/managed.yml(provisioner): engine, version, region, database name, per-environment sizing, and labels. - Link it to your workload in
config/runtimes/eks/workloads.yml(provisioner): references the instance by itsnamevia anrds_instanceslist. Steps 1 and 2 go in one MR. - Declare the dependency in your Fairway manifest (
fairway.yaml):spec.infrastructure.postgresql. - Reference the instance from your Runway deployment manifest (
deployment.yaml):spec.infrastructure.rds_instance.
An instance belongs to a single service: each instance must be referenced by exactly one workload. One instance is created per environment you configure (staging, production, or both), each in its own AWS account and region, inside the Runway VPC and reachable only from the EKS cluster.
Step 1: Define the instance
Section titled “Step 1: Define the instance”File an MR to add your instance to
config/runtimes/eks/rds/managed.yml
in the Runway provisioner:
- name: runway-db-example engine: postgres engine_version: "18" region: us-east-1 database_name: example env_configuration: staging: instance_type: 2xsmall disk_size_gb: 20 production: instance_type: xsmall disk_size_gb: 50 backup_retention_days: 14 labels: gl_service: example-service-eks # required owner_email_handle: my-team department: eng-infra department_group: eng-infra-my-group product_category: my-categoryField reference
Section titled “Field reference”| Field | Required | Notes |
|---|---|---|
name |
yes | Must start with runway-db- (max 50 chars). Used as the RDS instance identifier and in the Vault and Secrets Manager paths. |
engine |
yes | Only postgres today. |
engine_version |
yes | PostgreSQL major version as a string, for example "18". Minor versions are applied automatically in the maintenance window. |
region |
yes | Must be an EKS region from config/runtimes/eks/regions.yml and one your workload is deployed to. |
database_name |
yes | The database created with the instance. Published to your workload as databaseName. |
labels |
yes | Infrastructure labels for cost attribution. gl_service is required; owner_email_handle, department, department_group, and product_category are optional. See the Infrastructure Standards. |
Per environment under env_configuration:
| Field | Required | Default | Notes |
|---|---|---|---|
instance_type |
yes | Load category, one of 2xsmall, xsmall, small, medium, large, xlarge, 2xlarge. See Sizing tiers. |
|
disk_size_gb |
yes | Allocated gp3 storage. Minimum 20. | |
max_disk_size_gb |
no | 2 × disk_size_gb |
Storage autoscaling cap. RDS grows the disk automatically when free space runs low. Set to 0 to disable autoscaling. |
multi_az |
no | true in production, false in staging |
Synchronous standby in a second availability zone. |
backup_retention_days |
no | 7 |
Automated backup retention; point-in-time recovery works within this window. 1 to 35. |
prevent_db_deletion |
no | true |
Deletion protection. See Removing an instance. |
password_version |
no | 1 |
Increase to rotate the master password. See Rotating the password. |
Sizing tiers
Section titled “Sizing tiers”Each instance_type maps to a Graviton instance class:
instance_type |
RDS class | vCPU / memory |
|---|---|---|
2xsmall |
db.t4g.medium | 2 / 4 GiB (burstable; staging and tests) |
xsmall |
db.m7g.large | 2 / 8 GiB |
small |
db.m7g.xlarge | 4 / 16 GiB |
medium |
db.m7g.2xlarge | 8 / 32 GiB |
large |
db.m7g.4xlarge | 16 / 64 GiB |
xlarge |
db.m7g.8xlarge | 32 / 128 GiB |
2xlarge |
db.m7g.16xlarge | 64 / 256 GiB |
Storage is always encrypted at rest with a dedicated KMS key, the instance is never publicly
accessible, and unencrypted connections are rejected (rds.force_ssl). These are fixed and not
configurable. Performance Insights and Enhanced Monitoring are enabled, and the maintenance window is
Sunday 06:00 to 07:00 UTC, the same as Cloud SQL on GKE.
Create the MR and assign it to a Runway team member (see
CODEOWNERS),
and ensure the pipeline passes before requesting review.
Step 2: Link the instance to your workload
Section titled “Step 2: Link the instance to your workload”In config/runtimes/eks/workloads.yml,
add an rds_instances key to your workload entry, referencing the instance by its name:
- runway_service_id: example-service-eks regions: - us-east-1 rds_instances: - runway-db-exampleThis is what tells the provisioner where to publish the connection details, and it grants your workload’s External Secrets role access to them. Inventory validation fails the MR if an instance is not linked to any workload, is linked to more than one, or is in a region the workload is not deployed to.
Step 3: Declare the dependency in your Fairway manifest
Section titled “Step 3: Declare the dependency in your Fairway manifest”In your service’s fairway.yaml, declare that the service needs PostgreSQL:
spec: infrastructure: postgresql: presence: REQUIREDDo not set spec.values.infrastructure.postgresql.secretRef yourself for a Runway-managed
instance. runwayctl sets the secret reference from the deployment manifest in Step 4 and overwrites
anything you put there. Other postgresql settings such as clientName, maxConns, or
queryExecMode are yours to set and are preserved.
Step 4: Reference the instance in your deployment manifest
Section titled “Step 4: Reference the instance in your deployment manifest”In your service’s deployment.yaml, add the instance under spec.infrastructure.rds_instance,
using the same name as in managed.yml:
apiVersion: runway/v2kind: RunwayManifestmetadata: name: example-service-eks
spec: infrastructure: rds_instance: name: runway-db-examplerunwayctl checks the two manifests against each other at deploy time:
Fairway presence |
rds_instance set |
Result |
|---|---|---|
REQUIRED |
yes | wired |
REQUIRED |
no | deploy fails |
OPTIONAL |
yes or no | wired when set |
UNUSED |
yes | deploy fails |
rds_instance is only meaningful on EKS; a GKE deployment ignores it with a warning, the same way EKS
ignores cloudsql_instance.
What gets created
Section titled “What gets created”On apply, the provisioner creates, for each configured environment:
-
The RDS instance
<name>, reachable only from the EKS cluster. -
The database named
database_name, owned by the master userrunway. -
A generated master password, applied to the instance through a write-only attribute. It is never stored in Terraform state.
-
Two secrets, published to both Vault (mount
runway) and AWS Secrets Manager under the same paths:Path Contents platform/<runway_service_id>/postgres/<name>/<environment>host,port,databaseName,username,regionplatform/<runway_service_id>/postgres/<name>/<environment>-passwordpassword
Vault is where you look as a human. Secrets Manager is what External Secrets reads on the cluster, since the EKS clusters have no connectivity to Vault. The instance itself is not reachable from outside the EKS cluster.
Consuming the instance
Section titled “Consuming the instance”Runway wires the connection details into your deployment automatically; nothing to configure.
Your service reads them through LabKit’s v2/postgres
package, the same way it would for any other postgresql dependency. postgres.New picks up the
host, port, database, username, and password from the mounted contract, so no DSN or environment
variables are needed:
import ( "context" "log"
"gitlab.com/gitlab-org/labkit/v2/postgres")
func main() { ctx := context.Background()
pg, err := postgres.New(ctx) if err != nil { log.Fatalf("postgres.New: %v", err) }
// Opens the connection pool and pings the database. if err := pg.Start(ctx); err != nil { log.Fatalf("pg.Start: %v", err) } defer pg.Shutdown(ctx)
// pg.DB() is a *sql.DB; pg.Pool() exposes the underlying pgx pool. rows, err := pg.DB().QueryContext(ctx, "SELECT id, name FROM users") if err != nil { log.Fatalf("query: %v", err) } defer rows.Close()
for rows.Next() { var ( id int name string ) if err := rows.Scan(&id, &name); err != nil { log.Fatalf("scan: %v", err) } log.Printf("user %d: %s", id, name) }}See Declaring a PostgreSQL dependency in the Fairway guide for the full contract, and the LabKit postgres README for pool tuning and tracing.
Rotating the password
Section titled “Rotating the password”Increase password_version for the environment in managed.yml and merge. In one apply, the
provisioner generates a new password, applies it to the instance, and republishes it to Vault and
Secrets Manager. External Secrets refreshes the Kubernetes Secret within ten minutes. Only ever
increase the version.
The same bump is the recovery step if an apply fails between creating the instance and writing the secrets, so that all three places agree again.
Removing an instance
Section titled “Removing an instance”Removing an instance, or one of its environments, takes two MRs, because RDS deletion protection blocks the destroy while it is on:
- Set
prevent_db_deletion: falsefor the environment(s) you are removing, merge, and let the apply run. - In a follow-up MR, remove the entry (or the environment key) from
managed.ymltogether with therds_instancesreference inworkloads.yml.
An MR that removes the entry while prevent_db_deletion is still true on the default branch fails
inventory validation. On deletion, RDS takes a final snapshot named <name>-final-<timestamp> and the
automated backups are kept for their retention period, so a mistaken removal is recoverable.
Troubleshooting
Section titled “Troubleshooting”- The deploy fails with “did not find RDS configuration despite the application declaring Postgres”:
fairway.yamlhaspresence: REQUIREDbutdeployment.yamlhas nords_instance. Add it, or change the presence toOPTIONAL.
Ask the Runway team in #f_runway for help.