AWS RDS Setup
Creating an RDS instance correctly. Subnet groups, the settings you cannot change later, parameter groups, connecting from a private subnet, and first-run checks.
Creating an RDS instance takes one command. Creating one you will not regret takes a handful of decisions, several of which are permanent.
What you need first
A DB subnet group. RDS requires subnets in at least two Availability Zones, even for a single-AZ instance, because it needs somewhere to fail over to if you later enable Multi-AZ.
aws rds create-db-subnet-group \
--db-subnet-group-name payments-db \
--db-subnet-group-description "Private database subnets" \
--subnet-ids subnet-db-a subnet-db-b subnet-db-c
Use private subnets with no route to an internet gateway. A database should not be reachable from the internet under any circumstances.
A security group that references your application's security group rather than a CIDR range:
aws ec2 create-security-group --group-name payments-db-sg \
--description "Payments database" --vpc-id vpc-0abc123
aws ec2 authorize-security-group-ingress \
--group-id sg-database --protocol tcp --port 5432 \
--source-group sg-application
Referencing the application's security group means the rule keeps working as instances are replaced and scaled, which a CIDR-based rule does not.
Creating the instance
aws rds create-db-instance \
--db-instance-identifier payments-prod \
--db-instance-class db.m6g.large \
--engine postgres --engine-version 16.4 \
--master-username dbadmin \
--manage-master-user-password \
--allocated-storage 100 \
--max-allocated-storage 500 \
--storage-type gp3 \
--storage-encrypted \
--db-subnet-group-name payments-db \
--vpc-security-group-ids sg-database \
--multi-az \
--backup-retention-period 14 \
--preferred-backup-window "02:00-03:00" \
--preferred-maintenance-window "sun:04:00-sun:05:00" \
--enable-performance-insights \
--no-publicly-accessible \
--deletion-protection
Several of those are worth explaining.
--manage-master-user-password has RDS create and rotate the master password in Secrets Manager automatically. There is then no password in your shell history, your Terraform state or anyone's notes. Use it.
db.m6g is Graviton, around 20% cheaper than the Intel equivalent for the same performance.
--max-allocated-storage enables storage autoscaling, so the instance grows when it approaches full rather than failing at 3am. Storage can only ever grow, so set the ceiling thoughtfully.
--storage-encrypted must be set now. See below.
--deletion-protection requires an explicit disable before the instance can be deleted, which is a cheap guard on something irreversible.
The settings you cannot change later
| Setting | Why it matters |
|---|---|
| Encryption at rest | Cannot be enabled afterwards. Migration means snapshot, copy encrypted, restore |
| VPC | An instance cannot move to a different VPC |
| Engine | PostgreSQL to MySQL is a migration, not a setting |
| Storage can only grow | Over-provisioning is permanent cost |
Encryption is the one that catches people. It costs nothing, so there is no reason not to enable it on every instance, and no convenient path back if you forget.
Parameter and option groups
The default parameter group cannot be modified. To change any database setting you need your own:
aws rds create-db-parameter-group \
--db-parameter-group-name payments-pg16 \
--db-parameter-group-family postgres16 \
--description "Payments tuning"
aws rds modify-db-parameter-group \
--db-parameter-group-name payments-pg16 \
--parameters "ParameterName=log_min_duration_statement,ParameterValue=1000,ApplyMethod=immediate"
Create one at instance creation even if empty, because attaching one later requires a reboot.
Parameters are either dynamic (applied immediately) or static (requiring a reboot). describe-engine-default-parameters tells you which, and a static parameter change silently waits until the next reboot, which is confusing if you expect an immediate effect.
Connecting
# The endpoint
aws rds describe-db-instances --db-instance-identifier payments-prod \
--query 'DBInstances[0].Endpoint.{Address:Address,Port:Port}'
# The managed password
aws secretsmanager get-secret-value \
--secret-id $(aws rds describe-db-instances --db-instance-identifier payments-prod \
--query 'DBInstances[0].MasterUserSecret.SecretArn' --output text) \
--query SecretString --output text | jq -r .password
The instance is in a private subnet, so connecting from a laptop needs a tunnel. Session Manager port forwarding does this without a bastion host:
aws ssm start-session --target i-0bastion \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters '{"host":["payments-prod.abc123.eu-west-1.rds.amazonaws.com"],"portNumber":["5432"],"localPortNumber":["5432"]}'
psql -h localhost -U dbadmin -d payments
Always use the endpoint DNS name, never a resolved IP. The address changes on failover, and an application caching the IP will fail in a way that looks like a network problem.
Use IAM authentication where you can
aws rds modify-db-instance \
--db-instance-identifier payments-prod \
--enable-iam-database-authentication \
--apply-immediately
Applications then request a short-lived token instead of holding a password:
TOKEN=$(aws rds generate-db-auth-token \
--hostname payments-prod.abc123.eu-west-1.rds.amazonaws.com \
--port 5432 --username app_user)
Tokens last 15 minutes, so there is no long-lived database credential in your application at all.
Maintenance and backups
Both windows are worth setting explicitly rather than accepting a random assignment.
Put the backup window before the maintenance window, and both during genuinely low traffic. On Multi-AZ, backups are taken from the standby so there is no performance impact on the primary. One of the less-known benefits of Multi-AZ.
Minor version upgrades apply during the maintenance window with --auto-minor-version-upgrade enabled, which is the default and generally worth keeping. Major version upgrades are always deliberate: test against a restored snapshot first, because extension compatibility and query plan changes routinely surprise people.
First-run checks
# Is it actually available?
aws rds describe-db-instances --db-instance-identifier payments-prod \
--query 'DBInstances[0].{Status:DBInstanceStatus,MultiAZ:MultiAZ,Encrypted:StorageEncrypted,Public:PubliclyAccessible}'
# Prove failover works, before you need it to
aws rds reboot-db-instance --db-instance-identifier payments-prod --force-failover
That second command is worth running in a non-production environment at least once. Failover takes 60 to 120 seconds and your application's reconnection behaviour during it is something to discover deliberately rather than during an incident.
Go deeper in this section
| Sub-topic | What you will learn |
|---|---|
| AWS RDS | Multi-AZ vs read replicas, backups, RDS Proxy |
| RDS Security | Network isolation, encryption and credentials |
Quick reference
| Task | Command |
|---|---|
| Create a subnet group | aws rds create-db-subnet-group --db-subnet-group-name <n> --subnet-ids <ids> |
| Create an instance | aws rds create-db-instance ... |
| Get the endpoint | aws rds describe-db-instances --db-instance-identifier <id> --query 'DBInstances[0].Endpoint' |
| Managed password | aws secretsmanager get-secret-value --secret-id <arn> |
| Modify | aws rds modify-db-instance --db-instance-identifier <id> --apply-immediately |
| Test failover | aws rds reboot-db-instance --db-instance-identifier <id> --force-failover |
| Recent events | aws rds describe-events --source-identifier <id> --source-type db-instance |
| IAM auth token | aws rds generate-db-auth-token --hostname <e> --port 5432 --username <u> |
Key takeaways
- A DB subnet group needs at least two Availability Zones, even for single-AZ.
- Encryption at rest cannot be added later. Always create encrypted; it is free.
- Use
--manage-master-user-passwordso the password lives in Secrets Manager and rotates itself. - Storage only grows: enable autoscaling with
--max-allocated-storageand set the ceiling thoughtfully. - Create a custom parameter group at creation, since attaching one later needs a reboot.
- Reference the application's security group, not a CIDR range.
- Always connect via the endpoint DNS name: the address changes on failover.
- Test a forced failover in non-production before you depend on it.
Check your understanding
Encrypting an existing database
A production RDS instance was created without encryption. What is required to encrypt it?
An application that breaks on failover
After a Multi-AZ failover, an application cannot reach the database until it is restarted. What is the likely cause?
Frequently Asked Questions
Why does RDS need subnets in two Availability Zones?
Because a DB subnet group defines where RDS may place the instance and any future standby, and it requires at least two zones even for a single-AZ deployment. That way enabling Multi-AZ later needs no network changes. Use private subnets with no route to an internet gateway. A database should never be reachable from the internet, and --no-publicly-accessible enforces that on the instance itself.
Can I enable encryption on an existing RDS instance?
No. Encryption at rest is set at creation and cannot be changed. To encrypt an existing database you snapshot it, copy the snapshot specifying a KMS key, restore a new instance from the encrypted copy, and cut over, which means downtime or a replication exercise. Since there is no extra charge for encryption, enable it on every instance at creation as a matter of course.
How should I manage the RDS master password?
Use --manage-master-user-password, which has RDS generate the password, store it in Secrets Manager and rotate it automatically. Nothing sensitive then appears in your shell history, your Terraform state or a colleague's notes. For application connections, go further and enable IAM database authentication, where applications request a 15-minute token instead of holding a password at all.
How do I connect to an RDS instance in a private subnet?
Through a tunnel. Session Manager port forwarding does it without a bastion host: start a session against any instance with the SSM agent using the AWS-StartPortForwardingSessionToRemoteHost document, pointing at the RDS endpoint, then connect your client to localhost. This needs no open inbound ports, no SSH keys, and access is controlled by IAM with every session logged to CloudTrail.
What is a parameter group and do I need a custom one?
A parameter group holds the database engine's configuration settings. The default group attached to a new instance cannot be modified, so changing any setting requires creating your own. Create one at instance creation even if it initially matches the defaults, because attaching a parameter group to an existing instance requires a reboot. Note that static parameters only take effect after a reboot, which is a common source of confusion when a change appears to have done nothing.
What should the maintenance and backup windows be set to?
Both during your genuinely lowest-traffic period, with the backup window scheduled before the maintenance window. Setting them explicitly is better than accepting the random assignment AWS makes, which may land in the middle of your business day. On Multi-AZ instances backups are taken from the standby, so there is no performance impact on the primary. A useful and often overlooked benefit of Multi-AZ beyond the availability itself.