AWS CLI First #13: Managed Relational & NoSQL Databases: RDS PostgreSQL Multi-AZ & DynamoDB via CLI
Provision managed databases from the terminal: configure private DB Subnet Groups, deploy high-availability RDS PostgreSQL with Multi-AZ standby failover, tune custom Parameter Groups, authenticate via IAM database tokens, and create on-demand DynamoDB NoSQL tables.
AWS with CLI: The Terminal-First Mastery Path
Part of the CLI-first comprehensive learning path from foundational setup to production cloud architectures.
Running databases on self-managed EC2 instances is an operational anti-pattern for most cloud architectures. You are forced to manually configure OS patching, synchronous replication across Availability Zones, automated backup storage, and failure detection.
AWS solves this through two complementary managed database paradigms:
- Amazon Relational Database Service (RDS): Fully managed, automated relational engines (PostgreSQL, MySQL) featuring synchronous Multi-AZ Standby failover, automated point-in-time recovery, and storage auto-scaling.
- Amazon DynamoDB: Fully serverless, non-relational NoSQL database providing consistent, single-digit millisecond latency at any scale with zero server provisioning.
In this guide, we master enterprise database provisioning entirely from the command line. We will build an isolated private DB Subnet Group, launch a Multi-AZ RDS PostgreSQL cluster, tune engine parameters via DB Parameter Groups, eliminate database password leaks using IAM Database Authentication, and provision an on-demand DynamoDB table with Global Secondary Indexes.
๐ฐ Estimated Lab Cost & Resource Architecture
1. Cost Breakdown for this Lab
All resources in this lab are provisioned on-demand in the Asia Pacific (Singapore) ap-southeast-1 region:
| Resource | Quantity | Unit Cost (Hourly) | Estimated Lab Cost (1 Hour) |
|---|---|---|---|
RDS PostgreSQL (db.t4g.micro Multi-AZ) | 1 Primary + 1 Standby | $0.036 / hour (Single AZ is $0.018) | ~$0.036 |
| General Purpose gp3 Storage (20 GB) | 20 GB Allocated Storage | $0.115 / GB / month | < $0.004 |
| Amazon DynamoDB (On-Demand Mode) | 1 Table (< 50 read/write ops) | $1.25 / million writes, Free Tier | $0.000 |
| DB Subnet Groups & Parameter Groups | 1 Subnet Group + 1 Param Group | Free AWS Management | $0.000 |
| Total Estimated Lab Cost | ~$0.040 USD |
2. Architecture Diagram
+----------------------------------------------------------------+
| Multi-Tier Database Architecture |
+-------------------------------+--------------------------------+
|
v
+-------------------------------+--------------------------------+
| Application Compute Tier (App-Tier-SG) |
+-------------------------------+--------------------------------+
|
+-----------------------+-----------------------+
| (Port 5432 Ingress) | (HTTPS Port 443)
v v
+-------------------------------+ +------------------------------+
| RDS PostgreSQL Multi-AZ | | Amazon DynamoDB (NoSQL) |
| (Private DB Subnets) | | - Partition Key: PK |
+---------------+---------------+ | - Sort Key: SK |
| Primary Node | Standby Node | | - Billing: PAY_PER_REQUEST |
| (AZ 1a) | (AZ 1b Sync) | | - GSI: StatusIndex |
+---------------+---------------+ +------------------------------+
|
v
+----------------------------------------------------------------+
| Security Guardrails & Operations: |
| - DB Subnet Group: 100% Private (No Public Access) |
| - Auth: IAM DB Auth Token (Zero hardcoded credentials) |
| - Automated Snapshots: 7-day retention + Storage Auto-Scaling |
+----------------------------------------------------------------+
๐ ๏ธ Phase 1: Private Subnet Groups & Security Group Chaining
Never place a database instance in a public subnet. A secure database architecture isolates the data tier in private subnets with zero inbound internet routes, accepting connections solely from the application security group on TCP port 5432.
1. Creating Dedicated Private Database Subnets
If using our existing VPC (Production-VPC), create two private subnets dedicated to the database tier:
REGION="ap-southeast-1"
PROFILE="cloud-dev-admin"
VPC_ID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=Production-VPC" --region "$REGION" --profile "$PROFILE" --query "Vpcs[0].VpcId" --output text)
# Create Private DB Subnet in Zone A (10.0.10.0/24)
DB_SUB_A=$(aws ec2 create-subnet \
--vpc-id "$VPC_ID" \
--cidr-block "10.0.10.0/24" \
--availability-zone "${REGION}a" \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=DB-Private-Subnet-A}]' \
--region "$REGION" \
--profile "$PROFILE" \
--query "Subnet.SubnetId" \
--output text)
# Create Private DB Subnet in Zone B (10.0.20.0/24)
DB_SUB_B=$(aws ec2 create-subnet \
--vpc-id "$VPC_ID" \
--cidr-block "10.0.20.0/24" \
--availability-zone "${REGION}b" \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=DB-Private-Subnet-B}]' \
--region "$REGION" \
--profile "$PROFILE" \
--query "Subnet.SubnetId" \
--output text)
echo "Private DB Subnets created: $DB_SUB_A, $DB_SUB_B"
2. Creating the RDS DB Subnet Group
Amazon RDS requires a DB Subnet Group spanning at least two Availability Zones to support Multi-AZ high-availability failover:
aws rds create-db-subnet-group \
--db-subnet-group-name "production-db-subnet-group" \
--db-subnet-group-description "Multi-AZ Private Subnets for RDS PostgreSQL" \
--subnet-ids "$DB_SUB_A" "$DB_SUB_B" \
--tags "Key=Environment,Value=Production" \
--region "$REGION" \
--profile "$PROFILE"
echo "DB Subnet Group successfully provisioned."
3. Restricting Ingress with Security Group Chaining
Create a database security group that permits incoming PostgreSQL traffic (port 5432) strictly from our application tier security group (App-Tier-SG):
DB_SG_ID=$(aws ec2 create-security-group \
--group-name "RDS-Database-SG" \
--description "Inbound PostgreSQL access restricted to App-Tier-SG" \
--vpc-id "$VPC_ID" \
--tag-specifications 'ResourceType=security-group,Tags=[{Key=Name,Value=RDS-Database-SG}]' \
--region "$REGION" \
--profile "$PROFILE" \
--query "GroupId" \
--output text)
APP_SG_ID=$(aws ec2 describe-security-groups --filters "Name=vpc-id,Values=$VPC_ID" "Name=group-name,Values=App-Tier-SG" --region "$REGION" --profile "$PROFILE" --query "SecurityGroups[0].GroupId" --output text 2>/dev/null || true)
if [ -n "$APP_SG_ID" ] && [ "$APP_SG_ID" != "None" ]; then
aws ec2 authorize-security-group-ingress \
--group-id "$DB_SG_ID" \
--protocol tcp \
--port 5432 \
--source-group "$APP_SG_ID" \
--region "$REGION" \
--profile "$PROFILE"
else
# Fallback to VPC CIDR if App SG is absent
aws ec2 authorize-security-group-ingress \
--group-id "$DB_SG_ID" \
--protocol tcp \
--port 5432 \
--cidr "10.0.0.0/16" \
--region "$REGION" \
--profile "$PROFILE"
fi
echo "Database Security Group Created: $DB_SG_ID"
โ๏ธ Phase 2: Custom DB Parameter Groups for Production Tuning
Never use the default parameter group in production. Default parameter groups cannot be modified and lack performance auditing, slow query logging, and memory tuning.
1. Creating a Custom PostgreSQL Parameter Group
aws rds create-db-parameter-group \
--db-parameter-group-name "custom-postgres16-prod" \
--db-parameter-group-family "postgres16" \
--description "Tuned PostgreSQL 16 Parameter Group with query auditing" \
--region "$REGION" \
--profile "$PROFILE"
2. Tuning Slow Query Logging & Extensions
Configure PostgreSQL to log queries exceeding 250 milliseconds (log_min_duration_statement = 250) and load pg_stat_statements for execution plan telemetry:
aws rds modify-db-parameter-group \
--db-parameter-group-name "custom-postgres16-prod" \
--parameters \
"ParameterName=log_min_duration_statement,ParameterValue=250,ApplyMethod=immediate" \
"ParameterName=shared_preload_libraries,ParameterValue=pg_stat_statements,ApplyMethod=pending-reboot" \
--region "$REGION" \
--profile "$PROFILE"
echo "Custom DB Parameter Group tuned."
๐ Phase 3: Launching Multi-AZ RDS PostgreSQL with CLI
We now launch our managed PostgreSQL database instance using Graviton processors (db.t4g.micro), encrypted gp3 storage, Multi-AZ standby replication, and automated storage auto-scaling.
REGION="ap-southeast-1"
PROFILE="cloud-dev-admin"
DB_PASSWORD="MasterPassword2026!"
echo "Provisioning Multi-AZ RDS PostgreSQL Instance (this takes 5-10 minutes)..."
aws rds create-db-instance \
--db-instance-identifier "production-postgres-db" \
--db-instance-class "db.t4g.micro" \
--engine "postgres" \
--engine-version "16.3" \
--master-username "dbadmin" \
--master-user-password "$DB_PASSWORD" \
--db-name "appdb" \
--allocated-storage 20 \
--max-allocated-storage 100 \
--storage-type "gp3" \
--storage-encrypted \
--multi-az \
--no-publicly-accessible \
--db-subnet-group-name "production-db-subnet-group" \
--vpc-security-group-ids "$DB_SG_ID" \
--db-parameter-group-name "custom-postgres16-prod" \
--backup-retention-period 7 \
--enable-iam-database-authentication \
--auto-minor-version-upgrade \
--region "$REGION" \
--profile "$PROFILE" \
--output text
Parameter Breakdown
--multi-az: Launches a primary database in Availability Zone A and an automatic synchronous physical standby replica in Availability Zone B. In the event of an AZ outage or hardware degradation, AWS automatically fails over to the standby in < 60 seconds with zero data loss.--max-allocated-storage 100: Enables Storage Auto-Scaling. If database volume utilization exceeds 90%, AWS automatically scales disk capacity up to 100 GB without downtime.--enable-iam-database-authentication: Enables passwordless authentication via short-lived AWS IAM tokens.
Tracking Provisioning State
Query the instance status until it transitions from creating to available:
aws rds describe-db-instances \
--db-instance-identifier "production-postgres-db" \
--region "$REGION" \
--profile "$PROFILE" \
--query "DBInstances[0].[DBInstanceIdentifier,DBInstanceStatus,DBInstanceClass,MultiAZ,Endpoint.Address]" \
--output table
Once available, extract the private database connection endpoint:
DB_ENDPOINT=$(aws rds describe-db-instances \
--db-instance-identifier "production-postgres-db" \
--region "$REGION" \
--profile "$PROFILE" \
--query "DBInstances[0].Endpoint.Address" \
--output text)
echo "PostgreSQL Endpoint: $DB_ENDPOINT"
๐ Phase 4: Zero-Password IAM Database Authentication
Hardcoding static database passwords into application configuration files is a serious security hazard. With IAM Database Authentication, client applications request a cryptographically signed, short-lived (15-minute) authentication token using their AWS credentials:
REGION="ap-southeast-1"
PROFILE="cloud-dev-admin"
# Generate a temporary IAM database authentication token valid for 15 minutes
DB_TOKEN=$(aws rds generate-db-auth-token \
--hostname "$DB_ENDPOINT" \
--port 5432 \
--username "dbadmin" \
--region "$REGION" \
--profile "$PROFILE")
echo "Generated DB Auth Token (Sample Preview):"
echo "$DB_TOKEN" | cut -c 1-80
echo "...[Token truncated for readability]"
Clients pass this token as their database password during connection initialization, completely eliminating password rotation headaches!
โก Phase 5: Serverless NoSQL with Amazon DynamoDB
While relational databases excel at structured ACID transactions and complex joins, modern high-scale systems require Amazon DynamoDB for key-value and document workloads requiring consistent single-digit millisecond latency.
1. Creating an On-Demand DynamoDB Table
We provision a production table with a composite primary key (PK partition key, SK sort key) and on-demand (PAY_PER_REQUEST) capacity:
REGION="ap-southeast-1"
PROFILE="cloud-dev-admin"
aws dynamodb create-table \
--table-name "Production-Orders-Table" \
--attribute-definitions \
AttributeName=PK,AttributeType=S \
AttributeName=SK,AttributeType=S \
AttributeName=OrderStatus,AttributeType=S \
AttributeName=CreatedAt,AttributeType=S \
--key-schema \
AttributeName=PK,KeyType=HASH \
AttributeName=SK,KeyType=RANGE \
--billing-mode "PAY_PER_REQUEST" \
--global-secondary-indexes '[
{
"IndexName": "StatusDateIndex",
"KeySchema": [
{"AttributeName": "OrderStatus", "KeyType": "HASH"},
{"AttributeName": "CreatedAt", "KeyType": "RANGE"}
],
"Projection": {
"ProjectionType": "ALL"
}
}
]' \
--point-in-time-recovery-specification PointInTimeRecoveryEnabled=true \
--region "$REGION" \
--profile "$PROFILE"
echo "DynamoDB Table Created with Point-in-Time Recovery enabled."
2. Putting Items into DynamoDB via CLI
Insert two order records formatted with single-table design patterns:
# Insert Order Record 1
aws dynamodb put-item \
--table-name "Production-Orders-Table" \
--item '{
"PK": {"S": "CUSTOMER#1001"},
"SK": {"S": "ORDER#8001"},
"TotalAmount": {"N": "149.99"},
"OrderStatus": {"S": "CONFIRMED"},
"CreatedAt": {"S": "2026-09-09T08:30:00Z"}
}' \
--region "$REGION" \
--profile "$PROFILE"
# Insert Order Record 2
aws dynamodb put-item \
--table-name "Production-Orders-Table" \
--item '{
"PK": {"S": "CUSTOMER#1001"},
"SK": {"S": "ORDER#8002"},
"TotalAmount": {"N": "89.50"},
"OrderStatus": {"S": "SHIPPED"},
"CreatedAt": {"S": "2026-09-09T09:15:00Z"}
}' \
--region "$REGION" \
--profile "$PROFILE"
echo "Sample order records inserted."
3. Querying Items by Partition Key
Query all orders for CUSTOMER#1001:
aws dynamodb query \
--table-name "Production-Orders-Table" \
--key-condition-expression "PK = :pk" \
--expression-attribute-values '{":pk": {"S": "CUSTOMER#1001"}}' \
--region "$REGION" \
--profile "$PROFILE" \
--query "Items[*].[PK.S,SK.S,TotalAmount.N,OrderStatus.S]" \
--output table
๐ธ Phase 6: Snapshot Management & Point-in-Time Recovery
Before making significant migrations or schema changes, take an on-demand manual DB snapshot:
REGION="ap-southeast-1"
PROFILE="cloud-dev-admin"
echo "Creating Manual RDS DB Snapshot..."
aws rds create-db-snapshot \
--db-instance-identifier "production-postgres-db" \
--db-snapshot-identifier "pre-migration-backup-$(date +%Y%m%d)" \
--region "$REGION" \
--profile "$PROFILE" \
--query "DBSnapshot.[DBSnapshotIdentifier,Status,PercentProgress]" \
--output table
๐งน Phase 7: 1-Click Hardened Lab Teardown
Managed databases incur significant hourly run-rate costs if left running. This script skips the final snapshot, deletes the RDS instance, removes the subnet groups, and deletes the DynamoDB table cleanly:
cat <<'EOF' > destroy-db-lab.sh
#!/usr/bin/env bash
set -euo pipefail
REGION="ap-southeast-1"
PROFILE="cloud-dev-admin"
echo "=== 1. Deleting RDS PostgreSQL Instance ==="
if aws rds describe-db-instances --db-instance-identifier "production-postgres-db" --region "$REGION" --profile "$PROFILE" 2>/dev/null | grep -q "production-postgres-db"; then
echo "Terminating RDS instance (skipping final snapshot for lab)..."
aws rds delete-db-instance \
--db-instance-identifier "production-postgres-db" \
--skip-final-snapshot \
--delete-automated-backups \
--region "$REGION" \
--profile "$PROFILE"
echo "Waiting for RDS instance deletion to finalize..."
aws rds wait db-instance-deleted \
--db-instance-identifier "production-postgres-db" \
--region "$REGION" \
--profile "$PROFILE"
fi
echo "=== 2. Deleting DB Subnet & Parameter Groups ==="
aws rds delete-db-subnet-group \
--db-subnet-group-name "production-db-subnet-group" \
--region "$REGION" \
--profile "$PROFILE" 2>/dev/null || true
aws rds delete-db-parameter-group \
--db-parameter-group-name "custom-postgres16-prod" \
--region "$REGION" \
--profile "$PROFILE" 2>/dev/null || true
echo "=== 3. Deleting DynamoDB Table ==="
aws dynamodb delete-table \
--table-name "Production-Orders-Table" \
--region "$REGION" \
--profile "$PROFILE" 2>/dev/null || true
echo "=== 4. Cleaning up Security Groups & Subnets ==="
VPC_ID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=Production-VPC" --region "$REGION" --profile "$PROFILE" --query "Vpcs[0].VpcId" --output text 2>/dev/null || true)
if [ -n "$VPC_ID" ] && [ "$VPC_ID" != "None" ]; then
# Delete DB Security Group
DB_SG_ID=$(aws ec2 describe-security-groups --filters "Name=vpc-id,Values=$VPC_ID" "Name=group-name,Values=RDS-Database-SG" --region "$REGION" --profile "$PROFILE" --query "SecurityGroups[0].GroupId" --output text 2>/dev/null || true)
if [ -n "$DB_SG_ID" ] && [ "$DB_SG_ID" != "None" ]; then
aws ec2 delete-security-group --group-id "$DB_SG_ID" --region "$REGION" --profile "$PROFILE" 2>/dev/null || true
fi
# Delete DB Private Subnets
for sub in $(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID" "Name=tag:Name,Values=DB-Private-Subnet-A,DB-Private-Subnet-B" --region "$REGION" --profile "$PROFILE" --query "Subnets[*].SubnetId" --output text 2>/dev/null || true); do
aws ec2 delete-subnet --subnet-id "$sub" --region "$REGION" --profile "$PROFILE" 2>/dev/null || true
done
fi
rm -f destroy-db-lab.sh
echo "=== Part 13 Teardown Complete! Zero Database Spend Remaining. ==="
EOF
chmod +x destroy-db-lab.sh
./destroy-db-lab.sh
๐ Key Takeaways & Enterprise Architecture Patterns
- Multi-AZ for High Availability: Multi-AZ RDS ensures zero data loss through synchronous physical replication across Availability Zones, failing over automatically if a zone fails.
- Never Put DBs in Public Subnets: Always isolate databases in private subnets with a Multi-AZ DB Subnet Group and restrict security group ingress strictly to the application tier.
- Use Storage Auto-Scaling: Specifying
--max-allocated-storageensures your relational database dynamically expands disk capacity without manual downtime when workloads surge. - IAM Database Authentication: Eliminate hardcoded passwords by leveraging 15-minute temporary STS authentication tokens (
generate-db-auth-token). - DynamoDB for Scalable Key-Value Workloads: When applications demand sub-10ms response times at arbitrary scale without managing database connections, DynamoDB with on-demand capacity provides a zero-maintenance solution.
๐ฎ Whatโs Next in Part 14?
Now that we have mastered storage and database infrastructure, how do we package and run modern microservices?
In Part 14: Container Workflows: ECR Push & ECS Fargate Task Provisioning, we dive into production containerization. We will provision private Elastic Container Registry (ECR) repositories, authenticate Docker from the CLI, register serverless ECS Task Definitions, and orchestrate scalable Fargate containers attached to our Application Load Balancer!