DEV Community

Cover image for SAM CLI + Language Extensions: o fim do copy-paste em templates serverless
Diego Broetto
Diego Broetto

Posted on

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

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.

Erro do SAM CLI ao tentar processar um template com Fn::ForEach, exibindo traceback do Python e a mensagem enganosa de validação

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:

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

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

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

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
    }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Terminal exibindo a saída do comando sam --version com a versão atualizada do SAM CLI

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.

Estrutura de pastas do projeto com os diretórios modo-antigo e modo-novo, contendo templates e código fonte por ambiente

4.3 Testando o modo antigo

Faça a validação do template:

cd modo-antigo/

sam validate --template template-old.yaml
Enter fullscreen mode Exit fullscreen mode

Terminal exibindo a saída do sam validate confirmando que o template-old.yaml é um template SAM válido
Após terminar, execute o build:

sam build --template template-old.yaml
Enter fullscreen mode Exit fullscreen mode

Terminal exibindo a saída do sam build com o build das três funções concluído com sucesso
Agora vamos testar:

sam local invoke DevFunction --no-event
sam local invoke HmlFunction --no-event
sam local invoke ProdFunction --no-event
Enter fullscreen mode Exit fullscreen mode

Terminal exibindo a saída do sam local invoke da DevFunction com a resposta JSON retornando ambiente dev e tier free

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
Enter fullscreen mode Exit fullscreen mode

Terminal exibindo a saída do sam validate confirmando que o template.yaml é um template SAM válido
Novamente, vamos fazer o build:

sam build --template template.yaml --language-extensions
Enter fullscreen mode Exit fullscreen mode

Terminal exibindo a saída do sam build com o build das três funções concluído com sucesso
E por fim, testar novamente:

# 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
Enter fullscreen mode Exit fullscreen mode

Terminal exibindo a saída do sam local invoke da ProdFunction com a resposta JSON retornando ambiente

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
Enter fullscreen mode Exit fullscreen mode

4.5.2 Adicionar entrada no Mappings

Mappings:
  EnvConfig:
    # ... entradas existentes ...
    Staging:
      Tier: standard
      CodeUri: ./src/stg/
Enter fullscreen mode Exit fullscreen mode

4.5.3 Criar nova pasta e mover código

mkdir src/stg

cp src/hml/handler.py src/stg/handler.py
Enter fullscreen mode Exit fullscreen mode

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.

# Correto
Transform:
  - AWS::LanguageExtensions
  - AWS::Serverless-2016-10-31

# Errado
Transform:
  - AWS::Serverless-2016-10-31
  - AWS::LanguageExtensions
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Top comments (4)

Collapse
 
thiagosagara profile image
Thiago Sagara

Sensacional Broetto.

Otima construção e um assunto totalmente relevante

Collapse
 
carlosfilho profile image
Carlos Filho

Que post didático, man! Sensacional.

Collapse
 
rrdev_07 profile image
rafael

Top demais!

Collapse
 
woodsperks profile image
Kleber

Melhor documentação que ja vi.