There are two shapes of relational database on AWS, and provisioning them differs in a way worth internalizing early. A standard RDS database is a single instance you create in one call. Aurora is a cluster whose storage and compute are separate, so you create the cluster first and then attach instances to it.
This page provisions both, side by side, with encryption and Secrets Manager credentials on from the start - then shows where the app takes over with a driver.
# --- Python (boto3) ---import boto3rds = boto3.client("rds", region_name="us-east-1")# Standard RDS: one call, Multi-AZ for a failover standby, encrypted at rest.rds.create_db_instance( DBInstanceIdentifier="app-db", Engine="postgres", DBInstanceClass="db.t3.medium", AllocatedStorage=50, MultiAZ=True, # standby in another AZ (failover only) StorageEncrypted=True, # encrypt at rest with the default KMS key MasterUsername="appuser", ManageMasterUserPassword=True, # credential lives in Secrets Manager)
// --- TypeScript (AWS SDK v3) ---import { RDSClient, CreateDBInstanceCommand } from "@aws-sdk/client-rds";const rds = new RDSClient({ region: "us-east-1" });// Standard RDS: one call, Multi-AZ for a failover standby, encrypted at rest.await rds.send(new CreateDBInstanceCommand({ DBInstanceIdentifier: "app-db", Engine: "postgres", DBInstanceClass: "db.t3.medium", AllocatedStorage: 50, MultiAZ: true, // standby in another AZ (failover only) StorageEncrypted: true, // encrypt at rest with the default KMS key MasterUsername: "appuser", ManageMasterUserPassword: true, // credential lives in Secrets Manager}));
When to reach for this:
Standing up a durable relational database for an app that needs SQL, joins, or transactions.
Choosing between a single Multi-AZ instance and an Aurora writer/reader cluster.
Automating environment creation with encryption and managed credentials from the start.
Learning the RDS and Aurora control planes before wrapping them in IaC.
Provision a Multi-AZ PostgreSQL instance, then an Aurora PostgreSQL cluster with a writer and one reader. Wait on the right waiter for each, then read the endpoints your driver will use.
# --- Python (boto3) ---import boto3rds = boto3.client("rds", region_name="us-east-1")# === A) Standard RDS instance (single endpoint, Multi-AZ standby) ===rds.create_db_instance( DBInstanceIdentifier="app-db", Engine="postgres", DBInstanceClass="db.t3.medium", AllocatedStorage=50, MultiAZ=True, StorageEncrypted=True, MasterUsername="appuser", ManageMasterUserPassword=True,)rds.get_waiter("db_instance_available").wait(DBInstanceIdentifier="app-db")inst = rds.describe_db_instances(DBInstanceIdentifier="app-db")["DBInstances"][0]print("RDS endpoint:", inst["Endpoint"]["Address"], inst["Endpoint"]["Port"])# === B) Aurora cluster: create the cluster (storage), then instances (compute) ===rds.create_db_cluster( DBClusterIdentifier="app-aurora", Engine="aurora-postgresql", StorageEncrypted=True, MasterUsername="appuser", ManageMasterUserPassword=True,)# Writer: first instance attached to the cluster.rds.create_db_instance( DBInstanceIdentifier="app-aurora-writer", DBClusterIdentifier="app-aurora", Engine="aurora-postgresql", DBInstanceClass="db.r6g.large",)# Reader: a second instance in the same cluster.rds.create_db_instance( DBInstanceIdentifier="app-aurora-reader-1", DBClusterIdentifier="app-aurora", Engine="aurora-postgresql", DBInstanceClass="db.r6g.large",)rds.get_waiter("db_cluster_available").wait(DBClusterIdentifier="app-aurora")cl = rds.describe_db_clusters(DBClusterIdentifier="app-aurora")["DBClusters"][0]print("Aurora writer:", cl["Endpoint"]) # writes hereprint("Aurora reader:", cl["ReaderEndpoint"]) # read-only queries here
// --- TypeScript (AWS SDK v3) ---import { RDSClient, CreateDBInstanceCommand, CreateDBClusterCommand, DescribeDBInstancesCommand, DescribeDBClustersCommand, waitUntilDBInstanceAvailable, waitUntilDBClusterAvailable,} from "@aws-sdk/client-rds";const rds = new RDSClient({ region: "us-east-1" });// === A) Standard RDS instance (single endpoint, Multi-AZ standby) ===await rds.send(new CreateDBInstanceCommand({ DBInstanceIdentifier: "app-db", Engine: "postgres", DBInstanceClass: "db.t3.medium", AllocatedStorage: 50, MultiAZ: true, StorageEncrypted: true, MasterUsername: "appuser", ManageMasterUserPassword: true,}));await waitUntilDBInstanceAvailable({ client: rds, maxWaitTime: 1200 }, { DBInstanceIdentifier: "app-db" });const di = await rds.send(new DescribeDBInstancesCommand({ DBInstanceIdentifier: "app-db" }));const ep = di.DBInstances![0].Endpoint!;console.log("RDS endpoint:", ep.Address, ep.Port);// === B) Aurora cluster: create the cluster (storage), then instances (compute) ===await rds.send(new CreateDBClusterCommand({ DBClusterIdentifier: "app-aurora", Engine: "aurora-postgresql", StorageEncrypted: true, MasterUsername: "appuser", ManageMasterUserPassword: true,}));// Writer: first instance attached to the cluster.await rds.send(new CreateDBInstanceCommand({ DBInstanceIdentifier: "app-aurora-writer", DBClusterIdentifier: "app-aurora", Engine: "aurora-postgresql", DBInstanceClass: "db.r6g.large",}));// Reader: a second instance in the same cluster.await rds.send(new CreateDBInstanceCommand({ DBInstanceIdentifier: "app-aurora-reader-1", DBClusterIdentifier: "app-aurora", Engine: "aurora-postgresql", DBInstanceClass: "db.r6g.large",}));await waitUntilDBClusterAvailable({ client: rds, maxWaitTime: 1200 }, { DBClusterIdentifier: "app-aurora" });const dc = await rds.send(new DescribeDBClustersCommand({ DBClusterIdentifier: "app-aurora" }));const cl = dc.DBClusters![0];console.log("Aurora writer:", cl.Endpoint); // writes hereconsole.log("Aurora reader:", cl.ReaderEndpoint); // read-only queries here
What this demonstrates:
A standard instance is one call; MultiAZ=True adds a failover standby, not a read replica.
Aurora needs CreateDBCluster (storage) before any CreateDBInstance (compute) attaches to it.
The first instance in a cluster becomes the writer; additional instances become readers.
Encryption and managed credentials are set at create time, not bolted on later.
A standard RDS database couples storage and compute into one instance. AllocatedStorage sizes its disk, DBInstanceClass sizes its CPU and memory, and there is exactly one endpoint. MultiAZ=True provisions a synchronous standby in a second Availability Zone; AWS promotes it automatically on failure, but it never serves queries and does not reduce read load.
Aurora decouples the two. The cluster owns a distributed, self-healing storage volume replicated six ways across three AZs. Compute is separate: each CreateDBInstance with a DBClusterIdentifier adds a node that reads and writes that shared volume. Because storage is shared, adding a reader does not copy data - the new instance simply attaches. That is why Aurora scales reads by adding instances in seconds rather than provisioning and replicating a full copy.
Use db_instance_available when you provision a standard instance, and db_cluster_available when you provision Aurora. The cluster waiter returns when the cluster and its members are ready. A create call returns in milliseconds with the resource in creating; the endpoint is unusable until the waiter completes, which can take several minutes (longer for Multi-AZ). Give the waiter a generous maxWaitTime so it does not time out before a slow provisioning finishes.
StorageEncrypted=True enables encryption at rest with the default KMS key (pass KmsKeyId for a customer-managed key). You cannot toggle encryption on an existing unencrypted database in place - you would snapshot, copy the snapshot with encryption, and restore. So decide at create time. Likewise, ManageMasterUserPassword=True puts the credential in Secrets Manager from the first moment, avoiding a plaintext password in your provisioning code.
Real deployments also pass DBSubnetGroupName and VpcSecurityGroupIds to place the database in private subnets and control access, and leave PubliclyAccessible at its default of false. Those are omitted above for focus but are essential in production - a database should not be reachable from the public internet.
Calling CreateDBInstance for Aurora without a cluster. Aurora compute must attach to an existing cluster. Fix: call CreateDBCluster first, then CreateDBInstance with that DBClusterIdentifier.
Expecting Multi-AZ to serve reads. The standby is a failover target only. Fix: use read replicas (RDS) or Aurora reader instances for read scaling.
Reading the endpoint before it exists. The endpoint is absent while creating. Fix: block on the correct waiter, then describe.
Trying to encrypt later. Encryption at rest is set at create time. Fix: enable StorageEncrypted now, or snapshot-copy-restore into an encrypted copy.
Leaving the database publicly accessible. A default or misconfigured network can expose the port. Fix: use a private DBSubnetGroupName, tight security groups, and PubliclyAccessible=False.
Under-sizing the waiter timeout. Multi-AZ and large storage take longer to provision. Fix: raise maxWaitTime so the waiter outlasts provisioning.
What is the difference between provisioning an instance and a cluster?
A standard RDS instance is a single CreateDBInstance call that couples storage and compute. Aurora is a cluster: you call CreateDBCluster for the shared storage, then one or more CreateDBInstance calls to attach compute nodes as a writer and readers.
Does MultiAZ give me a read replica?
No. A Multi-AZ standby exists only to take over automatically on failure. It does not serve queries and does not reduce read load. For reads, add RDS read replicas or Aurora reader instances and use the reader endpoint.
Which instance in an Aurora cluster is the writer?
The first instance you attach to the cluster becomes the writer. Additional instances become readers. You can confirm with DescribeDBClusters, where each member has an IsClusterWriter flag, and route writes to Endpoint and reads to ReaderEndpoint.
Can I turn on encryption after the database exists?
Not in place. Encryption at rest is a create-time choice. To encrypt an existing unencrypted database, take a snapshot, copy the snapshot with encryption enabled, and restore from the encrypted copy into a new database.
Why does my endpoint come back empty right after creating?
Provisioning is asynchronous and the resource is still creating. The endpoint is populated only when it becomes available. Wait on db_instance_available or db_cluster_available before reading Endpoint or ReaderEndpoint.
How do I keep the master password out of my code?
Set ManageMasterUserPassword=True instead of passing MasterUserPassword. RDS generates and stores the credential in AWS Secrets Manager and rotates it. Your app reads the secret at connect time.
How should I network the database?
Place it in private subnets via a DBSubnetGroupName, restrict access with VpcSecurityGroupIds, and keep PubliclyAccessible false. The database should be reachable only from your application tier, never the public internet.
Should I provision this from the SDK or IaC?
The SDK is ideal for tooling, automation scripts, and learning the control plane. For durable, reviewed, repeatable infrastructure, define the same resources in CloudFormation, CDK, or Terraform. The underlying API calls and parameters are identical.