Skip to main content

Overview

The RDS Instance resource provides a managed PostgreSQL database instance with flexible configuration options for production workloads. Unlike Aurora Serverless, it uses provisioned compute with predictable pricing and performance. It includes Multi-AZ deployment for high availability, optional RDS Proxy for connection pooling, and integrated AWS Secrets Manager for credential management. RDS Instance is ideal for:
  • Applications requiring predictable performance and costs
  • Workloads with steady, consistent traffic patterns
  • Legacy applications requiring specific PostgreSQL versions
  • Scenarios where Aurora features are not needed
  • Cost-sensitive production workloads with known capacity needs
Key features:
  • PostgreSQL 17.5 latest engine version
  • Multi-AZ deployment for automatic failover
  • Optional RDS Proxy for connection pooling (disabled by default)
  • Optional read replicas for scaling reads
  • Automatic credential rotation every 30 days via Secrets Manager
  • Optional Performance Insights for monitoring
  • Customer-managed KMS encryption for storage
  • 14-day backup retention with automated backups

Resource Class

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

Basic Usage

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

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

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

Configuration Options

Core Properties

PropertyTypeDescriptionDefault
vpcIVpcVPC for database deployment (required)-
databaseNamestringDatabase name"postgres"
engineIInstanceEnginePostgreSQL engine versionPostgreSQL 17.5
instanceTypeInstanceTypeInstance sizer6g.large
portnumberDatabase port5432
securityGroupIdsstring[]Existing security group IDsAuto-created

Storage Configuration

PropertyTypeDescriptionDefault
allocatedStoragenumberInitial storage in GB100
maxAllocatedStoragenumberMaximum auto-scaled storage500

Backup Configuration

PropertyTypeDescriptionDefault
backupRetentionDurationBackup retention periodDuration.days(14)

High Availability

PropertyTypeDescriptionDefault
multiAzbooleanEnable Multi-AZtrue

Monitoring Configuration

PropertyTypeDescriptionDefault
monitoringIntervalDurationEnhanced monitoring intervalDuration.minutes(1)
enablePerformanceInsightsbooleanEnable Performance Insightsfalse

Optional Features

PropertyTypeDescriptionDefault
databaseProxybooleanEnable RDS Proxyfalse
readReplicabooleanCreate read replicafalse

Default Configuration

The RDS Instance construct includes these defaults:
  • PostgreSQL 17.5 latest stable version
  • r6g.large instance type (2 vCPU, 16 GB RAM)
  • Multi-AZ enabled for high availability
  • 100 GB initial storage, auto-scales to 500 GB
  • Customer-managed KMS encryption for storage
  • Auto-rotating credentials every 30 days
  • 14-day backup retention
  • Enhanced monitoring every 1 minute
  • No RDS Proxy (opt-in feature)
  • No read replicas (opt-in feature)
  • No Performance Insights (opt-in feature)

Usage Patterns

Pattern 1: Basic Production Instance

const database = new RdsInstance(this, "ProductionDatabase", {
  vpc: prodVpc,
  databaseName: "production",
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.R6G,
    ec2.InstanceSize.XLARGE
  ),
  allocatedStorage: 200,
  maxAllocatedStorage: 1000,
  backupRetention: Duration.days(30)
});

// Standard production setup:
// - Multi-AZ for HA
// - Larger instance for performance
// - Extended backup retention

Pattern 2: With RDS Proxy and Read Replica

const database = new RdsInstance(this, "ScalableDatabase", {
  vpc: vpc,
  databaseName: "app",
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.R6G,
    ec2.InstanceSize.LARGE
  ),
  databaseProxy: true,      // Enable connection pooling
  readReplica: true,        // Enable read scaling
  enablePerformanceInsights: true
});

// Optimized for:
// - High connection counts (RDS Proxy)
// - Read-heavy workloads (read replica)
// - Query performance monitoring

Pattern 3: Cost-Optimized Development

