1.0 Introdução
Se você já trabalhou com AWS SAM em projetos com múltiplos ambientes, provavelmente já viveu essa cena: precisa adicionar uma nova função Lambda ao projeto, abre o template.yaml, encontra o bloco da função existente, seleciona tudo, cola abaixo, começa a trocar os valores um por um. Nome da função, variável de ambiente, tag, CodeUri. Torcer pra não esquecer nada.
Um tempo depois, alguém aumenta o timeout de uma das funções e esquece de replicar nas outras duas. É um caso bem comum e que já vi se repetir em vários clientes do meu dia a dia.
Esse problema se chama copy-paste em IaC. E no SAM, ele era difícil de resolver.
Quem usa CloudFormation no dia a dia sabe que desde 2023 o AWS::LanguageExtension com Fn::ForEach permite gerar múltiplos recursos a partir de uma única definição. O problema é que o SAM CLI simplesmente não entendia esse bloco. Você tentava rodar um sam local invoke com um template que usava Fn::ForEach e levava um erro. O resultado era uma escolha ingrata: templates DRY ou desenvolvimento local com o SAM. Não dava pra ter os dois.
Em maio de 2026, a AWS resolveu isso. O SAM CLI agora processa Language Extensions em memória durante as operações locais e o template original segue intacto para o deploy no CloudFormation.
Nesse artigo vou mostrar como migrar esses templates repetitivos para uma estrutura gerada com Fn::ForEach, com exemplos que você pode testar agora mesmo no seu ambiente.
2.0 Contexto
Antes de entrar no código, vale entender por que esse problema durou tanto tempo.
O AWS::LanguageExtensions é um transform do CloudFormation lançado em 2023. Ele desbloqueia um conjunto de funções que não estão disponíveis por padrão nos templates, a principal delas sendo o Fn::ForEach: você itera sobre uma lista e gera múltiplos recursos a partir de uma única definição. Parecido com um for em qualquer linguagem de programação.
O problema é que o SAM CLI e o CloudFormation são camadas diferentes. Quando você roda sam local invoke ou sam build, o SAM CLI processa o template localmente, sem passar pelo CloudFormation. E até maio de 2026, esse processamento local não sabia o que fazer com o bloco Fn::ForEach.
O erro é totalmente enganoso, parece problema de indentação no YAML, mas na verdade é o SAM CLI não reconhecendo a sintaxe do Language Extensions. Quem já bateu nesse erro e ficou olhando pro template por meia hora sem achar nada errado sabe o quanto isso frustra.
Isso criava um dilema real no dia a dia. Você tinha três caminhos, e nenhum era bom:
Usar Language Extensions e abrir mão do SAM CLI local: deploy direto no CloudFormation para testar. Ciclo lento, custo de iteração alto, debug difícil.
Usar SAM CLI local e abrir mão do DRY: manter os blocos repetidos no template, aceitar o risco de inconsistência entre ambientes e torcer pra ninguém esquecer de replicar uma mudança.
Manter dois templates: um enxuto com
Fn::ForEachpara o deploy, outro expandido manualmente para uso local. O pior dos mundos, duplicação do template inteiro, sincronização manual entre os dois, confusão garantida na revisão de código.
Eu ficava sempre na opção 2. O template ficava feio, mas pelo menos o sam local invoke funcionava.
O que a AWS fez em maio de 2026 foi processar as Language Extensions em memória, expandindo os loops antes de qualquer operação local. O template original no disco nunca é tocado, o CloudFormation continua recebendo o template com Fn::ForEach intacto e faz a própria expansão no deploy.
O SAM CLI cuida do desenvolvimento local, o CloudFormation cuida do deploy. Sem interferência entre os dois.
3.0 Testando na prática: antes e depois
Vamos usar um cenário com três funções Lambda, uma por ambiente (DEV, HML e PROD). Cada função vai ter o mesmo código base, mas com variáveis de ambiente e configurações diferentes.
3.1 O modo antigo: copy-paste inevitável
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Runtime: python3.13
MemorySize: 256
Timeout: 10
Resources:
DevFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: hello-world-dev
CodeUri: ./src/dev/
Handler: handler.lambda_handler
Description: "Ambiente DEV"
Environment:
Variables:
ENV: dev
TIER: free
Tags:
Environment: dev
Project: hello-word-old
HmlFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: demo-old-hml
CodeUri: ./src/hml/
Handler: handler.lambda_handler
Description: "Ambiente hml"
Environment:
Variables:
ENV: hml
TIER: standard
Tags:
Environment: hml
Project: hello-word-old
ProdFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: demo-old-prod
CodeUri: ./src/prod/
Handler: handler.lambda_handler
Description: "Ambiente Prod"
Environment:
Variables:
ENV: prod
TIER: premium
Tags:
Environment: prod
Project: hello-word-old
Outputs:
AlphaFunctionArn:
Value: !GetAtt DevFunction.Arn
hmlFunctionArn:
Value: !GetAtt HmlFunction.Arn
ProdFunctionArn:
Value: !GetAtt ProdFunction.Arn
São três blocos quase idênticos. Se você precisar mudar o runtime de python3.13 para python3.14, precisa alterar em três lugares, ou no Globals, mas só funciona para propriedades suportadas pelo bloco global. Qualquer propriedade que não vai para Globals vira copy-paste obrigatório.
E se precisar adicionar um quarto ambiente (Staging)? Copiar um dos blocos, renomear, ajustar variáveis, criar o src/stg/, adicionar o Output manualmente. São no mínimo seis pontos de toque para uma mudança que deveria ser trivial.
3.2 O modo novo: uma definição, N funções
AWSTemplateFormatVersion: "2010-09-09"
Transform:
- AWS::LanguageExtensions
- AWS::Serverless-2016-10-31
Parameters:
Environments:
Type: CommaDelimitedList
Default: "Dev,Hml,Prod"
Globals:
Function:
Runtime: python3.13
MemorySize: 256
Timeout: 10
Mappings:
EnvConfig:
dev:
Tier: free
CodeUri: ./src/dev/
hml:
Tier: standard
CodeUri: ./src/hml/
prod:
Tier: premium
CodeUri: ./src/prod/
Resources:
Fn::ForEach::CreateFunctions:
- EnvName
- !Ref Environments
- ${EnvName}Function:
Type: AWS::Serverless::Function
Properties:
FunctionName:
Fn::Sub: "hello-world-${EnvName}"
CodeUri:
Fn::FindInMap:
- EnvConfig
- Ref: EnvName
- CodeUri
Handler: handler.lambda_handler
Description:
Fn::Sub: "Funcao do ambiente ${EnvName}"
Environment:
Variables:
ENV:
Ref: EnvName
TIER:
Fn::FindInMap:
- EnvConfig
- Ref: EnvName
- Tier
Tags:
Environment:
Ref: EnvName
Project: hello-word
Outputs:
Fn::ForEach::FunctionOutputs:
- EnvName
- !Ref Environments
- ${EnvName}FunctionArn:
Description:
Fn::Sub: "ARN da funcao ${EnvName}"
Value:
Fn::GetAtt:
- ${EnvName}Function
- Arn
O Fn::ForEach::CreateFunctions itera sobre [hml, dev, prod] e gera automaticamente DevFunction, HmlFunction e ProdFunction, cada uma com as configurações corretas vindas do Mappings. Os Outputs também são gerados pelo segundo ForEach, sem nenhum bloco manual.
O handler Python é o mesmo para todos os ambientes, todas as diferenças chegam via variáveis de ambiente:
import os
import json
def lambda_handler(event, context):
env = os.environ.get("ENV", "unknown")
tier = os.environ.get("TIER", "standard")
return {
"statusCode": 200,
"body": json.dumps({
"message": f"Oi, eu sou o ambiente de {env}!",
"tier": tier,
"env": env
}, indent=2)
}
4.0 Testando Localmente
4.1 Pré-requisitos
Faça a atualização do seu AWS SAM:
pip install aws-sam-cli --upgrade
sam --version # 1.161.1 ou superior
4.2 Estrutura de pastas
Vou disponibilizar no final do artigo um link para o repositório do github onde você pode fazer um clone e realizar os testes.
4.3 Testando o modo antigo
Faça a validação do template:
cd modo-antigo/
sam validate --template template-old.yaml

