Skip to main content

Overview

The RDS Aurora resource provides a fully managed, highly available PostgreSQL database cluster using Aurora Serverless v2. It includes automatic scaling, built-in RDS Proxy for connection pooling, Performance Insights for monitoring, and integrated AWS Secrets Manager for credential management with automatic rotation. RDS Aurora is ideal for:
  • Production applications requiring high availability
  • Workloads with variable or unpredictable traffic patterns
  • Applications needing automatic scaling without downtime
  • Multi-region disaster recovery setups
  • Microservices architectures requiring connection pooling
Key features:
  • Aurora Serverless v2 with 1 writer and 2 reader instances
  • Built-in RDS Proxy for connection pooling and failover
  • Automatic credential rotation every 30 days via Secrets Manager
  • Performance Insights with 7-day retention
  • Customer-managed KMS encryption for storage and monitoring data
  • 14-day backup retention with automated backups

Resource Class

import { RdsAurora } from "@fjall/components-infrastructure/lib/resources/aws/database/rdsAurora";

Basic Usage

import { Vpc } from "@fjall/components-infrastructure/lib/resources/aws/networking/vpc";

const vpc = new Vpc(this, "Vpc");

const database = new RdsAurora(this, "Database", {
  vpc: vpc,
  databaseName: "myapp"
});

Configuration Options

Core Properties

PropertyTypeDescriptionDefault
vpcIVpcVPC for database deployment (required)-
databaseNamestringDatabase name"postgres"
engineDatabaseClusterEngineAurora engine versionAurora PostgreSQL 15.6
clusterIdentifierstringCluster identifier-
portnumberDatabase port5432

Backup Configuration

PropertyTypeDescriptionDefault
backupBackupPropsBackup retention days{ retention: 14 }

Capacity Configuration

PropertyTypeDescriptionDefault
writerServerlessV2ScalingConfigurationWriter instance scaling{ minCapacity: 0.5, maxCapacity: 2 }
readersServerlessV2ScalingConfiguration[]Reader instances scaling2 readers with default scaling

Monitoring Configuration

PropertyTypeDescriptionDefault
monitoringIntervalDurationEnhanced monitoring intervalDuration.minutes(1)
preferredMaintenanceWindowstringMaintenance window"sat:12:30-sat:20:30"

Default Configuration

The RDS Aurora construct includes these defaults:
  • 1 writer instance (Aurora Serverless v2, 0.5-2 ACU capacity)
  • 2 reader instances (Aurora Serverless v2, 0.5-2 ACU capacity each)
  • Aurora PostgreSQL 15.6 engine
  • RDS Proxy with 50% max connections, 5-minute idle timeout
  • Performance Insights enabled with 7-day retention
  • Customer-managed KMS keys (4 total: storage + 3 for insights)
  • Auto-rotating credentials every 30 days
  • 14-day backup retention
  • Enhanced monitoring every 1 minute

Architecture Overview

Multi-AZ High Availability

const database = new RdsAurora(this, "HADatabase", {
  vpc: vpc,
  databaseName: "production",
  writer: {
    minCapacity: 1,
    maxCapacity: 4
  },
  readers: [
    { minCapacity: 0.5, maxCapacity: 2 },
    { minCapacity: 0.5, maxCapacity: 2 }
  ]
});

// Architecture created:
// - Writer instance in AZ-1 (auto-failover)
// - Reader instance in AZ-2
// - Reader instance in AZ-3
// - RDS Proxy across all AZs
// - Automatic cross-AZ replication

Built-in Components

The Aurora cluster automatically provisions:
  1. Database Cluster: Aurora Serverless v2 cluster
  2. Writer Instance: Primary database instance
  3. Reader Instances: 2 read replicas for scaling
  4. RDS Proxy: Connection pooling and failover handling
  5. Secrets Manager Secret: PostgreSQL credentials
  6. KMS Keys: 4 customer-managed keys for encryption
  7. Performance Insights: Monitoring and query analysis
  8. Security Group: Database network access control

Usage Patterns

Pattern 1: Development Environment

const devDatabase = new RdsAurora(this, "DevDatabase", {
  vpc: devVpc,
  databaseName: "development",
  writer: {
    minCapacity: 0.5,
    maxCapacity: 1
  },
  readers: [
    { minCapacity: 0.5, maxCapacity: 1 }
  ],
  backup: {
    retention: 7
  }
});

