Skip to content

Composites

Composites group related resources into reusable factories. See also the core Composite Resources guide.

lambda-api.ts
import { Role, Function, Permission, Code, Environment, Sub, Role_Policy } from "@intentius/chant-lexicon-aws";
import { Composite } from "@intentius/chant";
import { lambdaTrustPolicy, lambdaBasicExecutionArn } from "./defaults";
export interface LambdaApiProps {
name: string | ReturnType<typeof Sub>;
runtime: string;
handler: string;
code: InstanceType<typeof Code> | { ZipFile: string };
timeout?: number;
memorySize?: number;
environment?: InstanceType<typeof Environment> | { Variables: Record<string, unknown> };
policies?: InstanceType<typeof Role_Policy>[];
}
export const LambdaApi = Composite<LambdaApiProps>((props) => {
const role = new Role({
AssumeRolePolicyDocument: lambdaTrustPolicy,
ManagedPolicyArns: [lambdaBasicExecutionArn],
Policies: props.policies,
});
const func = new Function({
FunctionName: props.name,
Runtime: props.runtime,
Handler: props.handler,
Code: props.code,
Role: role.Arn,
Timeout: props.timeout,
MemorySize: props.memorySize,
Environment: props.environment,
});
const permission = new Permission({
FunctionName: func.Arn,
Action: "lambda:InvokeFunction",
Principal: "apigateway.amazonaws.com",
});
return { role, func, permission };
}, "LambdaApi");
export function SecureApi(props: Partial<LambdaApiProps>) {
return LambdaApi({
name: props.name!,
runtime: props.runtime ?? "nodejs20.x",
handler: props.handler ?? "index.handler",
code: props.code!,
timeout: props.timeout ?? 10,
memorySize: props.memorySize ?? 256,
environment: props.environment,
policies: props.policies,
});
}
export function HighMemoryApi(props: Partial<LambdaApiProps>) {
return LambdaApi({
name: props.name!,
runtime: props.runtime ?? "nodejs20.x",
handler: props.handler ?? "index.handler",
code: props.code!,
timeout: props.timeout ?? 25,
memorySize: props.memorySize ?? 1024,
environment: props.environment,
policies: props.policies,
});
}

Instantiate and export:

health-api.ts
import { Sub, AWS, Ref } from "@intentius/chant-lexicon-aws";
import { SecureApi } from "./lambda-api";
import { environment } from "./params";
export const healthApi = SecureApi({
name: Sub`${AWS.StackName}-${Ref(environment)}-health`,
runtime: "nodejs20.x",
handler: "index.handler",
code: {
ZipFile: `exports.handler = async () => ({
statusCode: 200,
body: JSON.stringify({ status: 'healthy' })
});`,
},
});

During build, composites expand to flat CloudFormation resources: healthApiRole, healthApiFunc, healthApiPermission.