const devDatabase = new RdsInstance(this, "DevDatabase", {
  vpc: devVpc,
  databaseName: "development",
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.T4G,
    ec2.InstanceSize.MEDIUM
  ),
  multiAz: false,          // Single-AZ for cost savings
  allocatedStorage: 50,
  maxAllocatedStorage: 100,
  backupRetention: Duration.days(7),
  monitoringInterval: Duration.minutes(5)
});

// Development-friendly:
// - Smaller instance size
// - No Multi-AZ overhead
// - Reduced monitoring frequency

Pattern 4: Custom Security Groups

const customSg = new ec2.SecurityGroup(this, "DatabaseSG", {
  vpc: vpc,
  description: "Custom security group for database",
  allowAllOutbound: false
});

customSg.addIngressRule(
  ec2.Peer.ipv4(vpc.vpcCidrBlock),
  ec2.Port.tcp(5432),
  "Allow VPC traffic"
);

const database = new RdsInstance(this, "SecureDatabase", {
  vpc: vpc,
  databaseName: "secure",
  securityGroupIds: [customSg.securityGroupId]
});

// Fine-grained network control

Integration Examples

With ECS Services

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

const database = new RdsInstance(this, "Database", {
  vpc: vpc,
  databaseName: "app",
  databaseProxy: true  // Recommended for container workloads
});

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

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

// ECS environment variables
// getHostEndpoint() returns the appropriate endpoint (proxy or direct)
const dbEndpoint = database.getHostEndpoint();

With Lambda Functions

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

const database = new RdsInstance(this, "Database", {
  vpc: vpc,
  databaseName: "events",
  databaseProxy: true  // Important for Lambda
});

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

database.getCredentials().grantRead(lambda);
database.connections.allowFrom(
  lambda,
  ec2.Port.tcp(Number(database.getHostPort()))
);

// Note: When databaseProxy is enabled, getHostEndpoint() returns the proxy endpoint
// RDS Proxy prevents connection exhaustion from Lambda scaling

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 RdsInstance(this, "Database", {
  vpc: vpc,
  databaseName: "webapp",
  databaseProxy: true,
  readReplica: true
});

// Write traffic through primary
// Read traffic through replica endpoint

RDS Proxy Configuration

When to Enable RDS Proxy

Enable RDS Proxy when you have:
  • Lambda functions (prevent connection exhaustion)
  • Container workloads with frequent scaling
  • Applications with many short-lived connections
  • Need for faster failover recovery
const database = new RdsInstance(this, "ProxyDatabase", {
  vpc: vpc,
  databaseName: "app",
  databaseProxy: true
});

// Proxy provides:
// - Connection pooling
// - 66% faster failover
// - IAM authentication support
// - Reduced database load

Without RDS Proxy

const database = new RdsInstance(this, "DirectDatabase", {
  vpc: vpc,
  databaseName: "app",
  databaseProxy: false  // Default
});

// Connect directly to instance
const endpoint = database.getHostEndpoint();

// Better for:
// - Long-lived connections
// - Cost sensitivity
// - Applications with built-in pooling

Read Replica Configuration

Enabling Read Replicas

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

// Creates:
// - Primary instance (read/write)
// - Read replica instance (read-only)
// - Separate endpoints for each

Using Read Replicas

// Primary endpoint for all operations
const primaryEndpoint = database.getHostEndpoint();

// Note: Read replica endpoint is not directly exposed as a property
// For read/write splitting, consider using Aurora which provides
// built-in reader endpoints, or manage read replica endpoints
// separately through CloudFormation outputs

// Application configuration
const pool = new Pool({ host: primaryEndpoint });

Storage Configuration

Storage Auto-Scaling

const database = new RdsInstance(this, "GrowingDatabase", {
  vpc: vpc,
  databaseName: "app",
  allocatedStorage: 100,      // Starting size
  maxAllocatedStorage: 1000   // Maximum auto-scale
});

// Storage automatically scales when:
// - Free space < 10% of allocated
// - Low space persists for 5 minutes
// - 6 hours since last scaling operation

Storage Types

