Back to Aws Sam Cli

CloudFormation Language Extensions Support

docs/cfn-language-extensions.md

1.160.112.7 KB
Original Source

CloudFormation Language Extensions Support

SAM CLI now supports templates that use the AWS::LanguageExtensions transform, including Fn::ForEach, Fn::Length, Fn::ToJsonString, and Fn::FindInMap with DefaultValue.

How it works

When SAM CLI detects AWS::LanguageExtensions in a template's Transform section, it expands language extension constructs locally before running SAM transforms. This enables sam build, sam package, sam deploy, sam sync, sam validate, sam local invoke, and sam local start-api to work with templates that use these constructs.

The expansion happens in two phases:

  1. Phase 1 (Language Extensions)Fn::ForEach loops are expanded, intrinsic functions are resolved where possible, and the template is converted to standard CloudFormation.
  2. Phase 2 (SAM Transform) — The expanded template is processed by the SAM Translator as usual.

The original template (with Fn::ForEach intact) is preserved for CloudFormation deployment, since CloudFormation processes the AWS::LanguageExtensions transform server-side.

Fn::ForEach

Fn::ForEach generates multiple resources, conditions, or outputs from a single template definition:

yaml
Transform: AWS::LanguageExtensions

Parameters:
  ServiceNames:
    Type: CommaDelimitedList
    Default: "Users,Orders,Products"

Resources:
  Fn::ForEach::Services:
    - Name
    - !Ref ServiceNames
    - ${Name}Function:
        Type: AWS::Serverless::Function
        Properties:
          Handler: index.handler
          Runtime: python3.12
          CodeUri: ./services/${Name}

Running sam build expands this into UsersFunction, OrdersFunction, and ProductsFunction, each built from its respective source directory.

Dynamic artifact properties

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.

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:

Resource typeProperty
AWS::Serverless::FunctionCodeUri, ImageUri
AWS::Serverless::LayerVersionContentUri
AWS::Serverless::ApiDefinitionUri
AWS::Serverless::HttpApiDefinitionUri
AWS::Serverless::StateMachineDefinitionUri
AWS::Serverless::GraphQLApiSchemaUri, CodeUri
AWS::Serverless::ApplicationLocation
AWS::Lambda::FunctionCode, Code.ImageUri
AWS::Lambda::LayerVersionContent
AWS::ApiGateway::RestApiBodyS3Location
AWS::ApiGatewayV2::ApiBodyS3Location
AWS::AppSync::GraphQLSchemaDefinitionS3Location
AWS::AppSync::ResolverRequestMappingTemplateS3Location, ResponseMappingTemplateS3Location, CodeS3Location
AWS::AppSync::FunctionConfigurationRequestMappingTemplateS3Location, ResponseMappingTemplateS3Location, CodeS3Location
AWS::StepFunctions::StateMachineDefinitionS3Location
AWS::ElasticBeanstalk::ApplicationVersionSourceBundle
AWS::Glue::JobCommand.ScriptLocation
AWS::CloudFormation::StackTemplateURL
AWS::CloudFormation::StackSetTemplateURL
AWS::CloudFormation::ModuleVersionModulePackage
AWS::CloudFormation::ResourceVersionSchemaHandlerPackage

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).

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 below).

For example, after sam package:

yaml
Mappings:
  SAMCodeUriServices:
    Users:
      CodeUri: s3://my-bucket/abc123
    Orders:
      CodeUri: s3://my-bucket/def456
    Products:
      CodeUri: s3://my-bucket/ghi789

Resources:
  Fn::ForEach::Services:
    - Name
    - !Ref ServiceNames
    - ${Name}Function:
        Type: AWS::Serverless::Function
        Properties:
          Handler: index.handler
          Runtime: python3.12
          CodeUri: !FindInMap [SAMCodeUriServices, !Ref Name, CodeUri]

Multiple resources per ForEach body

A single Fn::ForEach body can emit more than one resource per iteration. Each resource is generated for every collection value:

yaml
Resources:
  Fn::ForEach::Tables:
    - TableName
    - [Users, Orders, Products]
    - ${TableName}Table:
        Type: AWS::DynamoDB::Table
        Properties:
          TableName: !Sub "${AWS::StackName}-${TableName}"
          # ...

      ${TableName}StreamProcessor:
        Type: AWS::Serverless::Function
        Properties:
          CodeUri: stream-processors/${TableName}/
          Events:
            DDBStream:
              Type: DynamoDB
              Properties:
                Stream: !GetAtt
                  - !Sub "${TableName}Table"
                  - StreamArn

Mapping name collision resolution

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:

Resource templatePropertyMapping name
${Svc}ApiDefinitionUriSAMDefinitionUriServicesApi
${Svc}StateMachineDefinitionUriSAMDefinitionUriServicesStateMachine

When there is no collision the base name (e.g., SAMDefinitionUriServices) is used.

Parameter-based collections

When the Fn::ForEach collection is a parameter reference (!Ref ServiceNames), the collection values are resolved at package time from:

  1. --parameter-overrides passed to sam build or sam package
  2. The parameter's Default value in the template

