
Amazon Web Services
- 61 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
amazon-web-services is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- amazon-web-services
- AI & Agent Building
- AI-coding skill
Amazon Web Services by the numbers
- 61 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #6,381 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oakoss/agent-skills --skill amazon-web-servicesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 61 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Amazon Web Services
Overview
Amazon Web Services (AWS) provides cloud computing services for building scalable applications. The AWS SDK for JavaScript v3 uses modular packages (@aws-sdk/client-*) with first-class TypeScript support. AWS CDK v2 defines infrastructure as code using TypeScript constructs that synthesize to CloudFormation templates.
When to use: Building cloud-native applications, serverless architectures, container deployments, managed databases, CDN distribution, event-driven systems, or infrastructure as code.
When NOT to use: Simple static sites (consider Vercel/Netlify), local-only development tools, projects with no cloud deployment requirement.
Quick Reference
| Service / Pattern | API / Construct | Key Points |
|---|---|---|
| S3 upload | PutObjectCommand | Modular import from @aws-sdk/client-s3 |
| S3 presigned URL | getSignedUrl() | From @aws-sdk/s3-request-presigner, max 7 days |
| Lambda function | new lambda.Function() | CDK L2 construct, set memorySize and timeout |
| Lambda layers | new lambda.LayerVersion() | Share code/deps across functions |
| IAM policy | new iam.PolicyStatement() | Always use least privilege, avoid * resources |
| DynamoDB table | new dynamodb.Table() | Single-table design, PAY_PER_REQUEST for variable loads |
| DynamoDB GSI | table.addGlobalSecondaryIndex() | Separate throughput, eventual consistency |
| SQS queue | new sqs.Queue() | DLQ for failed messages, long polling with WaitTimeSeconds |
| SNS topic | new sns.Topic() | Fan-out to SQS, Lambda, HTTP endpoints |
| CloudFront | new cloudfront.Distribution() | OAC for S3 origins, cache policies |
| RDS/Aurora | new rds.DatabaseCluster() | Use RDS Proxy for connection pooling |
| ECS Fargate | new ecs_patterns.ApplicationLoadBalancedFargateService() | Higher-level pattern construct |
| Route 53 | new route53.ARecord() | Alias records for AWS resources |
| Secrets Manager | secretsmanager.Secret.fromSecretNameV2() | Automatic rotation, never hardcode secrets |
| CDK stack | new cdk.Stack(app, 'Id') | One stack per deployment unit |
| CDK testing | Template.fromStack(stack) | Fine-grained assertions and snapshot tests |
Common Mistakes
| Mistake | Correct Pattern |
|---|---|
Using AWS SDK v2 (aws-sdk) | Use modular v3 (@aws-sdk/client-*) for smaller bundles |
IAM Action: "*" or Resource: "*" | Scope to specific actions and resource ARNs |
| No DLQ on SQS queues | Always attach a dead-letter queue for failed messages |
| DynamoDB scan for queries | Design access patterns first, use Query with GSI/LSI |
| Hardcoding secrets in code or env vars | Use Secrets Manager or SSM Parameter Store |
Lambda bundling node_modules without tree-shaking | Use NodejsFunction with esbuild bundling |
Missing RemovalPolicy on stateful resources | Set RemovalPolicy.RETAIN for production databases and buckets |
| Creating one Lambda per CRUD operation | Group related operations, use event routing |
| No connection pooling for RDS | Use RDS Proxy or limit max_connections per Lambda |
| CloudFront without cache policy | Define explicit CachePolicy to control TTL and headers |
| CDK testing only with snapshots | Combine fine-grained assertions with snapshot tests |
| Presigned URL without content-type | Include ContentType in PutObjectCommand for uploads |
Delegation
- Infrastructure patterns: Use
Exploreagent for AWS architecture discovery - Security review: Use
Taskagent for IAM policy auditing - Cost optimization: Use
Taskagent for resource right-sizing
If the docker skill is available, delegate container build patterns and Dockerfile optimization to it.If the github-actions skill is available, delegate CI/CD pipeline patterns for AWS deployments to it.If the typescript-patterns skill is available, delegate TypeScript strict mode and type patterns used in CDK code to it.If the application-security skill is available, delegate AWS security best practices and threat modeling to it.References
- S3 storage, presigned URLs, and lifecycle policies
- Lambda functions, layers, cold starts, and event sources
- IAM roles, policies, and least-privilege patterns
- DynamoDB single-table design, GSI/LSI, and streams
- SQS queues, SNS topics, and fan-out messaging
- ECS/Fargate container deployment and ECR
- CloudFront CDN, Route 53 DNS, and networking
- CDK v2 infrastructure as code, constructs, stacks, and testing
CDK Infrastructure
App and Stack Structure
import * as cdk from 'aws-cdk-lib';
import type { Construct } from 'constructs';
class AppStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Define resources here
}
}
const app = new cdk.App();
new AppStack(app, 'MyApp-Dev', {
env: { account: '123456789012', region: 'us-east-1' },
});
new AppStack(app, 'MyApp-Prod', {
env: { account: '987654321098', region: 'us-east-1' },
});
app.synth();Construct Levels
| Level | Description | Example |
|---|---|---|
| L1 (Cfn\*) | Direct CloudFormation mapping | CfnBucket, CfnFunction |
| L2 | AWS-curated with sensible defaults | Bucket, Function |
| L3 (Patterns) | Multi-resource compositions | ApplicationLoadBalancedFargateService |
Prefer L2 constructs. Use L1 only when L2 does not expose a needed property.
Custom Construct
import * as cdk from 'aws-cdk-lib';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import type { Construct } from 'constructs';
interface ApiConstructProps {
tableName: string;
stage: string;
}
class ApiConstruct extends cdk.NestedStack {
public readonly table: dynamodb.Table;
public readonly handler: NodejsFunction;
constructor(scope: Construct, id: string, props: ApiConstructProps) {
super(scope, id);
this.table = new dynamodb.Table(this, 'Table', {
tableName: props.tableName,
partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'sk', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy:
props.stage === 'production'
? cdk.RemovalPolicy.RETAIN
: cdk.RemovalPolicy.DESTROY,
});
this.handler = new NodejsFunction(this, 'Handler', {
entry: 'src/handlers/api.ts',
runtime: lambda.Runtime.NODEJS_20_X,
environment: { TABLE_NAME: this.table.tableName },
});
this.table.grantReadWriteData(this.handler);
}
}Stack Configuration with Context
const app = new cdk.App();
const stage = app.node.tryGetContext('stage') ?? 'dev';
new AppStack(app, `MyApp-${stage}`, {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
},
});cdk deploy -c stage=productionCDK CLI Commands
cdk init app --language typescript # Scaffold new project
cdk synth # Synthesize CloudFormation template
cdk diff # Preview changes
cdk deploy # Deploy stack
cdk deploy --all # Deploy all stacks
cdk deploy --hotswap # Fast deploy for dev (Lambda, StepFunctions)
cdk destroy # Remove stack
cdk ls # List all stacksTesting with Assertions
Fine-Grained Assertions
import * as cdk from 'aws-cdk-lib';
import { Template, Match } from 'aws-cdk-lib/assertions';
import { AppStack } from '../lib/app-stack';
describe('AppStack', () => {
const app = new cdk.App();
const stack = new AppStack(app, 'TestStack');
const template = Template.fromStack(stack);
test('creates DynamoDB table with PAY_PER_REQUEST billing', () => {
template.hasResourceProperties('AWS::DynamoDB::Table', {
BillingMode: 'PAY_PER_REQUEST',
KeySchema: [
{ AttributeName: 'pk', KeyType: 'HASH' },
{ AttributeName: 'sk', KeyType: 'RANGE' },
],
});
});
test('creates Lambda function with correct runtime', () => {
template.hasResourceProperties('AWS::Lambda::Function', {
Runtime: 'nodejs20.x',
MemorySize: 256,
});
});
test('Lambda has read/write access to DynamoDB', () => {
template.hasResourceProperties('AWS::IAM::Policy', {
PolicyDocument: {
Statement: Match.arrayWith([
Match.objectLike({
Action: Match.arrayWith([
'dynamodb:BatchGetItem',
'dynamodb:GetItem',
'dynamodb:Query',
]),
Effect: 'Allow',
}),
]),
},
});
});
test('creates expected number of SQS queues', () => {
template.resourceCountIs('AWS::SQS::Queue', 2);
});
});Snapshot Testing
import * as cdk from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import { AppStack } from '../lib/app-stack';
test('matches snapshot', () => {
const app = new cdk.App();
const stack = new AppStack(app, 'TestStack');
const template = Template.fromStack(stack);
expect(template.toJSON()).toMatchSnapshot();
});Combine fine-grained assertions for critical properties with snapshot tests for detecting unintended changes.
Capture Values
import { Capture, Template } from 'aws-cdk-lib/assertions';
const envCapture = new Capture();
template.hasResourceProperties('AWS::Lambda::Function', {
Environment: {
Variables: envCapture,
},
});
expect(envCapture.asObject()).toEqual(
expect.objectContaining({
TABLE_NAME: expect.any(String),
}),
);Aspects for Policy Enforcement
import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import type { IConstruct } from 'constructs';
class BucketEncryptionChecker implements cdk.IAspect {
visit(node: IConstruct): void {
if (node instanceof s3.CfnBucket) {
if (!node.bucketEncryption) {
cdk.Annotations.of(node).addError(
'S3 buckets must have encryption enabled',
);
}
}
}
}
cdk.Aspects.of(app).add(new BucketEncryptionChecker());Tags
cdk.Tags.of(app).add('Project', 'my-app');
cdk.Tags.of(app).add('Environment', stage);
cdk.Tags.of(app).add('ManagedBy', 'cdk');Cross-Stack References
class NetworkStack extends cdk.Stack {
public readonly vpc: ec2.Vpc;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
this.vpc = new ec2.Vpc(this, 'Vpc', { maxAzs: 2 });
}
}
interface AppStackProps extends cdk.StackProps {
vpc: ec2.Vpc;
}
class AppStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: AppStackProps) {
super(scope, id, props);
// Use props.vpc
}
}
const networkStack = new NetworkStack(app, 'Network');
new AppStack(app, 'App', { vpc: networkStack.vpc });CDK automatically creates CloudFormation exports and imports for cross-stack references.
Removal Policies
| Policy | Behavior | Use Case |
|---|---|---|
DESTROY | Delete resource on stack deletion | Dev/test environments |
RETAIN | Keep resource when stack is deleted | Production databases, S3 buckets |
SNAPSHOT | Create snapshot before deletion | RDS databases |
Always use RETAIN for production stateful resources (databases, S3 buckets with data).
Project Structure
my-cdk-app/
├── bin/
│ └── app.ts # App entry point, stack instantiation
├── lib/
│ ├── stacks/
│ │ ├── network-stack.ts
│ │ └── app-stack.ts
│ └── constructs/
│ ├── api-construct.ts
│ └── database-construct.ts
├── src/
│ └── handlers/ # Lambda handler code
├── test/
│ └── stacks/
│ └── app-stack.test.ts
├── cdk.json
└── tsconfig.jsonSeparate CDK infrastructure code (lib/) from application code (src/). Keep Lambda handlers in src/handlers/ so NodejsFunction can reference them.
Containers with ECS and Fargate
ECR Repository (CDK)
import * as ecr from 'aws-cdk-lib/aws-ecr';
import * as cdk from 'aws-cdk-lib';
const repo = new ecr.Repository(this, 'AppRepo', {
repositoryName: 'my-app',
imageScanOnPush: true,
lifecycleRules: [
{
maxImageCount: 10,
description: 'Keep last 10 images',
},
],
removalPolicy: cdk.RemovalPolicy.DESTROY,
});Push Image to ECR (CLI)
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com
docker build -t my-app .
docker tag my-app:latest 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latestFargate Service with Load Balancer (CDK)
The ApplicationLoadBalancedFargateService pattern construct handles ALB, target group, security groups, and service discovery.
import * as cdk from 'aws-cdk-lib';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ecs_patterns from 'aws-cdk-lib/aws-ecs-patterns';
import * as ecr from 'aws-cdk-lib/aws-ecr';
const cluster = new ecs.Cluster(this, 'AppCluster', {
clusterName: 'my-app-cluster',
});
const repo = ecr.Repository.fromRepositoryName(this, 'Repo', 'my-app');
const service = new ecs_patterns.ApplicationLoadBalancedFargateService(
this,
'AppService',
{
cluster,
cpu: 512,
memoryLimitMiB: 1024,
desiredCount: 2,
taskImageOptions: {
image: ecs.ContainerImage.fromEcrRepository(repo, 'latest'),
containerPort: 3000,
environment: {
NODE_ENV: 'production',
},
},
publicLoadBalancer: true,
circuitBreaker: { rollback: true },
},
);
service.targetGroup.configureHealthCheck({
path: '/health',
interval: cdk.Duration.seconds(30),
healthyThresholdCount: 2,
unhealthyThresholdCount: 3,
});Auto Scaling
const scaling = service.service.autoScaleTaskCount({
minCapacity: 2,
maxCapacity: 10,
});
scaling.scaleOnCpuUtilization('CpuScaling', {
targetUtilizationPercent: 70,
scaleInCooldown: cdk.Duration.seconds(60),
scaleOutCooldown: cdk.Duration.seconds(60),
});
scaling.scaleOnMemoryUtilization('MemoryScaling', {
targetUtilizationPercent: 80,
});Custom Task Definition
For more control over container configuration, define the task directly.
const taskDef = new ecs.FargateTaskDefinition(this, 'TaskDef', {
cpu: 1024,
memoryLimitMiB: 2048,
});
const container = taskDef.addContainer('app', {
image: ecs.ContainerImage.fromEcrRepository(repo, 'latest'),
logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'my-app' }),
environment: {
NODE_ENV: 'production',
},
secrets: {
DB_URL: ecs.Secret.fromSecretsManager(dbSecret),
},
healthCheck: {
command: ['CMD-SHELL', 'curl -f http://localhost:3000/health || exit 1'],
interval: cdk.Duration.seconds(30),
timeout: cdk.Duration.seconds(5),
retries: 3,
},
});
container.addPortMappings({ containerPort: 3000 });Secrets in ECS Tasks
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
const dbSecret = secretsmanager.Secret.fromSecretNameV2(
this,
'DbSecret',
'prod/db/credentials',
);
taskDef.addContainer('app', {
image: ecs.ContainerImage.fromEcrRepository(repo),
secrets: {
DB_HOST: ecs.Secret.fromSecretsManager(dbSecret, 'host'),
DB_PASSWORD: ecs.Secret.fromSecretsManager(dbSecret, 'password'),
},
});Secrets are injected as environment variables at container start. The task execution role is automatically granted read access.
CDK Docker Image Asset
Build and push directly from CDK without managing ECR separately.
import * as ecs from 'aws-cdk-lib/aws-ecs';
const service = new ecs_patterns.ApplicationLoadBalancedFargateService(
this,
'Service',
{
cluster,
taskImageOptions: {
image: ecs.ContainerImage.fromAsset('./app', {
buildArgs: { NODE_ENV: 'production' },
}),
containerPort: 3000,
},
},
);fromAsset builds the Docker image locally and pushes it to an auto-managed ECR repository during cdk deploy.
Fargate Sizing Guide
| Workload | CPU | Memory | Notes |
|---|---|---|---|
| Lightweight API | 256 (.25 vCPU) | 512 MB | Minimal Node.js service |
| Standard API | 512 (.5 vCPU) | 1024 MB | Typical web application |
| Compute-heavy | 1024 (1 vCPU) | 2048 MB | Image processing, heavy computation |
| Memory-heavy | 1024 (1 vCPU) | 4096 MB | Large data processing |
| High performance | 4096 (4 vCPU) | 8192 MB | Maximum Fargate configuration |
Fargate charges per-second based on vCPU and memory. Provision the minimum needed and use auto scaling.
ECS Exec for Debugging
Enable interactive command execution in running containers.
const service = new ecs.FargateService(this, 'Service', {
cluster,
taskDefinition: taskDef,
enableExecuteCommand: true,
});aws ecs execute-command --cluster my-cluster --task TASK_ID --container app --interactive --command "/bin/sh"Deployment Strategies
| Strategy | CDK Config | Behavior |
|---|---|---|
| Rolling update | Default | Replace tasks gradually |
| Circuit breaker | circuitBreaker: { rollback: true } | Auto-rollback on deployment failure |
| Blue/green | Use CodeDeploy | Zero-downtime with traffic shifting |
The circuit breaker monitors deployment health and rolls back if new tasks fail to stabilize.
DynamoDB
CDK Table Definition
import * as cdk from 'aws-cdk-lib';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
const table = new dynamodb.Table(this, 'AppTable', {
tableName: 'MyAppTable',
partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'sk', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,
pointInTimeRecoverySpecification: { pointInTimeRecoveryEnabled: true },
removalPolicy: cdk.RemovalPolicy.RETAIN,
});Global Secondary Indexes (GSI)
GSIs have their own partition and sort keys, enabling alternate access patterns. They have separate throughput and support eventual consistency only.
table.addGlobalSecondaryIndex({
indexName: 'GSI1',
partitionKey: { name: 'gsi1pk', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'gsi1sk', type: dynamodb.AttributeType.STRING },
projectionType: dynamodb.ProjectionType.ALL,
});
table.addGlobalSecondaryIndex({
indexName: 'GSI2',
partitionKey: { name: 'gsi2pk', type: dynamodb.AttributeType.STRING },
projectionType: dynamodb.ProjectionType.INCLUDE,
nonKeyAttributes: ['email', 'name', 'status'],
});Local Secondary Indexes (LSI)
LSIs share the table partition key but with an alternate sort key. Must be defined at table creation time. Support strongly consistent reads.
const table = new dynamodb.Table(this, 'Table', {
partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'sk', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
});
table.addLocalSecondaryIndex({
indexName: 'LSI1',
sortKey: { name: 'createdAt', type: dynamodb.AttributeType.STRING },
projectionType: dynamodb.ProjectionType.ALL,
});Single-Table Design
Store multiple entity types in one table using composite keys. Design access patterns before choosing key schema.
Key Schema Example
| Entity | pk | sk | GSI1pk | GSI1sk |
|---|---|---|---|---|
| User | USER#<userId> | PROFILE | EMAIL#<email> | USER#<userId> |
| Order | USER#<userId> | ORDER#<orderId> | ORDER#<orderId> | <status>#<date> |
| Product | PRODUCT#<productId> | METADATA | CATEGORY#<cat> | <price> |
| OrderItem | ORDER#<orderId> | ITEM#<itemId> | - | - |
Access Patterns
| Pattern | Operation | Key Condition |
|---|---|---|
| Get user profile | GetItem | pk = USER#123, sk = PROFILE |
| List user orders | Query | pk = USER#123, sk begins_with ORDER# |
| Get order by ID | Query (GSI1) | gsi1pk = ORDER#456 |
| Orders by status | Query (GSI1) | gsi1pk = ORDER#456, gsi1sk begins_with SHIPPED# |
| User by email | Query (GSI1) | gsi1pk = EMAIL#user@example.com |
SDK Operations
PutItem
import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';
import { marshall } from '@aws-sdk/util-dynamodb';
const client = new DynamoDBClient({ region: 'us-east-1' });
await client.send(
new PutItemCommand({
TableName: 'MyAppTable',
Item: marshall({
pk: 'USER#123',
sk: 'PROFILE',
name: 'Jane Doe',
email: 'jane@example.com',
gsi1pk: 'EMAIL#jane@example.com',
gsi1sk: 'USER#123',
}),
ConditionExpression: 'attribute_not_exists(pk)',
}),
);Query
import { DynamoDBClient, QueryCommand } from '@aws-sdk/client-dynamodb';
import { unmarshall } from '@aws-sdk/util-dynamodb';
const client = new DynamoDBClient({ region: 'us-east-1' });
const result = await client.send(
new QueryCommand({
TableName: 'MyAppTable',
KeyConditionExpression: 'pk = :pk AND begins_with(sk, :prefix)',
ExpressionAttributeValues: {
':pk': { S: 'USER#123' },
':prefix': { S: 'ORDER#' },
},
ScanIndexForward: false,
Limit: 20,
}),
);
const orders = result.Items?.map(unmarshall) ?? [];Query on GSI
const result = await client.send(
new QueryCommand({
TableName: 'MyAppTable',
IndexName: 'GSI1',
KeyConditionExpression: 'gsi1pk = :email',
ExpressionAttributeValues: {
':email': { S: 'EMAIL#jane@example.com' },
},
}),
);UpdateItem with Expressions
import { DynamoDBClient, UpdateItemCommand } from '@aws-sdk/client-dynamodb';
const client = new DynamoDBClient({ region: 'us-east-1' });
await client.send(
new UpdateItemCommand({
TableName: 'MyAppTable',
Key: {
pk: { S: 'USER#123' },
sk: { S: 'PROFILE' },
},
UpdateExpression: 'SET #name = :name, updatedAt = :now ADD loginCount :inc',
ExpressionAttributeNames: { '#name': 'name' },
ExpressionAttributeValues: {
':name': { S: 'Jane Smith' },
':now': { S: new Date().toISOString() },
':inc': { N: '1' },
},
ConditionExpression: 'attribute_exists(pk)',
}),
);TransactWriteItems
import {
DynamoDBClient,
TransactWriteItemsCommand,
} from '@aws-sdk/client-dynamodb';
const client = new DynamoDBClient({ region: 'us-east-1' });
await client.send(
new TransactWriteItemsCommand({
TransactItems: [
{
Put: {
TableName: 'MyAppTable',
Item: {
pk: { S: 'ORDER#789' },
sk: { S: 'METADATA' },
status: { S: 'CREATED' },
},
},
},
{
Update: {
TableName: 'MyAppTable',
Key: { pk: { S: 'USER#123' }, sk: { S: 'PROFILE' } },
UpdateExpression: 'ADD orderCount :inc',
ExpressionAttributeValues: { ':inc': { N: '1' } },
},
},
],
}),
);DynamoDB Streams
Streams capture item-level changes for event-driven architectures.
| Stream View Type | Captures |
|---|---|
KEYS_ONLY | Only the key attributes |
NEW_IMAGE | Entire item after modification |
OLD_IMAGE | Entire item before modification |
NEW_AND_OLD_IMAGES | Both before and after (most flexible) |
Stream records are ordered by partition key, enabling sequential processing per item.
DynamoDB Document Client
Higher-level abstraction that handles marshalling automatically.
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import {
DynamoDBDocumentClient,
PutCommand,
QueryCommand,
} from '@aws-sdk/lib-dynamodb';
const ddbClient = new DynamoDBClient({ region: 'us-east-1' });
const docClient = DynamoDBDocumentClient.from(ddbClient);
await docClient.send(
new PutCommand({
TableName: 'MyAppTable',
Item: { pk: 'USER#123', sk: 'PROFILE', name: 'Jane', tags: ['admin'] },
}),
);
const result = await docClient.send(
new QueryCommand({
TableName: 'MyAppTable',
KeyConditionExpression: 'pk = :pk',
ExpressionAttributeValues: { ':pk': 'USER#123' },
}),
);Capacity Planning
| Billing Mode | When to Use |
|---|---|
| PAY_PER_REQUEST (on-demand) | Unpredictable traffic, new tables, development |
| PROVISIONED | Predictable steady-state traffic, cost optimization |
| PROVISIONED + Auto Scaling | Predictable with periodic spikes |
IAM Security
Core Concepts
IAM controls who (principal) can do what (action) on which resources (resource). Every AWS request is evaluated against IAM policies.
- Principal: Entity making the request (user, role, service)
- Action: API operation (
s3:GetObject,dynamodb:PutItem) - Resource: ARN of the target (
arn:aws:s3:::my-bucket/*) - Condition: Optional constraints (IP range, time, tags)
CDK Grant Methods (Preferred)
CDK L2 constructs provide grant* methods that automatically create least-privilege policies.
import type { Function as LambdaFunction } from 'aws-cdk-lib/aws-lambda';
import type { Table } from 'aws-cdk-lib/aws-dynamodb';
import type { Bucket } from 'aws-cdk-lib/aws-s3';
import type { Queue } from 'aws-cdk-lib/aws-sqs';
declare const fn: LambdaFunction;
declare const table: Table;
declare const bucket: Bucket;
declare const queue: Queue;
table.grantReadWriteData(fn);
bucket.grantRead(fn);
queue.grantSendMessages(fn);Each grant* call creates an IAM policy scoped to the specific resource ARN and only the actions needed.
Custom Policy Statements
When grant methods are insufficient, create explicit policy statements.
import * as iam from 'aws-cdk-lib/aws-iam';
fn.addToRolePolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['ses:SendEmail', 'ses:SendRawEmail'],
resources: ['arn:aws:ses:us-east-1:123456789012:identity/myapp.com'],
}),
);Service Roles
Create roles that AWS services assume to perform actions on your behalf.
import * as iam from 'aws-cdk-lib/aws-iam';
const ecsTaskRole = new iam.Role(this, 'EcsTaskRole', {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
description: 'Role for ECS tasks to access application resources',
});
table.grantReadWriteData(ecsTaskRole);
bucket.grantReadWrite(ecsTaskRole);Common Service Principals
| Service | Principal |
|---|---|
| Lambda | lambda.amazonaws.com |
| ECS Tasks | ecs-tasks.amazonaws.com |
| API Gateway | apigateway.amazonaws.com |
| CloudFront | cloudfront.amazonaws.com |
| Step Functions | states.amazonaws.com |
| EventBridge | events.amazonaws.com |
| CodeBuild | codebuild.amazonaws.com |
Least-Privilege Patterns
Scoped to Specific DynamoDB Operations
{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:Query"
],
"Resource": [
"arn:aws:dynamodb:us-east-1:123456789012:table/MyTable",
"arn:aws:dynamodb:us-east-1:123456789012:table/MyTable/index/*"
]
}S3 Bucket with Path Restriction
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-bucket/uploads/${aws:PrincipalTag/userId}/*",
"Condition": {
"StringEquals": {
"s3:x-amz-acl": "bucket-owner-full-control"
}
}
}Cross-Account Access
const crossAccountRole = new iam.Role(this, 'CrossAccountRole', {
assumedBy: new iam.AccountPrincipal('987654321098'),
externalIds: ['shared-secret-id'],
});
bucket.grantRead(crossAccountRole);Policy Conditions
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['s3:PutObject'],
resources: [bucket.arnForObjects('*')],
conditions: {
StringEquals: {
's3:x-amz-server-side-encryption': 'aws:kms',
},
IpAddress: {
'aws:SourceIp': '203.0.113.0/24',
},
},
});Permission Boundaries
Restrict the maximum permissions a role can have, even if broader policies are attached.
const boundary = new iam.ManagedPolicy(this, 'Boundary', {
statements: [
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['s3:*', 'dynamodb:*', 'lambda:*', 'logs:*', 'sqs:*', 'sns:*'],
resources: ['*'],
}),
new iam.PolicyStatement({
effect: iam.Effect.DENY,
actions: ['iam:*', 'organizations:*', 'account:*'],
resources: ['*'],
}),
],
});
const role = new iam.Role(this, 'AppRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
permissionsBoundary: boundary,
});Common Anti-Patterns
| Anti-Pattern | Correct Approach |
|---|---|
Action: "*" | List specific actions needed |
Resource: "*" | Scope to specific ARNs |
| Shared IAM users | Use roles with temporary credentials |
| Long-lived access keys | Use IAM roles, instance profiles, or OIDC |
| Inline policies on users | Use managed policies attached to roles |
| No permission boundary | Set boundaries for delegated admin roles |
Secrets Manager Integration
Never store secrets in environment variables or code. Use Secrets Manager with automatic rotation.
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
const secret = new secretsmanager.Secret(this, 'DbCredentials', {
secretName: 'prod/db/credentials',
generateSecretString: {
secretStringTemplate: JSON.stringify({ username: 'admin' }),
generateStringKey: 'password',
excludePunctuation: true,
passwordLength: 32,
},
});
secret.grantRead(fn);Read Secret in Lambda
import {
SecretsManagerClient,
GetSecretValueCommand,
} from '@aws-sdk/client-secrets-manager';
const client = new SecretsManagerClient({ region: process.env.AWS_REGION });
const { SecretString } = await client.send(
new GetSecretValueCommand({ SecretId: 'prod/db/credentials' }),
);
const credentials = JSON.parse(SecretString!);Lambda Functions
CDK NodejsFunction (Recommended)
The NodejsFunction construct bundles TypeScript/JavaScript with esbuild automatically.
import * as cdk from 'aws-cdk-lib';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
const fn = new NodejsFunction(this, 'ProcessOrder', {
entry: 'src/handlers/process-order.ts',
handler: 'handler',
runtime: lambda.Runtime.NODEJS_20_X,
memorySize: 256,
timeout: cdk.Duration.seconds(30),
environment: {
TABLE_NAME: table.tableName,
STAGE: 'production',
},
bundling: {
minify: true,
sourceMap: true,
externalModules: ['@aws-sdk/*'],
},
});
table.grantReadWriteData(fn);The externalModules: ['@aws-sdk/*'] excludes the SDK from the bundle since Lambda provides it in the runtime.
Basic Lambda Function
import * as lambda from 'aws-cdk-lib/aws-lambda';
const fn = new lambda.Function(this, 'MyFunction', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda/my-function'),
memorySize: 128,
timeout: cdk.Duration.seconds(10),
});Handler Pattern
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
export const handler = async (
event: APIGatewayProxyEvent,
): Promise<APIGatewayProxyResult> => {
const body = JSON.parse(event.body ?? '{}');
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: 'Success', data: body }),
};
};Lambda Layers
Share dependencies or utility code across multiple functions.
const sharedLayer = new lambda.LayerVersion(this, 'SharedLayer', {
code: lambda.Code.fromAsset('layers/shared'),
compatibleRuntimes: [lambda.Runtime.NODEJS_20_X],
description: 'Shared utilities and common dependencies',
});
const fn = new NodejsFunction(this, 'MyFunction', {
entry: 'src/handlers/my-function.ts',
layers: [sharedLayer],
bundling: {
externalModules: ['@aws-sdk/*', '/opt/nodejs/*'],
},
});Layer code is available at /opt/nodejs/ in the Lambda execution environment.
Cold Start Optimization
| Technique | Impact | Trade-off |
|---|---|---|
| Increase memory | Proportionally faster CPU | Higher cost per invocation |
| Minimize bundle size | Faster code loading | Requires bundler config |
Use NODEJS_20_X or later | Faster startup than older runtimes | Maintain runtime updates |
| Provisioned concurrency | Eliminates cold starts | Constant cost even when idle |
| Keep initialization outside handler | Reuse across invocations | Connection limits in concurrency |
| Lazy-load heavy dependencies | Faster initial response for simple paths | Slower first use of loaded module |
Provisioned Concurrency
const alias = fn.addAlias('live');
const scaling = alias.addAutoScaling({
minCapacity: 5,
maxCapacity: 50,
});
scaling.scaleOnUtilization({ utilizationTarget: 0.7 });Connection Reuse
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
// Initialized once per container, reused across invocations
const client = new DynamoDBClient({ region: process.env.AWS_REGION });
export const handler = async (event: unknown) => {
// client is reused across warm invocations
return client.send(/* ... */);
};Event Sources
SQS Event Source
import * as eventsources from 'aws-cdk-lib/aws-lambda-event-sources';
import type { Queue } from 'aws-cdk-lib/aws-sqs';
declare const queue: Queue;
fn.addEventSource(
new eventsources.SqsEventSource(queue, {
batchSize: 10,
maxBatchingWindow: cdk.Duration.seconds(5),
reportBatchItemFailures: true,
}),
);S3 Event Source
import * as eventsources from 'aws-cdk-lib/aws-lambda-event-sources';
import type { Bucket } from 'aws-cdk-lib/aws-s3';
declare const bucket: Bucket;
fn.addEventSource(
new eventsources.S3EventSource(bucket, {
events: [s3.EventType.OBJECT_CREATED],
filters: [{ prefix: 'uploads/', suffix: '.csv' }],
}),
);DynamoDB Streams Event Source
import * as eventsources from 'aws-cdk-lib/aws-lambda-event-sources';
import type { Table } from 'aws-cdk-lib/aws-dynamodb';
declare const table: Table;
fn.addEventSource(
new eventsources.DynamoEventSource(table, {
startingPosition: lambda.StartingPosition.TRIM_HORIZON,
batchSize: 100,
retryAttempts: 3,
bisectBatchOnError: true,
}),
);API Gateway Integration
import * as apigw from 'aws-cdk-lib/aws-apigateway';
const api = new apigw.RestApi(this, 'Api', {
restApiName: 'MyService',
deployOptions: { stageName: 'prod' },
});
api.root
.addResource('orders')
.addMethod('POST', new apigw.LambdaIntegration(fn));Function URL (No API Gateway)
const fnUrl = fn.addFunctionUrl({
authType: lambda.FunctionUrlAuthType.NONE,
cors: {
allowedOrigins: ['https://myapp.com'],
allowedMethods: [lambda.HttpMethod.POST],
},
});
new cdk.CfnOutput(this, 'FunctionUrl', { value: fnUrl.url });Invocation via SDK
import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda';
const client = new LambdaClient({ region: 'us-west-2' });
const response = await client.send(
new InvokeCommand({
FunctionName: 'my-function',
InvocationType: 'RequestResponse',
Payload: JSON.stringify({ action: 'process', data: [1, 2, 3] }),
}),
);
const result = JSON.parse(response.Payload!.transformToString());
if (response.FunctionError) {
throw new Error(`Lambda error: ${response.FunctionError}`);
}Memory and Timeout Guidelines
| Workload | Memory | Timeout |
|---|---|---|
| API handler (simple) | 128-256 MB | 10-30s |
| API handler (DB queries) | 256-512 MB | 30s |
| File processing | 512-1024 MB | 60-300s |
| Heavy computation | 1024-3008 MB | 300-900s |
Maximum timeout is 900 seconds (15 minutes). For longer tasks, use Step Functions or ECS tasks.
Messaging with SQS and SNS
SQS Queue (CDK)
import * as cdk from 'aws-cdk-lib';
import * as sqs from 'aws-cdk-lib/aws-sqs';
const dlq = new sqs.Queue(this, 'DeadLetterQueue', {
queueName: 'my-app-dlq',
retentionPeriod: cdk.Duration.days(14),
});
const queue = new sqs.Queue(this, 'ProcessingQueue', {
queueName: 'my-app-queue',
visibilityTimeout: cdk.Duration.seconds(60),
retentionPeriod: cdk.Duration.days(4),
deadLetterQueue: {
queue: dlq,
maxReceiveCount: 3,
},
});Always attach a dead-letter queue. Messages that fail maxReceiveCount times move to the DLQ for investigation.
FIFO Queue
FIFO queues guarantee exactly-once processing and message ordering within a message group.
const fifoQueue = new sqs.Queue(this, 'OrderQueue', {
queueName: 'orders.fifo',
fifo: true,
contentBasedDeduplication: true,
visibilityTimeout: cdk.Duration.seconds(30),
deadLetterQueue: {
queue: new sqs.Queue(this, 'OrderDLQ', {
queueName: 'orders-dlq.fifo',
fifo: true,
}),
maxReceiveCount: 3,
},
});FIFO queue names must end with .fifo. Maximum throughput is 300 messages/second (3,000 with high throughput mode).
Send Messages (SDK)
import {
SQSClient,
SendMessageCommand,
SendMessageBatchCommand,
} from '@aws-sdk/client-sqs';
const sqs = new SQSClient({ region: 'us-west-2' });
await sqs.send(
new SendMessageCommand({
QueueUrl: process.env.QUEUE_URL,
MessageBody: JSON.stringify({ orderId: '123', action: 'process' }),
MessageAttributes: {
EventType: { DataType: 'String', StringValue: 'order_created' },
},
}),
);Batch Send
await sqs.send(
new SendMessageBatchCommand({
QueueUrl: process.env.QUEUE_URL,
Entries: items.map((item, idx) => ({
Id: `msg-${idx}`,
MessageBody: JSON.stringify(item),
})),
}),
);Maximum 10 messages per batch. Total batch size limit is 256 KB.
Receive and Process Messages (SDK)
import {
SQSClient,
ReceiveMessageCommand,
DeleteMessageCommand,
} from '@aws-sdk/client-sqs';
const sqs = new SQSClient({ region: 'us-west-2' });
const queueUrl = process.env.QUEUE_URL!;
const result = await sqs.send(
new ReceiveMessageCommand({
QueueUrl: queueUrl,
MaxNumberOfMessages: 10,
WaitTimeSeconds: 20,
MessageAttributeNames: ['All'],
}),
);
for (const message of result.Messages ?? []) {
const body = JSON.parse(message.Body!);
await processMessage(body);
await sqs.send(
new DeleteMessageCommand({
QueueUrl: queueUrl,
ReceiptHandle: message.ReceiptHandle!,
}),
);
}WaitTimeSeconds: 20 enables long polling, reducing empty responses and API costs.
Lambda SQS Handler with Batch Item Failures
import type { SQSEvent, SQSBatchResponse } from 'aws-lambda';
export const handler = async (event: SQSEvent): Promise<SQSBatchResponse> => {
const batchItemFailures: SQSBatchResponse['batchItemFailures'] = [];
for (const record of event.Records) {
try {
const body = JSON.parse(record.body);
await processMessage(body);
} catch {
batchItemFailures.push({ itemIdentifier: record.messageId });
}
}
return { batchItemFailures };
};Enable reportBatchItemFailures on the event source mapping so only failed messages return to the queue.
SNS Topic (CDK)
import * as sns from 'aws-cdk-lib/aws-sns';
import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions';
const topic = new sns.Topic(this, 'OrderEvents', {
topicName: 'order-events',
});
topic.addSubscription(new subscriptions.SqsSubscription(queue));
topic.addSubscription(new subscriptions.LambdaSubscription(notifyFn));
topic.addSubscription(new subscriptions.EmailSubscription('alerts@myapp.com'));Fan-Out Pattern
SNS topic publishes to multiple SQS queues, each processing independently.
const orderTopic = new sns.Topic(this, 'OrderTopic');
const fulfillmentQueue = new sqs.Queue(this, 'FulfillmentQueue');
const analyticsQueue = new sqs.Queue(this, 'AnalyticsQueue');
const notificationQueue = new sqs.Queue(this, 'NotificationQueue');
orderTopic.addSubscription(new subscriptions.SqsSubscription(fulfillmentQueue));
orderTopic.addSubscription(new subscriptions.SqsSubscription(analyticsQueue));
orderTopic.addSubscription(
new subscriptions.SqsSubscription(notificationQueue),
);Filtered Subscriptions
orderTopic.addSubscription(
new subscriptions.SqsSubscription(highPriorityQueue, {
filterPolicy: {
priority: sns.SubscriptionFilter.stringFilter({
allowlist: ['high', 'critical'],
}),
},
}),
);Publish to SNS (SDK)
import { SNSClient, PublishCommand } from '@aws-sdk/client-sns';
const sns = new SNSClient({ region: 'us-east-1' });
await sns.send(
new PublishCommand({
TopicArn: process.env.TOPIC_ARN,
Message: JSON.stringify({ orderId: '123', status: 'shipped' }),
MessageAttributes: {
priority: { DataType: 'String', StringValue: 'high' },
},
}),
);SQS vs SNS Comparison
| Feature | SQS | SNS |
|---|---|---|
| Pattern | Point-to-point | Pub/sub |
| Consumers | Single consumer per message | Multiple subscribers |
| Persistence | Messages retained until processed | No persistence (push-based) |
| Ordering | FIFO queues support ordering | FIFO topics support ordering |
| Max message size | 256 KB | 256 KB |
| Use case | Work queues, task buffering | Event fanout, notifications |
Visibility Timeout Guidelines
| Processing Time | Visibility Timeout |
|---|---|
| < 5s | 30s |
| 5-30s | 60s |
| 30s-2min | 5min |
| > 2min | 6x processing time |
Set visibility timeout to at least 6x the expected processing time. If a Lambda processes the queue, set it to match or exceed the Lambda timeout.
CloudFront and Route 53 Networking
CloudFront Distribution with S3 Origin (CDK)
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
import * as s3 from 'aws-cdk-lib/aws-s3';
const bucket = new s3.Bucket(this, 'AssetsBucket', {
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
});
const distribution = new cloudfront.Distribution(this, 'CDN', {
defaultBehavior: {
origin: origins.S3BucketOrigin.withOriginAccessControl(bucket),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD,
compress: true,
},
priceClass: cloudfront.PriceClass.PRICE_CLASS_100,
});S3BucketOrigin.withOriginAccessControl creates an OAC automatically and configures the bucket policy. This is the current method, replacing the legacy OAI approach.
Multiple Origins and Behaviors
import * as cdk from 'aws-cdk-lib';
const distribution = new cloudfront.Distribution(this, 'CDN', {
defaultBehavior: {
origin: origins.S3BucketOrigin.withOriginAccessControl(assetsBucket),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
},
additionalBehaviors: {
'/api/*': {
origin: new origins.HttpOrigin('api.myapp.com'),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.HTTPS_ONLY,
cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
originRequestPolicy:
cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
},
'/static/*': {
origin: origins.S3BucketOrigin.withOriginAccessControl(staticBucket),
cachePolicy: new cloudfront.CachePolicy(this, 'LongCache', {
defaultTtl: cdk.Duration.days(30),
maxTtl: cdk.Duration.days(365),
minTtl: cdk.Duration.days(1),
headerBehavior: cloudfront.CacheHeaderBehavior.none(),
queryStringBehavior: cloudfront.CacheQueryStringBehavior.none(),
}),
},
},
});Custom Cache Policy
import * as cdk from 'aws-cdk-lib';
const apiCachePolicy = new cloudfront.CachePolicy(this, 'ApiCache', {
cachePolicyName: 'api-cache-policy',
defaultTtl: cdk.Duration.seconds(0),
maxTtl: cdk.Duration.hours(1),
minTtl: cdk.Duration.seconds(0),
headerBehavior: cloudfront.CacheHeaderBehavior.allowList(
'Authorization',
'Accept',
),
queryStringBehavior: cloudfront.CacheQueryStringBehavior.all(),
cookieBehavior: cloudfront.CacheCookieBehavior.none(),
enableAcceptEncodingGzip: true,
enableAcceptEncodingBrotli: true,
});Built-In Cache Policies
| Policy | TTL | Headers | Query Strings | Use Case |
|---|---|---|---|---|
CACHING_OPTIMIZED | 24h default, 1yr max | None | None | Static assets |
CACHING_DISABLED | 0 | None | None | Dynamic API calls |
CACHING_OPTIMIZED_FOR_UNCOMPRESSED_OBJECTS | 24h default | None | None | Pre-compressed content |
CloudFront Functions
Lightweight edge functions for URL rewrites, header manipulation, and simple redirects.
const rewriteFunction = new cloudfront.Function(this, 'RewriteFunction', {
code: cloudfront.FunctionCode.fromInline(`
function handler(event) {
var request = event.request;
var uri = request.uri;
if (uri.endsWith('/')) {
request.uri += 'index.html';
} else if (!uri.includes('.')) {
request.uri += '/index.html';
}
return request;
}
`),
runtime: cloudfront.FunctionRuntime.JS_2_0,
});
const distribution = new cloudfront.Distribution(this, 'CDN', {
defaultBehavior: {
origin: origins.S3BucketOrigin.withOriginAccessControl(bucket),
functionAssociations: [
{
function: rewriteFunction,
eventType: cloudfront.FunctionEventType.VIEWER_REQUEST,
},
],
},
});CloudFront with Custom Domain and Certificate
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as route53 from 'aws-cdk-lib/aws-route53';
const zone = route53.HostedZone.fromLookup(this, 'Zone', {
domainName: 'myapp.com',
});
const certificate = new acm.Certificate(this, 'Cert', {
domainName: 'cdn.myapp.com',
validation: acm.CertificateValidation.fromDns(zone),
});
const distribution = new cloudfront.Distribution(this, 'CDN', {
defaultBehavior: {
origin: origins.S3BucketOrigin.withOriginAccessControl(bucket),
},
domainNames: ['cdn.myapp.com'],
certificate,
});CloudFront certificates must be in us-east-1. If the stack is in another region, create the certificate in a separate us-east-1 stack.
Route 53 DNS Configuration
Hosted Zone Lookup
import * as route53 from 'aws-cdk-lib/aws-route53';
const zone = route53.HostedZone.fromLookup(this, 'Zone', {
domainName: 'myapp.com',
});Alias Records for AWS Resources
import * as targets from 'aws-cdk-lib/aws-route53-targets';
new route53.ARecord(this, 'CloudFrontAlias', {
zone,
recordName: 'cdn',
target: route53.RecordTarget.fromAlias(
new targets.CloudFrontTarget(distribution),
),
});
new route53.ARecord(this, 'AlbAlias', {
zone,
recordName: 'api',
target: route53.RecordTarget.fromAlias(new targets.LoadBalancerTarget(alb)),
});Alias records are free (no Route 53 query charges) and support apex domains.
Common Record Types
new route53.CnameRecord(this, 'CnameRecord', {
zone,
recordName: 'mail',
domainName: 'mail.provider.com',
});
new route53.TxtRecord(this, 'TxtRecord', {
zone,
recordName: '_verification',
values: ['verify=abc123'],
});
new route53.MxRecord(this, 'MxRecord', {
zone,
values: [
{ priority: 10, hostName: 'mx1.provider.com' },
{ priority: 20, hostName: 'mx2.provider.com' },
],
});Cache Invalidation
aws cloudfront create-invalidation --distribution-id E12345 --paths "/*"import {
CloudFrontClient,
CreateInvalidationCommand,
} from '@aws-sdk/client-cloudfront';
const cf = new CloudFrontClient({ region: 'us-east-1' });
await cf.send(
new CreateInvalidationCommand({
DistributionId: 'E12345',
InvalidationBatch: {
CallerReference: Date.now().toString(),
Paths: {
Quantity: 2,
Items: ['/index.html', '/api/*'],
},
},
}),
);First 1,000 invalidation paths per month are free. Use wildcard paths to reduce path count.
Security Headers
const responseHeadersPolicy = new cloudfront.ResponseHeadersPolicy(
this,
'SecurityHeaders',
{
securityHeadersBehavior: {
strictTransportSecurity: {
accessControlMaxAge: cdk.Duration.days(365),
includeSubdomains: true,
override: true,
},
contentTypeOptions: { override: true },
frameOptions: {
frameOption: cloudfront.HeadersFrameOption.DENY,
override: true,
},
referrerPolicy: {
referrerPolicy:
cloudfront.HeadersReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
override: true,
},
},
},
);S3 Storage
SDK v3 Client Setup
import { S3Client } from '@aws-sdk/client-s3';
const s3 = new S3Client({ region: 'us-east-1' });Upload Objects
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({ region: 'us-east-1' });
await s3.send(
new PutObjectCommand({
Bucket: 'my-bucket',
Key: 'uploads/document.pdf',
Body: fileBuffer,
ContentType: 'application/pdf',
}),
);Download Objects
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({ region: 'us-east-1' });
const response = await s3.send(
new GetObjectCommand({
Bucket: 'my-bucket',
Key: 'uploads/document.pdf',
}),
);
const bodyString = await response.Body?.transformToString();Presigned URLs
Generate time-limited URLs for secure client-side uploads and downloads without exposing AWS credentials.
Download URL
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3 = new S3Client({ region: 'us-east-1' });
const downloadUrl = await getSignedUrl(
s3,
new GetObjectCommand({
Bucket: 'my-bucket',
Key: 'private-file.pdf',
}),
{ expiresIn: 3600 },
);Upload URL
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3 = new S3Client({ region: 'us-east-1' });
const uploadUrl = await getSignedUrl(
s3,
new PutObjectCommand({
Bucket: 'my-bucket',
Key: 'uploads/new-file.jpg',
ContentType: 'image/jpeg',
}),
{
expiresIn: 900,
signableHeaders: new Set(['content-type']),
},
);The client must include the matching Content-Type header when using the presigned upload URL. Maximum expiration is 7 days (604800 seconds).
Client-Side Upload with Presigned URL
await fetch(uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': 'image/jpeg' },
body: file,
});Lifecycle Policies
import {
S3Client,
PutBucketLifecycleConfigurationCommand,
} from '@aws-sdk/client-s3';
const s3 = new S3Client({ region: 'us-east-1' });
await s3.send(
new PutBucketLifecycleConfigurationCommand({
Bucket: 'my-bucket',
LifecycleConfiguration: {
Rules: [
{
ID: 'archive-old-objects',
Status: 'Enabled',
Filter: { Prefix: 'logs/' },
Transitions: [
{ Days: 30, StorageClass: 'STANDARD_IA' },
{ Days: 90, StorageClass: 'GLACIER' },
],
Expiration: { Days: 365 },
},
{
ID: 'cleanup-incomplete-uploads',
Status: 'Enabled',
Filter: { Prefix: '' },
AbortIncompleteMultipartUpload: { DaysAfterInitiation: 7 },
},
],
},
}),
);CDK Bucket Configuration
import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
const bucket = new s3.Bucket(this, 'AppBucket', {
bucketName: 'my-app-assets',
versioned: true,
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
removalPolicy: cdk.RemovalPolicy.RETAIN,
lifecycleRules: [
{
id: 'archive-old',
transitions: [
{
storageClass: s3.StorageClass.INFREQUENT_ACCESS,
transitionAfter: cdk.Duration.days(30),
},
{
storageClass: s3.StorageClass.GLACIER,
transitionAfter: cdk.Duration.days(90),
},
],
expiration: cdk.Duration.days(365),
},
{
id: 'cleanup-multipart',
abortIncompleteMultipartUploadAfter: cdk.Duration.days(7),
},
],
});CORS Configuration for Browser Uploads
const bucket = new s3.Bucket(this, 'UploadBucket', {
cors: [
{
allowedMethods: [s3.HttpMethods.PUT, s3.HttpMethods.POST],
allowedOrigins: ['https://myapp.com'],
allowedHeaders: ['*'],
maxAge: 3600,
},
],
});S3 Event Notifications
import * as s3n from 'aws-cdk-lib/aws-s3-notifications';
import type { Function as LambdaFunction } from 'aws-cdk-lib/aws-lambda';
declare const processUpload: LambdaFunction;
bucket.addEventNotification(
s3.EventType.OBJECT_CREATED,
new s3n.LambdaDestination(processUpload),
{ prefix: 'uploads/', suffix: '.pdf' },
);Storage Classes
| Class | Use Case | Retrieval |
|---|---|---|
| STANDARD | Frequently accessed data | Immediate |
| STANDARD_IA | Infrequent access, rapid retrieval needed | Immediate |
| ONE_ZONE_IA | Infrequent, non-critical, single AZ | Immediate |
| GLACIER_IR | Archive with immediate retrieval | Immediate |
| GLACIER | Archive, minutes to hours retrieval | 1-5 min (expedited) to 3-5 hrs |
| DEEP_ARCHIVE | Long-term archive, rarely accessed | 12-48 hrs |