// Default: gp3 (General Purpose SSD)
const database = new RdsInstance(this, "Database", {
  vpc: vpc,
  databaseName: "app",
  storageType: rds.StorageType.GP3,
  allocatedStorage: 100
});

// For higher IOPS requirements
const highPerfDatabase = new RdsInstance(this, "HighPerfDatabase", {
  vpc: vpc,
  databaseName: "performance",
  storageType: rds.StorageType.IO1,
  iops: 10000,
  allocatedStorage: 500
});

Security Configuration

Network Isolation

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

// No direct internet access

Encryption at Rest

// Automatic with customer-managed KMS key
const database = new RdsInstance(this, "EncryptedDatabase", {
  vpc: vpc,
  databaseName: "encrypted"
});

// Optional: Additional KMS key for Performance Insights
const database = new RdsInstance(this, "MonitoredDatabase", {
  vpc: vpc,
  databaseName: "monitored",
  enablePerformanceInsights: true
});
// Creates 2 KMS keys: storage + insights

Access Credentials

// Programmatic credential access
const secret = database.getCredentials();

// Application usage
const AWS = require('aws-sdk');
const secretsManager = new AWS.SecretsManager();

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

const { username, password, host, port } = JSON.parse(secret.SecretString);

Performance Configuration

Instance Sizing

// Compute-optimized (high CPU)
const computeDb = new RdsInstance(this, "ComputeDatabase", {
  vpc: vpc,
  databaseName: "compute",
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.C6G,  // Compute optimized
    ec2.InstanceSize.XLARGE
  )
});

// Memory-optimized (large datasets)
const memoryDb = new RdsInstance(this, "MemoryDatabase", {
  vpc: vpc,
  databaseName: "memory",
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.R6G,  // Memory optimized
    ec2.InstanceSize.XLARGE2
  )
});

// Burstable (variable workloads)
const burstableDb = new RdsInstance(this, "BurstableDatabase", {
  vpc: vpc,
  databaseName: "burstable",
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.T4G,  // Burstable
    ec2.InstanceSize.LARGE
  )
});

Performance Insights

const database = new RdsInstance(this, "MonitoredDatabase", {
  vpc: vpc,
  databaseName: "monitored",
  enablePerformanceInsights: true
});

// Provides:
// - Query performance monitoring
// - Wait event analysis
// - Top SQL queries
// - Database load metrics

Cost Optimization

Instance Type Selection

// Production: r6g.large (~$0.18/hour = ~$130/month)
const prodDb = new RdsInstance(this, "ProdDB", {
  vpc: vpc,
  databaseName: "prod",
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.R6G,
    ec2.InstanceSize.LARGE
  )
});

// Development: t4g.medium (~$0.07/hour = ~$50/month)
const devDb = new RdsInstance(this, "DevDB", {
  vpc: vpc,
  databaseName: "dev",
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.T4G,
    ec2.InstanceSize.MEDIUM
  ),
  multiAz: false
});

Multi-AZ Costs

// Single-AZ (half the cost)
const singleAzDb = new RdsInstance(this, "SingleAZ", {
  vpc: vpc,
  databaseName: "dev",
  multiAz: false
});

// Multi-AZ (2x instance cost)
const multiAzDb = new RdsInstance(this, "MultiAZ", {
  vpc: vpc,
  databaseName: "prod",
  multiAz: true
});

// Multi-AZ costs:
// - 2x compute (primary + standby)
// - Same storage cost
// - Cross-AZ data transfer

Storage Optimization

// Right-size storage
const optimizedDb = new RdsInstance(this, "OptimizedDB", {
  vpc: vpc,
  databaseName: "app",
  allocatedStorage: 50,      // Start small
  maxAllocatedStorage: 200   // Cap growth
});

// Storage costs:
// - gp3: $0.08/GB-month
// - io1: $0.125/GB-month + $0.10/IOPS-month

Methods

getHostEndpoint()

const endpoint = database.getHostEndpoint();
// Returns: Database instance endpoint
// Use this when RDS Proxy is disabled

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