A top-level composite call assigned directly to an export — like healthApi above — is one of the patterns folding can reduce with no module execution (chant #1023). Defining a composite (the Composite(...) call inside lambda-api.ts itself) doesn’t fold — its factory callback is a function, which is outside the fold subset — and neither does a composite call embedded as a nested value inside another resource’s own properties; only a composite call that is itself a file’s top-level export (or destructured/re-exported from one) is eligible.

The AWS lexicon ships ready-to-use composites for common patterns. Import them from @intentius/chant-lexicon-aws:

builtin-composites.ts
import { Sub, AWS, Ref } from "@intentius/chant-lexicon-aws";
import { LambdaNode, LambdaApi, LambdaScheduled, S3Actions, VpcDefault, FargateAlb, RdsInstance } from "@intentius/chant-lexicon-aws";
import { Role_Policy, Parameter } from "@intentius/chant-lexicon-aws";
// LambdaNode: Role + Function with nodejs20.x defaults
export const worker = LambdaNode({
name: Sub`${AWS.StackName}-worker`,
Code: { ZipFile: `exports.handler = async () => ({ statusCode: 200 });` },
});
// LambdaApi: Role + Function + Permission for API Gateway
export const api = LambdaApi({
name: Sub`${AWS.StackName}-api`,
Runtime: "nodejs20.x",
Handler: "index.handler",
Code: { ZipFile: `exports.handler = async () => ({ statusCode: 200 });` },
Timeout: 10,
sourceArn: Sub`arn:aws:execute-api:${AWS.Region}:${AWS.AccountId}:*/prod/*`,
});
// LambdaScheduled: Role + Function + EventBridge Rule + Permission
export const cron = LambdaScheduled({
name: Sub`${AWS.StackName}-cron`,
Runtime: "python3.12",
Handler: "handler.handler",
Code: { ZipFile: `def handler(event, context): return {"statusCode": 200}` },
schedule: "rate(5 minutes)",
});
// VpcDefault: Production-ready VPC (17 resources)
export const network = VpcDefault({});
// FargateAlb: Fargate service behind an ALB (11 resources)
export const web = FargateAlb({
image: "nginx:latest",
vpcId: network.vpc.VpcId,
publicSubnetIds: [network.publicSubnet1.SubnetId, network.publicSubnet2.SubnetId],
privateSubnetIds: [network.privateSubnet1.SubnetId, network.privateSubnet2.SubnetId],
});
// RdsInstance: DBSubnetGroup + SecurityGroup + DBInstance (3-4 resources)
export const dbPassword = new Parameter("AWS::SSM::Parameter::Value<String>", {
description: "SSM path to the database password",
defaultValue: "/myapp/dev/db-password",
});
export const rdsDatabase = RdsInstance({
engine: "postgres",
vpcId: network.vpc.VpcId,
subnetIds: [network.privateSubnet1.SubnetId, network.privateSubnet2.SubnetId],
masterPassword: Ref(dbPassword) as unknown as string,
databaseName: "myapp",
});
// Composites accept extra IAM policies
export const reader = LambdaNode({
name: Sub`${AWS.StackName}-reader`,
Code: { ZipFile: `exports.handler = async () => ({});` },
Policies: [
new Role_Policy({
PolicyName: "S3Read",
PolicyDocument: {
Version: "2012-10-17",
Statement: [{
Effect: "Allow",
Action: S3Actions.ReadOnly,
Resource: "*",
}],
},
}),
],
});
CompositeMembersDescription
LambdaFunctionrole, funcIAM Role + Lambda Function. Auto-attaches AWSLambdaBasicExecutionRole; adds AWSLambdaVPCAccessExecutionRole when VpcConfig is provided.
LambdaNoderole, funcLambdaFunction preset with Runtime: "nodejs20.x" and Handler: "index.handler"
LambdaPythonrole, funcLambdaFunction preset with Runtime: "python3.12" and Handler: "handler.handler"
LambdaApirole, func, permissionLambdaFunction + Lambda Permission for API Gateway invocation
LambdaScheduledrole, func, rule, permissionLambdaFunction + EventBridge Rule + Lambda Permission
LambdaSqsqueue, role, funcSQS Queue + Lambda + EventSourceMapping. Auto-attaches SQS receive policy.
LambdaEventBridgerule, role, func, permissionEventBridge Rule + Lambda. Supports schedule and/or eventPattern.
LambdaDynamoDBtable, role, funcDynamoDB Table + Lambda. Auto-attaches DynamoDB policy and injects TABLE_NAME env var.
EcrRepositoryrepositoryECR repository with scan-on-push enabled and a lifecycle policy by default (expires untagged images after 14 days). Both are configurable; pass lifecycleRules: [] to ship the repository with no lifecycle policy at all.
DynamoDBTabletableStandalone DynamoDB Table (no Lambda glue) for the GSI/TTL/stream/billing-mode configuration CDK apps hand-roll repeatedly. PAY_PER_REQUEST by default; PROVISIONED accepts table- and per-GSI provisionedThroughput, falling back to the table’s own capacity. Dedupes AttributeDefinitions across the table’s keys and every GSI’s keys.
LambdaS3bucket, role, funcS3 Bucket (encrypted, public access blocked) + Lambda. Auto-attaches S3 policy and injects BUCKET_NAME env var.
LambdaSnstopic, role, func, subscription, permissionSNS Topic + Lambda via Subscription. Auto-attaches invoke permission for SNS.
VpcDefaultvpc, igw, igwAttachment, publicSubnet1, publicSubnet2, privateSubnet1, privateSubnet2, publicRouteTable, publicRoute, publicRta1, publicRta2, privateRouteTable, privateRta1, privateRta2, natEip, natGateway, privateRouteProduction-ready VPC: 2 public + 2 private subnets across 2 AZs, internet gateway, single NAT gateway.
FargateAlbcluster, executionRole, taskRole, logGroup, taskDef, albSg, taskSg, alb, targetGroup, listener, serviceFargate service behind an ALB. Accepts VPC outputs as props.
AlbSharedcluster, executionRole, albSg, alb, listenerShared ALB infrastructure (ECS cluster, execution role, ALB, listener with 404 default). Created once, consumed by multiple FargateService instances.
FargateServicetaskRole, logGroup, taskDef, taskSg, targetGroup, rule, servicePer-service Fargate resources with listener rule routing. Wire to an AlbShared instance for multi-service ALB patterns.
NlbServicenlb, targetGroup, listenerNetwork Load Balancer + target group + listener for TCP/UDP/TLS workloads. No security group is created — NLBs pass through client source IPs, so access control belongs on the targets. Accepts static targets (instance IDs, IPs) or leaves registration to a self-registering service.
RdsInstancesubnetGroup, sg, db (+ parameterGroup if configured)RDS instance (postgres, mysql, mariadb) in private subnets. Creates DB subnet group, security group, and optionally a parameter group. Engine-specific defaults for port, username, and version. Encrypted by default.
StepFunctionsWorkflowrole, logGroup, stateMachineIAM Role (states.amazonaws.com trust) + CloudWatch log group + Step Functions state machine. definition takes the ASL as a plain object (CFN’s Definition property, not DefinitionString), so task Resource fields can carry Ref/Fn::GetAtt intrinsics directly. lambdaFunctionArns auto-grants lambda:InvokeFunction for Lambda-backed tasks.
Ec2InstanceBundlerole, instanceProfile, sg, instanceEC2 instance + security group + instance role/profile, placed into a given VPC/subnet. Inbound rules are opt-in per port (CIDR, IPv6 CIDR, or source security group); no inbound traffic by default.

All built-in composites accept ManagedPolicyArns and Policies for adding IAM permissions to the auto-created role.

Typed IAM action constants for common AWS services. Use them in policy documents instead of hand-typing action strings:

action-constants.ts
import { Role, Role_Policy, Bucket, Sub, AWS } from "@intentius/chant-lexicon-aws";
import { S3Actions } from "@intentius/chant-lexicon-aws";
export const s3DataBucket = new Bucket({
BucketName: Sub`${AWS.StackName}-data`,
PublicAccessBlockConfiguration: {
BlockPublicAcls: true,
BlockPublicPolicy: true,
IgnorePublicAcls: true,
RestrictPublicBuckets: true,
},
});
export const s3ReaderRole = new Role({
AssumeRolePolicyDocument: {
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Principal: { Service: "lambda.amazonaws.com" },
Action: "sts:AssumeRole",
},
],
},
Policies: [
new Role_Policy({
PolicyName: "S3Read",
PolicyDocument: {
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Action: S3Actions.ReadOnly,
Resource: s3DataBucket.Arn,
},
],
},
}),
],
});

