RDS configuration per environment¶
How a client environment's database sizing, backup retention, and read replica are configured, how each kind of change is applied, and where the availability guarantees come from.
The declaration chain¶
Six values are per-environment GitHub Environment variables: RDS_ALLOCATED_STORAGE, RDS_MAX_ALLOCATED_STORAGE, RDS_INSTANCE_CLASS, RDS_BACKUP_RETENTION_DAYS, ENABLE_READ_REPLICA, and REPLICA_INSTANCE_CLASS.
The full table with defaults and examples lives in the GitHub Environment Variables Reference.
The chain has one rule: a GitHub variable reaches Terraform only because deploy-aws.yml emits it as a TF_VAR_* line, and for these six the line is emitted only when the variable is non-empty.
An unset variable therefore falls through to the single default, which lives in infrastructure/base/variables.tf - the module variables in infrastructure/modules/rds/variables.tf deliberately have no defaults, so no second copy can drift.
The variables are declarative: removing one from a GitHub Environment reverts that knob to the default on the next deploy.
Value floors are validated in the module and fail the plan with an error naming the GitHub variable: storage at least 20 GB, the autoscaling ceiling 0 (off) or at least the allocated storage, retention between 1 and 35 days, and both classes shaped like db.*.
The retention floor of 1 exists so no environment can turn off point-in-time recovery.
The default retention is 7 days, the shortest window the AWS Foundational Security Best Practices standard accepts, so an environment that sets RDS_BACKUP_RETENTION_DAYS should raise it rather than lower it.
Two attributes are fixed for every environment rather than being knobs. The instance copies its tags to every snapshot, so a restored database is still identifiable by environment. IAM database authentication is on alongside the password login the application uses, so an operator can connect with a short-lived IAM token instead of the shared credential.
How each change applies¶
The design rule is that a deploy never reduces availability, so the six knobs split into three behaviors.
Immediately at deploy time. Storage growth, the autoscaling ceiling, backup retention, the replica toggle, and the replica class apply in place when the operator deploys (apply_immediately = true in the module).
None of these touch primary availability.
Three caveats.
RDS only ever grows storage: with autoscaling off a decrease fails the deploy with the AWS error, and with autoscaling on the provider ignores a lower value.
A large storage increase keeps the deploy job running while RDS finishes storage optimization, which can take hours - the database serves normally throughout.
A replica class change reboots only the replica, degrading agent reads for a few minutes while the primary serves normally.
Blue/green for a live instance's class. instance_class sits in ignore_changes next to engine_version, so Terraform never resizes a live database - an in-place class change would be a five-to-fifteen-minute reboot.
Instead the class is a declaration, and the db-upgrade workflow performs the transition with a switchover measured in seconds, exactly as it does for PostgreSQL majors.
Until an operator runs it, every deploy prints a declared-vs-actual warning so the pending change cannot be quietly forgotten.
The workflow's class mode, and the convergence rule that a run applies the full declaration, are documented in PostgreSQL version upgrades.
At birth. A new instance is created with its declared class directly, so a sandbox that sets RDS_INSTANCE_CLASS before its first deploy gets small hardware from the start with no blue/green ceremony.
The read replica, end to end¶
ENABLE_READ_REPLICA=true provisions an RDS read replica of the primary (infrastructure/modules/rds/main.tf, resource campuscore_replica); replication is asynchronous, so reads there can lag the primary briefly.
The flag then travels to the application without further configuration.
The base layer exports rds_replica_enabled and rds_replica_endpoint, the app layer turns them into the USE_READ_REPLICA, DB_REPLICA_HOST, and DB_REPLICA_PORT container env vars on every task definition (infrastructure/app/container_env.tf), and Django adds a replica database alias when the flag is on (campus_core/settings.py).
AgentReadRouter (campus_core/db_routers.py) then steers reads for the replica-safe knowledge models to the replica, with force_primary() as the read-after-write escape hatch; writes always go to the primary.
Turning the flag off reverses the whole chain on the next deploy - the router is not even registered when the replica is absent.
During a blue/green transition, AWS rebuilds replicas against green and switches them over with the primary, so the flag needs no special handling there.
The VPC-cutover coupling¶
scripts/vpc-cutover-rds.sh restores a snapshot during a one-time VPC migration, and its restore parameters must match what the environment actually ran or the post-import Terraform apply stops being a no-op.
Because the class and parameter group are now per-environment, snapshot-and-delete captures both from the live instance before deleting it and prints the exact restore command; restore requires them as arguments and has no defaults.
The runbook is VPC migration cutover.