
Cdk Infrastructure
- 25 installs
- 15 repo stars
- Updated August 3, 2026
- boise-state-development/agentcore-public-stack
cdk-infrastructure is a Claude Code skill that guides AWS CDK infrastructure development in TypeScript for a Bedrock AgentCore stack.
About
cdk-infrastructure is a Claude Code skill that documents AWS CDK infrastructure development in TypeScript for a Bedrock AgentCore stack. It covers stack organization, a centralized configuration loader, resource naming conventions, cross-stack references via SSM parameters, and patterns for DynamoDB, ECS/Fargate, Lambda, S3, IAM, and networking. A developer uses it when creating or modifying CDK stacks and CloudFormation resources in this monorepo. It also encodes AgentCore-specific constraints like underscore naming and Secrets Manager ARN wildcards.
- Guides AWS CDK infrastructure development in TypeScript for a Bedrock AgentCore stack
- Codifies cross-stack references via SSM, resource naming, and deployment ordering
- Covers DynamoDB, ECS/Fargate, Lambda, S3, IAM, and networking patterns
Cdk Infrastructure by the numbers
- 25 all-time installs (skills.sh)
- Ranked #803 of 1,039 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
cdk-infrastructure capabilities & compatibility
- Capabilities
- provision cdk stack · configure dynamodb · configure ecs fargate · manage ssm references · configure iam
- Works with
- aws
- Use cases
- devops · ci cd
- Runs
- Runs locally
- Pricing
- Free
What cdk-infrastructure says it does
AWS CDK infrastructure development with TypeScript. Use when creating or modifying CDK stacks, constructs, DynamoDB tables, ECS/Fargate services, Lambda functions, S3 buckets, networking, IAM roles, o
`InfrastructureStack` - VPC, ALB, ECS Cluster (always first)
**AgentCore Names:** Use underscores, not hyphens:
npx skills add https://github.com/boise-state-development/agentcore-public-stack --skill cdk-infrastructureAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 25 |
|---|---|
| repo stars | ★ 15 |
| Last updated | August 3, 2026 |
| Repository | boise-state-development/agentcore-public-stack ↗ |
What it does
Create or modify AWS CDK stacks and CloudFormation resources for a Bedrock AgentCore deployment.
Who is it for?
Developers creating or modifying CDK stacks, constructs, and CloudFormation resources in this AgentCore monorepo
Skip if: Hardcoding secrets or using hyphens in AgentCore resource names, both of which the skill forbids
When should I use this skill?
creating or modifying CDK stacks, DynamoDB tables, ECS/Fargate services, Lambda functions, S3 buckets, or IAM roles
What you get
CDK stacks follow the config system, deploy in the right order, and share resources via SSM without hardcoded secrets.
- new or modified CDK stacks
- cross-stack SSM references
- AgentCore-compliant resource configuration
By the numbers
- 9 reference files (agentcore, configuration, dynamodb, ecs-fargate, iam, lambda, networking, s3)
- SSM categories: network, quota, cost-tracking, auth, frontend, gateway
Files
AWS CDK Infrastructure Best Practices
TypeScript
- Use strict type checking
- Import from
aws-cdk-libandconstructs - Use L2 constructs when available, L1 (Cfn*) when necessary
Stack Organization
infrastructure/
├── bin/infrastructure.ts # App entrypoint
├── lib/
│ ├── config.ts # Configuration loader
│ ├── infrastructure-stack.ts # Network resources (deploy first)
│ ├── app-api-stack.ts # Backend services
│ └── my-new-stack.ts # New stacks go here
└── cdk.context.json # ConfigurationDeployment Order: 1. InfrastructureStack - VPC, ALB, ECS Cluster (always first) 2. Other stacks import network resources via SSM
Configuration
Use the centralized config system:
import { loadConfig, getResourceName, getStackEnv, applyStandardTags } from './config';
export class MyStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
const config = loadConfig(scope);
super(scope, id, {
...props,
env: getStackEnv(config),
stackName: getResourceName(config, 'my-stack'),
});
applyStandardTags(this, config);
}
}For configuration patterns, see references/configuration.md.
Naming Conventions
Resource Names: Use getResourceName():
getResourceName(config, 'user-quotas') // "bsu-agentcore-user-quotas"SSM Parameters: Hierarchical naming:
/{projectPrefix}/{category}/{resource-type}Categories: /network/, /quota/, /cost-tracking/, /auth/, /frontend/, /gateway/
Cross-Stack References
Export:
new ssm.StringParameter(this, 'VpcIdParam', {
parameterName: `/${config.projectPrefix}/network/vpc-id`,
stringValue: vpc.vpcId,
});Import:
const vpcId = ssm.StringParameter.valueForStringParameter(
this,
`/${config.projectPrefix}/network/vpc-id`
);DynamoDB Tables
- Always use PK + SK for flexibility
- Use
PAY_PER_REQUESTbilling - Enable point-in-time recovery
- Environment-based removal policy
For table patterns, see references/dynamodb.md.
ECS/Fargate
- Import cluster from SSM
- Health checks mandatory
- Auto-scaling with CPU/memory targets
- Circuit breaker for rollback
For service patterns, see references/ecs-fargate.md.
Lambda
- Use ARM64 architecture (cost optimization)
- Role with least privilege
- Secrets Manager access requires wildcard suffix
For Lambda patterns, see references/lambda.md.
S3 Buckets
- Block public access
- Enable versioning
- Lifecycle rules for cost optimization
- Include account ID for global uniqueness
For bucket patterns, see references/s3.md.
Security
- Separate security groups for ALB and ECS
- Private subnets for services
- IAM roles with SIDs for clarity
- Never hardcode secrets
For IAM patterns, see references/iam.md.
Important Constraints
AgentCore Names: Use underscores, not hyphens:
name: getResourceName(config, 'memory').replace(/-/g, '_')Secrets Manager ARN: Include wildcard for random suffix:
resources: [`${secret.secretArn}*`]Environment Removal Policy:
removalPolicy: config.environment === 'prod'
? cdk.RemovalPolicy.RETAIN
: cdk.RemovalPolicy.DESTROYCDK Commands
cd infrastructure
npm install # Install dependencies
npx cdk synth # Synthesize CloudFormation
npx cdk deploy --all # Deploy all stacks
npx cdk diff # Preview changesBedrock AgentCore Patterns
Important: Naming Convention
AgentCore resources require underscores instead of hyphens:
// CORRECT
name: getResourceName(config, 'agentcore_memory').replace(/-/g, '_')
// INCORRECT - will fail
name: getResourceName(config, 'agentcore-memory')Memory Configuration
import * as bedrock from 'aws-cdk-lib/aws-bedrock';
import { getResourceName } from './config';
const memory = new bedrock.CfnMemory(this, 'Memory', {
name: getResourceName(config, 'agentcore_memory').replace(/-/g, '_'),
eventExpiryDuration: 90, // Days (min: 7, max: 365)
memoryExecutionRoleArn: memoryRole.roleArn,
description: 'AgentCore Memory for conversation context',
memoryStrategies: [
{
semanticMemoryStrategy: {
name: 'SemanticFactExtraction',
description: 'Extracts semantic facts from conversations',
},
},
{
summaryMemoryStrategy: {
name: 'ConversationSummary',
description: 'Generates conversation summaries',
},
},
{
userPreferenceMemoryStrategy: {
name: 'UserPreferenceExtraction',
description: 'Stores user preferences',
},
},
],
});
// Add dependency on role
memory.node.addDependency(memoryRole);Memory Role
const memoryRole = new iam.Role(this, 'MemoryRole', {
roleName: getResourceName(config, 'agentcore-memory-role'),
assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
description: 'Execution role for AgentCore Memory',
});
memoryRole.addToPolicy(new iam.PolicyStatement({
sid: 'BedrockModelAccess',
actions: [
'bedrock:InvokeModel',
'bedrock:InvokeModelWithResponseStream',
],
resources: ['arn:aws:bedrock:*::foundation-model/*'],
}));Gateway Configuration
import * as agentcore from 'aws-cdk-lib/aws-bedrockagentcore';
const gateway = new agentcore.CfnGateway(this, 'MCPGateway', {
name: getResourceName(config, 'mcp-gateway'),
description: 'MCP Gateway for custom tools',
roleArn: gatewayRole.roleArn,
authorizerType: 'AWS_IAM', // SigV4 authentication
protocolType: 'MCP', // MCP protocol only
exceptionLevel: 'DEBUG', // Only DEBUG supported currently
protocolConfiguration: {
mcp: {
supportedVersions: ['2025-11-25'],
searchType: 'SEMANTIC',
},
},
});Gateway Role
const gatewayRole = new iam.Role(this, 'GatewayRole', {
roleName: getResourceName(config, 'agentcore-gateway-role'),
assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
description: 'Execution role for AgentCore MCP Gateway',
});
// Allow invoking Lambda functions
gatewayRole.addToPolicy(new iam.PolicyStatement({
sid: 'LambdaInvoke',
actions: ['lambda:InvokeFunction'],
resources: [
`arn:aws:lambda:${config.awsRegion}:${config.awsAccount}:function:${config.projectPrefix}-mcp-*`,
],
}));Gateway Target (MCP Tool)
const gatewayTarget = new agentcore.CfnGatewayTarget(this, 'GoogleWebSearchTarget', {
name: 'google-web-search',
gatewayIdentifier: gateway.attrGatewayId,
description: 'Google web search via MCP',
credentialProviderConfigurations: [{
credentialProviderType: 'GATEWAY_IAM_ROLE',
}],
targetConfiguration: {
mcp: {
lambda: {
lambdaArn: googleSearchFunction.functionArn,
toolSchema: {
inlinePayload: [{
name: 'google_web_search',
description: 'Search the web using Google Custom Search API',
inputSchema: {
type: 'object',
required: ['query'],
properties: {
query: {
type: 'string',
description: 'Search query string',
},
num_results: {
type: 'integer',
description: 'Number of results (1-10)',
default: 10,
},
},
},
}],
},
},
},
},
});
gatewayTarget.node.addDependency(gateway);Lambda Permission for Gateway
// Allow Gateway to invoke Lambda
googleSearchFunction.addPermission('GatewayPermission', {
principal: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
action: 'lambda:InvokeFunction',
sourceArn: gateway.attrGatewayArn,
});Code Interpreter
const codeInterpreter = new bedrock.CfnCodeInterpreterCustom(this, 'CodeInterpreter', {
name: getResourceName(config, 'code_interpreter').replace(/-/g, '_'),
description: 'Custom Code Interpreter for Python code execution',
networkConfiguration: { networkMode: 'PUBLIC' },
executionRoleArn: codeInterpreterRole.roleArn,
});
codeInterpreter.node.addDependency(codeInterpreterRole);Code Interpreter Role
const codeInterpreterRole = new iam.Role(this, 'CodeInterpreterRole', {
roleName: getResourceName(config, 'agentcore-code-interpreter-role'),
assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
description: 'Execution role for AgentCore Code Interpreter',
});
// S3 access for file operations
codeInterpreterRole.addToPolicy(new iam.PolicyStatement({
sid: 'S3Access',
actions: [
's3:GetObject',
's3:PutObject',
's3:ListBucket',
],
resources: [
workspaceBucket.bucketArn,
`${workspaceBucket.bucketArn}/*`,
],
}));Browser
const browser = new bedrock.CfnBrowserCustom(this, 'Browser', {
name: getResourceName(config, 'browser').replace(/-/g, '_'),
description: 'Custom Browser for web interaction',
networkConfiguration: { networkMode: 'PUBLIC' },
executionRoleArn: browserRole.roleArn,
});
browser.node.addDependency(browserRole);Browser Role
const browserRole = new iam.Role(this, 'BrowserRole', {
roleName: getResourceName(config, 'agentcore-browser-role'),
assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
description: 'Execution role for AgentCore Browser',
});
// No additional permissions typically needed for basic browsingRuntime Role (for invoking AgentCore services)
const runtimeRole = new iam.Role(this, 'RuntimeRole', {
roleName: getResourceName(config, 'agentcore-runtime-role'),
assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
description: 'Execution role for AWS Bedrock AgentCore Runtime',
});
// Model invocation
runtimeRole.addToPolicy(new iam.PolicyStatement({
sid: 'BedrockModelInvocation',
actions: [
'bedrock:InvokeModel',
'bedrock:InvokeModelWithResponseStream',
],
resources: [
'arn:aws:bedrock:*::foundation-model/*',
`arn:aws:bedrock:${config.awsRegion}:${config.awsAccount}:*`,
],
}));
// Memory access
runtimeRole.addToPolicy(new iam.PolicyStatement({
sid: 'MemoryAccess',
actions: [
'bedrock-agentcore:CreateEvent',
'bedrock-agentcore:RetrieveMemory',
'bedrock-agentcore:ListMemorySessions',
'bedrock-agentcore:GetMemorySession',
],
resources: [memory.attrMemoryArn],
}));
// Code Interpreter access
runtimeRole.addToPolicy(new iam.PolicyStatement({
sid: 'CodeInterpreterAccess',
actions: [
'bedrock-agentcore:InvokeCodeInterpreter',
'bedrock-agentcore:CreateCodeInterpreterSession',
],
resources: [codeInterpreter.attrCodeInterpreterArn],
}));
// Browser access
runtimeRole.addToPolicy(new iam.PolicyStatement({
sid: 'BrowserAccess',
actions: ['bedrock-agentcore:InvokeBrowser'],
resources: [browser.attrBrowserArn],
}));
// Gateway access
runtimeRole.addToPolicy(new iam.PolicyStatement({
sid: 'GatewayAccess',
actions: ['bedrock-agentcore:InvokeGateway'],
resources: [gateway.attrGatewayArn],
}));Exporting AgentCore Resources to SSM
// Memory
new ssm.StringParameter(this, 'MemoryIdParam', {
parameterName: `/${config.projectPrefix}/agentcore/memory-id`,
stringValue: memory.attrMemoryId,
});
new ssm.StringParameter(this, 'MemoryArnParam', {
parameterName: `/${config.projectPrefix}/agentcore/memory-arn`,
stringValue: memory.attrMemoryArn,
});
// Gateway
new ssm.StringParameter(this, 'GatewayIdParam', {
parameterName: `/${config.projectPrefix}/gateway/id`,
stringValue: gateway.attrGatewayId,
});
new ssm.StringParameter(this, 'GatewayUrlParam', {
parameterName: `/${config.projectPrefix}/gateway/url`,
stringValue: gateway.attrGatewayUrl,
});
// Code Interpreter
new ssm.StringParameter(this, 'CodeInterpreterIdParam', {
parameterName: `/${config.projectPrefix}/agentcore/code-interpreter-id`,
stringValue: codeInterpreter.attrCodeInterpreterId,
});
// Browser
new ssm.StringParameter(this, 'BrowserIdParam', {
parameterName: `/${config.projectPrefix}/agentcore/browser-id`,
stringValue: browser.attrBrowserId,
});Environment Variables for Backend
// In ECS task definition
environment: {
AGENTCORE_MEMORY_ID: ssm.StringParameter.valueForStringParameter(
this,
`/${config.projectPrefix}/agentcore/memory-id`
),
AGENTCORE_GATEWAY_URL: ssm.StringParameter.valueForStringParameter(
this,
`/${config.projectPrefix}/gateway/url`
),
AGENTCORE_CODE_INTERPRETER_ID: ssm.StringParameter.valueForStringParameter(
this,
`/${config.projectPrefix}/agentcore/code-interpreter-id`
),
AGENTCORE_BROWSER_ID: ssm.StringParameter.valueForStringParameter(
this,
`/${config.projectPrefix}/agentcore/browser-id`
),
},Configuration Patterns
Two-Level Configuration System
1. Environment variables (highest priority) - prefix: CDK_, ENV_ 2. cdk.context.json values (fallback) 3. Hardcoded defaults in config.ts
Using Configuration
import { loadConfig, getResourceName, getStackEnv, applyStandardTags } from './config';
export class MyStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
const config = loadConfig(scope);
super(scope, id, {
...props,
env: getStackEnv(config),
stackName: getResourceName(config, 'my-stack'),
});
applyStandardTags(this, config);
// Use config values
const cpu = config.myService.cpu;
const memory = config.myService.memory;
}
}Adding New Configuration
Update lib/config.ts:
// 1. Add to Config interface
myService: {
enabled: boolean;
cpu: number;
memory: number;
desiredCount: number;
maxCapacity: number;
};
// 2. Add to loadConfig function
myService: {
enabled: parseBooleanEnv('CDK_MY_SERVICE_ENABLED') ??
(scope.node.tryGetContext('myService')?.enabled ?? true),
cpu: parseIntEnv('CDK_MY_SERVICE_CPU') ??
(scope.node.tryGetContext('myService')?.cpu ?? 512),
memory: parseIntEnv('CDK_MY_SERVICE_MEMORY') ??
(scope.node.tryGetContext('myService')?.memory ?? 1024),
desiredCount: parseIntEnv('CDK_MY_SERVICE_DESIRED_COUNT') ??
(scope.node.tryGetContext('myService')?.desiredCount ?? 1),
maxCapacity: parseIntEnv('CDK_MY_SERVICE_MAX_CAPACITY') ??
(scope.node.tryGetContext('myService')?.maxCapacity ?? 4),
},Adding to cdk.context.json
{
"projectPrefix": "bsu-agentcore",
"environment": "dev",
"awsRegion": "us-west-2",
"myService": {
"enabled": true,
"cpu": 512,
"memory": 1024,
"desiredCount": 1,
"maxCapacity": 4
}
}Environment Variable Overrides
# Override via environment
CDK_MY_SERVICE_ENABLED=true
CDK_MY_SERVICE_CPU=1024
ENV_MY_SERVICE_API_KEY=secret-valueNaming Convention
Pattern: {projectPrefix}-{environment-if-not-prod}-{resource-type}
// Production
getResourceName(config, 'vpc') // "bsu-agentcore-vpc"
// Development
getResourceName(config, 'vpc') // "bsu-agentcore-dev-vpc"
// With account ID for global uniqueness (S3)
getResourceName(config, 'frontend', config.awsAccount)
// "bsu-agentcore-frontend-123456789012"SSM Parameter Naming
Hierarchical naming for cross-stack references:
/{projectPrefix}/{category}/{resource-type}/{property}
Categories:
- /network/ - VPC, subnets, ALB, ECS cluster
- /quota/ - Quota management resources
- /cost-tracking/ - Cost tracking resources
- /auth/ - Authentication resources
- /admin/ - Admin resources
- /file-upload/ - File upload resources
- /frontend/ - Frontend resources
- /gateway/ - Gateway resources
- /app-api/ - App API resources
- /inference-api/ - Inference API resourcesCloudFormation Export Naming
new cdk.CfnOutput(this, 'VpcId', {
value: vpc.vpcId,
description: 'VPC ID',
exportName: `${config.projectPrefix}-VpcId`,
});Registering New Stacks
Add to bin/infrastructure.ts:
import { MyNewStack } from '../lib/my-new-stack';
if (config.myNewStack?.enabled !== false) {
new MyNewStack(app, 'MyNewStack', {
env: getStackEnv(config),
});
}DynamoDB Table Patterns
Standard Table Structure
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import { getResourceName } from './config';
const table = new dynamodb.Table(this, 'UserQuotasTable', {
tableName: getResourceName(config, 'user-quotas'),
// Always use PK + SK for flexibility
partitionKey: { name: 'PK', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'SK', type: dynamodb.AttributeType.STRING },
// PAY_PER_REQUEST for variable workloads
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
// Production safety
pointInTimeRecovery: true,
encryption: dynamodb.TableEncryption.AWS_MANAGED,
// Environment-based retention
removalPolicy: config.environment === 'prod'
? cdk.RemovalPolicy.RETAIN
: cdk.RemovalPolicy.DESTROY,
});GSI Pattern
// GSI naming: DescriptiveNameIndex
// GSI keys: GSI{n}PK / GSI{n}SK
table.addGlobalSecondaryIndex({
indexName: 'OwnerStatusIndex',
partitionKey: { name: 'GSI1PK', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'GSI1SK', type: dynamodb.AttributeType.STRING },
projectionType: dynamodb.ProjectionType.ALL,
});
table.addGlobalSecondaryIndex({
indexName: 'EmailDomainIndex',
partitionKey: { name: 'GSI2PK', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'GSI2SK', type: dynamodb.AttributeType.STRING },
projectionType: dynamodb.ProjectionType.ALL,
});
table.addGlobalSecondaryIndex({
indexName: 'CreatedAtIndex',
partitionKey: { name: 'GSI3PK', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'GSI3SK', type: dynamodb.AttributeType.STRING },
projectionType: dynamodb.ProjectionType.ALL,
});TTL Configuration
// For ephemeral data (sessions, auth state)
const table = new dynamodb.Table(this, 'OidcStateTable', {
tableName: getResourceName(config, 'oidc-state'),
partitionKey: { name: 'PK', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'SK', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
// Enable TTL for automatic expiration
timeToLiveAttribute: 'ttl', // or 'expiresAt'
removalPolicy: cdk.RemovalPolicy.DESTROY,
});Table Key Patterns
Use composite keys with type prefixes:
| Pattern | Example | Use Case |
|---|---|---|
USER#{id} | USER#abc123 | User-scoped data |
SESSION#{id} | SESSION#sess-001 | Session data |
ROLE#{id} | ROLE#admin | Role definitions |
QUOTA#{type} | QUOTA#monthly | Quota tiers |
TIMESTAMP#{ts} | TIMESTAMP#2024-01-15T10:30:00Z | Time-ordered data |
MESSAGE#{id} | MESSAGE#msg-001 | Message records |
CONFIG | CONFIG | Configuration record |
Example Tables
User Quotas Table
const userQuotasTable = new dynamodb.Table(this, 'UserQuotasTable', {
tableName: getResourceName(config, 'user-quotas'),
partitionKey: { name: 'PK', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'SK', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
pointInTimeRecovery: true,
encryption: dynamodb.TableEncryption.AWS_MANAGED,
removalPolicy: config.environment === 'prod'
? cdk.RemovalPolicy.RETAIN
: cdk.RemovalPolicy.DESTROY,
});
// GSI for looking up by tier
userQuotasTable.addGlobalSecondaryIndex({
indexName: 'TierLookupIndex',
partitionKey: { name: 'GSI1PK', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'GSI1SK', type: dynamodb.AttributeType.STRING },
projectionType: dynamodb.ProjectionType.ALL,
});Sessions Metadata Table
const sessionsMetadataTable = new dynamodb.Table(this, 'SessionsMetadataTable', {
tableName: getResourceName(config, 'sessions-metadata'),
partitionKey: { name: 'PK', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'SK', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
pointInTimeRecovery: true,
encryption: dynamodb.TableEncryption.AWS_MANAGED,
removalPolicy: config.environment === 'prod'
? cdk.RemovalPolicy.RETAIN
: cdk.RemovalPolicy.DESTROY,
});
// GSI for user + timestamp lookups
sessionsMetadataTable.addGlobalSecondaryIndex({
indexName: 'UserTimestampIndex',
partitionKey: { name: 'GSI1PK', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'GSI1SK', type: dynamodb.AttributeType.STRING },
projectionType: dynamodb.ProjectionType.ALL,
});
// GSI for session lookups
sessionsMetadataTable.addGlobalSecondaryIndex({
indexName: 'SessionLookupIndex',
partitionKey: { name: 'GSI2PK', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'GSI2SK', type: dynamodb.AttributeType.STRING },
projectionType: dynamodb.ProjectionType.ALL,
});Exporting Table Names to SSM
// Export table name for cross-stack reference
new ssm.StringParameter(this, 'UserQuotasTableNameParam', {
parameterName: `/${config.projectPrefix}/quota/user-quotas-table-name`,
stringValue: userQuotasTable.tableName,
description: 'User quotas DynamoDB table name',
});
new ssm.StringParameter(this, 'SessionsMetadataTableNameParam', {
parameterName: `/${config.projectPrefix}/cost-tracking/sessions-metadata-table-name`,
stringValue: sessionsMetadataTable.tableName,
description: 'Sessions metadata DynamoDB table name',
});Granting Access
// In ECS task definition
userQuotasTable.grantReadWriteData(taskDefinition.taskRole);
sessionsMetadataTable.grantReadWriteData(taskDefinition.taskRole);
// Read-only access
quotaEventsTable.grantReadData(taskDefinition.taskRole);ECS/Fargate Patterns
Task Definition
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as logs from 'aws-cdk-lib/aws-logs';
import { getResourceName } from './config';
const taskDefinition = new ecs.FargateTaskDefinition(this, 'AppApiTaskDef', {
family: getResourceName(config, 'app-api-task'),
cpu: config.appApi.cpu, // 512, 1024, 2048, 4096
memoryLimitMiB: config.appApi.memory, // Must match CPU tier
});
// Log group per service
const logGroup = new logs.LogGroup(this, 'AppApiLogGroup', {
logGroupName: `/ecs/${config.projectPrefix}/app-api`,
retention: logs.RetentionDays.ONE_WEEK,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});Container Configuration
const container = taskDefinition.addContainer('AppApiContainer', {
containerName: 'app-api',
image: ecs.ContainerImage.fromEcrRepository(ecrRepository, imageTag),
logging: ecs.LogDrivers.awsLogs({
streamPrefix: 'app-api',
logGroup,
}),
// Environment variables for all resources
environment: {
PROJECT_PREFIX: config.projectPrefix,
AWS_REGION: config.awsRegion,
// DynamoDB table names
DYNAMODB_USER_QUOTAS_TABLE: userQuotasTable.tableName,
DYNAMODB_QUOTA_EVENTS_TABLE: quotaEventsTable.tableName,
DYNAMODB_SESSIONS_METADATA_TABLE: sessionsMetadataTable.tableName,
// S3 bucket names
S3_USER_FILES_BUCKET: userFilesBucket.bucketName,
// Service URLs
INFERENCE_API_URL: `http://${inferenceApiHostname}:8001`,
},
// Health check (mandatory)
healthCheck: {
command: ['CMD-SHELL', 'curl -f http://localhost:8000/health || exit 1'],
interval: cdk.Duration.seconds(30),
timeout: cdk.Duration.seconds(5),
retries: 3,
startPeriod: cdk.Duration.seconds(60),
},
// Port mapping
portMappings: [{
containerPort: 8000,
protocol: ecs.Protocol.TCP,
}],
});Fargate Service
import * as ec2 from 'aws-cdk-lib/aws-ec2';
const service = new ecs.FargateService(this, 'AppApiService', {
cluster: ecsCluster,
serviceName: getResourceName(config, 'app-api-service'),
taskDefinition: taskDefinition,
desiredCount: config.appApi.desiredCount,
// Security - private subnets, no public IP
securityGroups: [ecsSecurityGroup],
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
assignPublicIp: false,
// Health & rolling deployment
healthCheckGracePeriod: cdk.Duration.seconds(60),
circuitBreaker: { rollback: true }, // Auto-rollback on failure
minHealthyPercent: 100, // Always have healthy task
maxHealthyPercent: 200, // Allow 2x for rolling update
// Enable ECS Exec for debugging (optional)
enableExecuteCommand: config.environment !== 'prod',
});
// Attach to ALB target group
service.attachToApplicationTargetGroup(targetGroup);Auto-Scaling
const scaling = service.autoScaleTaskCount({
minCapacity: config.appApi.desiredCount,
maxCapacity: config.appApi.maxCapacity,
});
// CPU-based scaling
scaling.scaleOnCpuUtilization('CpuScaling', {
targetUtilizationPercent: 70,
scaleInCooldown: cdk.Duration.seconds(60),
scaleOutCooldown: cdk.Duration.seconds(60),
});
// Memory-based scaling
scaling.scaleOnMemoryUtilization('MemoryScaling', {
targetUtilizationPercent: 80,
scaleInCooldown: cdk.Duration.seconds(60),
scaleOutCooldown: cdk.Duration.seconds(60),
});
// Optional: Request count scaling
scaling.scaleOnRequestCount('RequestScaling', {
targetGroup: targetGroup,
requestsPerTarget: 1000,
scaleInCooldown: cdk.Duration.seconds(60),
scaleOutCooldown: cdk.Duration.seconds(60),
});Target Group
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
const targetGroup = new elbv2.ApplicationTargetGroup(this, 'AppApiTargetGroup', {
vpc: vpc,
targetGroupName: getResourceName(config, 'app-api-tg'),
port: 8000,
protocol: elbv2.ApplicationProtocol.HTTP,
targetType: elbv2.TargetType.IP,
healthCheck: {
enabled: true,
path: '/health',
interval: cdk.Duration.seconds(30),
timeout: cdk.Duration.seconds(5),
healthyThresholdCount: 2,
unhealthyThresholdCount: 3,
healthyHttpCodes: '200',
},
deregistrationDelay: cdk.Duration.seconds(30),
});
// Add listener rule with path pattern
albListener.addTargetGroups('AppApiTarget', {
targetGroups: [targetGroup],
priority: 1, // Lower number = higher priority
conditions: [
elbv2.ListenerCondition.pathPatterns(['/api/*']),
],
});Importing Network Resources
// Import VPC
const vpcId = ssm.StringParameter.valueForStringParameter(
this,
`/${config.projectPrefix}/network/vpc-id`
);
const vpc = ec2.Vpc.fromVpcAttributes(this, 'ImportedVpc', {
vpcId: vpcId,
availabilityZones: cdk.Fn.split(',', ssm.StringParameter.valueForStringParameter(
this,
`/${config.projectPrefix}/network/availability-zones`
)),
privateSubnetIds: cdk.Fn.split(',', ssm.StringParameter.valueForStringParameter(
this,
`/${config.projectPrefix}/network/private-subnet-ids`
)),
});
// Import ECS Cluster
const clusterName = ssm.StringParameter.valueForStringParameter(
this,
`/${config.projectPrefix}/network/ecs-cluster-name`
);
const ecsCluster = ecs.Cluster.fromClusterAttributes(this, 'ImportedCluster', {
clusterName: clusterName,
vpc: vpc,
securityGroups: [],
});
// Import ALB Listener
const listenerArn = ssm.StringParameter.valueForStringParameter(
this,
`/${config.projectPrefix}/network/alb-listener-arn`
);
const albListener = elbv2.ApplicationListener.fromApplicationListenerAttributes(
this,
'ImportedListener',
{
listenerArn: listenerArn,
securityGroup: ec2.SecurityGroup.fromSecurityGroupId(
this,
'ImportedAlbSg',
ssm.StringParameter.valueForStringParameter(
this,
`/${config.projectPrefix}/network/alb-security-group-id`
)
),
}
);Security Group for ECS Tasks
const ecsSecurityGroup = new ec2.SecurityGroup(this, 'EcsSecurityGroup', {
vpc: vpc,
securityGroupName: getResourceName(config, 'app-api-ecs-sg'),
description: 'Security group for App API ECS tasks',
allowAllOutbound: true,
});
// Allow traffic from ALB only
const albSecurityGroupId = ssm.StringParameter.valueForStringParameter(
this,
`/${config.projectPrefix}/network/alb-security-group-id`
);
ecsSecurityGroup.addIngressRule(
ec2.SecurityGroup.fromSecurityGroupId(this, 'AlbSg', albSecurityGroupId),
ec2.Port.tcp(8000),
'Allow traffic from ALB'
);Granting Database Access
// Grant DynamoDB permissions
userQuotasTable.grantReadWriteData(taskDefinition.taskRole);
quotaEventsTable.grantReadWriteData(taskDefinition.taskRole);
sessionsMetadataTable.grantReadWriteData(taskDefinition.taskRole);
// Grant S3 permissions
userFilesBucket.grantReadWrite(taskDefinition.taskRole);
// Grant Secrets Manager access
secret.grantRead(taskDefinition.taskRole);ECR Repository Import
// ECR repository created by CI/CD pipeline
const ecrRepository = ecr.Repository.fromRepositoryName(
this,
'AppApiRepository',
`${config.projectPrefix}/app-api`
);
// Use latest or specific tag
const imageTag = config.appApi.imageTag || 'latest';IAM Patterns
Service Role
import * as iam from 'aws-cdk-lib/aws-iam';
import { getResourceName } from './config';
const serviceRole = new iam.Role(this, 'AppApiRole', {
roleName: getResourceName(config, 'app-api-role'),
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
description: 'Execution role for App API ECS tasks',
});Policy Statements with SIDs
Always use descriptive SIDs for clarity and auditability:
serviceRole.addToPolicy(new iam.PolicyStatement({
sid: 'DynamoDBReadWrite',
effect: iam.Effect.ALLOW,
actions: [
'dynamodb:GetItem',
'dynamodb:PutItem',
'dynamodb:UpdateItem',
'dynamodb:DeleteItem',
'dynamodb:Query',
'dynamodb:Scan',
],
resources: [
table.tableArn,
`${table.tableArn}/index/*`,
],
}));
serviceRole.addToPolicy(new iam.PolicyStatement({
sid: 'S3ReadWrite',
effect: iam.Effect.ALLOW,
actions: [
's3:GetObject',
's3:PutObject',
's3:DeleteObject',
's3:ListBucket',
],
resources: [
bucket.bucketArn,
`${bucket.bucketArn}/*`,
],
}));ECS Task Execution Role
Separate from task role - handles pulling images and logging:
const executionRole = new iam.Role(this, 'ExecutionRole', {
roleName: getResourceName(config, 'app-api-execution-role'),
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
description: 'Execution role for ECS task definition',
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName(
'service-role/AmazonECSTaskExecutionRolePolicy'
),
],
});
// If using Secrets Manager for container secrets
executionRole.addToPolicy(new iam.PolicyStatement({
sid: 'SecretsManagerAccess',
actions: ['secretsmanager:GetSecretValue'],
resources: [`${secret.secretArn}*`], // Wildcard for random suffix
}));Lambda Execution Role
const lambdaRole = new iam.Role(this, 'LambdaRole', {
roleName: getResourceName(config, 'my-lambda-role'),
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
description: 'Execution role for My Lambda function',
});
// Basic Lambda execution (CloudWatch Logs)
lambdaRole.addManagedPolicy(
iam.ManagedPolicy.fromAwsManagedPolicyName(
'service-role/AWSLambdaBasicExecutionRole'
)
);
// VPC access if Lambda is in VPC
lambdaRole.addManagedPolicy(
iam.ManagedPolicy.fromAwsManagedPolicyName(
'service-role/AWSLambdaVPCAccessExecutionRole'
)
);Bedrock AgentCore Roles
Runtime Role
const runtimeRole = new iam.Role(this, 'RuntimeRole', {
roleName: getResourceName(config, 'agentcore-runtime-role'),
assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
description: 'Execution role for AWS Bedrock AgentCore Runtime',
});
// Model invocation
runtimeRole.addToPolicy(new iam.PolicyStatement({
sid: 'BedrockModelInvocation',
actions: [
'bedrock:InvokeModel',
'bedrock:InvokeModelWithResponseStream',
],
resources: [
'arn:aws:bedrock:*::foundation-model/*',
`arn:aws:bedrock:${config.awsRegion}:${config.awsAccount}:*`,
],
}));
// Code Interpreter access
runtimeRole.addToPolicy(new iam.PolicyStatement({
sid: 'CodeInterpreterAccess',
actions: [
'bedrock-agentcore:InvokeCodeInterpreter',
'bedrock-agentcore:CreateCodeInterpreterSession',
],
resources: [codeInterpreterArn],
}));
// Browser access
runtimeRole.addToPolicy(new iam.PolicyStatement({
sid: 'BrowserAccess',
actions: ['bedrock-agentcore:InvokeBrowser'],
resources: [browserArn],
}));
// Gateway access
runtimeRole.addToPolicy(new iam.PolicyStatement({
sid: 'GatewayAccess',
actions: ['bedrock-agentcore:InvokeGateway'],
resources: [gatewayArn],
}));Memory Role
const memoryRole = new iam.Role(this, 'MemoryRole', {
roleName: getResourceName(config, 'agentcore-memory-role'),
assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
description: 'Execution role for AgentCore Memory',
});
memoryRole.addToPolicy(new iam.PolicyStatement({
sid: 'BedrockModelAccess',
actions: [
'bedrock:InvokeModel',
'bedrock:InvokeModelWithResponseStream',
],
resources: ['arn:aws:bedrock:*::foundation-model/*'],
}));Gateway Role
const gatewayRole = new iam.Role(this, 'GatewayRole', {
roleName: getResourceName(config, 'agentcore-gateway-role'),
assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
description: 'Execution role for AgentCore MCP Gateway',
});
// Allow invoking Lambda functions
gatewayRole.addToPolicy(new iam.PolicyStatement({
sid: 'LambdaInvoke',
actions: ['lambda:InvokeFunction'],
resources: [
`arn:aws:lambda:${config.awsRegion}:${config.awsAccount}:function:${config.projectPrefix}-mcp-*`,
],
}));Granting Permissions (Preferred)
Use CDK grant methods when available:
// DynamoDB
table.grantReadWriteData(role);
table.grantReadData(role);
table.grantWriteData(role);
// S3
bucket.grantReadWrite(role);
bucket.grantRead(role);
bucket.grantPut(role);
bucket.grantDelete(role);
// Secrets Manager
secret.grantRead(role);
// KMS
key.grantEncryptDecrypt(role);
// SQS
queue.grantSendMessages(role);
queue.grantConsumeMessages(role);
// SNS
topic.grantPublish(role);Cross-Account Access
const crossAccountRole = new iam.Role(this, 'CrossAccountRole', {
roleName: getResourceName(config, 'cross-account-role'),
assumedBy: new iam.AccountPrincipal('123456789012'),
description: 'Role for cross-account access',
externalIds: ['unique-external-id'], // For security
});Service-Linked Roles
// Some services require service-linked roles
// They're created automatically, but you can reference them
const serviceLinkedRole = iam.Role.fromRoleName(
this,
'EcsServiceLinkedRole',
'AWSServiceRoleForECS'
);Condition Keys
role.addToPolicy(new iam.PolicyStatement({
sid: 'RestrictedS3Access',
actions: ['s3:GetObject'],
resources: [`${bucket.bucketArn}/*`],
conditions: {
StringEquals: {
's3:ExistingObjectTag/classification': 'public',
},
IpAddress: {
'aws:SourceIp': ['10.0.0.0/8'],
},
},
}));Permission Boundaries
const permissionBoundary = new iam.ManagedPolicy(this, 'PermissionBoundary', {
managedPolicyName: getResourceName(config, 'permission-boundary'),
statements: [
new iam.PolicyStatement({
sid: 'AllowedServices',
actions: [
'dynamodb:*',
's3:*',
'logs:*',
],
resources: ['*'],
}),
],
});
const role = new iam.Role(this, 'BoundedRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
permissionsBoundary: permissionBoundary,
});Important: Secrets Manager ARN Wildcard
AWS appends a random 6-character suffix to secret ARNs:
// CORRECT - includes wildcard
role.addToPolicy(new iam.PolicyStatement({
actions: ['secretsmanager:GetSecretValue'],
resources: [`${secret.secretArn}*`],
}));
// INCORRECT - will fail to match
role.addToPolicy(new iam.PolicyStatement({
actions: ['secretsmanager:GetSecretValue'],
resources: [secret.secretArn],
}));Lambda Function Patterns
Basic Lambda Function
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as iam from 'aws-cdk-lib/aws-iam';
import { getResourceName } from './config';
const lambdaFunction = new lambda.Function(this, 'MyFunction', {
functionName: getResourceName(config, 'my-function'),
description: 'Description of what this function does',
runtime: lambda.Runtime.PYTHON_3_13,
handler: 'lambda_function.lambda_handler',
code: lambda.Code.fromAsset('../backend/lambda-functions/my-function'),
role: lambdaRole,
architecture: lambda.Architecture.ARM_64, // Cost optimization (~20% cheaper)
timeout: cdk.Duration.seconds(60),
memorySize: 512,
environment: {
LOG_LEVEL: config.gateway?.logLevel || 'INFO',
PROJECT_PREFIX: config.projectPrefix,
},
});Lambda Role with Least Privilege
const lambdaRole = new iam.Role(this, 'LambdaRole', {
roleName: getResourceName(config, 'my-function-role'),
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
description: 'Execution role for My Function Lambda',
});
// Basic execution role (CloudWatch Logs)
lambdaRole.addManagedPolicy(
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')
);Secrets Manager Access
// IMPORTANT: AWS appends random 6-char suffix to secret ARNs
// Must use wildcard to match
lambdaRole.addToPolicy(new iam.PolicyStatement({
sid: 'SecretsManagerAccess',
actions: ['secretsmanager:GetSecretValue'],
resources: [`${secret.secretArn}*`], // Wildcard required!
}));DynamoDB Access
lambdaRole.addToPolicy(new iam.PolicyStatement({
sid: 'DynamoDBReadWrite',
actions: [
'dynamodb:GetItem',
'dynamodb:PutItem',
'dynamodb:UpdateItem',
'dynamodb:DeleteItem',
'dynamodb:Query',
'dynamodb:Scan',
],
resources: [
table.tableArn,
`${table.tableArn}/index/*`,
],
}));S3 Access
lambdaRole.addToPolicy(new iam.PolicyStatement({
sid: 'S3ReadWrite',
actions: [
's3:GetObject',
's3:PutObject',
's3:DeleteObject',
's3:ListBucket',
],
resources: [
bucket.bucketArn,
`${bucket.bucketArn}/*`,
],
}));Bedrock Access
lambdaRole.addToPolicy(new iam.PolicyStatement({
sid: 'BedrockModelInvocation',
actions: [
'bedrock:InvokeModel',
'bedrock:InvokeModelWithResponseStream',
],
resources: [
'arn:aws:bedrock:*::foundation-model/*',
`arn:aws:bedrock:${config.awsRegion}:${config.awsAccount}:*`,
],
}));VPC Lambda (for private resources)
const vpcLambda = new lambda.Function(this, 'VpcFunction', {
functionName: getResourceName(config, 'vpc-function'),
runtime: lambda.Runtime.PYTHON_3_13,
handler: 'lambda_function.lambda_handler',
code: lambda.Code.fromAsset('../backend/lambda-functions/vpc-function'),
vpc: vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
securityGroups: [lambdaSecurityGroup],
timeout: cdk.Duration.seconds(60),
memorySize: 512,
});
// Security group for Lambda
const lambdaSecurityGroup = new ec2.SecurityGroup(this, 'LambdaSg', {
vpc: vpc,
securityGroupName: getResourceName(config, 'lambda-sg'),
description: 'Security group for Lambda functions',
allowAllOutbound: true,
});Lambda for MCP Gateway
const mcpFunction = new lambda.Function(this, 'MyMcpFunction', {
functionName: getResourceName(config, 'mcp-my-tool'),
description: 'MCP tool Lambda function',
runtime: lambda.Runtime.PYTHON_3_13,
handler: 'lambda_function.lambda_handler',
code: lambda.Code.fromAsset('../backend/lambda-functions/my-tool'),
role: lambdaRole,
architecture: lambda.Architecture.ARM_64,
timeout: cdk.Duration.seconds(60),
memorySize: 512,
environment: {
LOG_LEVEL: config.gateway?.logLevel || 'INFO',
},
});
// Allow Gateway to invoke Lambda
mcpFunction.addPermission('GatewayPermission', {
principal: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
action: 'lambda:InvokeFunction',
sourceArn: gatewayArn,
});Heavy Workload Lambda (ML, long-running)
const heavyLambda = new lambda.Function(this, 'HeavyFunction', {
functionName: getResourceName(config, 'heavy-function'),
runtime: lambda.Runtime.PYTHON_3_13,
handler: 'lambda_function.lambda_handler',
code: lambda.Code.fromAsset('../backend/lambda-functions/heavy-function'),
// Maximum resources for heavy workloads
timeout: cdk.Duration.minutes(15), // Maximum 15 minutes
memorySize: 10240, // 10 GB (maximum)
ephemeralStorageSize: cdk.Size.gibibytes(10), // 10 GB /tmp
architecture: lambda.Architecture.ARM_64,
});Lambda with Environment Variables from SSM
const lambdaFunction = new lambda.Function(this, 'MyFunction', {
// ... other config
environment: {
// Reference SSM parameters
DYNAMODB_TABLE_NAME: ssm.StringParameter.valueForStringParameter(
this,
`/${config.projectPrefix}/tables/my-table-name`
),
S3_BUCKET_NAME: ssm.StringParameter.valueForStringParameter(
this,
`/${config.projectPrefix}/buckets/my-bucket-name`
),
},
});Lambda Layers
const layer = new lambda.LayerVersion(this, 'SharedLayer', {
layerVersionName: getResourceName(config, 'shared-layer'),
code: lambda.Code.fromAsset('../backend/layers/shared'),
compatibleRuntimes: [lambda.Runtime.PYTHON_3_13],
compatibleArchitectures: [lambda.Architecture.ARM_64],
description: 'Shared dependencies for Lambda functions',
});
const lambdaFunction = new lambda.Function(this, 'MyFunction', {
// ... other config
layers: [layer],
});Event Source Mappings
// DynamoDB Streams
lambdaFunction.addEventSourceMapping('DynamoDBTrigger', {
eventSourceArn: table.tableStreamArn,
startingPosition: lambda.StartingPosition.LATEST,
batchSize: 100,
retryAttempts: 3,
});
// SQS Queue
lambdaFunction.addEventSourceMapping('SqsTrigger', {
eventSourceArn: queue.queueArn,
batchSize: 10,
reportBatchItemFailures: true,
});CloudWatch Alarms
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
// Error rate alarm
new cloudwatch.Alarm(this, 'LambdaErrorAlarm', {
alarmName: getResourceName(config, 'lambda-errors'),
metric: lambdaFunction.metricErrors({
period: cdk.Duration.minutes(5),
}),
threshold: 5,
evaluationPeriods: 2,
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
// Duration alarm
new cloudwatch.Alarm(this, 'LambdaDurationAlarm', {
alarmName: getResourceName(config, 'lambda-duration'),
metric: lambdaFunction.metricDuration({
period: cdk.Duration.minutes(5),
statistic: 'p95',
}),
threshold: 30000, // 30 seconds
evaluationPeriods: 2,
});Networking Patterns
VPC Configuration
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { getResourceName } from './config';
const vpc = new ec2.Vpc(this, 'Vpc', {
vpcName: getResourceName(config, 'vpc'),
ipAddresses: ec2.IpAddresses.cidr(config.vpcCidr), // e.g., '10.0.0.0/16'
maxAzs: 2, // High availability across 2 AZs
natGateways: 1, // Single NAT for cost (increase for HA)
subnetConfiguration: [
{
cidrMask: 24,
name: 'Public',
subnetType: ec2.SubnetType.PUBLIC,
},
{
cidrMask: 24,
name: 'Private',
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
],
enableDnsHostnames: true,
enableDnsSupport: true,
});Security Groups
ALB Security Group
const albSg = new ec2.SecurityGroup(this, 'AlbSg', {
vpc: vpc,
securityGroupName: getResourceName(config, 'alb-sg'),
description: 'Security group for Application Load Balancer',
allowAllOutbound: true,
});
// Allow HTTP from internet
albSg.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(80),
'Allow HTTP from anywhere'
);
// Allow HTTPS from internet
albSg.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(443),
'Allow HTTPS from anywhere'
);ECS Security Group
const ecsSg = new ec2.SecurityGroup(this, 'EcsSg', {
vpc: vpc,
securityGroupName: getResourceName(config, 'ecs-sg'),
description: 'Security group for ECS tasks',
allowAllOutbound: true,
});
// Only allow traffic from ALB
ecsSg.addIngressRule(
albSg,
ec2.Port.tcp(8000),
'Allow traffic from ALB'
);Lambda Security Group
const lambdaSg = new ec2.SecurityGroup(this, 'LambdaSg', {
vpc: vpc,
securityGroupName: getResourceName(config, 'lambda-sg'),
description: 'Security group for Lambda functions',
allowAllOutbound: true,
});Database Security Group
const dbSg = new ec2.SecurityGroup(this, 'DatabaseSg', {
vpc: vpc,
securityGroupName: getResourceName(config, 'database-sg'),
description: 'Security group for databases',
allowAllOutbound: false, // Databases don't need outbound
});
// Allow from ECS tasks
dbSg.addIngressRule(
ecsSg,
ec2.Port.tcp(5432),
'Allow PostgreSQL from ECS'
);
// Allow from Lambda
dbSg.addIngressRule(
lambdaSg,
ec2.Port.tcp(5432),
'Allow PostgreSQL from Lambda'
);Application Load Balancer
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
const alb = new elbv2.ApplicationLoadBalancer(this, 'Alb', {
vpc: vpc,
loadBalancerName: getResourceName(config, 'alb'),
internetFacing: true,
securityGroup: albSg,
vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
});ALB with HTTPS
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
// Import certificate
const certificate = acm.Certificate.fromCertificateArn(
this,
'Certificate',
config.certificateArn
);
// HTTPS listener (443)
const httpsListener = alb.addListener('HttpsListener', {
port: 443,
protocol: elbv2.ApplicationProtocol.HTTPS,
certificates: [certificate],
defaultAction: elbv2.ListenerAction.fixedResponse(404, {
contentType: 'text/plain',
messageBody: 'Not Found',
}),
});
// HTTP redirect to HTTPS (80 -> 443)
const httpListener = alb.addListener('HttpListener', {
port: 80,
protocol: elbv2.ApplicationProtocol.HTTP,
defaultAction: elbv2.ListenerAction.redirect({
protocol: 'HTTPS',
port: '443',
permanent: true,
}),
});Target Groups
const targetGroup = new elbv2.ApplicationTargetGroup(this, 'AppTargetGroup', {
vpc: vpc,
targetGroupName: getResourceName(config, 'app-tg'),
port: 8000,
protocol: elbv2.ApplicationProtocol.HTTP,
targetType: elbv2.TargetType.IP,
healthCheck: {
enabled: true,
path: '/health',
interval: cdk.Duration.seconds(30),
timeout: cdk.Duration.seconds(5),
healthyThresholdCount: 2,
unhealthyThresholdCount: 3,
healthyHttpCodes: '200',
},
deregistrationDelay: cdk.Duration.seconds(30),
});Listener Rules
// Add target group with path-based routing
httpsListener.addTargetGroups('AppTarget', {
targetGroups: [appTargetGroup],
priority: 1, // Lower = higher priority
conditions: [
elbv2.ListenerCondition.pathPatterns(['/api/*']),
],
});
// Host-based routing
httpsListener.addTargetGroups('ApiSubdomain', {
targetGroups: [apiTargetGroup],
priority: 2,
conditions: [
elbv2.ListenerCondition.hostHeaders(['api.example.com']),
],
});Route53 Integration
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as route53Targets from 'aws-cdk-lib/aws-route53-targets';
// Create or import hosted zone
const hostedZone = new route53.PublicHostedZone(this, 'HostedZone', {
zoneName: config.domainName,
comment: `Hosted zone for ${config.projectPrefix}`,
});
// A record for ALB
new route53.ARecord(this, 'AlbARecord', {
zone: hostedZone,
recordName: 'api', // api.example.com
target: route53.RecordTarget.fromAlias(
new route53Targets.LoadBalancerTarget(alb)
),
});ECS Cluster
import * as ecs from 'aws-cdk-lib/aws-ecs';
const cluster = new ecs.Cluster(this, 'EcsCluster', {
clusterName: getResourceName(config, 'cluster'),
vpc: vpc,
containerInsights: true, // Enable Container Insights
});VPC Endpoints (for private subnets)
// ECR API endpoint
vpc.addInterfaceEndpoint('EcrApiEndpoint', {
service: ec2.InterfaceVpcEndpointAwsService.ECR,
privateDnsEnabled: true,
subnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
});
// ECR Docker endpoint
vpc.addInterfaceEndpoint('EcrDkrEndpoint', {
service: ec2.InterfaceVpcEndpointAwsService.ECR_DOCKER,
privateDnsEnabled: true,
subnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
});
// S3 gateway endpoint (free)
vpc.addGatewayEndpoint('S3Endpoint', {
service: ec2.GatewayVpcEndpointAwsService.S3,
subnets: [{ subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }],
});
// DynamoDB gateway endpoint (free)
vpc.addGatewayEndpoint('DynamoDbEndpoint', {
service: ec2.GatewayVpcEndpointAwsService.DYNAMODB,
subnets: [{ subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }],
});
// CloudWatch Logs endpoint
vpc.addInterfaceEndpoint('CloudWatchLogsEndpoint', {
service: ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS,
privateDnsEnabled: true,
subnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
});
// Secrets Manager endpoint
vpc.addInterfaceEndpoint('SecretsManagerEndpoint', {
service: ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER,
privateDnsEnabled: true,
subnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
});Exporting Network Resources to SSM
// VPC
new ssm.StringParameter(this, 'VpcIdParam', {
parameterName: `/${config.projectPrefix}/network/vpc-id`,
stringValue: vpc.vpcId,
});
new ssm.StringParameter(this, 'VpcCidrParam', {
parameterName: `/${config.projectPrefix}/network/vpc-cidr`,
stringValue: vpc.vpcCidrBlock,
});
// Availability Zones
new ssm.StringParameter(this, 'AzsParam', {
parameterName: `/${config.projectPrefix}/network/availability-zones`,
stringValue: vpc.availabilityZones.join(','),
});
// Subnets
new ssm.StringParameter(this, 'PrivateSubnetsParam', {
parameterName: `/${config.projectPrefix}/network/private-subnet-ids`,
stringValue: vpc.privateSubnets.map(s => s.subnetId).join(','),
});
new ssm.StringParameter(this, 'PublicSubnetsParam', {
parameterName: `/${config.projectPrefix}/network/public-subnet-ids`,
stringValue: vpc.publicSubnets.map(s => s.subnetId).join(','),
});
// ECS Cluster
new ssm.StringParameter(this, 'ClusterNameParam', {
parameterName: `/${config.projectPrefix}/network/ecs-cluster-name`,
stringValue: cluster.clusterName,
});
// ALB
new ssm.StringParameter(this, 'AlbArnParam', {
parameterName: `/${config.projectPrefix}/network/alb-arn`,
stringValue: alb.loadBalancerArn,
});
new ssm.StringParameter(this, 'AlbDnsParam', {
parameterName: `/${config.projectPrefix}/network/alb-dns-name`,
stringValue: alb.loadBalancerDnsName,
});
new ssm.StringParameter(this, 'ListenerArnParam', {
parameterName: `/${config.projectPrefix}/network/alb-listener-arn`,
stringValue: httpsListener.listenerArn,
});
new ssm.StringParameter(this, 'AlbSgParam', {
parameterName: `/${config.projectPrefix}/network/alb-security-group-id`,
stringValue: albSg.securityGroupId,
});Importing Network Resources
// Import VPC
const vpcId = ssm.StringParameter.valueForStringParameter(
this,
`/${config.projectPrefix}/network/vpc-id`
);
const vpc = ec2.Vpc.fromVpcAttributes(this, 'ImportedVpc', {
vpcId: vpcId,
availabilityZones: cdk.Fn.split(',', ssm.StringParameter.valueForStringParameter(
this,
`/${config.projectPrefix}/network/availability-zones`
)),
privateSubnetIds: cdk.Fn.split(',', ssm.StringParameter.valueForStringParameter(
this,
`/${config.projectPrefix}/network/private-subnet-ids`
)),
publicSubnetIds: cdk.Fn.split(',', ssm.StringParameter.valueForStringParameter(
this,
`/${config.projectPrefix}/network/public-subnet-ids`
)),
});
// Import ECS Cluster
const clusterName = ssm.StringParameter.valueForStringParameter(
this,
`/${config.projectPrefix}/network/ecs-cluster-name`
);
const cluster = ecs.Cluster.fromClusterAttributes(this, 'ImportedCluster', {
clusterName: clusterName,
vpc: vpc,
securityGroups: [],
});
// Import ALB Listener
const listenerArn = ssm.StringParameter.valueForStringParameter(
this,
`/${config.projectPrefix}/network/alb-listener-arn`
);
const albSgId = ssm.StringParameter.valueForStringParameter(
this,
`/${config.projectPrefix}/network/alb-security-group-id`
);
const listener = elbv2.ApplicationListener.fromApplicationListenerAttributes(
this,
'ImportedListener',
{
listenerArn: listenerArn,
securityGroup: ec2.SecurityGroup.fromSecurityGroupId(this, 'AlbSg', albSgId),
}
);S3 Bucket Patterns
Standard Private Bucket
import * as s3 from 'aws-cdk-lib/aws-s3';
import { getResourceName } from './config';
const bucket = new s3.Bucket(this, 'UserFilesBucket', {
// Include account ID for global uniqueness
bucketName: getResourceName(config, 'user-files', config.awsAccount),
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
enforceSSL: true,
versioned: true,
// Environment-based retention
removalPolicy: config.environment === 'prod'
? cdk.RemovalPolicy.RETAIN
: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: config.environment !== 'prod',
});Bucket with CORS (Pre-signed URL Uploads)
const corsOrigins = config.environment === 'prod'
? ['https://example.com']
: ['http://localhost:4200', 'https://dev.example.com'];
const bucket = new s3.Bucket(this, 'UserFilesBucket', {
bucketName: getResourceName(config, 'user-files', config.awsAccount),
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
enforceSSL: true,
// CORS for pre-signed URL uploads
cors: [{
allowedOrigins: corsOrigins,
allowedMethods: [
s3.HttpMethods.GET,
s3.HttpMethods.PUT,
s3.HttpMethods.HEAD,
],
allowedHeaders: ['Content-Type', 'Content-Length', 'x-amz-*'],
exposedHeaders: ['ETag', 'Content-Length', 'Content-Type'],
maxAge: 3600,
}],
removalPolicy: config.environment === 'prod'
? cdk.RemovalPolicy.RETAIN
: cdk.RemovalPolicy.DESTROY,
});Bucket with Lifecycle Rules
const bucket = new s3.Bucket(this, 'UserFilesBucket', {
bucketName: getResourceName(config, 'user-files', config.awsAccount),
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
enforceSSL: true,
versioned: true,
lifecycleRules: [
// Transition to Infrequent Access after 30 days
{
id: 'transition-to-ia',
transitions: [{
storageClass: s3.StorageClass.INFREQUENT_ACCESS,
transitionAfter: cdk.Duration.days(30),
}],
},
// Transition to Glacier after 90 days
{
id: 'transition-to-glacier',
transitions: [{
storageClass: s3.StorageClass.GLACIER,
transitionAfter: cdk.Duration.days(90),
}],
},
// Expire objects after retention period
{
id: 'expire-objects',
expiration: cdk.Duration.days(config.fileUpload?.retentionDays || 365),
},
// Clean up old versions
{
id: 'delete-old-versions',
noncurrentVersionExpiration: cdk.Duration.days(30),
},
// Abort incomplete multipart uploads
{
id: 'abort-incomplete-multipart',
abortIncompleteMultipartUploadAfter: cdk.Duration.days(1),
},
],
removalPolicy: config.environment === 'prod'
? cdk.RemovalPolicy.RETAIN
: cdk.RemovalPolicy.DESTROY,
});Frontend Bucket with CloudFront
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
// S3 bucket for static files
const bucket = new s3.Bucket(this, 'FrontendBucket', {
bucketName: getResourceName(config, 'frontend', config.awsAccount),
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
versioned: true,
encryption: s3.BucketEncryption.S3_MANAGED,
lifecycleRules: [{
id: 'DeleteOldVersions',
noncurrentVersionExpiration: cdk.Duration.days(30),
}],
removalPolicy: cdk.RemovalPolicy.RETAIN,
autoDeleteObjects: false,
});
// Origin Access Control for CloudFront
const oac = new cloudfront.CfnOriginAccessControl(this, 'OAC', {
originAccessControlConfig: {
name: getResourceName(config, 'frontend-oac'),
originAccessControlOriginType: 's3',
signingBehavior: 'always',
signingProtocol: 'sigv4',
},
});
// CloudFront distribution
const distribution = new cloudfront.Distribution(this, 'Distribution', {
comment: `${config.projectPrefix} Frontend Distribution`,
defaultBehavior: {
origin: origins.S3BucketOrigin.withOriginAccessControl(bucket),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS,
},
defaultRootObject: 'index.html',
// SPA routing: serve index.html for 403/404
errorResponses: [
{
httpStatus: 403,
responseHttpStatus: 200,
responsePagePath: '/index.html',
ttl: cdk.Duration.minutes(5),
},
{
httpStatus: 404,
responseHttpStatus: 200,
responsePagePath: '/index.html',
ttl: cdk.Duration.minutes(5),
},
],
priceClass: cloudfront.PriceClass.PRICE_CLASS_100, // US, Canada, Europe
enabled: true,
httpVersion: cloudfront.HttpVersion.HTTP2_AND_3,
});Bucket with Custom Domain
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as route53Targets from 'aws-cdk-lib/aws-route53-targets';
// Import certificate (must be in us-east-1 for CloudFront)
const certificate = acm.Certificate.fromCertificateArn(
this,
'Certificate',
config.frontend.certificateArn
);
// Import hosted zone
const hostedZone = route53.HostedZone.fromLookup(this, 'HostedZone', {
domainName: config.frontend.hostedZoneDomain,
});
const distribution = new cloudfront.Distribution(this, 'Distribution', {
// ... other config
domainNames: [config.frontend.domainName],
certificate: certificate,
});
// DNS record pointing to CloudFront
new route53.ARecord(this, 'AliasRecord', {
zone: hostedZone,
recordName: config.frontend.domainName,
target: route53.RecordTarget.fromAlias(
new route53Targets.CloudFrontTarget(distribution)
),
});Granting Access
// Grant ECS task read/write access
bucket.grantReadWrite(taskDefinition.taskRole);
// Grant Lambda read access
bucket.grantRead(lambdaFunction);
// Grant specific actions
bucket.grantPut(uploadRole);
bucket.grantDelete(cleanupRole);
// Custom policy for pre-signed URLs
taskRole.addToPolicy(new iam.PolicyStatement({
sid: 'S3PreSignedUrls',
actions: [
's3:PutObject',
's3:GetObject',
],
resources: [`${bucket.bucketArn}/*`],
conditions: {
StringEquals: {
's3:x-amz-acl': 'bucket-owner-full-control',
},
},
}));Exporting Bucket to SSM
new ssm.StringParameter(this, 'BucketNameParam', {
parameterName: `/${config.projectPrefix}/file-upload/bucket-name`,
stringValue: bucket.bucketName,
description: 'User files S3 bucket name',
});
new ssm.StringParameter(this, 'BucketArnParam', {
parameterName: `/${config.projectPrefix}/file-upload/bucket-arn`,
stringValue: bucket.bucketArn,
description: 'User files S3 bucket ARN',
});Event Notifications
import * as s3n from 'aws-cdk-lib/aws-s3-notifications';
// Trigger Lambda on object creation
bucket.addEventNotification(
s3.EventType.OBJECT_CREATED,
new s3n.LambdaDestination(processingLambda),
{ prefix: 'uploads/', suffix: '.pdf' }
);
// Send to SQS on object deletion
bucket.addEventNotification(
s3.EventType.OBJECT_REMOVED,
new s3n.SqsDestination(cleanupQueue)
);Intelligent Tiering
const bucket = new s3.Bucket(this, 'DataBucket', {
bucketName: getResourceName(config, 'data', config.awsAccount),
intelligentTieringConfigurations: [{
name: 'AutoTiering',
archiveAccessTierTime: cdk.Duration.days(90),
deepArchiveAccessTierTime: cdk.Duration.days(180),
}],
});Related skills
FAQ
What is the CDK deployment order in this repo?
InfrastructureStack (VPC, ALB, ECS Cluster) always deploys first, then other stacks import network resources via SSM.
What naming constraint applies to AgentCore resources?
AgentCore names must use underscores, not hyphens, so resource names are transformed with replace(/-/g, '_').