@intentius/chant-lexicon-aws 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +438 -0
- package/package.json +30 -0
- package/src/codegen/__snapshots__/snapshot.test.ts.snap +197 -0
- package/src/codegen/docs-cli.ts +3 -0
- package/src/codegen/docs.ts +1206 -0
- package/src/codegen/extensions.ts +171 -0
- package/src/codegen/fallback.ts +33 -0
- package/src/codegen/generate-cli.ts +17 -0
- package/src/codegen/generate-lexicon.ts +98 -0
- package/src/codegen/generate-typescript.ts +257 -0
- package/src/codegen/generate.test.ts +125 -0
- package/src/codegen/generate.ts +226 -0
- package/src/codegen/idempotency.test.ts +28 -0
- package/src/codegen/naming.ts +120 -0
- package/src/codegen/package.test.ts +60 -0
- package/src/codegen/package.ts +84 -0
- package/src/codegen/patches.ts +98 -0
- package/src/codegen/rollback.test.ts +80 -0
- package/src/codegen/rollback.ts +20 -0
- package/src/codegen/sam.ts +387 -0
- package/src/codegen/snapshot.test.ts +84 -0
- package/src/codegen/typecheck.test.ts +50 -0
- package/src/codegen/typecheck.ts +4 -0
- package/src/codegen/versions.ts +37 -0
- package/src/coverage.ts +14 -0
- package/src/generated/index.d.ts +160753 -0
- package/src/generated/index.ts +14396 -0
- package/src/generated/lexicon-aws.json +114563 -0
- package/src/generated/runtime.ts +4 -0
- package/src/import/generator.test.ts +181 -0
- package/src/import/generator.ts +349 -0
- package/src/import/parser.test.ts +200 -0
- package/src/import/parser.ts +350 -0
- package/src/import/roundtrip-fixtures.test.ts +78 -0
- package/src/import/roundtrip.test.ts +195 -0
- package/src/index.ts +63 -0
- package/src/integration.test.ts +129 -0
- package/src/intrinsics.test.ts +167 -0
- package/src/intrinsics.ts +223 -0
- package/src/lint/post-synth/cf-refs.ts +91 -0
- package/src/lint/post-synth/cor020.ts +72 -0
- package/src/lint/post-synth/ext001.test.ts +68 -0
- package/src/lint/post-synth/ext001.ts +222 -0
- package/src/lint/post-synth/post-synth.test.ts +280 -0
- package/src/lint/post-synth/waw010.ts +49 -0
- package/src/lint/post-synth/waw011.ts +49 -0
- package/src/lint/post-synth/waw013.ts +45 -0
- package/src/lint/post-synth/waw014.ts +50 -0
- package/src/lint/post-synth/waw015.ts +100 -0
- package/src/lint/rules/hardcoded-region.ts +43 -0
- package/src/lint/rules/iam-wildcard.ts +66 -0
- package/src/lint/rules/index.ts +7 -0
- package/src/lint/rules/rules.test.ts +175 -0
- package/src/lint/rules/s3-encryption.ts +69 -0
- package/src/lsp/completions.test.ts +72 -0
- package/src/lsp/completions.ts +18 -0
- package/src/lsp/hover.test.ts +53 -0
- package/src/lsp/hover.ts +53 -0
- package/src/nested-stack.test.ts +83 -0
- package/src/nested-stack.ts +125 -0
- package/src/plugin.test.ts +316 -0
- package/src/plugin.ts +514 -0
- package/src/pseudo.test.ts +55 -0
- package/src/pseudo.ts +29 -0
- package/src/serializer.test.ts +507 -0
- package/src/serializer.ts +333 -0
- package/src/spec/fetch.test.ts +27 -0
- package/src/spec/fetch.ts +107 -0
- package/src/spec/parse.test.ts +153 -0
- package/src/spec/parse.ts +202 -0
- package/src/testdata/load-fixtures.ts +17 -0
- package/src/testdata/roundtrip/conditions.json +21 -0
- package/src/testdata/roundtrip/intrinsic-calls.json +31 -0
- package/src/testdata/roundtrip/intrinsics.json +18 -0
- package/src/testdata/roundtrip/multi-resource.json +37 -0
- package/src/testdata/roundtrip/parameters.json +23 -0
- package/src/testdata/roundtrip/simple.json +12 -0
- package/src/testdata/sam-fixtures/api.yaml +14 -0
- package/src/testdata/sam-fixtures/application.yaml +13 -0
- package/src/testdata/sam-fixtures/function.yaml +22 -0
- package/src/testdata/sam-fixtures/graphql-api.yaml +13 -0
- package/src/testdata/sam-fixtures/http-api.yaml +15 -0
- package/src/testdata/sam-fixtures/layer-version.yaml +15 -0
- package/src/testdata/sam-fixtures/multi-type-a.yaml +23 -0
- package/src/testdata/sam-fixtures/multi-type-b.yaml +29 -0
- package/src/testdata/sam-fixtures/simple-table.yaml +12 -0
- package/src/testdata/sam-fixtures/state-machine.yaml +14 -0
- package/src/testdata/schemas/aws-dynamodb-table.json +126 -0
- package/src/testdata/schemas/aws-iam-role.json +85 -0
- package/src/testdata/schemas/aws-lambda-function.json +90 -0
- package/src/testdata/schemas/aws-s3-bucket.json +83 -0
- package/src/testdata/schemas/aws-sns-topic.json +71 -0
- package/src/validate-cli.ts +19 -0
- package/src/validate.ts +34 -0
|
@@ -0,0 +1,1206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS CloudFormation documentation generator.
|
|
3
|
+
*
|
|
4
|
+
* Calls the core docsPipeline with AWS-specific config:
|
|
5
|
+
* service grouping, resource type URLs, and overview content.
|
|
6
|
+
*
|
|
7
|
+
* Produces a standalone Starlight docs site at lexicons/aws/docs/.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { dirname, join } from "path";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
import { docsPipeline, writeDocsSite, type DocsConfig } from "@intentius/chant/codegen/docs";
|
|
13
|
+
|
|
14
|
+
const __dirname_ = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const pkgDir = join(__dirname_, "..", "..");
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Extract the AWS service name from a CloudFormation resource type.
|
|
19
|
+
* e.g. "AWS::S3::Bucket" → "S3", "AWS::Lambda::Function" → "Lambda"
|
|
20
|
+
*/
|
|
21
|
+
function serviceFromType(resourceType: string): string {
|
|
22
|
+
const parts = resourceType.split("::");
|
|
23
|
+
return parts.length >= 2 ? parts[1] : "Other";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const overview = `The **AWS CloudFormation** lexicon provides full support for defining AWS infrastructure using chant's declarative TypeScript syntax. Resources are serialized to CloudFormation JSON templates.
|
|
27
|
+
|
|
28
|
+
This lexicon is generated from the official [CloudFormation Resource Provider Schemas](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-resource-specification.html) and includes coverage for all publicly available resource types.
|
|
29
|
+
|
|
30
|
+
Install it with:
|
|
31
|
+
|
|
32
|
+
\`\`\`bash
|
|
33
|
+
npm install --save-dev @intentius/chant-lexicon-aws
|
|
34
|
+
\`\`\``;
|
|
35
|
+
|
|
36
|
+
const outputFormat = `The AWS lexicon serializes resources into **CloudFormation JSON templates**.
|
|
37
|
+
|
|
38
|
+
## Building
|
|
39
|
+
|
|
40
|
+
Run \`chant build\` to produce a CloudFormation template from your declarations:
|
|
41
|
+
|
|
42
|
+
\`\`\`bash
|
|
43
|
+
chant build
|
|
44
|
+
# Writes dist/template.json
|
|
45
|
+
\`\`\`
|
|
46
|
+
|
|
47
|
+
The generated template includes:
|
|
48
|
+
|
|
49
|
+
- \`AWSTemplateFormatVersion\` header
|
|
50
|
+
- \`Parameters\` section (if any parameters are declared)
|
|
51
|
+
- \`Resources\` section with typed resource definitions
|
|
52
|
+
- \`Outputs\` section for exported values
|
|
53
|
+
- Full support for intrinsic functions (\`Fn::Sub\`, \`Ref\`, \`Fn::GetAtt\`, etc.)
|
|
54
|
+
|
|
55
|
+
## Deploying
|
|
56
|
+
|
|
57
|
+
The output is standard CloudFormation JSON. Deploy with any CF-compatible tool:
|
|
58
|
+
|
|
59
|
+
\`\`\`bash
|
|
60
|
+
# AWS CLI
|
|
61
|
+
aws cloudformation deploy \\
|
|
62
|
+
--template-file dist/template.json \\
|
|
63
|
+
--stack-name my-stack \\
|
|
64
|
+
--capabilities CAPABILITY_IAM
|
|
65
|
+
|
|
66
|
+
# Rain (faster, with diff preview)
|
|
67
|
+
rain deploy dist/template.json my-stack
|
|
68
|
+
|
|
69
|
+
# SAM CLI (if using serverless transforms)
|
|
70
|
+
sam deploy --template-file dist/template.json --stack-name my-stack
|
|
71
|
+
\`\`\`
|
|
72
|
+
|
|
73
|
+
## Multi-file output (nested stacks)
|
|
74
|
+
|
|
75
|
+
When your project uses [nested stacks](./nested-stacks), \`chant build\` produces multiple template files:
|
|
76
|
+
|
|
77
|
+
\`\`\`bash
|
|
78
|
+
chant build -o template.json
|
|
79
|
+
# Produces:
|
|
80
|
+
# template.json — parent template
|
|
81
|
+
# network.template.json — child template (one per nestedStack)
|
|
82
|
+
\`\`\`
|
|
83
|
+
|
|
84
|
+
The parent template includes a \`TemplateBasePath\` parameter that controls where CloudFormation looks for child templates. Override it at deploy time to point to an S3 bucket:
|
|
85
|
+
|
|
86
|
+
\`\`\`bash
|
|
87
|
+
aws cloudformation deploy \\
|
|
88
|
+
--template-file template.json \\
|
|
89
|
+
--stack-name my-stack \\
|
|
90
|
+
--parameter-overrides TemplateBasePath=https://my-bucket.s3.amazonaws.com/templates
|
|
91
|
+
\`\`\`
|
|
92
|
+
|
|
93
|
+
All child template files must be uploaded alongside the parent template (or to the S3 path specified by \`TemplateBasePath\`).
|
|
94
|
+
|
|
95
|
+
## Compatibility
|
|
96
|
+
|
|
97
|
+
The output is compatible with:
|
|
98
|
+
- AWS CloudFormation service (direct deployment)
|
|
99
|
+
- AWS SAM CLI
|
|
100
|
+
- AWS CDK (as an escape hatch via \`CfnInclude\`)
|
|
101
|
+
- Rain and other CloudFormation tooling
|
|
102
|
+
- Any tool that accepts CloudFormation JSON templates`;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Generate AWS lexicon documentation as a standalone Starlight site.
|
|
106
|
+
*/
|
|
107
|
+
export async function generateDocs(options?: { verbose?: boolean }): Promise<void> {
|
|
108
|
+
const log = options?.verbose
|
|
109
|
+
? (msg: string) => console.error(msg)
|
|
110
|
+
: (_msg: string) => {};
|
|
111
|
+
|
|
112
|
+
const distDir = join(pkgDir, "dist");
|
|
113
|
+
const srcDir = join(pkgDir, "src");
|
|
114
|
+
const outDir = join(pkgDir, "docs");
|
|
115
|
+
|
|
116
|
+
const config: DocsConfig = {
|
|
117
|
+
name: "aws",
|
|
118
|
+
basePath: process.env.DOCS_BASE_PATH ?? "/lexicons/aws/",
|
|
119
|
+
displayName: "AWS CloudFormation",
|
|
120
|
+
description: "AWS CloudFormation lexicon for chant — resource types, intrinsics, and lint rules",
|
|
121
|
+
distDir,
|
|
122
|
+
outDir,
|
|
123
|
+
srcDir,
|
|
124
|
+
overview,
|
|
125
|
+
outputFormat,
|
|
126
|
+
serviceFromType,
|
|
127
|
+
suppressPages: ["pseudo-parameters", "intrinsics", "rules"],
|
|
128
|
+
extraPages: [
|
|
129
|
+
{
|
|
130
|
+
slug: "cloudformation",
|
|
131
|
+
title: "CloudFormation Concepts",
|
|
132
|
+
description: "Templates, resources, parameters, outputs, dependencies, and tagging in the AWS CloudFormation lexicon",
|
|
133
|
+
content: `Every exported resource declaration becomes a logical resource in a CloudFormation template. The serializer handles the translation automatically:
|
|
134
|
+
|
|
135
|
+
- Wraps output in \`AWSTemplateFormatVersion: "2010-09-09"\`
|
|
136
|
+
- Converts camelCase property names to PascalCase (CloudFormation convention)
|
|
137
|
+
- Resolves \`AttrRef\` references to \`Fn::GetAtt\`
|
|
138
|
+
- Resolves resource references to \`Ref\` intrinsics
|
|
139
|
+
|
|
140
|
+
\`\`\`typescript
|
|
141
|
+
// This chant declaration...
|
|
142
|
+
export const dataBucket = new Bucket({
|
|
143
|
+
bucketName: Sub\`\${AWS.StackName}-data\`,
|
|
144
|
+
versioningConfiguration: $.versioningEnabled,
|
|
145
|
+
});
|
|
146
|
+
\`\`\`
|
|
147
|
+
|
|
148
|
+
Produces this CloudFormation resource:
|
|
149
|
+
|
|
150
|
+
\`\`\`json
|
|
151
|
+
"DataBucket": {
|
|
152
|
+
"Type": "AWS::S3::Bucket",
|
|
153
|
+
"Properties": {
|
|
154
|
+
"BucketName": { "Fn::Sub": "\${AWS::StackName}-data" },
|
|
155
|
+
"VersioningConfiguration": { "Status": "Enabled" }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
\`\`\`
|
|
159
|
+
|
|
160
|
+
Notice how \`dataBucket\` becomes \`DataBucket\` (PascalCase logical ID), and \`bucketName\` becomes \`BucketName\`. This mapping is automatic.
|
|
161
|
+
|
|
162
|
+
## Resource types and naming
|
|
163
|
+
|
|
164
|
+
CloudFormation resource types like \`AWS::S3::Bucket\` are mapped to short TypeScript class names. The lexicon uses a naming strategy that prioritizes readability:
|
|
165
|
+
|
|
166
|
+
| CloudFormation Type | Chant Class | Rule |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| \`AWS::S3::Bucket\` | \`Bucket\` | Priority name (common resource) |
|
|
169
|
+
| \`AWS::Lambda::Function\` | \`Function\` | Priority name |
|
|
170
|
+
| \`AWS::IAM::Role\` | \`Role\` | Priority name |
|
|
171
|
+
| \`AWS::EC2::Instance\` | \`Instance\` | Short name (last segment) |
|
|
172
|
+
| \`AWS::EC2::SecurityGroup\` | \`SecurityGroup\` | Short name |
|
|
173
|
+
| \`AWS::ECS::Service\` | \`EcsService\` | Service-prefixed (avoids collision with \`AWS::AppRunner::Service\`) |
|
|
174
|
+
|
|
175
|
+
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.
|
|
176
|
+
|
|
177
|
+
**Discovering available resources:** Your editor's autocomplete is the best tool — 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.
|
|
178
|
+
|
|
179
|
+
## The barrel file
|
|
180
|
+
|
|
181
|
+
Every chant project has a barrel file (conventionally \`_.ts\`) that re-exports the lexicon and provides cross-file references:
|
|
182
|
+
|
|
183
|
+
\`\`\`typescript
|
|
184
|
+
// _.ts — the barrel file
|
|
185
|
+
export * from "@intentius/chant-lexicon-aws";
|
|
186
|
+
import * as core from "@intentius/chant";
|
|
187
|
+
export const $ = core.barrel(import.meta.dir);
|
|
188
|
+
\`\`\`
|
|
189
|
+
|
|
190
|
+
Other files import the barrel and use \`$\` to reference sibling exports:
|
|
191
|
+
|
|
192
|
+
\`\`\`typescript
|
|
193
|
+
// data-bucket.ts
|
|
194
|
+
import * as _ from "./_";
|
|
195
|
+
|
|
196
|
+
export const dataBucket = new _.Bucket({
|
|
197
|
+
bucketName: _.Sub\`\${_.AWS.StackName}-data\`,
|
|
198
|
+
serverSideEncryptionConfiguration: _.$.encryptionDefault, // from defaults.ts
|
|
199
|
+
});
|
|
200
|
+
\`\`\`
|
|
201
|
+
|
|
202
|
+
The \`$\` proxy lazily resolves exports from other files in the same directory. When the serializer encounters \`_.$.encryptionDefault\`, it resolves to the actual exported value and serializes the reference as \`Fn::GetAtt\` or \`Ref\` as appropriate. This is how cross-file references work without circular imports.
|
|
203
|
+
|
|
204
|
+
## Parameters
|
|
205
|
+
|
|
206
|
+
CloudFormation parameters let you customize a stack at deploy time. Export a \`Parameter\` to add it to the template's \`Parameters\` section:
|
|
207
|
+
|
|
208
|
+
\`\`\`typescript
|
|
209
|
+
import { Parameter } from "@intentius/chant-lexicon-aws";
|
|
210
|
+
|
|
211
|
+
export const environment = new Parameter("String", {
|
|
212
|
+
description: "Deployment environment",
|
|
213
|
+
defaultValue: "dev",
|
|
214
|
+
});
|
|
215
|
+
\`\`\`
|
|
216
|
+
|
|
217
|
+
Produces:
|
|
218
|
+
|
|
219
|
+
\`\`\`json
|
|
220
|
+
"Parameters": {
|
|
221
|
+
"Environment": {
|
|
222
|
+
"Type": "String",
|
|
223
|
+
"Description": "Deployment environment",
|
|
224
|
+
"Default": "dev"
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
\`\`\`
|
|
228
|
+
|
|
229
|
+
Reference parameters with \`Ref\`:
|
|
230
|
+
|
|
231
|
+
\`\`\`typescript
|
|
232
|
+
import { Ref } from "@intentius/chant-lexicon-aws";
|
|
233
|
+
|
|
234
|
+
export const bucket = new Bucket({
|
|
235
|
+
bucketName: Sub\`\${Ref("Environment")}-data\`,
|
|
236
|
+
});
|
|
237
|
+
\`\`\`
|
|
238
|
+
|
|
239
|
+
## Outputs
|
|
240
|
+
|
|
241
|
+
Use \`output()\` to create explicit stack outputs. Cross-resource \`AttrRef\` usage is also auto-detected and promoted to outputs when needed.
|
|
242
|
+
|
|
243
|
+
\`\`\`typescript
|
|
244
|
+
import { output } from "@intentius/chant";
|
|
245
|
+
|
|
246
|
+
export const bucketArn = output(dataBucket.arn, "DataBucketArn");
|
|
247
|
+
\`\`\`
|
|
248
|
+
|
|
249
|
+
Produces:
|
|
250
|
+
|
|
251
|
+
\`\`\`json
|
|
252
|
+
"Outputs": {
|
|
253
|
+
"DataBucketArn": {
|
|
254
|
+
"Value": { "Fn::GetAtt": ["DataBucket", "Arn"] }
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
\`\`\`
|
|
258
|
+
|
|
259
|
+
## Pseudo-parameters
|
|
260
|
+
|
|
261
|
+
Runtime context values available in every template, accessed via the \`AWS\` namespace:
|
|
262
|
+
|
|
263
|
+
\`\`\`typescript
|
|
264
|
+
import { AWS, Sub } from "@intentius/chant-lexicon-aws";
|
|
265
|
+
|
|
266
|
+
const endpoint = Sub\`https://s3.\${AWS.Region}.\${AWS.URLSuffix}\`;
|
|
267
|
+
\`\`\`
|
|
268
|
+
|
|
269
|
+
| Pseudo-parameter | Description |
|
|
270
|
+
|---|---|
|
|
271
|
+
| \`AWS.StackName\` | Name of the stack |
|
|
272
|
+
| \`AWS.Region\` | AWS region |
|
|
273
|
+
| \`AWS.AccountId\` | AWS account ID |
|
|
274
|
+
| \`AWS.StackId\` | Stack ID |
|
|
275
|
+
| \`AWS.URLSuffix\` | Domain suffix (usually \`amazonaws.com\`) |
|
|
276
|
+
| \`AWS.Partition\` | Partition (\`aws\`, \`aws-cn\`, \`aws-us-gov\`) |
|
|
277
|
+
| \`AWS.NotificationARNs\` | Notification ARNs |
|
|
278
|
+
| \`AWS.NoValue\` | Removes property when used with \`Fn::If\` |
|
|
279
|
+
|
|
280
|
+
## Intrinsic functions
|
|
281
|
+
|
|
282
|
+
The lexicon provides 8 intrinsic functions (\`Sub\`, \`Ref\`, \`GetAtt\`, \`If\`, \`Join\`, \`Select\`, \`Split\`, \`Base64\`) that map directly to CloudFormation \`Fn::\` calls. See [Intrinsic Functions](./intrinsics) for full usage examples.
|
|
283
|
+
|
|
284
|
+
## Dependencies
|
|
285
|
+
|
|
286
|
+
CloudFormation automatically creates dependencies between resources when you use \`Ref\` or \`Fn::GetAtt\`. Chant leverages this — when you reference \`$.myBucket.arn\`, the serializer emits \`Fn::GetAtt\` and CloudFormation infers the dependency.
|
|
287
|
+
|
|
288
|
+
For cases where you need an explicit dependency without a property reference, set \`dependsOn\`:
|
|
289
|
+
|
|
290
|
+
\`\`\`typescript
|
|
291
|
+
export const appServer = new Instance({
|
|
292
|
+
imageId: "ami-12345678",
|
|
293
|
+
instanceType: "t3.micro",
|
|
294
|
+
dependsOn: ["DatabaseCluster"],
|
|
295
|
+
});
|
|
296
|
+
\`\`\`
|
|
297
|
+
|
|
298
|
+
The \`WAW010\` post-synth check warns if a \`DependsOn\` target is already referenced via \`Ref\` or \`Fn::GetAtt\` in properties — in that case the explicit dependency is redundant.
|
|
299
|
+
|
|
300
|
+
## Policy documents
|
|
301
|
+
|
|
302
|
+
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.
|
|
303
|
+
|
|
304
|
+
The \`PolicyDocument\` interface and its supporting types:
|
|
305
|
+
|
|
306
|
+
| Type | Fields |
|
|
307
|
+
|------|--------|
|
|
308
|
+
| \`PolicyDocument\` | \`Version?\` (\`"2012-10-17"\` \\| \`"2008-10-17"\`), \`Id?\`, \`Statement\` |
|
|
309
|
+
| \`IamPolicyStatement\` | \`Effect\` (\`"Allow"\` \\| \`"Deny"\`), \`Action?\`, \`Resource?\`, \`Principal?\`, \`Condition?\`, and their \`Not\` variants |
|
|
310
|
+
| \`IamPolicyPrincipal\` | \`"*"\` or \`{ AWS?, Service?, Federated? }\` |
|
|
311
|
+
|
|
312
|
+
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.
|
|
313
|
+
|
|
314
|
+
The recommended pattern is to extract policies into your \`defaults.ts\` and reference them via the barrel:
|
|
315
|
+
|
|
316
|
+
\`\`\`typescript
|
|
317
|
+
// defaults.ts — shared trust policies and permission policies
|
|
318
|
+
import { Sub, AWS, type PolicyDocument } from "@intentius/chant-lexicon-aws";
|
|
319
|
+
|
|
320
|
+
export const lambdaTrustPolicy: PolicyDocument = {
|
|
321
|
+
Version: "2012-10-17",
|
|
322
|
+
Statement: [{
|
|
323
|
+
Effect: "Allow",
|
|
324
|
+
Principal: { Service: "lambda.amazonaws.com" },
|
|
325
|
+
Action: "sts:AssumeRole",
|
|
326
|
+
}],
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
export const s3ReadPolicy: PolicyDocument = {
|
|
330
|
+
Statement: [{
|
|
331
|
+
Effect: "Allow",
|
|
332
|
+
Action: ["s3:GetObject", "s3:ListBucket"],
|
|
333
|
+
Resource: "*",
|
|
334
|
+
}],
|
|
335
|
+
};
|
|
336
|
+
\`\`\`
|
|
337
|
+
|
|
338
|
+
Then reference them from resource files:
|
|
339
|
+
|
|
340
|
+
\`\`\`typescript
|
|
341
|
+
// role.ts
|
|
342
|
+
import * as _ from "./_";
|
|
343
|
+
|
|
344
|
+
export const functionRole = new _.Role({
|
|
345
|
+
assumeRolePolicyDocument: _.$.lambdaTrustPolicy,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
export const readPolicy = new _.ManagedPolicy({
|
|
349
|
+
policyDocument: _.$.s3ReadPolicy,
|
|
350
|
+
roles: [_.$.functionRole],
|
|
351
|
+
});
|
|
352
|
+
\`\`\`
|
|
353
|
+
|
|
354
|
+
For scoped resource ARNs, use \`Sub\` in the policy constant:
|
|
355
|
+
|
|
356
|
+
\`\`\`typescript
|
|
357
|
+
// defaults.ts
|
|
358
|
+
export const bucketWritePolicy: PolicyDocument = {
|
|
359
|
+
Statement: [{
|
|
360
|
+
Effect: "Allow",
|
|
361
|
+
Action: ["s3:PutObject"],
|
|
362
|
+
Resource: Sub\`arn:aws:s3:::\${AWS.StackName}-data/*\`,
|
|
363
|
+
}],
|
|
364
|
+
};
|
|
365
|
+
\`\`\`
|
|
366
|
+
|
|
367
|
+
The \`IamPolicyPrincipal\` type supports all principal forms — wildcard (\`"*"\`), AWS accounts, services, and federated providers:
|
|
368
|
+
|
|
369
|
+
\`\`\`typescript
|
|
370
|
+
// Wildcard principal
|
|
371
|
+
Principal: "*",
|
|
372
|
+
|
|
373
|
+
// Service principal
|
|
374
|
+
Principal: { Service: "lambda.amazonaws.com" },
|
|
375
|
+
|
|
376
|
+
// Cross-account
|
|
377
|
+
Principal: { AWS: "arn:aws:iam::123456789012:root" },
|
|
378
|
+
|
|
379
|
+
// Multiple services
|
|
380
|
+
Principal: { Service: ["lambda.amazonaws.com", "edgelambda.amazonaws.com"] },
|
|
381
|
+
\`\`\`
|
|
382
|
+
|
|
383
|
+
## Conditions
|
|
384
|
+
|
|
385
|
+
Use the \`If\` intrinsic for conditional values within resource properties:
|
|
386
|
+
|
|
387
|
+
\`\`\`typescript
|
|
388
|
+
import { If } from "@intentius/chant-lexicon-aws";
|
|
389
|
+
|
|
390
|
+
export const bucket = new Bucket({
|
|
391
|
+
bucketName: If("IsProduction", "prod-data", "dev-data"),
|
|
392
|
+
});
|
|
393
|
+
\`\`\`
|
|
394
|
+
|
|
395
|
+
CloudFormation \`Conditions\` blocks are recognized by the serializer when importing existing templates. For new stacks, use TypeScript logic for build-time decisions and \`If\` for deploy-time decisions.
|
|
396
|
+
|
|
397
|
+
## Mappings
|
|
398
|
+
|
|
399
|
+
CloudFormation Mappings are a static lookup mechanism. In chant, use TypeScript objects instead — they're evaluated at build time and produce the same result:
|
|
400
|
+
|
|
401
|
+
\`\`\`typescript
|
|
402
|
+
const regionAMIs: Record<string, string> = {
|
|
403
|
+
"us-east-1": "ami-12345678",
|
|
404
|
+
"us-west-2": "ami-87654321",
|
|
405
|
+
"eu-west-1": "ami-abcdef01",
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// Use directly in resource properties
|
|
409
|
+
export const server = new Instance({
|
|
410
|
+
imageId: regionAMIs["us-east-1"],
|
|
411
|
+
instanceType: "t3.micro",
|
|
412
|
+
});
|
|
413
|
+
\`\`\`
|
|
414
|
+
|
|
415
|
+
For deploy-time region lookups, combine \`AWS.Region\` with \`If\` or use \`Fn::Sub\` with SSM parameter store references.
|
|
416
|
+
|
|
417
|
+
## Nested stacks
|
|
418
|
+
|
|
419
|
+
CloudFormation nested stacks (\`AWS::CloudFormation::Stack\`) let you decompose large templates into smaller, reusable child templates. Use \`nestedStack()\` to reference a child project directory — a subdirectory with its own barrel file that builds independently:
|
|
420
|
+
|
|
421
|
+
\`\`\`typescript
|
|
422
|
+
// Child project declares outputs with stackOutput()
|
|
423
|
+
// src/network/outputs.ts
|
|
424
|
+
export const vpcId = stackOutput(_.$.vpc.vpcId);
|
|
425
|
+
export const subnetId = stackOutput(_.$.subnet.subnetId);
|
|
426
|
+
export const lambdaSgId = stackOutput(_.$.lambdaSg.groupId);
|
|
427
|
+
|
|
428
|
+
// Parent references the child project
|
|
429
|
+
// src/app.ts
|
|
430
|
+
const network = _.nestedStack("network", import.meta.dir + "/network");
|
|
431
|
+
|
|
432
|
+
export const handler = new _.Function({
|
|
433
|
+
vpcConfig: {
|
|
434
|
+
subnetIds: [network.outputs.subnetId], // cross-stack ref
|
|
435
|
+
securityGroupIds: [network.outputs.lambdaSgId],
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
\`\`\`
|
|
439
|
+
|
|
440
|
+
chant handles the wiring: child template gets an \`Outputs\` section, parent uses \`Fn::GetAtt\` on the stack resource. A \`TemplateBasePath\` parameter lets you configure child template URLs per environment.
|
|
441
|
+
|
|
442
|
+
See [Nested Stacks](./nested-stacks) for the full guide.
|
|
443
|
+
|
|
444
|
+
## Tagging
|
|
445
|
+
|
|
446
|
+
Tags are standard CloudFormation \`Key\`/\`Value\` arrays. Pass them on any resource that supports tagging:
|
|
447
|
+
|
|
448
|
+
\`\`\`typescript
|
|
449
|
+
export const bucket = new Bucket({
|
|
450
|
+
bucketName: "my-bucket",
|
|
451
|
+
tags: [
|
|
452
|
+
{ key: "Environment", value: "production" },
|
|
453
|
+
{ key: "Team", value: "platform" },
|
|
454
|
+
],
|
|
455
|
+
});
|
|
456
|
+
\`\`\`
|
|
457
|
+
|
|
458
|
+
To apply tags across all members of a composite, use [\`propagate\`](./composites#propagate--shared-properties):
|
|
459
|
+
|
|
460
|
+
\`\`\`typescript
|
|
461
|
+
import { propagate } from "@intentius/chant";
|
|
462
|
+
|
|
463
|
+
export const api = propagate(
|
|
464
|
+
LambdaApi({ name: "myApi", code: lambdaCode }),
|
|
465
|
+
{ tags: [{ key: "env", value: "prod" }] },
|
|
466
|
+
);
|
|
467
|
+
\`\`\``,
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
slug: "intrinsics",
|
|
471
|
+
title: "Intrinsic Functions",
|
|
472
|
+
description: "CloudFormation intrinsic functions and their chant syntax",
|
|
473
|
+
content: `CloudFormation intrinsic functions are available as imports from the lexicon. They produce the corresponding \`Fn::\` calls in the serialized template.
|
|
474
|
+
|
|
475
|
+
\`\`\`typescript
|
|
476
|
+
import { Sub, Ref, GetAtt, If, Join, Select, Split, Base64, AWS } from "@intentius/chant-lexicon-aws";
|
|
477
|
+
\`\`\`
|
|
478
|
+
|
|
479
|
+
## \`Sub\` — string substitution
|
|
480
|
+
|
|
481
|
+
Tagged template literal that produces \`Fn::Sub\`. The most common intrinsic — use it for dynamic naming with pseudo-parameters and attribute references:
|
|
482
|
+
|
|
483
|
+
\`\`\`typescript
|
|
484
|
+
// Simple pseudo-parameter substitution
|
|
485
|
+
const bucketName = Sub\`\${AWS.StackName}-data\`;
|
|
486
|
+
// → { "Fn::Sub": "\${AWS::StackName}-data" }
|
|
487
|
+
|
|
488
|
+
// Multiple pseudo-parameters
|
|
489
|
+
const arn = Sub\`arn:aws:s3:::\${AWS.AccountId}:\${AWS.Region}:*\`;
|
|
490
|
+
|
|
491
|
+
// With resource attribute references
|
|
492
|
+
const url = Sub\`https://\${bucket.domainName}/path\`;
|
|
493
|
+
// → { "Fn::Sub": "https://\${DataBucket.DomainName}/path" }
|
|
494
|
+
\`\`\`
|
|
495
|
+
|
|
496
|
+
\`Sub\` is a tagged template — use it with backticks, not as a function call.
|
|
497
|
+
|
|
498
|
+
## \`Ref\` — resource and parameter references
|
|
499
|
+
|
|
500
|
+
References a resource's physical ID or a parameter's value:
|
|
501
|
+
|
|
502
|
+
\`\`\`typescript
|
|
503
|
+
// Reference a parameter
|
|
504
|
+
const envRef = Ref("Environment");
|
|
505
|
+
// → { "Ref": "Environment" }
|
|
506
|
+
|
|
507
|
+
// Reference a resource (returns its physical ID)
|
|
508
|
+
const bucketRef = Ref("DataBucket");
|
|
509
|
+
// → { "Ref": "DataBucket" }
|
|
510
|
+
\`\`\`
|
|
511
|
+
|
|
512
|
+
In most cases you don't need \`Ref\` directly — the serializer automatically generates \`Ref\` when you reference a resource via the barrel (e.g. \`_.$.dataBucket\`).
|
|
513
|
+
|
|
514
|
+
## \`GetAtt\` — resource attributes
|
|
515
|
+
|
|
516
|
+
Retrieves an attribute from a resource:
|
|
517
|
+
|
|
518
|
+
\`\`\`typescript
|
|
519
|
+
// Explicit GetAtt
|
|
520
|
+
const bucketArn = GetAtt("DataBucket", "Arn");
|
|
521
|
+
// → { "Fn::GetAtt": ["DataBucket", "Arn"] }
|
|
522
|
+
\`\`\`
|
|
523
|
+
|
|
524
|
+
**Preferred:** Use AttrRef directly via the resource's typed properties. When you write \`$.dataBucket.arn\`, the serializer automatically emits \`Fn::GetAtt\`. Explicit \`GetAtt\` is only needed for dynamic or imported resource names.
|
|
525
|
+
|
|
526
|
+
## \`If\` — conditional values
|
|
527
|
+
|
|
528
|
+
Returns one of two values based on a condition:
|
|
529
|
+
|
|
530
|
+
\`\`\`typescript
|
|
531
|
+
const value = If("IsProduction", "prod-value", "dev-value");
|
|
532
|
+
// → { "Fn::If": ["IsProduction", "prod-value", "dev-value"] }
|
|
533
|
+
\`\`\`
|
|
534
|
+
|
|
535
|
+
Use with \`AWS.NoValue\` to conditionally omit a property:
|
|
536
|
+
|
|
537
|
+
\`\`\`typescript
|
|
538
|
+
export const bucket = new Bucket({
|
|
539
|
+
bucketName: "my-bucket",
|
|
540
|
+
accelerateConfiguration: If("EnableAcceleration",
|
|
541
|
+
{ accelerationStatus: "Enabled" },
|
|
542
|
+
AWS.NoValue,
|
|
543
|
+
),
|
|
544
|
+
});
|
|
545
|
+
\`\`\`
|
|
546
|
+
|
|
547
|
+
## \`Join\` — join values
|
|
548
|
+
|
|
549
|
+
Joins values with a delimiter:
|
|
550
|
+
|
|
551
|
+
\`\`\`typescript
|
|
552
|
+
const joined = Join("-", ["prefix", AWS.StackName, "suffix"]);
|
|
553
|
+
// → { "Fn::Join": ["-", ["prefix", { "Ref": "AWS::StackName" }, "suffix"]] }
|
|
554
|
+
\`\`\`
|
|
555
|
+
|
|
556
|
+
## \`Select\` — select by index
|
|
557
|
+
|
|
558
|
+
Selects a value from a list by index:
|
|
559
|
+
|
|
560
|
+
\`\`\`typescript
|
|
561
|
+
const first = Select(0, Split(",", "a,b,c"));
|
|
562
|
+
// → { "Fn::Select": [0, { "Fn::Split": [",", "a,b,c"] }] }
|
|
563
|
+
\`\`\`
|
|
564
|
+
|
|
565
|
+
## \`Split\` — split string
|
|
566
|
+
|
|
567
|
+
Splits a string by a delimiter:
|
|
568
|
+
|
|
569
|
+
\`\`\`typescript
|
|
570
|
+
const parts = Split(",", "a,b,c");
|
|
571
|
+
// → { "Fn::Split": [",", "a,b,c"] }
|
|
572
|
+
\`\`\`
|
|
573
|
+
|
|
574
|
+
## \`Base64\` — encode to Base64
|
|
575
|
+
|
|
576
|
+
Encodes a string to Base64, commonly used for EC2 user data:
|
|
577
|
+
|
|
578
|
+
\`\`\`typescript
|
|
579
|
+
const userData = Base64(Sub\`#!/bin/bash
|
|
580
|
+
echo "Stack: \${AWS.StackName}"
|
|
581
|
+
yum update -y
|
|
582
|
+
\`);
|
|
583
|
+
// → { "Fn::Base64": { "Fn::Sub": "..." } }
|
|
584
|
+
\`\`\``,
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
slug: "composites",
|
|
588
|
+
title: "Composites",
|
|
589
|
+
description: "Composite resources, withDefaults presets, and propagate in the AWS CloudFormation lexicon",
|
|
590
|
+
content: `Composites group related resources into reusable factories. See also the core [Composite Resources](/guide/composite-resources/) guide.
|
|
591
|
+
|
|
592
|
+
\`\`\`typescript
|
|
593
|
+
import * as _ from "./_";
|
|
594
|
+
|
|
595
|
+
export const LambdaApi = _.Composite<LambdaApiProps>((props) => {
|
|
596
|
+
const role = new _.Role({
|
|
597
|
+
assumeRolePolicyDocument: _.$.lambdaTrustPolicy,
|
|
598
|
+
managedPolicyArns: [_.$.lambdaBasicExecutionArn],
|
|
599
|
+
policies: props.policies,
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
const func = new _.Function({
|
|
603
|
+
functionName: props.name,
|
|
604
|
+
runtime: props.runtime,
|
|
605
|
+
handler: props.handler,
|
|
606
|
+
code: props.code,
|
|
607
|
+
role: role.arn,
|
|
608
|
+
timeout: props.timeout,
|
|
609
|
+
memorySize: props.memorySize,
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
const permission = new _.Permission({
|
|
613
|
+
functionName: func.arn,
|
|
614
|
+
action: "lambda:InvokeFunction",
|
|
615
|
+
principal: "apigateway.amazonaws.com",
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
return { role, func, permission };
|
|
619
|
+
}, "LambdaApi");
|
|
620
|
+
\`\`\`
|
|
621
|
+
|
|
622
|
+
Instantiate and export:
|
|
623
|
+
|
|
624
|
+
\`\`\`typescript
|
|
625
|
+
export const healthApi = LambdaApi({
|
|
626
|
+
name: Sub\`\${AWS.StackName}-health\`,
|
|
627
|
+
runtime: "nodejs20.x",
|
|
628
|
+
handler: "index.handler",
|
|
629
|
+
code: { zipFile: \`exports.handler = async () => ({ statusCode: 200 });\` },
|
|
630
|
+
});
|
|
631
|
+
\`\`\`
|
|
632
|
+
|
|
633
|
+
During build, composites expand to flat CloudFormation resources: \`healthApi_role\` → \`HealthApiRole\`, \`healthApi_func\` → \`HealthApiFunc\`, \`healthApi_permission\` → \`HealthApiPermission\`.
|
|
634
|
+
|
|
635
|
+
## \`withDefaults\` — composite presets
|
|
636
|
+
|
|
637
|
+
Wrap a composite with pre-applied defaults. Defaulted props become optional:
|
|
638
|
+
|
|
639
|
+
\`\`\`typescript
|
|
640
|
+
import { withDefaults } from "@intentius/chant";
|
|
641
|
+
|
|
642
|
+
const SecureApi = withDefaults(LambdaApi, {
|
|
643
|
+
runtime: "nodejs20.x",
|
|
644
|
+
handler: "index.handler",
|
|
645
|
+
timeout: 10,
|
|
646
|
+
memorySize: 256,
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// Only name and code are required now
|
|
650
|
+
export const healthApi = SecureApi({
|
|
651
|
+
name: Sub\`\${AWS.StackName}-health\`,
|
|
652
|
+
code: { zipFile: \`exports.handler = async () => ({ statusCode: 200 });\` },
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// Composable — stack defaults on top of defaults
|
|
656
|
+
const HighMemoryApi = withDefaults(SecureApi, { memorySize: 2048, timeout: 25 });
|
|
657
|
+
\`\`\`
|
|
658
|
+
|
|
659
|
+
\`withDefaults\` preserves the original composite's identity — same \`_id\` and \`compositeName\`, no new registry entry.
|
|
660
|
+
|
|
661
|
+
## \`propagate\` — shared properties
|
|
662
|
+
|
|
663
|
+
Attach properties that merge into every member during expansion:
|
|
664
|
+
|
|
665
|
+
\`\`\`typescript
|
|
666
|
+
import { propagate } from "@intentius/chant";
|
|
667
|
+
|
|
668
|
+
export const api = propagate(
|
|
669
|
+
LambdaApi({ name: "myApi", code: lambdaCode }),
|
|
670
|
+
{ tags: [{ key: "env", value: "prod" }] },
|
|
671
|
+
);
|
|
672
|
+
// role, func, and permission all receive the env tag
|
|
673
|
+
\`\`\`
|
|
674
|
+
|
|
675
|
+
Merge semantics:
|
|
676
|
+
- **Scalars** — member-specific value wins over shared
|
|
677
|
+
- **Arrays** (e.g. tags) — shared values prepended, member values appended
|
|
678
|
+
- **\`undefined\`** — stripped from shared props, never overwrites
|
|
679
|
+
|
|
680
|
+
## Nested stacks
|
|
681
|
+
|
|
682
|
+
When resources should produce a separate CloudFormation template instead of expanding into the parent, use a **child project** — a subdirectory with its own barrel file (\`_.ts\`) that builds independently. The parent references it with \`nestedStack()\`:
|
|
683
|
+
|
|
684
|
+
\`\`\`typescript
|
|
685
|
+
// src/app.ts — parent references child project directory
|
|
686
|
+
const network = _.nestedStack("network", import.meta.dir + "/network");
|
|
687
|
+
|
|
688
|
+
// Cross-stack reference via outputs proxy
|
|
689
|
+
export const handler = new _.Function({
|
|
690
|
+
vpcConfig: { subnetIds: [network.outputs.subnetId] },
|
|
691
|
+
});
|
|
692
|
+
\`\`\`
|
|
693
|
+
|
|
694
|
+
See [Nested Stacks](./nested-stacks) for the full guide.`,
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
slug: "nested-stacks",
|
|
698
|
+
title: "Nested Stacks",
|
|
699
|
+
description: "Splitting resources into child CloudFormation templates with automatic cross-stack reference wiring",
|
|
700
|
+
content: `CloudFormation nested stacks (\`AWS::CloudFormation::Stack\`) let you decompose large templates into smaller, reusable child templates. The AWS lexicon's \`nestedStack()\` function references a **child project directory** — a subdirectory with its own barrel file that builds independently to a valid CloudFormation template.
|
|
701
|
+
|
|
702
|
+
## Project structure
|
|
703
|
+
|
|
704
|
+
A nested stack is a child project — a subdirectory with its own \`_.ts\` barrel, resource files, and explicit \`stackOutput()\` declarations:
|
|
705
|
+
|
|
706
|
+
\`\`\`
|
|
707
|
+
src/
|
|
708
|
+
_.ts # parent barrel
|
|
709
|
+
app.ts # parent resources
|
|
710
|
+
network/ # ← child project (nested stack)
|
|
711
|
+
_.ts # its own barrel
|
|
712
|
+
vpc.ts # VPC, subnet, internet gateway, routing
|
|
713
|
+
security.ts # security group for Lambda
|
|
714
|
+
outputs.ts # declares cross-stack outputs
|
|
715
|
+
\`\`\`
|
|
716
|
+
|
|
717
|
+
## Declaring outputs in the child
|
|
718
|
+
|
|
719
|
+
Use \`stackOutput()\` to mark values that the parent can reference. Each \`stackOutput()\` becomes an entry in the child template's \`Outputs\` section:
|
|
720
|
+
|
|
721
|
+
\`\`\`typescript
|
|
722
|
+
// src/network/outputs.ts
|
|
723
|
+
import * as _ from "./_";
|
|
724
|
+
import { stackOutput } from "@intentius/chant";
|
|
725
|
+
|
|
726
|
+
export const vpcId = stackOutput(_.$.vpc.vpcId, { description: "VPC ID" });
|
|
727
|
+
export const subnetId = stackOutput(_.$.subnet.subnetId, { description: "Public subnet ID" });
|
|
728
|
+
export const lambdaSgId = stackOutput(_.$.lambdaSg.groupId, { description: "Lambda security group ID" });
|
|
729
|
+
\`\`\`
|
|
730
|
+
|
|
731
|
+
The child can be built independently:
|
|
732
|
+
|
|
733
|
+
\`\`\`bash
|
|
734
|
+
chant build src/network/ -o network.json
|
|
735
|
+
# Produces a standalone, valid CloudFormation template with Outputs
|
|
736
|
+
\`\`\`
|
|
737
|
+
|
|
738
|
+
## Referencing from the parent
|
|
739
|
+
|
|
740
|
+
Use \`nestedStack()\` in the parent to reference a child project directory. It returns an object with an \`outputs\` proxy for cross-stack references:
|
|
741
|
+
|
|
742
|
+
\`\`\`typescript
|
|
743
|
+
// src/app.ts
|
|
744
|
+
import * as _ from "./_";
|
|
745
|
+
|
|
746
|
+
const network = _.nestedStack("network", import.meta.dir + "/network");
|
|
747
|
+
|
|
748
|
+
export const handler = new _.Function({
|
|
749
|
+
functionName: _.Sub\`\${_.AWS.StackName}-handler\`,
|
|
750
|
+
runtime: "nodejs20.x",
|
|
751
|
+
handler: "index.handler",
|
|
752
|
+
role: _.Ref("LambdaExecutionRole"),
|
|
753
|
+
code: { zipFile: "exports.handler = async () => ({ statusCode: 200 });" },
|
|
754
|
+
vpcConfig: {
|
|
755
|
+
subnetIds: [network.outputs.subnetId],
|
|
756
|
+
securityGroupIds: [network.outputs.lambdaSgId],
|
|
757
|
+
},
|
|
758
|
+
});
|
|
759
|
+
\`\`\`
|
|
760
|
+
|
|
761
|
+
\`network.outputs.subnetId\` produces a \`NestedStackOutputRef\` that serializes to \`{ "Fn::GetAtt": ["Network", "Outputs.SubnetId"] }\`.
|
|
762
|
+
|
|
763
|
+
## Build output
|
|
764
|
+
|
|
765
|
+
\`chant build\` produces multiple template files:
|
|
766
|
+
|
|
767
|
+
\`\`\`bash
|
|
768
|
+
chant build -o template.json
|
|
769
|
+
# Produces:
|
|
770
|
+
# template.json — parent template
|
|
771
|
+
# network.template.json — child template
|
|
772
|
+
\`\`\`
|
|
773
|
+
|
|
774
|
+
The parent template includes an \`AWS::CloudFormation::Stack\` resource pointing to the child:
|
|
775
|
+
|
|
776
|
+
\`\`\`json
|
|
777
|
+
"Network": {
|
|
778
|
+
"Type": "AWS::CloudFormation::Stack",
|
|
779
|
+
"Properties": {
|
|
780
|
+
"TemplateURL": { "Fn::Sub": "\${TemplateBasePath}/network.template.json" }
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
\`\`\`
|
|
784
|
+
|
|
785
|
+
## \`TemplateBasePath\` parameter
|
|
786
|
+
|
|
787
|
+
Every parent template gets a \`TemplateBasePath\` parameter (default \`"."\`) that controls where CloudFormation looks for child templates:
|
|
788
|
+
|
|
789
|
+
\`\`\`bash
|
|
790
|
+
# Local dev — default "." works with rain and similar tools
|
|
791
|
+
chant build -o template.json
|
|
792
|
+
|
|
793
|
+
# Production — override with S3 URL
|
|
794
|
+
aws cloudformation deploy \\
|
|
795
|
+
--template-file template.json \\
|
|
796
|
+
--stack-name my-stack \\
|
|
797
|
+
--parameter-overrides TemplateBasePath=https://my-bucket.s3.amazonaws.com/templates
|
|
798
|
+
\`\`\`
|
|
799
|
+
|
|
800
|
+
Child templates also receive the \`TemplateBasePath\` parameter so it propagates through all nesting levels.
|
|
801
|
+
|
|
802
|
+
## Explicit parameters
|
|
803
|
+
|
|
804
|
+
Pass CloudFormation Parameters to child stacks with the \`parameters\` option:
|
|
805
|
+
|
|
806
|
+
\`\`\`typescript
|
|
807
|
+
const network = _.nestedStack("network", import.meta.dir + "/network", {
|
|
808
|
+
parameters: { Environment: "prod", CidrBlock: "10.0.0.0/16" },
|
|
809
|
+
});
|
|
810
|
+
\`\`\`
|
|
811
|
+
|
|
812
|
+
## Recursive nesting
|
|
813
|
+
|
|
814
|
+
Child projects can themselves reference grandchild projects. Each level produces its own template file:
|
|
815
|
+
|
|
816
|
+
\`\`\`
|
|
817
|
+
src/
|
|
818
|
+
_.ts
|
|
819
|
+
app.ts
|
|
820
|
+
infra/
|
|
821
|
+
_.ts
|
|
822
|
+
network/
|
|
823
|
+
_.ts
|
|
824
|
+
vpc.ts
|
|
825
|
+
outputs.ts
|
|
826
|
+
database/
|
|
827
|
+
_.ts
|
|
828
|
+
cluster.ts
|
|
829
|
+
outputs.ts
|
|
830
|
+
\`\`\`
|
|
831
|
+
|
|
832
|
+
The build pipeline detects circular references and reports an error if child A references child B which references child A.
|
|
833
|
+
|
|
834
|
+
## Lint rules
|
|
835
|
+
|
|
836
|
+
Three lint rules help catch common nested stack issues:
|
|
837
|
+
|
|
838
|
+
| Rule | Severity | Description |
|
|
839
|
+
|------|----------|-------------|
|
|
840
|
+
| **WAW013** | error | Child project has no \`stackOutput()\` exports — parent can't reference anything |
|
|
841
|
+
| **WAW014** | warning | \`nestedStack()\` outputs never referenced from parent — could be a separate build |
|
|
842
|
+
| **WAW015** | error | Circular project references |
|
|
843
|
+
|
|
844
|
+
## When to use nested stacks
|
|
845
|
+
|
|
846
|
+
**Use nested stacks when:**
|
|
847
|
+
- Your template exceeds CloudFormation's 500-resource limit
|
|
848
|
+
- You want to reuse a group of resources across multiple parent stacks
|
|
849
|
+
- You need independent update/rollback boundaries for parts of your infrastructure
|
|
850
|
+
|
|
851
|
+
**Use flat composites when:**
|
|
852
|
+
- Resources are tightly coupled and always deploy together
|
|
853
|
+
- You don't need independent update boundaries
|
|
854
|
+
- Your template is within resource limits
|
|
855
|
+
|
|
856
|
+
See [Composites](./composites) for the flat composite approach, and [Examples](./examples#nested-stacks) for a runnable nested stack example.`,
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
slug: "lint-rules",
|
|
860
|
+
title: "Lint Rules",
|
|
861
|
+
description: "Built-in lint rules and post-synth checks for AWS CloudFormation",
|
|
862
|
+
content: `The AWS lexicon ships lint rules that run during \`chant lint\` and post-synth checks that validate the serialized CloudFormation output after \`chant build\`.
|
|
863
|
+
|
|
864
|
+
## Lint rules
|
|
865
|
+
|
|
866
|
+
Lint rules analyze your TypeScript source code before build.
|
|
867
|
+
|
|
868
|
+
### WAW001 — Hardcoded Region
|
|
869
|
+
|
|
870
|
+
**Severity:** warning | **Category:** security
|
|
871
|
+
|
|
872
|
+
Flags hardcoded AWS region strings like \`us-east-1\`. Use \`AWS.Region\` instead so templates are portable across regions.
|
|
873
|
+
|
|
874
|
+
\`\`\`typescript
|
|
875
|
+
// Triggers WAW001
|
|
876
|
+
const endpoint = "s3.us-east-1.amazonaws.com";
|
|
877
|
+
|
|
878
|
+
// Fixed
|
|
879
|
+
const endpoint = Sub\`s3.\${AWS.Region}.amazonaws.com\`;
|
|
880
|
+
\`\`\`
|
|
881
|
+
|
|
882
|
+
### WAW006 — S3 Bucket Encryption
|
|
883
|
+
|
|
884
|
+
**Severity:** warning | **Category:** security
|
|
885
|
+
|
|
886
|
+
Flags S3 buckets that don't configure server-side encryption. AWS recommends enabling encryption on all buckets.
|
|
887
|
+
|
|
888
|
+
\`\`\`typescript
|
|
889
|
+
// Triggers WAW006
|
|
890
|
+
export const bucket = new Bucket({ bucketName: "my-bucket" });
|
|
891
|
+
|
|
892
|
+
// Fixed — add encryption configuration
|
|
893
|
+
export const bucket = new Bucket({
|
|
894
|
+
bucketName: "my-bucket",
|
|
895
|
+
bucketEncryption: {
|
|
896
|
+
serverSideEncryptionConfiguration: [{
|
|
897
|
+
serverSideEncryptionByDefault: { sseAlgorithm: "AES256" },
|
|
898
|
+
}],
|
|
899
|
+
},
|
|
900
|
+
});
|
|
901
|
+
\`\`\`
|
|
902
|
+
|
|
903
|
+
### WAW009 — IAM Wildcard Resource
|
|
904
|
+
|
|
905
|
+
**Severity:** warning | **Category:** security
|
|
906
|
+
|
|
907
|
+
Flags IAM policy statements that use \`"Resource": "*"\`. Prefer scoped resource ARNs following the principle of least privilege.
|
|
908
|
+
|
|
909
|
+
\`\`\`typescript
|
|
910
|
+
// Triggers WAW009
|
|
911
|
+
{ Effect: "Allow", Action: ["s3:GetObject"], Resource: "*" }
|
|
912
|
+
|
|
913
|
+
// Fixed — scope to a specific bucket
|
|
914
|
+
{ Effect: "Allow", Action: ["s3:GetObject"], Resource: Sub\`arn:aws:s3:::\${AWS.StackName}-data/*\` }
|
|
915
|
+
\`\`\`
|
|
916
|
+
|
|
917
|
+
IAM policy documents use PascalCase keys (\`Effect\`, \`Action\`, \`Resource\`) matching the IAM JSON Policy Language spec. The \`PolicyDocument\` and \`IamPolicyStatement\` types provide full autocomplete for these fields.
|
|
918
|
+
|
|
919
|
+
## Post-synth checks
|
|
920
|
+
|
|
921
|
+
Post-synth checks run against the serialized CloudFormation JSON after build. They catch issues that are only visible in the final template.
|
|
922
|
+
|
|
923
|
+
### COR020 — Circular Resource Dependency
|
|
924
|
+
|
|
925
|
+
Detects cycles in the resource dependency graph built from \`Ref\`, \`Fn::GetAtt\`, and \`DependsOn\` entries. Circular dependencies cause CloudFormation deployments to fail.
|
|
926
|
+
|
|
927
|
+
### EXT001 — Extension Constraint Violation
|
|
928
|
+
|
|
929
|
+
Validates cross-property constraints from CloudFormation's cfn-lint extension schemas. For example, an EC2 instance might require \`SubnetId\` when \`NetworkInterfaces\` is not set.
|
|
930
|
+
|
|
931
|
+
### WAW010 — Redundant DependsOn
|
|
932
|
+
|
|
933
|
+
Flags \`DependsOn\` entries where the target resource is already referenced via \`Ref\` or \`Fn::GetAtt\` in the resource's properties. CloudFormation automatically creates dependencies for these references, making the explicit \`DependsOn\` unnecessary.
|
|
934
|
+
|
|
935
|
+
### WAW011 — Deprecated Lambda Runtime
|
|
936
|
+
|
|
937
|
+
Flags Lambda functions using deprecated or approaching-end-of-life runtimes (e.g. \`nodejs16.x\`, \`python3.8\`). Using deprecated runtimes prevents function updates and may cause deployment failures.
|
|
938
|
+
|
|
939
|
+
### WAW013 — No Stack Outputs
|
|
940
|
+
|
|
941
|
+
**Severity:** error | **Category:** correctness
|
|
942
|
+
|
|
943
|
+
Flags child projects (nested stacks) that have no \`stackOutput()\` exports. Without outputs, the parent stack can't reference any values from the child — either add \`stackOutput()\` declarations or remove the \`nestedStack()\` reference.
|
|
944
|
+
|
|
945
|
+
### WAW014 — Unreferenced Stack Outputs
|
|
946
|
+
|
|
947
|
+
**Severity:** warning | **Category:** style
|
|
948
|
+
|
|
949
|
+
Flags \`nestedStack()\` references whose outputs are never used from the parent. If no cross-stack references exist, the child project could just be built and deployed independently.
|
|
950
|
+
|
|
951
|
+
### WAW015 — Circular Project References
|
|
952
|
+
|
|
953
|
+
**Severity:** error | **Category:** correctness
|
|
954
|
+
|
|
955
|
+
Detects circular references between child projects (e.g. project A references project B which references project A). Circular project dependencies cause infinite build recursion.
|
|
956
|
+
|
|
957
|
+
## Running lint
|
|
958
|
+
|
|
959
|
+
\`\`\`bash
|
|
960
|
+
# Lint your chant project
|
|
961
|
+
chant lint
|
|
962
|
+
|
|
963
|
+
# Lint with auto-fix where supported
|
|
964
|
+
chant lint --fix
|
|
965
|
+
\`\`\`
|
|
966
|
+
|
|
967
|
+
To suppress a rule on a specific line:
|
|
968
|
+
|
|
969
|
+
\`\`\`typescript
|
|
970
|
+
// chant-disable-next-line WAW001
|
|
971
|
+
const endpoint = "s3.us-east-1.amazonaws.com";
|
|
972
|
+
\`\`\`
|
|
973
|
+
|
|
974
|
+
To suppress globally in \`chant.config.ts\`:
|
|
975
|
+
|
|
976
|
+
\`\`\`typescript
|
|
977
|
+
export default {
|
|
978
|
+
lint: {
|
|
979
|
+
rules: {
|
|
980
|
+
WAW001: "off",
|
|
981
|
+
},
|
|
982
|
+
},
|
|
983
|
+
};
|
|
984
|
+
\`\`\`
|
|
985
|
+
|
|
986
|
+
See also [Custom Lint Rules](./custom-rules) for writing project-specific rules.`,
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
slug: "custom-rules",
|
|
990
|
+
title: "Custom Lint Rules",
|
|
991
|
+
description: "Writing and registering project-specific lint rules for AWS CloudFormation",
|
|
992
|
+
content: `Chant's lint engine runs TypeScript AST visitors. Write project-specific rules that enforce domain conventions.
|
|
993
|
+
|
|
994
|
+
## Anatomy of a lint rule
|
|
995
|
+
|
|
996
|
+
\`\`\`typescript
|
|
997
|
+
import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
|
|
998
|
+
import * as ts from "typescript";
|
|
999
|
+
|
|
1000
|
+
export const apiTimeoutRule: LintRule = {
|
|
1001
|
+
id: "WAW012", // unique ID (WAW = AWS-specific prefix)
|
|
1002
|
+
severity: "error", // "error" | "warning"
|
|
1003
|
+
category: "correctness", // "correctness" | "style" | "security"
|
|
1004
|
+
|
|
1005
|
+
check(context: LintContext): LintDiagnostic[] {
|
|
1006
|
+
const { sourceFile } = context;
|
|
1007
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
1008
|
+
|
|
1009
|
+
function visit(node: ts.Node): void {
|
|
1010
|
+
// Walk the AST looking for violations
|
|
1011
|
+
if (ts.isCallExpression(node)) {
|
|
1012
|
+
// Inspect arguments, report diagnostics
|
|
1013
|
+
}
|
|
1014
|
+
ts.forEachChild(node, visit);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
visit(sourceFile);
|
|
1018
|
+
return diagnostics;
|
|
1019
|
+
},
|
|
1020
|
+
};
|
|
1021
|
+
\`\`\`
|
|
1022
|
+
|
|
1023
|
+
## Example: API Gateway timeout (WAW012)
|
|
1024
|
+
|
|
1025
|
+
The advanced example includes a rule that flags Lambda API composites with \`timeout > 29\` — API Gateway's synchronous limit:
|
|
1026
|
+
|
|
1027
|
+
\`\`\`typescript
|
|
1028
|
+
const API_FACTORIES = new Set(["LambdaApi", "SecureApi", "HighMemoryApi"]);
|
|
1029
|
+
|
|
1030
|
+
export const apiTimeoutRule: LintRule = {
|
|
1031
|
+
id: "WAW012",
|
|
1032
|
+
severity: "error",
|
|
1033
|
+
category: "correctness",
|
|
1034
|
+
|
|
1035
|
+
check(context: LintContext): LintDiagnostic[] {
|
|
1036
|
+
// Walks AST for calls to API factory functions,
|
|
1037
|
+
// inspects the timeout property, reports if > 29
|
|
1038
|
+
},
|
|
1039
|
+
};
|
|
1040
|
+
\`\`\`
|
|
1041
|
+
|
|
1042
|
+
## Registering custom rules
|
|
1043
|
+
|
|
1044
|
+
Add a \`chant.config.ts\` to your project:
|
|
1045
|
+
|
|
1046
|
+
\`\`\`typescript
|
|
1047
|
+
export default {
|
|
1048
|
+
lint: {
|
|
1049
|
+
extends: ["@intentius/chant/lint/presets/strict"],
|
|
1050
|
+
rules: {
|
|
1051
|
+
COR004: "off", // disable a built-in rule
|
|
1052
|
+
},
|
|
1053
|
+
plugins: ["./lint/api-timeout.ts"], // load custom rules
|
|
1054
|
+
},
|
|
1055
|
+
};
|
|
1056
|
+
\`\`\`
|
|
1057
|
+
|
|
1058
|
+
The \`plugins\` array accepts relative paths. Each plugin module should export a \`LintRule\` object.`,
|
|
1059
|
+
},
|
|
1060
|
+
{
|
|
1061
|
+
slug: "examples",
|
|
1062
|
+
title: "Examples",
|
|
1063
|
+
description: "Walkthrough of the getting-started and advanced AWS CloudFormation examples",
|
|
1064
|
+
content: `Two runnable examples live in the lexicon's \`examples/\` directory. Clone the repo and try them:
|
|
1065
|
+
|
|
1066
|
+
\`\`\`bash
|
|
1067
|
+
cd examples/getting-started
|
|
1068
|
+
bun install
|
|
1069
|
+
chant build # produces CloudFormation JSON
|
|
1070
|
+
chant lint # runs lint rules
|
|
1071
|
+
bun test # runs the example's tests
|
|
1072
|
+
\`\`\`
|
|
1073
|
+
|
|
1074
|
+
## Getting Started
|
|
1075
|
+
|
|
1076
|
+
\`examples/getting-started/\` — 4 resources across separate files: two S3 buckets, an IAM role, and a Lambda function.
|
|
1077
|
+
|
|
1078
|
+
\`\`\`
|
|
1079
|
+
src/
|
|
1080
|
+
├── _.ts # Barrel — re-exports lexicon + auto-discovers siblings
|
|
1081
|
+
├── defaults.ts # Shared config: encryption, versioning, public access block
|
|
1082
|
+
├── data-bucket.ts # S3 bucket using barrel defaults
|
|
1083
|
+
├── logs-bucket.ts # S3 bucket for access logs
|
|
1084
|
+
├── role.ts # IAM role with Lambda assume-role policy
|
|
1085
|
+
└── handler.ts # Lambda function referencing role and bucket
|
|
1086
|
+
\`\`\`
|
|
1087
|
+
|
|
1088
|
+
**Patterns demonstrated:**
|
|
1089
|
+
|
|
1090
|
+
1. **Barrel file** — \`_.ts\` re-exports the AWS lexicon and creates the \`$\` proxy for cross-file references
|
|
1091
|
+
2. **Shared defaults** — \`defaults.ts\` exports reusable property objects (\`encryptionDefault\`, \`publicAccessBlock\`) that other files reference via \`_.$\`
|
|
1092
|
+
3. **Cross-resource references** — \`_.$.dataBucket.arn\` in \`handler.ts\` serializes to \`Fn::GetAtt\` in the template
|
|
1093
|
+
4. **Intrinsics** — \`Sub\` tagged templates with pseudo-parameters for dynamic naming
|
|
1094
|
+
|
|
1095
|
+
\`\`\`typescript
|
|
1096
|
+
// handler.ts — Lambda function referencing other resources
|
|
1097
|
+
import * as _ from "./_";
|
|
1098
|
+
|
|
1099
|
+
const lambdaCode = { zipFile: "exports.handler = async () => ({ statusCode: 200 });" };
|
|
1100
|
+
|
|
1101
|
+
export const handler = new _.Function({
|
|
1102
|
+
functionName: _.Sub\`\${_.AWS.StackName}-handler\`,
|
|
1103
|
+
handler: "index.handler",
|
|
1104
|
+
runtime: "nodejs20.x",
|
|
1105
|
+
role: _.$.functionRole.arn, // → Fn::GetAtt
|
|
1106
|
+
code: lambdaCode,
|
|
1107
|
+
environment: {
|
|
1108
|
+
variables: { BUCKET_ARN: _.$.dataBucket.arn }, // → Fn::GetAtt
|
|
1109
|
+
},
|
|
1110
|
+
});
|
|
1111
|
+
\`\`\`
|
|
1112
|
+
|
|
1113
|
+
## Advanced
|
|
1114
|
+
|
|
1115
|
+
\`examples/advanced/\` — builds on getting-started with composites, presets, inline IAM policies, and a custom lint rule.
|
|
1116
|
+
|
|
1117
|
+
\`\`\`
|
|
1118
|
+
src/
|
|
1119
|
+
├── _.ts # Barrel + re-exports Composite from core
|
|
1120
|
+
├── chant.config.ts # Lint config: strict preset + custom plugin
|
|
1121
|
+
├── defaults.ts # Encryption, versioning, access block, Lambda trust policy
|
|
1122
|
+
├── data-bucket.ts # S3 bucket
|
|
1123
|
+
├── lambda-api.ts # Composite factory + SecureApi/HighMemoryApi presets
|
|
1124
|
+
├── health-api.ts # SecureApi — minimal health check
|
|
1125
|
+
├── upload-api.ts # SecureApi + S3 PutObject policy
|
|
1126
|
+
├── process-api.ts # HighMemoryApi + S3 read/write policy
|
|
1127
|
+
└── lint/
|
|
1128
|
+
└── api-timeout.ts # Custom WAW012 rule
|
|
1129
|
+
\`\`\`
|
|
1130
|
+
|
|
1131
|
+
**What it adds:**
|
|
1132
|
+
|
|
1133
|
+
- **Composites** — \`LambdaApi\` groups Role + Function + Permission into a reusable unit (see [Composites](./composites))
|
|
1134
|
+
- **Composite presets** — \`SecureApi\` (low memory, short timeout) and \`HighMemoryApi\` (high memory, longer timeout) created with \`withDefaults\`
|
|
1135
|
+
- **Inline IAM policies** — \`upload-api.ts\` and \`process-api.ts\` attach \`Role_Policy\` objects for scoped S3 access
|
|
1136
|
+
- **Custom lint rule** — \`api-timeout.ts\` enforces API Gateway's 29-second timeout limit (see [Custom Lint Rules](./custom-rules))
|
|
1137
|
+
- **Lint config** — \`chant.config.ts\` extends the strict preset and loads the custom plugin
|
|
1138
|
+
|
|
1139
|
+
The example produces 10 CloudFormation resources: 1 S3 bucket + 3 composites × 3 members each.
|
|
1140
|
+
|
|
1141
|
+
## Nested Stacks
|
|
1142
|
+
|
|
1143
|
+
\`examples/nested-stacks/\` — demonstrates child projects for splitting resources into child CloudFormation templates with automatic cross-stack reference wiring.
|
|
1144
|
+
|
|
1145
|
+
\`\`\`
|
|
1146
|
+
src/
|
|
1147
|
+
├── _.ts # Parent barrel
|
|
1148
|
+
├── app.ts # Lambda function (references network outputs)
|
|
1149
|
+
└── network/ # Child project (nested stack)
|
|
1150
|
+
├── _.ts # Child barrel
|
|
1151
|
+
├── vpc.ts # VPC, subnet, internet gateway, route table
|
|
1152
|
+
├── security.ts # Security group for Lambda
|
|
1153
|
+
└── outputs.ts # stackOutput() declarations
|
|
1154
|
+
\`\`\`
|
|
1155
|
+
|
|
1156
|
+
**Patterns demonstrated:**
|
|
1157
|
+
|
|
1158
|
+
1. **Child project** — \`network/\` is a separate project directory with its own barrel, resources, and \`stackOutput()\` exports
|
|
1159
|
+
2. **Cross-stack references** — \`app.ts\` accesses \`network.outputs.subnetId\` and \`network.outputs.lambdaSgId\`, which serialize to \`Fn::GetAtt\` on the parent's \`AWS::CloudFormation::Stack\` resource
|
|
1160
|
+
3. **Multi-file output** — build produces \`template.json\` (parent) and \`network.template.json\` (child)
|
|
1161
|
+
4. **TemplateBasePath** — auto-generated parameter for configuring child template URLs per environment
|
|
1162
|
+
|
|
1163
|
+
\`\`\`typescript
|
|
1164
|
+
// network/outputs.ts — child declares what the parent can reference
|
|
1165
|
+
import * as _ from "./_";
|
|
1166
|
+
import { stackOutput } from "@intentius/chant";
|
|
1167
|
+
|
|
1168
|
+
export const vpcId = stackOutput(_.$.vpc.vpcId, { description: "VPC ID" });
|
|
1169
|
+
export const subnetId = stackOutput(_.$.subnet.subnetId, { description: "Public subnet ID" });
|
|
1170
|
+
export const lambdaSgId = stackOutput(_.$.lambdaSg.groupId, { description: "Lambda security group ID" });
|
|
1171
|
+
\`\`\`
|
|
1172
|
+
|
|
1173
|
+
\`\`\`typescript
|
|
1174
|
+
// app.ts — parent references child project
|
|
1175
|
+
import * as _ from "./_";
|
|
1176
|
+
|
|
1177
|
+
const network = _.nestedStack("network", import.meta.dir + "/network");
|
|
1178
|
+
|
|
1179
|
+
export const handler = new _.Function({
|
|
1180
|
+
functionName: _.Sub\`\${_.AWS.StackName}-handler\`,
|
|
1181
|
+
runtime: "nodejs20.x",
|
|
1182
|
+
handler: "index.handler",
|
|
1183
|
+
role: _.Ref("LambdaExecutionRole"),
|
|
1184
|
+
code: { zipFile: "exports.handler = async () => ({ statusCode: 200 });" },
|
|
1185
|
+
vpcConfig: {
|
|
1186
|
+
subnetIds: [network.outputs.subnetId],
|
|
1187
|
+
securityGroupIds: [network.outputs.lambdaSgId],
|
|
1188
|
+
},
|
|
1189
|
+
});
|
|
1190
|
+
\`\`\`
|
|
1191
|
+
|
|
1192
|
+
See [Nested Stacks](./nested-stacks) for the full guide.`,
|
|
1193
|
+
},
|
|
1194
|
+
],
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1197
|
+
log("Generating AWS documentation...");
|
|
1198
|
+
const result = docsPipeline(config);
|
|
1199
|
+
|
|
1200
|
+
log(`Writing standalone docs site to ${outDir}`);
|
|
1201
|
+
writeDocsSite(config, result);
|
|
1202
|
+
|
|
1203
|
+
console.error(
|
|
1204
|
+
`Docs generated: ${result.stats.resources} resources, ${result.stats.services} services, ${result.stats.rules} rules`,
|
|
1205
|
+
);
|
|
1206
|
+
}
|