Important: If you change the parameter value at deploy time (e.g., adding a new service), you must re-package first so the Mappings include entries for the new values.

bash
# Package with the values you intend to deploy with
sam package --parameter-overrides ServiceNames="Users,Orders,Products"

# Deploy with the same values
sam deploy --parameter-overrides ServiceNames="Users,Orders,Products"

Nested stacks

Fn::ForEach in nested stack templates (AWS::CloudFormation::Stack) is supported. SAM CLI passes the parent stack's Parameters property to the child template expansion, so child Fn::ForEach collections that reference parent-supplied parameters resolve correctly.

yaml
# parent.yaml
Resources:
  ChildStack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./child.yaml
      Parameters:
        ServiceNames: "Users,Orders,Products"

Nested Fn::ForEach

Up to 5 levels of nesting are supported, matching CloudFormation's limit:

yaml
Resources:
  Fn::ForEach::Envs:
    - Env
    - [Dev, Staging, Prod]
    - Fn::ForEach::Services:
        - Svc
        - [Users, Orders]
        - ${Env}${Svc}Function:
            Type: AWS::Serverless::Function
            Properties:
              CodeUri: ./services/${Svc}
              Environment:
                Variables:
                  STAGE: !Ref Env

ForEach in Outputs

Fn::ForEach blocks are also expanded inside the Outputs section, so you can emit one output per collection value:

yaml
Outputs:
  Fn::ForEach::FunctionArns:
    - Name
    - [alpha, beta]
    - ${Name}FunctionArn:
        Value: !GetAtt
          - !Sub "${Name}Function"
          - Arn

Conditions and DependsOn

Resources emitted by Fn::ForEach can carry Condition and DependsOn like any other resource. The condition or dependency is replicated onto each generated resource:

yaml
Conditions:
  IsProd: !Equals [!Ref Environment, prod]

Resources:
  SharedTable:
    Type: AWS::DynamoDB::Table
    # ...

  Fn::ForEach::Functions:
    - Name
    - [api, worker]
    - ${Name}Function:
        Type: AWS::Serverless::Function
        Condition: IsProd
        DependsOn: SharedTable
        Properties:
          Handler: main.handler
          CodeUri: functions/${Name}/

&{identifier} syntax

The &{identifier} syntax strips non-alphanumeric characters from the substituted value, useful for generating valid logical IDs from values like IP addresses:

yaml
Fn::ForEach::Hosts:
  - IP
  - ["10.0.0.1", "10.0.0.2"]
  - Host&{IP}:
      Type: AWS::EC2::Instance
      # Expands to Host10001, Host10002

Supported intrinsic functions

The following intrinsic functions are resolved locally during expansion:

FunctionDescription
Fn::ForEachLoop expansion
Fn::LengthReturns count of list elements
Fn::ToJsonStringConverts value to JSON string
Fn::FindInMapMap lookup (with optional DefaultValue)
Fn::IfConditional value selection
Fn::SubString substitution
Fn::JoinString concatenation
Fn::SplitString splitting
Fn::SelectList element selection
Fn::Base64Base64 encoding
Fn::Equals / Fn::And / Fn::Or / Fn::NotCondition evaluation
RefParameter and pseudo-parameter references

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

Validation errors

The following template issues are caught locally before the SAM transform runs:

CauseError message
The Fn::ForEach value is malformed — not a list, doesn't have exactly 3 elements, or has a non-string loop identifier.Fn::ForEach::<key> layout is incorrect (raised as InvalidTemplateException; see samcli/lib/cfn_language_extensions/processors/foreach.py).
More than 5 levels of Fn::ForEach are nested.Fn::ForEach nesting depth of <N> exceeds the maximum allowed depth of 5. CloudFormation supports up to 5 nested Fn::ForEach loops.
The collection resolves to an empty list (e.g., a CommaDelimitedList parameter with Default: "").No error — the loop is silently skipped and no resources are emitted.
The !Ref in the collection points at a parameter that is not declared in the template.No error in the typical sam build / sam package flow. SAM CLI runs intrinsic resolution in PARTIAL mode and preserves the unresolved {"Ref": "<name>"}. CloudFormation will reject the unresolved ref at deploy time.

Limitations

  • 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.
  • 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.
  • DeletionPolicy and UpdateReplacePolicy are validated and resolved during expansion. They support Ref to parameters but not other intrinsic functions.
  • Nesting limit. Up to 5 levels of Fn::ForEach may be nested, matching CloudFormation's server-side limit.
  • 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:
    • SAMCodeUri, SAMImageUri, SAMContentUri, SAMDefinitionUri, SAMSchemaUri, SAMBodyS3Location, SAMDefinitionS3Location, SAMTemplateURL, SAMCode, SAMContent — emitted by sam package for dynamic artifact properties (see the table above).
    • 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.

Telemetry

SAM CLI tracks usage of AWS::LanguageExtensions via the CFNLanguageExtensions telemetry feature flag. No template content is transmitted.