// Minimal capacity for cost savings
// Single reader for dev workloads
// Shorter backup retention

Pattern 2: Production with High Scaling

const prodDatabase = new RdsAurora(this, "ProdDatabase", {
  vpc: prodVpc,
  databaseName: "production",
  writer: {
    minCapacity: 2,
    maxCapacity: 16
  },
  readers: [
    { minCapacity: 1, maxCapacity: 8 },
    { minCapacity: 1, maxCapacity: 8 }
  ],
  backup: {
    retention: 30
  },
  monitoringInterval: Duration.seconds(30)
});

// Higher minimum capacity for baseline performance
// More aggressive scaling for peak loads
// Extended backup retention for compliance

Pattern 3: Custom Maintenance Window

const database = new RdsAurora(this, "Database", {
  vpc: vpc,
  databaseName: "myapp",
  preferredMaintenanceWindow: "sun:03:00-sun:04:00"
});

// Maintenance during low-traffic hours
// Default is Saturday 12:30-20:30 UTC

Integration Examples

With ECS Services

import { EcsCluster } from "@fjall/components-infrastructure/lib/resources/aws/compute/ecsCluster";

const database = new RdsAurora(this, "Database", {
  vpc: vpc,
  databaseName: "app"
});

const cluster = new EcsCluster(this, "AppCluster", {
  vpc: vpc,
  serviceName: "api"
});

// Allow ECS tasks to connect to database
database.connections.allowFrom(
  cluster,
  ec2.Port.tcp(Number(database.getHostPort()))
);

// ECS tasks use RDS Proxy endpoint
const proxyEndpoint = database.proxy.endpoint;
const credentials = database.getCredentials();

With Lambda Functions

import { Function } from "aws-cdk-lib/aws-lambda";

const database = new RdsAurora(this, "Database", {
  vpc: vpc,
  databaseName: "events"
});

const lambda = new Function(this, "Handler", {
  vpc: vpc,
  environment: {
    DB_HOST: database.proxy.endpoint,
    DB_PORT: database.getHostPort().toString(),
    DB_NAME: "events",
    DB_SECRET_ARN: database.getCredentials().secretArn
  }
});

// Grant Lambda permission to read credentials
database.getCredentials().grantRead(lambda);

// Allow Lambda to connect
database.connections.allowFrom(
  lambda,
  ec2.Port.tcp(Number(database.getHostPort()))
);

With Application Load Balancer

import { ApplicationLoadBalancer } from "@fjall/components-infrastructure/lib/resources/aws/networking/alb";

const alb = new ApplicationLoadBalancer(this, "ALB", {
  vpc: vpc,
  internetFacing: true
});

const database = new RdsAurora(this, "Database", {
  vpc: vpc,
  databaseName: "webapp"
});

// Backend services connect through RDS Proxy
// Connection pooling handles ALB-distributed traffic

Security Configuration

Network Isolation

// Deploy in isolated subnets
const database = new RdsAurora(this, "SecureDatabase", {
  vpc: vpc,
  databaseName: "secure",
  vpcSubnets: {
    subnetType: ec2.SubnetType.PRIVATE_ISOLATED
  }
});

// No direct internet access
// Access only through application tier

Connection Security

// Access credentials programmatically
const secret = database.getCredentials();

// In application code (Node.js example):
const AWS = require('aws-sdk');
const secretsManager = new AWS.SecretsManager();

const secret = await secretsManager.getSecretValue({
  SecretId: process.env.DB_SECRET_ARN
}).promise();

const credentials = JSON.parse(secret.SecretString);
// credentials.username (always "postgres")
// credentials.password
// credentials.host (RDS Proxy endpoint)
// credentials.port

Encryption at Rest

// Automatic encryption with customer-managed keys
const database = new RdsAurora(this, "EncryptedDatabase", {
  vpc: vpc,
  databaseName: "encrypted"
});

// Creates 4 KMS keys automatically:
// 1. Storage encryption key
// 2. Performance Insights key (writer)
// 3. Performance Insights key (reader 1)
// 4. Performance Insights key (reader 2)

IAM Authentication

// Grant IAM role access to database
const role = new iam.Role(this, "DatabaseRole", {
  assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com")
});

database.grantConnect(role, "postgres");

// Application uses IAM authentication instead of password

RDS Proxy Configuration

