Skip to content

Commit 324eea4

Browse files
committed
docs(cfn-lang-ext): expand CloudFormation Language Extensions reference
Adds documentation for behaviors that are now supported / verified by this PR's coverage: - Full table of recognized dynamic artifact properties (derived from the canonical packageable-resource list rather than a hand-maintained one), including AWS::Serverless::Application, AWS::CloudFormation::StackSet, AppSync, ElasticBeanstalk, Glue::Job, Lambda::Function image package - Note about how dotted property paths (Command.ScriptLocation, Code.ImageUri) are addressed on the resource vs. used as Mapping identifiers - Multiple resources per ForEach body, Mapping name collision resolution - ForEach in Outputs; Conditions and DependsOn on emitted resources - Validation errors (ForEach element count, nesting limit, missing parameter, empty collection)
1 parent b4e4772 commit 324eea4

1 file changed

Lines changed: 125 additions & 1 deletion

File tree

docs/cfn-language-extensions.md

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,37 @@ Running `sam build` expands this into `UsersFunction`, `OrdersFunction`, and `Pr
4141

4242
### Dynamic artifact properties
4343

44-
When a packageable property (like `CodeUri`, `ContentUri`, `ImageUri`) uses a loop variable (e.g., `./services/${Name}`), SAM CLI generates a CloudFormation `Mappings` section that maps each collection value to its S3 URI. The `Fn::ForEach` body is rewritten to use `Fn::FindInMap` so CloudFormation can resolve the correct artifact at deploy time.
44+
When a packageable property uses a loop variable (e.g., `./services/${Name}`), SAM CLI generates a CloudFormation `Mappings` section that maps each collection value to its S3 URI. The `Fn::ForEach` body is rewritten to use `Fn::FindInMap` so CloudFormation can resolve the correct artifact at deploy time.
45+
46+
The set of recognized artifact properties is derived from the same canonical list `sam package` already uses (`RESOURCES_WITH_LOCAL_PATHS` and `RESOURCES_WITH_IMAGE_COMPONENT` in `samcli/lib/utils/resources.py`), so every resource type whose artifact property `sam package` would normally rewrite is supported here too. That includes:
47+
48+
| Resource type | Property |
49+
|---------------|----------|
50+
| `AWS::Serverless::Function` | `CodeUri`, `ImageUri` |
51+
| `AWS::Serverless::LayerVersion` | `ContentUri` |
52+
| `AWS::Serverless::Api` | `DefinitionUri` |
53+
| `AWS::Serverless::HttpApi` | `DefinitionUri` |
54+
| `AWS::Serverless::StateMachine` | `DefinitionUri` |
55+
| `AWS::Serverless::GraphQLApi` | `SchemaUri`, `CodeUri` |
56+
| `AWS::Serverless::Application` | `Location` |
57+
| `AWS::Lambda::Function` | `Code`, `Code.ImageUri` |
58+
| `AWS::Lambda::LayerVersion` | `Content` |
59+
| `AWS::ApiGateway::RestApi` | `BodyS3Location` |
60+
| `AWS::ApiGatewayV2::Api` | `BodyS3Location` |
61+
| `AWS::AppSync::GraphQLSchema` | `DefinitionS3Location` |
62+
| `AWS::AppSync::Resolver` | `RequestMappingTemplateS3Location`, `ResponseMappingTemplateS3Location`, `CodeS3Location` |
63+
| `AWS::AppSync::FunctionConfiguration` | `RequestMappingTemplateS3Location`, `ResponseMappingTemplateS3Location`, `CodeS3Location` |
64+
| `AWS::StepFunctions::StateMachine` | `DefinitionS3Location` |
65+
| `AWS::ElasticBeanstalk::ApplicationVersion` | `SourceBundle` |
66+
| `AWS::Glue::Job` | `Command.ScriptLocation` |
67+
| `AWS::CloudFormation::Stack` | `TemplateURL` |
68+
| `AWS::CloudFormation::StackSet` | `TemplateURL` |
69+
| `AWS::CloudFormation::ModuleVersion` | `ModulePackage` |
70+
| `AWS::CloudFormation::ResourceVersion` | `SchemaHandlerPackage` |
71+
72+
When a property is dotted (e.g. `Command.ScriptLocation` on `AWS::Glue::Job` or `Code.ImageUri` on `AWS::Lambda::Function`), SAM CLI reads and writes the value at the dotted location on the resource — so it lands at `Properties.Command.ScriptLocation` rather than at a literal `Properties["Command.ScriptLocation"]` key — and uses only the leaf segment when it needs to construct an alphanumeric identifier (Mapping name suffix or `Fn::FindInMap` third argument).
73+
74+
When the property is loop-templated, the Mapping name is `SAM<LeafProperty><LoopName>` (e.g., `SAMCodeUriServices`, `SAMScriptLocationJobs`). Customer-authored mappings should not start with these `SAM*` prefixes — they are reserved for SAM CLI (see [Limitations](#limitations) below).
4575

4676
For example, after `sam package`:
4777

@@ -67,6 +97,45 @@ Resources:
6797
CodeUri: !FindInMap [SAMCodeUriServices, !Ref Name, CodeUri]
6898
```
6999

