Amazon RDS PostgreSQL & AWS Secrets Manager Integration
Run a Multi-AZ database that survives losing an availability zone, with a password no human ever types.
- Time
- 31 min
- Level
- Intermediate
- Objectives
- 4 objectives
- Cost
- Low cost
Where this fits in the platform
This lab adds
- A managed database whose password nobody has ever seen
Which lets you
Before you start
Cost — Low cost
A `db.t3.micro` RDS instance is free for 12 months on a new account and ~$13/month after. AWS Secrets Manager is $0.40 per secret per month with no free tier — small, but it does not stop on its own.
Nothing to pay in the browser. Open the terminal runs this against a simulated cloud — the same API calls and the same commands, with no account and no bill. The figure above applies only if you build it in your own.
The scenario#
The database password is in terraform.tfvars, which is in Git. The instance is single-AZ, so a zone failure is an outage of unknown length, and publicly_accessible is true because that was how somebody connected once from a laptop.
Hands-on environment
Run this lab in a real terminal, free and in your browser. The environment is temporary and yours alone — break it as much as you like.
Open the terminalOpens in Killercoda, in a new tab — keep this page open for the steps.
Run it on AWS
This lab builds real cloud infrastructure, so it needs your own AWS account. Follow the cost and cleanup notes above — the resources are yours, and so is the bill.
Anything you tick here is your own record. EgyKode cannot see inside that terminal, so the success criteria stay self-assessed even when the environment checks your work for you.
A password nobody types
Step 1 of 4
What you are building#
private subnet 1a private subnet 1b
┌──────────────────┐ ┌──────────────────┐
│ RDS primary │<=====>│ standby │ synchronous replication
└──────────────────┘ └──────────────────┘
^ ^
└──── one DNS endpoint ──────┘ failover swaps what it points toMulti-AZ is availability, not scale. The standby serves no reads and cannot be connected to. It exists so that losing an availability zone costs you 60–120 seconds of failover instead of a restore from backup. If you want read scaling, that is a read replica, and it is a different feature.
It also roughly doubles the instance cost, which is a trade-off worth stating rather than discovering.
Build it#
What you are proving: You can generate a password that appears in no file you wrote
Marking this settles success criterion 2.
resource "random_password" "db" {
length = 32
special = true
override_special = "!#$%&*()-_=+[]{}" # avoid characters that break URLs
}random_password generates a value and stores it in Terraform state. That
is the important consequence: your state file now contains a credential, which
is why the state bucket is encrypted and access-controlled. There is no way to
generate a secret in Terraform without this being true — the alternative is
letting Secrets Manager generate and rotate it, and having Terraform reference
it rather than create it.
override_special is not cosmetic. A / or @ in a password breaks a
connection string that nobody escaped, and the failure surfaces as an
authentication error rather than a parsing one.
What you are proving: You can run a database with no public endpoint, and say what Multi-AZ protects against and what it does not
Marking this settles success criteria 1 and 4.
resource "aws_db_subnet_group" "main" {
name = "platform"
subnet_ids = var.private_subnet_ids # private, in two AZs
}
resource "aws_db_instance" "main" {
identifier = "platform"
engine = "postgres"
engine_version = "16.4"
instance_class = "db.t4g.micro"
allocated_storage = 20
max_allocated_storage = 100 # autoscale storage, not compute
storage_encrypted = true
db_name = "platform"
username = "platform_admin"
password = random_password.db.result
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [var.rds_security_group_id]
publicly_accessible = false # the important line
multi_az = true
backup_retention_period = 7
backup_window = "03:00-04:00"
maintenance_window = "sun:04:00-sun:05:00"
performance_insights_enabled = true
deletion_protection = true
skip_final_snapshot = false
final_snapshot_identifier = "platform-final-${formatdate("YYYYMMDDhhmm", timestamp())}"
}max_allocated_storage enables storage autoscaling — the disk grows on its own
and never shrinks. Running out of storage on RDS takes the database down, and
this is the one-line prevention.
deletion_protection = true means terraform destroy fails until you turn it
off. That is deliberate friction, and it is correct for anything holding data.
What you are proving: You can store a credential where the application can fetch it and a person cannot read it by accident
This step settles no success criterion on its own.
resource "aws_secretsmanager_secret" "db" {
name = "platform/rds/credentials"
recovery_window_in_days = 7
}
resource "aws_secretsmanager_secret_version" "db" {
secret_id = aws_secretsmanager_secret.db.id
secret_string = jsonencode({
username = aws_db_instance.main.username
password = random_password.db.result
host = aws_db_instance.main.address
port = aws_db_instance.main.port
dbname = aws_db_instance.main.db_name
})
}Store the endpoint alongside the password. An application that reads one secret and gets everything it needs to connect never has a host name in its configuration — so a failover, a restore or a move to another region changes one secret rather than every deployment.
recovery_window_in_days means a deleted secret is recoverable for a week.
It also means the name is reserved for that week, so recreating with the same
name fails — use --force-delete-without-recovery when iterating in a lab.
What you are proving: You can retrieve credentials at runtime rather than baking them into an image
Marking this settles success criterion 3.
import boto3, json
def credentials():
client = boto3.client("secretsmanager")
raw = client.get_secret_value(SecretId="platform/rds/credentials")
return json.loads(raw["SecretString"])No password in the image, in an environment variable, or in a config file. The permission to read the secret is IRSA or an instance profile.
Verify it worked#
# Multi-AZ, private, encrypted
aws rds describe-db-instances --db-instance-identifier platform \
--query 'DBInstances[0].{multiAZ:MultiAZ,public:PubliclyAccessible,enc:StorageEncrypted,az:AvailabilityZone,standby:SecondaryAvailabilityZone}'
# The endpoint does not resolve to anything public
dig +short "$(terraform output -raw db_endpoint)" # a 10.x address
# From OUTSIDE the VPC — this must fail
nc -zv -w5 "$(terraform output -raw db_endpoint)" 5432
# From a pod or instance INSIDE — this must work
kubectl run pg --rm -it --image=postgres:16-alpine --restart=Never -- \
psql "postgresql://platform_admin:$(aws secretsmanager get-secret-value \
--secret-id platform/rds/credentials --query SecretString --output text \
| jq -r .password)@<endpoint>:5432/platform" -c "SELECT version();"
# The password is nowhere in your source
grep -ri "password" --include="*.tf" --include="*.tfvars" . | grep -v random_passwordThe pair of connection tests is the point. One proves it works; the other proves the isolation is real.
Connection times out from inside the VPC
The security group, not the database. It must allow 5432 from the client's
security group. A timeout is a firewall; connection refused would mean
something answered.
FATAL: password authentication failed with the right password
A special character was mangled somewhere in a connection string. This is what
override_special prevents.
InvalidParameterCombination: Cannot find version 16.4
Engine versions differ per region and are retired. aws rds describe-db-engine-versions --engine postgres --query 'DBEngineVersions[].EngineVersion'.
terraform destroy refuses
deletion_protection = true, working as intended. Set it false, apply, then
destroy.
Recreating the secret fails with already scheduled for deletion
The recovery window still holds the name. Delete it with
--force-delete-without-recovery when you are iterating in a lab.
Failover took longer than expected
Multi-AZ failover is 60–120 seconds and the DNS endpoint changes what it points to. Clients that cache DNS forever reconnect slowly — set a connection timeout and let the pool retry.
Clean up#
Destructive — This removes real resources. Check which environment you are in first.
aws rds modify-db-instance --db-instance-identifier platform \
--no-deletion-protection --apply-immediately
terraform destroy -auto-approve
aws secretsmanager delete-secret --secret-id platform/rds/credentials \
--force-delete-without-recovery
aws rds describe-db-snapshots --query 'DBSnapshots[].DBSnapshotIdentifier'Cost of this lab: Billable. db.t4g.micro Multi-AZ is roughly
$0.05/hour — about $30/month if left running, twice the single-AZ price.
Snapshots bill separately after the instance is gone, so check the last command.
Success criteria
0 of 4
The concept behind it
Next up
Lab 26 of 59 on the project path
Previous: Amazon ECR Container Registry & S3 Storage Buckets