credentials.grantRead(lambda);

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 { RdsInstance } from "@fjall/components-infrastructure/lib/resources/aws/database/rdsInstance";
import { EcsCluster } from "@fjall/components-infrastructure/lib/resources/aws/compute/ecsCluster";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as cloudwatch from "aws-cdk-lib/aws-cloudwatch";

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

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

    // Production database with all features
    const database = new RdsInstance(this, "ProductionDatabase", {
      vpc: vpc,
      databaseName: "production",

      // Performance configuration
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.R6G,
        ec2.InstanceSize.XLARGE
      ),

      // Storage configuration
      allocatedStorage: 200,
      maxAllocatedStorage: 1000,

      // High availability
      multiAz: true,

      // Scaling features
      databaseProxy: true,
      readReplica: true,

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

      // Compliance
      backupRetention: Duration.days(30)
    });

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

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

    // Lambda 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_HOST: database.proxy.endpoint,
        DB_PORT: "5432",
        DB_NAME: "production",
        DB_SECRET_ARN: database.getCredentials().secretArn
      }
    });

    database.getCredentials().grantRead(worker);
    database.connections.allowFrom(worker, ec2.Port.tcp(5432));

    // CloudWatch alarms
    new cloudwatch.Alarm(this, "HighCPU", {
      metric: database.metricCPUUtilization(),
      threshold: 80,
      evaluationPeriods: 2,
      alarmDescription: "Database CPU > 80%"
    });

    new cloudwatch.Alarm(this, "LowStorage", {
      metric: database.metricFreeStorageSpace(),
      threshold: 10 * 1024 * 1024 * 1024, // 10 GB
      evaluationPeriods: 1,
      comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD,
      alarmDescription: "Free storage < 10 GB"
    });

    // Outputs
    new CfnOutput(this, "DatabaseEndpoint", {
      value: database.getHostEndpoint(),
      description: "Database endpoint (proxy if enabled, otherwise primary)"
    });

    new CfnOutput(this, "DatabasePort", {
      value: database.getHostPort(),
      description: "Database port"
    });

    new CfnOutput(this, "SecretArn", {
      value: database.getCredentials().secretArn,
      description: "Database credentials secret"
    });
  }
}

Best Practices

  1. Enable Multi-AZ for production - automatic failover ensures high availability
  2. Use RDS Proxy with Lambda - prevents connection exhaustion from function scaling
  3. Right-size instance types - start with appropriate sizing, avoid over-provisioning
  4. Enable read replicas for read-heavy workloads - offload queries from primary
  5. Configure storage auto-scaling - prevents storage full incidents
  6. Use Performance Insights for optimization - identify slow queries and bottlenecks
  7. Implement connection pooling in applications - reduce database connection overhead
  8. Schedule maintenance windows - perform updates during low-traffic periods
  9. Monitor key metrics - CPU, storage, connections, replication lag
  10. Use Secrets Manager integration - automatic credential rotation enhances security

Common Patterns

Development, Staging, Production

// Development
const devDb = new RdsInstance(this, "DevDB", {
  vpc: devVpc,
  databaseName: "dev",
  instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.MEDIUM),
  multiAz: false,
  backupRetention: Duration.days(7)
});

// Staging (matches production size)
const stageDb = new RdsInstance(this, "StageDB", {
  vpc: stageVpc,
  databaseName: "staging",
  instanceType: ec2.InstanceType.of(ec2.InstanceClass.R6G, ec2.InstanceSize.LARGE),
  multiAz: true,
  databaseProxy: true
});

// Production (full features)
const prodDb = new RdsInstance(this, "ProdDB", {
  vpc: prodVpc,
  databaseName: "production",
  instanceType: ec2.InstanceType.of(ec2.InstanceClass.R6G, ec2.InstanceSize.XLARGE),
  multiAz: true,
  databaseProxy: true,
  readReplica: true,
  enablePerformanceInsights: true,
  backupRetention: Duration.days(30)
});

Microservices with Shared Database