Understanding RDS Proxy

The built-in RDS Proxy provides:
  • Connection pooling: Reduces database connection overhead
  • Automatic failover: Faster recovery during failures
  • IAM authentication: Token-based access
  • Connection limits: 50% of max connections by default
const database = new RdsAurora(this, "Database", {
  vpc: vpc,
  databaseName: "app"
});

// Access proxy properties
const proxyEndpoint = database.proxy.endpoint;
const proxyArn = database.proxy.dbProxyArn;

// Always connect through proxy, not directly to cluster
// Proxy endpoint: xxxx.proxy-xxxx.region.rds.amazonaws.com
// Cluster endpoint: xxxx.cluster-xxxx.region.rds.amazonaws.com (don't use)

Proxy Benefits

// Application configuration
const connectionConfig = {
  host: database.proxy.endpoint,  // Use proxy
  port: 5432,
  database: "myapp",
  // Proxy handles connection multiplexing
  // Supports thousands of application connections
  // Database sees far fewer actual connections
};

Cost Optimization

Scaling Strategy

// Cost-effective scaling configuration
const database = new RdsAurora(this, "OptimizedDatabase", {
  vpc: vpc,
  databaseName: "app",
  writer: {
    minCapacity: 0.5,  // Scale to minimum during idle
    maxCapacity: 4     // Cap maximum spend
  },
  readers: [
    { minCapacity: 0.5, maxCapacity: 2 }  // Single reader
  ]
});

// Aurora Serverless v2 pricing:
// - $0.12 per ACU-hour
// - 0.5 ACU minimum = ~$43/month baseline
// - Scales automatically based on load

Backup Optimization

// Balance retention with cost
const database = new RdsAurora(this, "Database", {
  vpc: vpc,
  databaseName: "app",
  backup: {
    retention: 7  // 1 week (vs default 14 days)
  }
});

// Backup storage costs:
// - Free up to DB size
// - $0.095/GB-month beyond that

Monitoring Optimization

// Reduce monitoring costs
const database = new RdsAurora(this, "Database", {
  vpc: vpc,
  databaseName: "app",
  monitoringInterval: Duration.minutes(5)  // vs 1 minute default
});

// Performance Insights costs:
// - 7-day retention included free
// - Longer retention has additional costs

Performance Tuning

Read Scaling

// Scale reads with multiple readers
const database = new RdsAurora(this, "ReadHeavyDatabase", {
  vpc: vpc,
  databaseName: "analytics",
  writer: {
    minCapacity: 1,
    maxCapacity: 4
  },
  readers: [
    { minCapacity: 1, maxCapacity: 8 },
    { minCapacity: 1, maxCapacity: 8 },
    { minCapacity: 1, maxCapacity: 8 }  // 3 readers for read-heavy workload
  ]
});

// Use reader endpoint for queries
// Aurora automatically load balances

Write Scaling

// Optimize for write-heavy workloads
const database = new RdsAurora(this, "WriteHeavyDatabase", {
  vpc: vpc,
  databaseName: "transactions",
  writer: {
    minCapacity: 2,   // Higher baseline
    maxCapacity: 16   // More headroom
  },
  readers: [
    { minCapacity: 0.5, maxCapacity: 2 }  // Minimal reads
  ]
});

Connection Management

// Application connection pooling with RDS Proxy
const pool = new Pool({
  host: database.proxy.endpoint,
  port: 5432,
  database: "myapp",
  max: 20,  // Application pool size
  // RDS Proxy handles actual database connections
  // No connection exhaustion at database
});

Methods

getHostEndpoint()

const endpoint = database.getHostEndpoint();
// Returns: RDS Proxy endpoint for connections
// Use this for all application connections

getHostPort()

const port = database.getHostPort();
// Returns: string - Database port (default "5432")
// Note: Returns string type, not number

getCredentials()

const credentials = database.getCredentials();
// Returns: ISecret reference to Secrets Manager
// Contains: username, password, host, port, dbname

// Grant read access
credentials.grantRead(lambda);

// In application
const secret = await secretsManager.getSecretValue({
  SecretId: credentials.secretArn
}).promise();

Complete Example

