Skip to content

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.

Provisioning an instance and wiring it into your deployment touches four files across two repositories:

  1. Define the instance in config/runtimes/eks/rds/managed.yml (provisioner): engine, version, region, database name, per-environment sizing, and labels.
  2. Link it to your workload in config/runtimes/eks/workloads.yml (provisioner): references the instance by its name via an rds_instances list. Steps 1 and 2 go in one MR.
  3. Declare the dependency in your Fairway manifest (fairway.yaml): spec.infrastructure.postgresql.
  4. 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.

File an MR to add your instance to config/runtimes/eks/rds/managed.yml in the Runway provisioner:

config/runtimes/eks/rds/managed.yml
- 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-category
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.

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.

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:

config/runtimes/eks/workloads.yml
- runway_service_id: example-service-eks
regions:
- us-east-1
rds_instances:
- runway-db-example

This 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:

.runway/fairway.yaml
spec:
infrastructure:
postgresql:
presence: REQUIRED

Do 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:

.runway/deployment.yaml
apiVersion: runway/v2
kind: RunwayManifest
metadata:
name: example-service-eks
spec:
infrastructure:
rds_instance:
name: runway-db-example

runwayctl 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.

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 user runway.

  • 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, region
    platform/<runway_service_id>/postgres/<name>/<environment>-password password

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.

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.

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, or one of its environments, takes two MRs, because RDS deletion protection blocks the destroy while it is on:

  1. Set prevent_db_deletion: false for the environment(s) you are removing, merge, and let the apply run.
  2. In a follow-up MR, remove the entry (or the environment key) from managed.yml together with the rds_instances reference in workloads.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.

  • The deploy fails with “did not find RDS configuration despite the application declaring Postgres”: fairway.yaml has presence: REQUIRED but deployment.yaml has no rds_instance. Add it, or change the presence to OPTIONAL.

Ask the Runway team in #f_runway for help.