AWS RDS Setup

By Vishvesh Patel · DevOps EngineerUpdated September 15, 2026

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

SettingWhy it matters
Encryption at restCannot be enabled afterwards. Migration means snapshot, copy encrypted, restore
VPCAn instance cannot move to a different VPC
EnginePostgreSQL to MySQL is a migration, not a setting
Storage can only growOver-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-topicWhat you will learn
AWS RDSMulti-AZ vs read replicas, backups, RDS Proxy
RDS SecurityNetwork isolation, encryption and credentials

Quick reference

TaskCommand
Create a subnet groupaws rds create-db-subnet-group --db-subnet-group-name <n> --subnet-ids <ids>
Create an instanceaws rds create-db-instance ...
Get the endpointaws rds describe-db-instances --db-instance-identifier <id> --query 'DBInstances[0].Endpoint'
Managed passwordaws secretsmanager get-secret-value --secret-id <arn>
Modifyaws rds modify-db-instance --db-instance-identifier <id> --apply-immediately
Test failoveraws rds reboot-db-instance --db-instance-identifier <id> --force-failover
Recent eventsaws rds describe-events --source-identifier <id> --source-type db-instance
IAM auth tokenaws 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-password so the password lives in Secrets Manager and rotates itself.
  • Storage only grows: enable autoscaling with --max-allocated-storage and 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

Exercise

Encrypting an existing database

A production RDS instance was created without encryption. What is required to encrypt it?

Exercise

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.

Continue Learning

Explore Related Topics

When it breaks

Related Resources