import { Stack, StackProps, CfnOutput, Duration } from "aws-cdk-lib";
import { Construct } from "constructs";
import { Vpc } from "@fjall/components-infrastructure/lib/resources/aws/networking/vpc";
import { RdsAurora } from "@fjall/components-infrastructure/lib/resources/aws/database/rdsAurora";
import { EcsCluster } from "@fjall/components-infrastructure/lib/resources/aws/compute/ecsCluster";
import * as ec2 from "aws-cdk-lib/aws-ec2";

export class ProductionDatabaseStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // Network setup
    const vpc = new Vpc(this, "ProductionVpc", {
      maxAzs: 3,
      natGateways: 2
    });

    // Production Aurora cluster
    const database = new RdsAurora(this, "ProductionDatabase", {
      vpc: vpc,
      databaseName: "production",

      // High availability configuration
      writer: {
        minCapacity: 2,    // Baseline performance
        maxCapacity: 16    // Peak capacity
      },
      readers: [
        { minCapacity: 1, maxCapacity: 8 },
        { minCapacity: 1, maxCapacity: 8 }
      ],

      // Compliance requirements
      backup: {
        retention: 30
      },

      // Monitoring
      monitoringInterval: Duration.seconds(30),

      // Maintenance during low traffic
      preferredMaintenanceWindow: "sun:03:00-sun:04:00"
    });

    // Application tier
    const appCluster = new EcsCluster(this, "AppCluster", {
      vpc: vpc,
      serviceName: "api"
    });

    // Security: Allow app to connect to database
    database.connections.allowFrom(
      appCluster,
      ec2.Port.tcp(database.getHostPort()),
      "Allow ECS tasks to connect to database"
    );

    // Lambda function for background jobs
    const worker = new lambda.Function(this, "Worker", {
      runtime: lambda.Runtime.NODEJS_20_X,
      handler: "index.handler",
      code: lambda.Code.fromAsset("lambda"),
      vpc: vpc,
      environment: {
        DB_PROXY_ENDPOINT: database.proxy.endpoint,
        DB_PORT: database.getHostPort().toString(),
        DB_NAME: "production",
        DB_SECRET_ARN: database.getCredentials().secretArn
      }
    });

    // Grant Lambda access to credentials
    database.getCredentials().grantRead(worker);
    database.connections.allowFrom(
      worker,
      ec2.Port.tcp(database.getHostPort())
    );

    // CloudWatch alarm for high CPU
    const cpuAlarm = new cloudwatch.Alarm(this, "HighCPU", {
      metric: database.metricCPUUtilization(),
      threshold: 80,
      evaluationPeriods: 2,
      alarmDescription: "Alert when database CPU exceeds 80%"
    });

    // Outputs
    new CfnOutput(this, "ProxyEndpoint", {
      value: database.proxy.endpoint,
      description: "RDS Proxy endpoint for application connections",
      exportName: "DatabaseProxyEndpoint"
    });

    new CfnOutput(this, "SecretArn", {
      value: database.getCredentials().secretArn,
      description: "Secrets Manager ARN for database credentials",
      exportName: "DatabaseSecretArn"
    });

    new CfnOutput(this, "ClusterIdentifier", {
      value: database.clusterIdentifier,
      description: "Aurora cluster identifier"
    });
  }
}

Best Practices

  1. Always use RDS Proxy endpoint for application connections - provides connection pooling and faster failover
  2. Deploy in private subnets (PRIVATE_ISOLATED) - never expose databases publicly
  3. Use Secrets Manager integration - avoid hardcoded credentials, leverage automatic rotation
  4. Enable Performance Insights - included by default, essential for query optimization
  5. Set appropriate scaling limits - balance performance needs with cost controls
  6. Configure maintenance windows - schedule during low-traffic periods
  7. Monitor with CloudWatch - set alarms for CPU, connections, and replication lag
  8. Use reader endpoints for read-heavy queries - distribute load across replicas
  9. Implement connection pooling in applications - even with RDS Proxy
  10. Test failover scenarios - verify application handles primary instance failures

Common Patterns

Multi-Environment Setup

// Shared base configuration
const baseConfig = {
  vpc: vpc,
  databaseName: "myapp"
};

// Development
const devDb = new RdsAurora(this, "DevDB", {
  ...baseConfig,
  writer: { minCapacity: 0.5, maxCapacity: 1 },
  readers: [{ minCapacity: 0.5, maxCapacity: 1 }]
});