const sharedDb = new RdsInstance(this, "SharedDB", {
  vpc: vpc,
  databaseName: "services",
  instanceType: ec2.InstanceType.of(ec2.InstanceClass.R6G, ec2.InstanceSize.XLARGE2),
  databaseProxy: true
});

// Multiple services connect via proxy
const service1 = new EcsCluster(this, "Service1", { vpc, serviceName: "users" });
const service2 = new EcsCluster(this, "Service2", { vpc, serviceName: "orders" });

sharedDb.connections.allowFrom(service1, ec2.Port.tcp(5432));
sharedDb.connections.allowFrom(service2, ec2.Port.tcp(5432));

// Each service uses separate schema/database

Read-Write Splitting

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

// Application configuration
const config = {
  write: {
    host: database.getHostEndpoint(),
    port: 5432
  },
  read: {
    host: database.readReplicaInstance!.instanceEndpoint.hostname,
    port: 5432
  }
};

// Route writes to primary, reads to replica

Cost Considerations

ComponentCostOptimization
Instance (r6g.large)~0.18/hour( 0.18/hour (~130/month)Use t4g for dev, right-size production
Multi-AZ2x instance costDisable for non-production
Storage (gp3)$0.08/GB-monthStart small, enable auto-scaling
RDS Proxy~$11/monthOnly enable when needed
Read ReplicaFull instance costOnly for read-heavy workloads
BackupsFree up to DB sizeReduce retention for dev
Performance InsightsFree (7-day retention)Extended retention adds cost
KMS Keys$1/month per key1-2 keys created automatically
Estimated Monthly Costs:
  • Development: ~$50-100 (t4g.medium, single-AZ, no extras)
  • Production: ~$300-600 (r6g.large, Multi-AZ, proxy)
  • Enterprise: ~$1000+ (r6g.xlarge+, Multi-AZ, proxy, replica, insights)

Troubleshooting

Common Issues

  1. Connection refused
    • Cause: Security group blocks traffic, wrong endpoint
    • Solution: Verify security group rules allow port 5432 from application
  2. Storage full
    • Cause: Auto-scaling not enabled, reached max storage
    • Solution: Enable auto-scaling, increase maxAllocatedStorage
  3. High CPU utilization
    • Cause: Under-sized instance, inefficient queries
    • Solution: Scale instance type, optimize queries with Performance Insights
  4. Slow queries
    • Cause: Missing indexes, table locks, parameter tuning
    • Solution: Analyze with Performance Insights, add indexes, adjust parameters
  5. Failover delay
    • Cause: Multi-AZ failover process
    • Solution: Enable RDS Proxy for 66% faster failover
  6. Replication lag (read replica)
    • Cause: High write volume, network latency
    • Solution: Scale primary instance, reduce write frequency

Debug Commands

# Describe instance
aws rds describe-db-instances \
  --db-instance-identifier <instance-id>

# Check instance status
aws rds describe-db-instances \
  --db-instance-identifier <instance-id> \
  --query 'DBInstances[0].DBInstanceStatus'

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

# Get current metrics
aws cloudwatch get-metric-statistics \
  --namespace AWS/RDS \
  --metric-name CPUUtilization \
  --dimensions Name=DBInstanceIdentifier,Value=<instance-id> \
  --start-time 2025-01-01T00:00:00Z \
  --end-time 2025-01-01T01:00:00Z \
  --period 300 \
  --statistics Average

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

# Test connectivity
psql -h <endpoint> -p 5432 -U postgres -d myapp

SQL Diagnostics

-- Check database size
SELECT pg_size_pretty(pg_database_size('myapp'));

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

-- Check active connections
SELECT count(*), state
FROM pg_stat_activity
GROUP BY state;

-- Identify locks
SELECT blocked_locks.pid AS blocked_pid,
       blocking_locks.pid AS blocking_pid
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_locks blocking_locks
  ON blocking_locks.locktype = blocked_locks.locktype
WHERE NOT blocked_locks.granted;

-- Table sizes
SELECT schemaname, tablename,
       pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename))
FROM pg_tables
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC
LIMIT 10;

See Also