Available constants:

ConstantKey groups
S3ActionsReadOnly, WriteOnly, ReadWrite, Full, GetObject, PutObject, DeleteObject, ListObjects
LambdaActionsInvoke, ReadOnly, Full
DynamoDBActionsReadOnly, WriteOnly, ReadWrite, Full, GetItem, PutItem, Query, Scan
SQSActionsSendMessage, ReceiveMessage, Full
SNSActionsPublish, Subscribe, Full
IAMActionsPassRole
ECRActionsPull, Full
LogsActionsWrite, Full
ECSActionsRunTask, Service, Full

Broad groups like ReadWrite are always supersets of their narrow counterparts (ReadOnly + WriteOnly). All values are as const arrays for full type safety.

Wrap a composite with pre-applied defaults. Defaulted props become optional:

with-defaults.ts
import { Sub, AWS, Function as LambdaFunction } from "@intentius/chant-lexicon-aws";
import { Composite, withDefaults, type Value } from "@intentius/chant";
interface LambdaApiProps {
// `Value<string>`, because the snippet below fills it with `Sub` — a prop a
// reader will build from an intrinsic should say so.
name: Value<string>;
runtime: string;
handler: string;
timeout: number;
memorySize: number;
code: { ZipFile: string };
}
const LambdaApi = Composite<LambdaApiProps>((props) => ({
fn: new LambdaFunction({
FunctionName: props.name,
Runtime: props.runtime,
Handler: props.handler,
Timeout: props.timeout,
MemorySize: props.memorySize,
Code: props.code,
}),
}), "LambdaApi");
const SecureApi = withDefaults(LambdaApi, {
runtime: "nodejs20.x",
handler: "index.handler",
timeout: 10,
memorySize: 256,
});
export const healthApi = SecureApi({
name: Sub`${AWS.StackName}-health`,
code: { ZipFile: `exports.handler = async () => ({ statusCode: 200 });` },
});
// Composable — stack defaults on top of defaults
export const HighMemoryApi = withDefaults(SecureApi, { memorySize: 2048, timeout: 25 });

