Cloud 18 min read

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.

Mohammad Rizky Prawira portrait
Mohammad Rizky Prawira
terminal Series Guide โ€ข Part 13 of 17

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:

  1. 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.
  2. 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:

ResourceQuantityUnit 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 Groups1 Subnet Group + 1 Param GroupFree 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

  1. 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.
  2. 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.
  3. Use Storage Auto-Scaling: Specifying --max-allocated-storage ensures your relational database dynamically expands disk capacity without manual downtime when workloads surge.
  4. IAM Database Authentication: Eliminate hardcoded passwords by leveraging 15-minute temporary STS authentication tokens (generate-db-auth-token).
  5. 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!

Related & Recommended Guides

Continue exploring related systems architectures and engineering field notes.