100+
### Multiple resources per ForEach body
101+
102+
A single `Fn::ForEach` body can emit more than one resource per iteration. Each resource is generated for every collection value:
103+
104+
```yaml
105+
Resources:
106+
Fn::ForEach::Tables:
107+
- TableName
108+
- [Users, Orders, Products]
109+
- ${TableName}Table:
110+
Type: AWS::DynamoDB::Table
111+
Properties:
112+
TableName: !Sub "${AWS::StackName}-${TableName}"
113+
# ...
114+
115+
${TableName}StreamProcessor:
116+
Type: AWS::Serverless::Function
117+
Properties:
118+
CodeUri: stream-processors/${TableName}/
119+
Events:
120+
DDBStream:
121+
Type: DynamoDB
122+
Properties:
123+
Stream: !GetAtt
124+
- !Sub "${TableName}Table"
125+
- StreamArn
126+
```
127+
128+
### Mapping name collision resolution
129+
130+
When two resources in the same `Fn::ForEach` body declare the same dynamic artifact property (for example, both an `Api` and a `StateMachine` use `DefinitionUri`), SAM CLI appends a sanitized suffix derived from the resource logical-ID template to keep Mapping names unique:
131+
132+
| Resource template | Property | Mapping name |
133+
|-------------------|----------|--------------|
134+
| `${Svc}Api` | `DefinitionUri` | `SAMDefinitionUriServicesApi` |
135+
| `${Svc}StateMachine` | `DefinitionUri` | `SAMDefinitionUriServicesStateMachine` |
136+
137+
When there is no collision the base name (e.g., `SAMDefinitionUriServices`) is used.
138+
70139
### Parameter-based collections
71140

72141
When the `Fn::ForEach` collection is a parameter reference (`!Ref ServiceNames`), the collection values are resolved at package time from:
@@ -120,6 +189,46 @@ Resources:
120189
STAGE: !Ref Env
121190
```
122191

192+
### ForEach in Outputs
193+
194+
`Fn::ForEach` blocks are also expanded inside the `Outputs` section, so you can emit one output per collection value:
195+
196+
```yaml
197+
Outputs:
198+
Fn::ForEach::FunctionArns:
199+
- Name
200+
- [alpha, beta]
201+
- ${Name}FunctionArn:
202+
Value: !GetAtt
203+
- !Sub "${Name}Function"
204+
- Arn
205+
```
206+
207+
### Conditions and DependsOn
208+
209+
Resources emitted by `Fn::ForEach` can carry `Condition` and `DependsOn` like any other resource. The condition or dependency is replicated onto each generated resource:
210+
211+
```yaml
212+
Conditions:
213+
IsProd: !Equals [!Ref Environment, prod]
214+
215+
Resources:
216+
SharedTable:
217+
Type: AWS::DynamoDB::Table
218+
# ...
219+
220+
Fn::ForEach::Functions:
221+
- Name
222+
- [api, worker]
223+
- ${Name}Function:
224+
Type: AWS::Serverless::Function
225+
Condition: IsProd
226+
DependsOn: SharedTable
227+
Properties:
228+
Handler: main.handler
229+
CodeUri: functions/${Name}/
230+
```
231+
123232
### &{identifier} syntax
124233

125234
The `&{identifier}` syntax strips non-alphanumeric characters from the substituted value, useful for generating valid logical IDs from values like IP addresses:
@@ -154,11 +263,26 @@ The following intrinsic functions are resolved locally during expansion:
154263

155264
Functions that require deployed resources (`Fn::GetAtt`, `Fn::ImportValue`, `Fn::GetAZs`) are preserved for CloudFormation to resolve at deploy time.
156265

266+
## Validation errors
267+
268+
The following template issues are caught locally before the SAM transform runs:
269+
270+
| Error | Cause |
271+
|-------|-------|
272+
| `Fn::ForEach must have exactly 3 elements` | The `Fn::ForEach` value is not a 3-element list (`[loop_variable, collection, output_template]`). |
273+
| `Maximum nesting depth of 5 exceeded` | More than 5 levels of `Fn::ForEach` are nested. |
274+
| `Parameter '<name>' referenced by Fn::ForEach not found` | The `!Ref` in the collection points at a parameter that is not declared in the template. |
275+
| Empty collection | If the collection resolves to an empty list (e.g., a `CommaDelimitedList` parameter with `Default: ""`), no resources are emitted — the loop is silently skipped. |
276+
157277
## Limitations
158278
159279
- **Collections must be resolvable at build/package time.** `Fn::ForEach` collections that use `Fn::GetAtt`, `Fn::ImportValue`, or SSM/Secrets Manager dynamic references cannot be expanded locally. Use a parameter with `--parameter-overrides` instead.
160280
- **Parameter values are fixed at package time.** If you change `--parameter-overrides` at deploy time without re-packaging, the Mappings won't include entries for new values and deployment will fail.
161281
- **`DeletionPolicy` and `UpdateReplacePolicy`** are validated and resolved during expansion. They support `Ref` to parameters but not other intrinsic functions.
282+
- **Nesting limit.** Up to 5 levels of `Fn::ForEach` may be nested, matching CloudFormation's server-side limit.
283+
- **Reserved Mapping names.** Mapping names starting with any of the following are reserved for SAM CLI — do not author your own mappings with these prefixes:
284+
- `SAMCodeUri`, `SAMImageUri`, `SAMContentUri`, `SAMDefinitionUri`, `SAMSchemaUri`, `SAMBodyS3Location`, `SAMDefinitionS3Location`, `SAMTemplateURL`, `SAMCode`, `SAMContent` — emitted by `sam package` for dynamic artifact properties (see the table above).
285+
- `SAMLayers` — emitted by `sam build` when a `Fn::ForEach`-generated function picks up auto-generated dependency-layer references (Lambda layers SAM CLI builds into a nested stack). This prefix has no corresponding user-authored property; it is added automatically.
162286

163287
## Telemetry
164288

0 commit comments

Comments
 (0)