Skip to content

CloudFormation Concepts

llms.txt lists every page for an agent

Every exported resource declaration becomes a logical resource in a CloudFormation template. The serializer:

  • Wraps output in AWSTemplateFormatVersion: "2010-09-09"
  • Converts camelCase property names to PascalCase (CloudFormation convention)
  • Resolves AttrRef references to Fn::GetAtt
  • Resolves resource references to Ref intrinsics
main.ts
import { LambdaS3, Sub, AWS, Ref } from "@intentius/chant-lexicon-aws";
import { environment } from "./params";
export const app = LambdaS3({
name: Sub`${AWS.StackName}-${Ref(environment)}-fn`,
bucketName: Sub`${AWS.StackName}-${Ref(environment)}-bucket`,
Runtime: "nodejs20.x",
Handler: "index.handler",
Code: {
ZipFile: `const { S3Client, ListObjectsV2Command } = require("@aws-sdk/client-s3");
const s3 = new S3Client();
exports.handler = async () => {
const result = await s3.send(
new ListObjectsV2Command({ Bucket: process.env.BUCKET_NAME })
);
return {
statusCode: 200,
body: JSON.stringify(result.Contents ?? []),
};
};`,
},
access: "ReadOnly",
});

The LambdaS3 composite expands to 3 CloudFormation resources: an S3 Bucket, an IAM Role (with S3 read policy), and a Lambda Function. Property names like BucketName use the CloudFormation spec-native PascalCase directly, and the export name app becomes the resource name prefix (e.g. appBucket, appRole, appFunc).

CloudFormation resource types like AWS::S3::Bucket are mapped to short TypeScript class names. The lexicon uses a naming strategy that prioritizes readability:

CloudFormation TypeChant ClassRule
AWS::S3::BucketBucketPriority name (common resource)
AWS::Lambda::FunctionFunctionPriority name
AWS::IAM::RoleRolePriority name
AWS::EC2::InstanceInstanceShort name (last segment)
AWS::EC2::SecurityGroupSecurityGroupShort name
AWS::ECS::ServiceEcsServiceService-prefixed (avoids collision with AWS::AppRunner::Service)

Common resources get fixed short names for stability. When two services define the same resource name (e.g. both ECS and AppRunner have Service), the less common one gets a service prefix.

Discovering available resources: Use your editor’s autocomplete; every resource is a named export from the lexicon. You can also run chant list to see all resource types, or browse the generated TypeScript types.

Where the CloudFormation Registry schema declares AllowedValues for a property, the generated declaration is a union of literals rather than a bare string or number:

new LogGroup({ RetentionInDays: 45 });
// Type '45' is not assignable to type '1 | 1096 | 120 | 14 | 150 | 180 | 1827 | ... | 90'

The compiler error then lists every valid value.

The Registry schema is silent on many properties that people write constantly, and it is inconsistent about which: AWS::SageMaker::NotebookInstance.InstanceType carried a union while AWS::EC2::Instance.InstanceType was a bare string. Two sources close that gap at generation time, both of them applied before anything is parsed, and neither of them reachable from chant build:

  1. cfn-lint patches. src/codegen/patches.ts fetches the cfn-lint patch set and applies manual.json, smithy.json and format.json to each raw schema. Most of the unions you see beyond the bare Registry come from here. A patch that fails to apply is a generation warning, unless KNOWN_STALE_PATCHES in the same file lists it, in which case generation logs it as a skip; an entry that stops failing is reported for removal.
  2. A curated enum overlay. src/codegen/enum-overlay.json is a checked-in list of (CloudFormation type, JSON pointer, values) entries, applied by src/codegen/enum-overlay.ts. The first set covers the properties written most often across this repo’s examples, docs and composites: Lambda Runtime, EC2 InstanceType, Tenancy and VolumeType, ELBv2 Protocol, TargetType and Scheme, SNS Protocol, the five DynamoDB table enums, RDS Engine, and Application Auto Scaling ServiceNamespace and ScalableDimension.

An overlay entry mints a named type beside the narrowed property, the same way a $ref to an enum definition in the Registry schema does. AWS::Lambda::Function.Runtime becomes Runtime?: Function_Runtime, and Function_Runtime is exported from the lexicon’s types entry point, so a composite can name the union instead of restating its values.