// Production
const prodDb = new RdsAurora(this, "ProdDB", {
  ...baseConfig,
  writer: { minCapacity: 2, maxCapacity: 16 },
  readers: [
    { minCapacity: 1, maxCapacity: 8 },
    { minCapacity: 1, maxCapacity: 8 }
  ],
  backup: { retention: 30 }
});

Read Replica for Analytics

const database = new RdsAurora(this, "Database", {
  vpc: vpc,
  databaseName: "app",
  writer: { minCapacity: 1, maxCapacity: 4 },
  readers: [
    { minCapacity: 0.5, maxCapacity: 2 },  // App queries
    { minCapacity: 1, maxCapacity: 8 }     // Analytics queries
  ]
});

// Analytics connects to reader endpoint
// Isolates heavy queries from production traffic

Blue/Green Deployments

// Blue environment (current)
const blueDb = new RdsAurora(this, "BlueDatabase", {
  vpc: vpc,
  databaseName: "production",
  clusterIdentifier: "prod-blue"
});

// Green environment (new version)
const greenDb = new RdsAurora(this, "GreenDatabase", {
  vpc: vpc,
  databaseName: "production",
  clusterIdentifier: "prod-green"
});

// Clone from blue to green for testing
// Switch traffic after validation

Cost Considerations

ComponentCostOptimization
Aurora Serverless v2$0.12/ACU-hourSet minCapacity to 0.5, cap maxCapacity
RDS Proxy0.015/hour+0.015/hour + 0.0000055/connectionIncluded automatically, reduces DB connections
Backup Storage$0.095/GB-month (beyond DB size)Reduce retention period for non-critical data
Performance InsightsFree (7-day retention)Extended retention adds cost
Data Transfer$0.01-0.09/GBUse VPC endpoints, minimize cross-AZ traffic
KMS Keys$1/month per key4 keys created automatically ($4/month)
Estimated Monthly Costs:
  • Development: ~$50-100 (0.5-1 ACU minimum)
  • Production: ~$200-500 (2-4 ACU baseline, scales up)
  • Enterprise: ~$1000+ (high minimum capacity, multiple regions)

Troubleshooting

Common Issues

  1. Connection timeouts
    • Cause: Security group not configured, wrong endpoint used
    • Solution: Verify security group allows traffic on port 5432, use proxy endpoint not cluster endpoint
  2. Authentication failures
    • Cause: Using outdated credentials, rotation in progress
    • Solution: Always fetch credentials from Secrets Manager at runtime, implement retry logic for rotation period
  3. High scaling costs
    • Cause: maxCapacity set too high, constant high load
    • Solution: Lower maxCapacity, optimize queries, add read replicas for read-heavy workloads
  4. Slow query performance
    • Cause: Missing indexes, inefficient queries, insufficient capacity
    • Solution: Use Performance Insights to identify slow queries, add indexes, increase minCapacity
  5. RDS Proxy connection errors
    • Cause: Proxy connection limits exceeded
    • Solution: Increase application connection pool efficiency, reduce connection churn

Debug Commands

# Describe cluster
aws rds describe-db-clusters \
  --db-cluster-identifier <cluster-id>

# Check instance status
aws rds describe-db-instances \
  --filters "Name=db-cluster-id,Values=<cluster-id>"

# View recent events
aws rds describe-events \
  --source-type db-cluster \
  --source-identifier <cluster-id> \
  --duration 60

# Get RDS Proxy details
aws rds describe-db-proxies \
  --db-proxy-name <proxy-name>

# Check Performance Insights
aws pi get-resource-metrics \
  --service-type RDS \
  --identifier <instance-resource-id> \
  --metric-queries file://metrics.json

# Retrieve credentials
aws secretsmanager get-secret-value \
  --secret-id <secret-arn>

# Test connectivity (from instance in same VPC)
psql -h <proxy-endpoint> -p 5432 -U postgres -d myapp

Performance Insights Queries

-- Find slow queries
SELECT query, mean_exec_time, calls
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;

-- Check connection count
SELECT count(*) FROM pg_stat_activity;

-- Identify blocking queries
SELECT blocked_locks.pid AS blocked_pid,
       blocking_locks.pid AS blocking_pid,
       blocked_activity.usename AS blocked_user,
       blocking_activity.usename AS blocking_user,
       blocked_activity.query AS blocked_statement
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks
  ON blocking_locks.locktype = blocked_locks.locktype
WHERE NOT blocked_locks.granted;

See Also