withDefaults preserves the original composite’s identity — same _id and compositeName, no new registry entry.

withDefaults also accepts a function that receives the caller’s props and returns defaults. This enables conditional logic without generating extra resources:

computed-defaults.ts
import { Composite, withDefaults } from "@intentius/chant";
import { Function as LambdaFunction } from "@intentius/chant-lexicon-aws";
interface ApiProps {
name: string;
runtime: string;
handler: string;
timeout: number;
memorySize: number;
code: { ZipFile: string };
}
const Api = Composite<ApiProps>((props) => ({
fn: new LambdaFunction({
FunctionName: props.name,
Runtime: props.runtime as any,
Handler: props.handler,
Timeout: props.timeout,
MemorySize: props.memorySize,
Code: props.code,
}),
}), "Api");
// Function-based defaults can compute values from caller props
const SmartApi = withDefaults(Api, (props) => ({
runtime: "nodejs20.x",
handler: "index.handler",
// Scale timeout with memory — higher memory → longer timeout
timeout: (props.memorySize ?? 128) >= 1024 ? 60 : 10,
memorySize: 256,
}));
export const fast = SmartApi({
name: "fast-api",
code: { ZipFile: `exports.handler = async () => ({ statusCode: 200 });` },
});
// memorySize >= 1024 triggers the longer timeout
export const heavy = SmartApi({
name: "heavy-api",
memorySize: 2048,
code: { ZipFile: `exports.handler = async () => ({ statusCode: 200 });` },
});

Merge order: computed defaults are applied first, then user-provided props override them.

Attach properties that merge into every member during expansion:

propagate.ts
import { propagate } from "@intentius/chant";
import { healthApi } from "./with-defaults";
export const propagatedApi = propagate(
healthApi,
{ Tags: [{ Key: "env", Value: "prod" }] },
);

Merge semantics:

  • Scalars — member-specific value wins over shared
  • Arrays (e.g. tags) — shared values prepended, member values appended
  • undefined — stripped from shared props, never overwrites

When you need to split resources into a separate CloudFormation template, the lexicon supports nested stacks via nestedStack(). See the Nested Stacks page for details.