Two rules govern the overlay:

  • The spec wins. An entry whose property already declares enum, from the Registry or from a cfn-lint patch, leaves the spec alone and reports itself as redundant so it can be retired. Upstream is refreshed on every generate and is the source that keeps tracking AWS.
  • An entry that matches nothing is an error. A renamed or removed property fails the generate rather than silently applying curated values to a property that no longer exists.

Each entry records where its values came from, either a botocore service model or an AWS documentation page, and the date they were read. just refresh-enum-overlay diffs the botocore-sourced entries against botocore’s current develop and prints the documentation-sourced ones for a human to re-read. It reaches the network, so it is a manual command and no build step calls it.

Narrowing a property is a breaking change for anyone passing a value the union omits, even though every such value was already rejected at deploy. Overlay changes therefore ship with a release of the lexicon, and the release notes name what newly fails to compile.

Generation reads a pinned copy of the Registry schema (src/spec/pin.ts), so a property AWS adds after the pin was accepted is missing from the generated types until the pin moves. Moving the pin regenerates every resource type, so it is its own change. When one property is needed sooner, src/codegen/property-overlay.json backfills it: each entry names a CloudFormation type, a top-level property, the schema fragment copied from the live Registry schema, and the date it was read. src/codegen/property-overlay.ts adds the property to the raw schema before parsing, so it reaches the generated declaration and the lexicon registry exactly as a spec property would.

The first entry was PlatformVersion on AWS::BedrockAgentCore::Runtime (#3953). The pin accepted on 2026-10-19 declares that property, so the entry was retired and the overlay is empty. PlatformVersion can still be set on Runtime directly or through AgentCoreAgent’s defaults.runtime.

The rules match the enum overlay. Once the spec declares the property, the spec’s schema is used and the entry is reported as redundant so it can be retired, and an entry on a type the spec does not contain fails the generate. The overlay cannot mark a property create-only, so it only takes properties that update in place.

Chant projects use standard TypeScript imports. Lexicon types come from the lexicon package, and cross-file references are standard imports:

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' })
});`,
},
});

When you reference a resource or attribute from another file (e.g. dataBucket.Arn), the serializer resolves it to Fn::GetAtt or Ref as appropriate.

CloudFormation parameters let you customize a stack at deploy time. Export a Parameter to add it to the template’s Parameters section:

parameter-declaration.ts
import { Parameter } from "@intentius/chant-lexicon-aws";
export const environment = new Parameter("String", {
description: "Deployment environment",
defaultValue: "dev",
});

Produces:

"Parameters": {
"Environment": {
"Type": "String",
"Default": "dev",
"Description": "Deployment environment"
}
}

Parameter also takes the constraints CloudFormation checks a value against: allowedValues, allowedPattern, constraintDescription, minLength, maxLength, minValue, maxValue and noEcho. Each is emitted under its CloudFormation key, as written.

export const dbName = new Parameter("String", {
allowedPattern: "[a-zA-Z][a-zA-Z0-9]*",
constraintDescription: "must begin with a letter",
minLength: 1,
maxLength: 64,
});

Reference parameters with Ref:

parameter-cross-file-ref.ts
import { Bucket, Sub, Ref } from "@intentius/chant-lexicon-aws";
import { environment } from "./parameter-declaration";
export const crossRefBucket = new Bucket({
BucketName: Sub`${Ref(environment)}-data`,
PublicAccessBlockConfiguration: {
BlockPublicAcls: true,
BlockPublicPolicy: true,
IgnorePublicAcls: true,
RestrictPublicBuckets: true,
},
});

A value read from a resource the template doesn’t hold, such as an Azure Container App’s configuration_ingress_fqdn used in a Route 53 record, can’t be a Fn::GetAtt. The template takes it as a String parameter instead, named like the output the other lexicon writes for it (appConfigurationIngressFqdn for app’s configuration.ingress.fqdn), and the build prints a warning naming both. Deploy the other lexicon’s resource first and pass its output in as that parameter. A Ref or Fn::GetAtt that still names nothing in the template fails the build (WAW070).

templateDescription(text) sets the template’s top-level Description. templateMetadata(entries) adds entries to its top-level Metadata, such as AWS::CloudFormation::Interface. Several templateMetadata declarations merge.

import { templateDescription, templateMetadata } from "@intentius/chant-lexicon-aws";
export const description = templateDescription("Web tier with an Application Load Balancer");
export const metadata = templateMetadata({ "cfn-lint": { config: { ignore_checks: ["W3005"] } } });

Use output() to create explicit stack outputs. Accepts an AttrRef (resource attribute) or any intrinsic (e.g. Sub, Join) for computed values. Cross-resource AttrRef usage is also auto-detected and promoted to outputs when needed.

output-explicit.ts
import { Bucket, Sub, AWS, output } from "@intentius/chant-lexicon-aws";
export const dataBucket = new Bucket({
BucketName: Sub`${AWS.StackName}-data`,
PublicAccessBlockConfiguration: {
BlockPublicAcls: true,
BlockPublicPolicy: true,
IgnorePublicAcls: true,
RestrictPublicBuckets: true,
},
});
export const dataBucketArn = output(dataBucket.Arn, "DataBucketArn");

Produces:

"Outputs": {
"DataBucketArn": {
"Value": { "Fn::GetAtt": ["DataBucket", "Arn"] }
}
}

Use Sub to export a computed value like a constructed URL:

export const solrUrl = output(Sub`http://${Ref(albDnsName)}/solr`, "solrUrl");