Após terminar, execute o build:
sam build --template template-old.yaml
sam local invoke DevFunction --no-event
sam local invoke HmlFunction --no-event
sam local invoke ProdFunction --no-event
4.4 Testando o modo novo
Acesse o repositório e valide o template:
cd modo-novo/
# flag --language-extensions obrigatoria em todos os comandos locais
sam validate --template template.yaml --language-extensions

Novamente, vamos fazer o build:
sam build --template template.yaml --language-extensions
# invocar pelo nome expandido pelo ForEach
sam local invoke DevFunction --language-extensions --no-event
sam local invoke HmlFunction --language-extensions --no-event
sam local invoke ProdFunction --language-extensions --no-event
4.5 E se eu precisar adicionar um novo ambiente?
Esse é o momento que deixa o benefício mais concreto. Para adicionar um ambiente Staging no modo novo, são apenas três mudanças:
4.5.1 Atualizar o parâmetro
Parameters:
Environments:
Type: CommaDelimitedList
Default: "Hml,Dev,Prod,Stg" # adicionar Staging aqui
4.5.2 Adicionar entrada no Mappings
Mappings:
EnvConfig:
# ... entradas existentes ...
Staging:
Tier: standard
CodeUri: ./src/stg/
4.5.3 Criar nova pasta e mover código
mkdir src/stg
cp src/hml/handler.py src/stg/handler.py
4.5.4 Conclusão
O Output StgFunctionArn é gerado automaticamente pelo segundo ForEach. Nenhuma outra alteração necessária.
No modo antigo, a mesma operação exigiria: copiar um bloco inteiro de ~15 linhas, renomear em todos os lugares, ajustar as variáveis, criar o diretório e o handler, e adicionar o Output manualmente. São pelo menos seis pontos de toque, cada um com risco de erro.
5.0 Outras funções que vieram junto
Além do Fn::ForEach, o SAM CLI passou a suportar mais funções do Language Extensions:
Fn::Lengthretorna o número de itens de uma lista. Útil para definirReservedConcurrentExecutionsouProvisionedConcurrencyConfigbaseado no tamanho dinâmico de uma lista de parâmetros.Fn::ToJsonStringserializa qualquer valor para string JSON. Prático para passar configurações complexas como variável de ambiente em formato JSON sem ter que escrever o JSON manualmente no template.Fn::FindInMapcomDefaultValuepermite definir um valor de fallback quando a chave não existe no mapeamento, sem precisar de um blocoConditionsinteiro para cobrir o caso base. Muito útil para templates multi-região onde algumas regiões têm configurações específicas e as demais usam o padrão.DeletionPolicyeUpdateReplacePolicycondicionais permitem usar!Ifnessas políticas, algo que antes não era suportado. Por exemplo, deletar automaticamente em ambientes de dev e reter em produção, tudo no mesmo template.
6.0 Pontos de atenção
Algumas coisas que aprendi testando na prática e que o anúncio oficial não deixa claro:
1. A flag --language-extensions é obrigatória em todos os comandos locais. O suporte é opt-in: sem a flag, o SAM CLI ignora o Language Extensions e retorna aquele erro confuso de "All Resources must be Objects", que parece indentação mas não é. Todos os comandos precisam da flag: sam validate, sam build, sam local invoke, sam local start-api e sam sync.
2. A ordem dos transforms importa. AWS::LanguageExtensions deve vir obrigatoriamente antes de AWS::Serverless-2016-10-31 na lista. Invertido não funciona.
# Correto
Transform:
- AWS::LanguageExtensions
- AWS::Serverless-2016-10-31
# Errado
Transform:
- AWS::Serverless-2016-10-31
- AWS::LanguageExtensions
3. Dentro do Fn::ForEach, use sempre a forma longa das funções. A short-form YAML (!Sub, !FindInMap, !Ref, !GetAtt) não funciona dentro dos blocos do loop. Use a forma explícita: Fn::Sub, Fn::FindInMap, Ref, Fn::GetAtt.
# Errado: short-form dentro do ForEach
CodeUri: !FindInMap [EnvConfig, !Ref EnvName, CodeUri]
# Correto: forma longa
CodeUri:
Fn::FindInMap:
- EnvConfig
- Ref: EnvName
- CodeUri
4. Sem comentários inline dentro do bloco Fn::ForEach. O parser YAML perde o fio do template com comentários na mesma linha dos valores do loop. Mantenha os comentários fora do bloco ou remova-os.
5. Não compatível com Lambda Managed Instances. Funções geradas pelo Fn::ForEach não podem usar capacity provider. Se o seu projeto usa Lambda Managed Instances, esse recurso ainda não se aplica.
7.0 Conclusão
Esse lançamento fecha um gap que existia há quase três anos: o CloudFormation suportava Fn::ForEach desde julho de 2023, mas o SAM CLI ficou para trás, e as equipes tinham que escolher entre templates DRY e desenvolvimento local rápido.
A solução foi processar o Language Extensions em memória durante operações locais sem tocar no template original. O CloudFormation faz sua própria expansão no deploy. Cada camada faz o seu trabalho.
O único detalhe que o anúncio oficial não deixa explícito é que o suporte é opt-in via flag --language-extensions. Sem ela, você vai continuar levando aquele erro de indentação que não tem nada a ver com indentação. Agora você sabe.
Se quiser testar, os templates de exemplo desse artigo estão disponíveis no repositório linkado abaixo. Qualquer dúvida ou feedback, comenta aqui.
🔗 Repositório com os exemplos: github.com/diegobroetto/sam-language-extensions-demo



















