AWS Builder Center
SAM CLI + Language Extensions: o fim do copy-paste em templates serverless

SAM CLI + Language Extensions: o fim do copy-paste em templates serverless

O SAM CLI agora processa AWS::LanguageExtensions em memória nas operações locais. Veja como migrar templates repetitivos para Fn::ForEach com exemplos práticos.

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::ForEach para 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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:
1
2
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

1
2
3
cd modo-antigo/

sam validate --template template-old.yaml
1
sam build --template template-old.yaml
1
2
3
sam local invoke DevFunction --no-event
sam local invoke HmlFunction --no-event
sam local invoke ProdFunction --no-event

4.4 Testando o modo novo

1
2
3
4
cd modo-novo/

# flag --language-extensions obrigatoria em todos os comandos locais
sam validate --template template.yaml --language-extensions
1
sam build --template template.yaml --language-extensions
1
2
3
4
# 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

1
2
3
4
Parameters:
Environments:
Type: CommaDelimitedList
Default: "Hml,Dev,Prod,Stg" # adicionar Staging aqui

4.5.2 Adicionar entrada no Mappings

1
2
3
4
5
6
Mappings:
EnvConfig:
# ... entradas existentes ...
Staging:
Tier: standard
CodeUri: ./src/stg/

4.5.3 Criar nova pasta e mover código

1
2
3
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:
  1. Fn::Length retorna o número de itens de uma lista. Útil para definir ReservedConcurrentExecutions ou ProvisionedConcurrencyConfig baseado no tamanho dinâmico de uma lista de parâmetros.
  2. Fn::ToJsonString serializa 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.
  3. Fn::FindInMap com DefaultValue permite definir um valor de fallback quando a chave não existe no mapeamento, sem precisar de um bloco Conditions inteiro 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.
  4. DeletionPolicy e UpdateReplacePolicy condicionais permitem usar !If nessas 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.
1
2
3
4
5
6
7
8
9
# 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.
1
2
3
4
5
6
7
8
9
# 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.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article