Runtime context values available in every template, accessed via the AWS namespace:

pseudo-params.ts
import { Sub, AWS } from "@intentius/chant-lexicon-aws";
export const s3Endpoint = Sub`https://s3.${AWS.Region}.${AWS.URLSuffix}`;
Pseudo-parameterDescription
AWS.StackNameName of the stack
AWS.RegionAWS region
AWS.AccountIdAWS account ID
AWS.StackIdStack ID
AWS.URLSuffixDomain suffix (usually amazonaws.com)
AWS.PartitionPartition (aws, aws-cn, aws-us-gov)
AWS.NotificationARNsNotification ARNs
AWS.NoValueRemoves property when used with Fn::If

The lexicon provides 9 intrinsic functions (Sub, Ref, GetAtt, If, Join, Select, Split, Base64, GetAZs) that map directly to CloudFormation Fn:: calls. See Intrinsic Functions for the reference table or the Intrinsics Guide for full usage examples.

CloudFormation automatically creates dependencies between resources when you use Ref or Fn::GetAtt. When you reference $.myBucket.arn, the serializer emits Fn::GetAtt and CloudFormation infers the dependency.

For cases where you need an explicit dependency without a property reference, pass DependsOn as a resource-level attribute (second constructor argument):

depends-on.ts
import { Instance, DbCluster } from "@intentius/chant-lexicon-aws";
// DependsOn with a string logical name
export const appServer = new Instance(
{
ImageId: "ami-12345678",
InstanceType: "t3.micro",
},
{ DependsOn: ["dbCluster"] },
);
// DependsOn with a Declarable reference (resolved automatically)
export const dbCluster = new DbCluster({
Engine: "aurora-postgresql",
MasterUsername: "admin",
MasterUserPassword: "changeme",
StorageEncrypted: true,
});
export const dependentWorker = new Instance(
{
ImageId: "ami-12345678",
InstanceType: "t3.micro",
},
{ DependsOn: [dbCluster] },
);

DependsOn values can be string logical names or references to other resource objects; Declarable references are resolved to their logical names automatically at build time.

The WAW010 post-synth check warns if a DependsOn target is already referenced via Ref or Fn::GetAtt in properties, where the explicit dependency is redundant.

Every resource constructor accepts an optional second argument for CloudFormation resource-level attributes. These control lifecycle behavior, conditional creation and metadata, and are separate from resource properties (the first argument).

