CDK Infrastructure Pattern¶
Purpose¶
Define a standard two-stack CDK architecture for service modules: a shared CommonResourcesStack for networking and database resources, and a per-module stack for Lambda functions, queues, and event wiring.
Stack Composition¶
┌─────────────────────────────────────────────┐
│ CommonResourcesStack │
│ │
│ ├── VPC (imported from existing) │
│ ├── Lambda Security Group │
│ ├── RDS Writer Proxy │
│ ├── RDS Reader Proxy (read-only endpoint) │
│ ├── SNS Topic (imported or created) │
│ └── Exports: proxy endpoints, ARNs │
└──────────────────┬──────────────────────────┘
│ addDependency()
│ Fn.importValue()
┌──────────────────▼──────────────────────────┐
│ DocLoad Stack (per-module) │
│ │
│ ├── Python Package Layer │
│ ├── Document Loader Lambda │
│ ├── Job Processor Lambda + SQS Queue + DLQ │
│ ├── Batch Processor Lambda + SQS Queue + DLQ│
│ ├── SNS Subscriptions with Filter Policies │
│ └── IAM Roles and Policies │
└──────────────────────────────────────────────┘
CommonResourcesStack¶
Shared resources created once per environment/region, consumed by all modules.
Exported Resources¶
// Cross-stack exports using CDK Fn.importValue()
new cdk.CfnOutput(this, 'WriterProxyEndpoint', {
value: writerProxy.endpoint,
exportName: `${stackNamePrefix}-WriterProxyEndpoint`,
});
new cdk.CfnOutput(this, 'ReaderProxyEndpoint', {
value: readerEndpoint.attrEndpoint,
exportName: `${stackNamePrefix}-ReaderProxyEndpoint`,
});
new cdk.CfnOutput(this, 'WriterProxyArn', {
value: writerProxy.dbProxyArn,
exportName: `${stackNamePrefix}-WriterProxyArn`,
});
new cdk.CfnOutput(this, 'ReaderProxyArn', {
value: readerProxy.dbProxyArn,
exportName: `${stackNamePrefix}-ReaderProxyArn`,
});
RDS Proxy Configuration¶
Separate writer and reader proxies with environment-specific connection limits:
// Writer Proxy — for mutations
const writerProxy = new rds.DatabaseProxy(this, 'WriterProxy', {
proxyTarget: rds.ProxyTarget.fromCluster(dbCluster),
secrets: [dbSecret],
vpc,
dbProxyName: `${stackNamePrefix}-database-writer-proxy`,
maxConnectionsPercent: config.rdsProxyConfig.writerMaxConnections,
maxIdleConnectionsPercent: config.rdsProxyConfig.writerMaxIdleConnections,
requireTLS: false,
iamAuth: false,
});
// Reader Proxy — for read-only queries
const readerProxy = new rds.DatabaseProxy(this, 'ReaderProxy', {
// Same config but with reader connection limits
maxConnectionsPercent: config.rdsProxyConfig.readerMaxConnections,
maxIdleConnectionsPercent: config.rdsProxyConfig.readerMaxIdleConnections,
});
// Dedicated read-only endpoint on reader proxy
const readerEndpoint = new rds.CfnDBProxyEndpoint(this, 'ReaderEndpoint', {
dbProxyEndpointName: `${stackNamePrefix}-reader-endpoint`,
dbProxyName: readerProxy.dbProxyName,
vpcSubnetIds: privateSubnetIds,
targetRole: 'READ_ONLY',
});
Connection Limits by Environment¶
| Environment | Writer Max | Writer Idle | Reader Max | Reader Idle |
|---|---|---|---|---|
| Development | 60 | 50 | 60 | 50 |
| QA | 20 | 3 | 20 | 3 |
| Staging | 60 | varies | 60 | varies |
| Production | 20 | varies | 20 | varies |
SNS Topic Strategy¶
// Production/Staging/QA: reuse topic from CommonResourcesStack
if (envName !== 'development') {
snsStatusMonitorTopic = commonResources.snsStatusMonitorTopic;
} else {
// Development: create a new local topic per developer
snsStatusMonitorTopic = new sns.Topic(this, 'StatusMonitorTopic', {
topicName: snsTopicName,
});
}
Global Tags¶
Applied to all stack resources:
function applyGlobalTags(stack, env) {
cdk.Tags.of(stack).add('map-migrated', 'mig6UGCR2HJGG');
cdk.Tags.of(stack).add('Source', 'CDK');
cdk.Tags.of(stack).add('APPLICATION', 'next-gen');
cdk.Tags.of(stack).add('MODULE', 'document-loader');
cdk.Tags.of(stack).add('ENVIRONMENT', env);
}
Module Stack (DocLoad)¶
Lambda Function Pattern¶
const documentLoader = new lambda.Function(this, 'DocumentLoader', {
functionName: `${stackNamePrefix}-DocumentLoader`,
runtime: lambda.Runtime.PYTHON_3_13,
handler: 'index.lambda_handler',
code: lambda.Code.fromAsset('lambda/src/loader'),
memorySize: 256,
timeout: cdk.Duration.minutes(15),
layers: [pythonPackageLayer],
vpc: commonResources.vpc,
vpcSubnets: { subnets: privateSubnets },
securityGroups: [lambdaSecurityGroup],
environment: {
ENV: envName,
STACK_NAME: stackName,
RDS_DBNAME: dbClusterDbName,
RDS_HOST: writerProxyEndpoint,
WRITER_PROXY_ENDPOINT: writerProxyEndpoint,
READER_PROXY_ENDPOINT: readerProxyEndpoint,
RDS_SECRET_ARN: dbSecret.secretArn,
RDS_SECRET_NAME: dbSecret.secretName,
SNS_STATUS_MONITOR_ARN: snsStatusMonitorTopic.topicArn,
DEPLOY_REGION: awsRegion,
},
});
SQS Queue + DLQ Pattern¶
// Dead Letter Queue
const dlq = new sqs.Queue(this, 'JobProcessingDLQ', {
queueName: `${queueName}-dead-letter`,
retentionPeriod: cdk.Duration.days(14),
});
// Main Queue
const jobQueue = new sqs.Queue(this, 'JobProcessingQueue', {
queueName: `${stackNamePrefix}-DocLoaderJobsSQSQueue`,
visibilityTimeout: cdk.Duration.minutes(15), // Match Lambda timeout
deadLetterQueue: {
queue: dlq,
maxReceiveCount: 3,
},
});
SNS → SQS Subscription with Filter¶
snsStatusMonitorTopic.addSubscription(
new subs.SqsSubscription(jobQueue, {
rawMessageDelivery: false,
filterPolicyWithMessageBody: {
eventType: sns.FilterOrPolicy.filter(
sns.SubscriptionFilter.stringFilter({
allowlist: ['JOB_STARTED', 'JOB_FINISHED', 'IMPORT_CANCELLED', 'BATCH_END_FINISHED'],
})
),
},
})
);
SQS → Lambda Event Source¶
Lambda Layer¶
const pythonPackageLayer = new lambda.LayerVersion(this, 'PythonPackageLayer', {
code: lambda.Code.fromAsset('assets/layer'),
compatibleRuntimes: [lambda.Runtime.PYTHON_3_13],
description: 'Python packages: sqlalchemy, pymysql, pyyaml, etc.',
});
Layer Contents¶
The layer contains production dependencies only. Test dependencies are
installed locally via pip install -r requirements-dev.txt.
| Package | Version | Purpose |
|---|---|---|
sqlalchemy |
2.x | ORM — database models, sessions, queries |
pymysql |
1.x | MySQL driver for SQLAlchemy |
pyyaml |
6.x | YAML parsing (ES mappings, config files) |
pydantic |
2.x | Data validation schemas (if used by module) |
elasticsearch |
7.x | ES client (if module interacts with ES) |
requests |
2.x | HTTP client (if module calls external APIs) |
NOT included in the layer (available in Lambda runtime or dev-only):
- boto3 / botocore — pre-installed in Lambda runtime
- pytest, moto, pytest-mock, pytest-cov — test dependencies only
- black, isort — formatting tools, not runtime dependencies
Layer is built from requirements.txt in the module's assets/layer/ directory.
Pin exact versions to prevent cross-environment drift.
IAM Least-Privilege Pattern¶
const lambdaRole = new iam.Role(this, 'LambdaRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
],
});
// Scoped permissions — specific resource ARNs
lambdaRole.addToPolicy(new iam.PolicyStatement({
actions: ['s3:GetObject', 's3:ListBucket'],
resources: bucketArns,
}));
lambdaRole.addToPolicy(new iam.PolicyStatement({
actions: ['rds-db:connect'],
resources: [writerProxyArn, readerProxyArn],
}));
// Secret access
dbSecret.grantRead(lambdaRole);
Environment Configuration¶
Infrastructure config is a TypeScript file mapping environment/region to resource IDs:
interface EnvironmentConfig {
account?: string;
region: string;
vpcId: string;
dbClusterEndpoint: string;
dbClusterIdentifier: string;
dbClusterDbName: string;
snsStatusMonitorTopicArn: string;
rdsProxyConfig: {
writerMaxConnections: number;
readerMaxConnections: number;
writerMaxIdleConnections: number;
readerMaxIdleConnections: number;
};
engine: string;
engineVersion: string;
privateSubnetIds: string[];
lambdaSG: string;
rdsSG: string;
rdsSecretArn: string;
bucketAccess: string[];
}
Stack Naming¶
{regionCode}-{envName} # e.g., "use1-staging"
{regionCode}-{envName}-{alias} # Dev includes developer alias
Lambda Configurations Summary¶
| Lambda | Memory | Timeout | Reserved Concurrency | Batch Size | Handler |
|---|---|---|---|---|---|
| DocumentLoader | 256 MB | 15 min | None (dynamic) | Per-batch | index.lambda_handler |
| JobProcessor | 128 MB | 15 min | 40 | 1 | job_processor_index.lambda_handler |
| BatchProcessor | 256 MB | 15 min | 40 | 1 | batch_index.lambda_handler |
Key Rules¶
- Visibility timeout = Lambda timeout — prevents messages reappearing while Lambda runs
- DLQ retention = 14 days — enough time to investigate and redrive
- maxReceiveCount = 3 — 3 failures before DLQ (separate from application-level retry)
- Batch size 1 for orchestrators — JobProcessor and BatchProcessor need single-record semantics
- Reserved concurrency on orchestrators — prevents runaway scaling
- RecursiveLoop = ALLOW — job processor re-enqueues to its own queue by design
- SNS topic shared in prod, separate in dev — dev isolation without infrastructure cost
- All env vars in CDK — Lambda code reads
os.environ, CDK sets the values - Layer for Python deps — single source of truth for package versions
- Private subnets only — Lambda in VPC with no public internet access
Ask questions about Nextpoint architecture, patterns, rules, or any module. Powered by Claude Opus 4.6.