resource-attributes.ts
import { Bucket, Condition, DbInstance, Equals, Instance, Ref } from "@intentius/chant-lexicon-aws";
import { environment } from "./parameter-declaration";
// The condition a resource-level Condition attribute references
export const IsProduction = new Condition(Equals(Ref(environment), "prod"));
// DeletionPolicy — protect data from accidental stack deletion
export const dbInstance = new DbInstance(
{
DBInstanceClass: "db.t3.micro",
Engine: "postgres",
MasterUsername: "admin",
MasterUserPassword: "changeme",
BackupRetentionPeriod: 7,
StorageEncrypted: true,
},
{ DeletionPolicy: "Snapshot", UpdateReplacePolicy: "Snapshot" },
);
// Condition — only create this resource when a condition is true
export const prodBucket = new Bucket(
{
BucketName: "prod-data",
PublicAccessBlockConfiguration: {
BlockPublicAcls: true,
BlockPublicPolicy: true,
IgnorePublicAcls: true,
RestrictPublicBuckets: true,
},
},
{ Condition: IsProduction },
);
// Metadata — attach cfn-init configuration to an EC2 instance
export const webServer = new Instance(
{
ImageId: "ami-12345678",
InstanceType: "t3.micro",
},
{
Metadata: {
"AWS::CloudFormation::Init": {
config: {
packages: { yum: { httpd: [] } },
services: { sysvinit: { httpd: { enabled: true, ensureRunning: true } } },
},
},
},
CreationPolicy: {
ResourceSignal: { Timeout: "PT15M" },
},
},
);
AttributeTypeDescription
DependsOnDeclarable | Declarable[] | string | string[]Explicit ordering dependency. Accepts resource references or logical name strings.
ConditionstringOnly create this resource when the named Condition evaluates to true.
DeletionPolicy"Delete" | "Retain" | "RetainExceptOnCreate" | "Snapshot"What happens when the resource is removed from the template or the stack is deleted.
UpdateReplacePolicy"Delete" | "Retain" | "Snapshot"What happens to the old resource when CloudFormation replaces it during an update.
UpdatePolicyobjectControls how Auto Scaling Groups perform rolling updates (AutoScalingRollingUpdate, AutoScalingReplacingUpdate).
CreationPolicyobjectWait for resource signals before marking creation complete (ResourceSignal with Count and Timeout).
MetadataRecord<string, unknown>Arbitrary metadata. Commonly used for AWS::CloudFormation::Init (cfn-init bootstrapping). Intrinsic functions in metadata values are resolved at build time.

All attributes are optional. When omitted, CloudFormation uses its defaults (e.g. DeletionPolicy: "Delete").

IAM policy documents appear on many AWS resources: Role.assumeRolePolicyDocument, ManagedPolicy.policyDocument, BucketPolicy.policyDocument, and others. These properties are typed as PolicyDocument, giving you autocomplete for the IAM JSON Policy Language.

The PolicyDocument interface and its supporting types:

TypeFields
PolicyDocumentVersion? ("2012-10-17" | "2008-10-17"), Id?, Statement
IamPolicyStatementEffect ("Allow" | "Deny"), Action?, Resource?, Principal?, Condition?, and their Not variants
IamPolicyPrincipal"*" or { AWS?, Service?, Federated? }

Policy documents use PascalCase keys (Effect, Action, Resource) because they follow the IAM JSON Policy Language spec. CloudFormation passes them through to IAM as-is, unlike resource properties which are automatically converted from camelCase.

The recommended pattern is to extract policies into your defaults.ts and import them directly:

policy-trust.ts
import { Sub, AWS } from "@intentius/chant-lexicon-aws";
// Trust policy — allows Lambda service to assume this role
export const lambdaTrustPolicy = {
Version: "2012-10-17",
Statement: [{
Effect: "Allow",
Principal: { Service: "lambda.amazonaws.com" },
Action: "sts:AssumeRole",
}],
};
// Read-only S3 policy
export const s3ReadPolicy = {
Statement: [{
Effect: "Allow",
Action: ["s3:GetObject", "s3:ListBucket"],
Resource: Sub`arn:aws:s3:::${AWS.StackName}-data/*`,
}],
};

Then reference them from resource files:

policy-role.ts
import { Role, ManagedPolicy } from "@intentius/chant-lexicon-aws";
import { lambdaTrustPolicy, s3ReadPolicy } from "./policy-trust";
export const functionRole = new Role({
AssumeRolePolicyDocument: lambdaTrustPolicy,
});
export const readPolicy = new ManagedPolicy({
PolicyDocument: s3ReadPolicy,
Roles: [functionRole],
});

For scoped resource ARNs, use Sub in the policy constant:

policy-scoped.ts
import { Sub, AWS } from "@intentius/chant-lexicon-aws";
export const bucketWritePolicy = {
Statement: [{
Effect: "Allow",
Action: ["s3:PutObject"],
Resource: Sub`arn:aws:s3:::${AWS.StackName}-data/*`,
}],
};

The IamPolicyPrincipal type supports all principal forms: wildcard ("*"), AWS accounts, services, and federated providers:

// Wildcard principal
Principal: "*",
// Service principal
Principal: { Service: "lambda.amazonaws.com" },
// Cross-account
Principal: { AWS: "arn:aws:iam::123456789012:root" },
// Multiple services
Principal: { Service: ["lambda.amazonaws.com", "edgelambda.amazonaws.com"] },

Declare a condition with the Condition declarable and the condition intrinsics (Equals, And, Or, Not). The serializer lifts each declared Condition into the template’s Conditions section, the way Parameter is lifted into Parameters. Reference it with the If intrinsic for conditional property values, or with the resource-level Condition attribute to gate a whole resource:

conditions.ts
import { Bucket, Condition, Equals, If, Parameter, Ref, AWS } from "@intentius/chant-lexicon-aws";
// Declare the condition — lifted into the template's Conditions section
export const accelerationEnabled = new Parameter("String", { defaultValue: "false" });
export const EnableAcceleration = new Condition(Equals(Ref(accelerationEnabled), "true"));
export const conditionalBucket = new Bucket({
BucketName: "my-bucket",
AccelerateConfiguration: If(
EnableAcceleration,
{ AccelerationStatus: "Enabled" },
AWS.NoValue,
),
BucketEncryption: {
ServerSideEncryptionConfiguration: [
{
ServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" },
},
],
},
PublicAccessBlockConfiguration: {
BlockPublicAcls: true,
BlockPublicPolicy: true,
IgnorePublicAcls: true,
RestrictPublicBuckets: true,
},
});

If, the resource-level Condition attribute, a stackOutput’s condition option, and And/Or/Not operands all accept either the Condition declarable or a condition name string. A template that references a condition it never declares fails the build (WAW069), since CloudFormation would reject it at deploy time. chant import regenerates Conditions sections from existing templates. Prefer plain TypeScript logic for decisions that can be made at build time; conditions are for decisions the deployment makes from parameter values.

For a static lookup, use a TypeScript object instead of a CloudFormation Mapping. It is evaluated at build time and gives the same result:

mappings.ts
import { Instance } from "@intentius/chant-lexicon-aws";
const regionAMIs: Record<string, string> = {
"us-east-1": "ami-12345678",
"us-west-2": "ami-87654321",
"eu-west-1": "ami-abcdef01",
};
export const server = new Instance({
ImageId: regionAMIs["us-east-1"],
InstanceType: "t3.micro",
});

For a lookup the deployment makes, declare a Mapping and read it with FindInMap. The serializer lifts each Mapping into the template’s Mappings section. chant import writes both for a template that uses them.

import { AWS, FindInMap, Instance, Mapping } from "@intentius/chant-lexicon-aws";
export const RegionAmi = new Mapping({
"us-east-1": { Ami: "ami-12345678" },
"us-west-2": { Ami: "ami-87654321" },
});
export const mappedServer = new Instance({
ImageId: FindInMap(RegionAmi, AWS.Region, "Ami"),
InstanceType: "t3.micro",
});

CloudFormation nested stacks (AWS::CloudFormation::Stack) split resources into child templates. The lexicon supports them via nestedStack() for cases where you exceed the 500-resource limit or need to package reusable infrastructure as a black box. See the Nested Stacks page for details.

Use defaultTags() to declare project-wide tags. The serializer adds them to every taggable resource at build time:

tagging.ts
import { defaultTags, Sub, AWS } from "@intentius/chant-lexicon-aws";
export const tags = defaultTags([
{ Key: "Environment", Value: "production" },
{ Key: "Team", Value: "platform" },
{ Key: "Stack", Value: Sub`${AWS.StackName}` },
]);

Resources with explicit Tags keep them (explicit key wins over default). Non-taggable resources like AWS::Lambda::Permission are never tagged.

Each type is tagged through its own tag property and shape, read from the Registry schema’s tagging.tagProperty. Most types get a [{Key, Value}] list in Tags. Map-tagged types such as AWS::SSM::Parameter get a map. AWS::Cognito::UserPool gets UserPoolTags, also a map. Explicit tags of either shape merge in. A type whose tags sit inside an array or a wrapper object, such as AWS::EC2::CapacityReservation (TagSpecifications[].Tags), gets no stamped tags. When the build stamps the ownership marker, that resource carries it in its own Metadata["chant:ownership"] instead. Post-synth check WAW071 flags tags in the wrong property or shape.

Tag values support strings, Parameter references, and intrinsic functions (Sub, Ref, etc.).

chant