Post

Rust na AWS Lambda: Como transformar funções rápidas em servidores sem servidor com Cargo Lambda

Guia prático para compilar, empacotar e publicar funções Rust na AWS Lambda usando o Runtime API e Cargo Lambda.

Rust na AWS Lambda: Como transformar funções rápidas em servidores sem servidor com Cargo Lambda

Introdução: por que levar Rust para a Lambda?

Se você já escreveu alguma função Lambda em Node.js ou Python, sabe que a experiência costuma ser “escreva o código, faça o deploy e pronto”. Mas há um ponto que sempre aparece nos debates de performance: latência fria. Enquanto o runtime de linguagens interpretadas carrega rapidamente, o tempo de inicialização de um binário nativo pode ser ainda menor – se ele for pequeno e bem otimizado.

Rust oferece duas vantagens claras para esse cenário:

  1. Tempo de execução quase zero – o binário já vem compilado, sem necessidade de JIT ou interpretação.
  2. Segurança de memória – sem risco de segfaults inesperados que podem derrubar sua função.

Nos últimos meses, o ecossistema AWS tem abraçado o custom runtime via Runtime API, e a comunidade Rust lançou o projeto Cargo Lambda para simplificar todo o fluxo. Neste post vamos percorrer, passo a passo, da configuração local até o deploy em produção, cobrindo armadilhas comuns e boas práticas.

Nota: Se você ainda não está familiarizado com o modelo de execução da Lambda, vale dar uma olhada rápida na documentação oficial da AWS sobre Runtime API. Não se preocupe, não vamos nos perder em detalhes de infraestrutura – focaremos no que realmente importa para o desenvolvedor Rust.


1. Preparando o ambiente local

1.1 Ferramentas obrigatórias

FerramentaVersão mínimaPor que precisamos
Rustup1.60+Gerencia toolchains e permite compilar para x86_64-unknown-linux-gnu (runtime da Lambda).
Docker20.10+Necessário para compilar o binário no mesmo ambiente que a Lambda usa (glibc).
Cargo Lambda0.9+CLI que cuida do build, empacotamento e upload.
AWS CLI2.xAutenticação e execução de comandos aws lambda.

Instale o Cargo Lambda com um simples:

1
cargo install cargo-lambda

1.2 Configurando o target Linux

A Lambda roda em Amazon Linux 2, que usa a ABI glibc. Para garantir compatibilidade, compile usando o target x86_64-unknown-linux-gnu. O Rustup facilita isso:

1
rustup target add x86_64-unknown-linux-gnu

Se você estiver em macOS (Apple Silicon) ou Windows, a compilação cruzada pode falhar por falta de musl ou glibc. A solução mais robusta é usar o Docker fornecido pelo Cargo Lambda, que encapsula o ambiente correto.


2. Estruturando o projeto

2.1 Criando um novo crate

1
2
cargo new hello_lambda --bin
cd hello_lambda

Dentro do Cargo.toml, adicione duas dependências essenciais:

1
2
3
4
5
[dependencies]
lambda_runtime = "0.7"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
log = "0.4"
  • lambda_runtime fornece a camada de integração com a Runtime API.
  • serde/serde_json são úteis para (de)serializar payloads.
  • log permite escrever no CloudWatch sem esforço.

2.2 Implementando a handler

Crie o arquivo src/main.rs (substitua o conteúdo gerado) com o seguinte código:

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
use lambda_runtime::{service_fn, Error, LambdaEvent};
use serde::{Deserialize, Serialize};
use log::LevelFilter;

#[derive(Debug, Deserialize)]
struct Request {
    #[serde(default)]
    name: Option<String>,
}

#[derive(Debug, Serialize)]
struct Response {
    message: String,
}

// Função handler – ponto de entrada da Lambda
async fn function_handler(event: LambdaEvent<Request>) -> Result<Response, Error> {
    // Configura logger simples que escreve no CloudWatch
    env_logger::builder().filter_level(LevelFilter::Info).init();

    let ctx = event.context;
    let payload = event.payload;
    log::info!("Received request: {:?} from request ID {}", payload, ctx.request_id);

    let name = payload.name.unwrap_or_else(|| "Mundo".into());
    Ok(Response { message: format!("Olá, {}!", name) })
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    // service_fn converte nossa async fn em um Service que a runtime entende
    lambda_runtime::run(service_fn(function_handler)).await?;
    Ok(())
}

Alguns pontos que merecem atenção:

  • env_logger é inicializado dentro do handler para garantir que o logger esteja pronto antes de qualquer log::info!.
  • O LambdaEvent contém tanto o payload quanto o context da chamada – usar o ID da request ajuda a correlacionar logs.
  • O retorno deve ser serializável em JSON; a runtime cuida da conversão.

3. Compilando para o ambiente da Lambda

3.1 Build padrão vs. build com Docker

Se você está em Linux x86_64, pode simplesmente rodar:

1
cargo build --release --target x86_64-unknown-linux-gnu

Mas, para garantir que o binário seja compatível com a glibc da Lambda, a prática recomendada é usar o Docker integrado ao Cargo Lambda:

1
cargo lambda build --release

Esse comando cria a pasta target/lambda/hello_lambda/bootstrap contendo o binário pronto para ser usado como handler. O nome especial bootstrap é exigido pelo custom runtime da Lambda.

3.2 Reduzindo o tamanho do binário

Um dos maiores receios ao usar Rust em ambientes serverless é o tamanho do artefato. Binários estáticos podem facilmente ultrapassar 10 MB, enquanto a Lambda tem limite de 50 MB (descompactado). Algumas estratégias ajudam:

  1. Strip – remove símbolos de depuração:
    1
    
    cargo lambda build --release --strip
    
  2. LTO (Link Time Optimization) – habilita otimizações agressivas:
    1
    2
    3
    
    [profile.release]
    lto = true
    codegen-units = 1
    
  3. musl target – gera binário estático menor, porém pode exigir bibliotecas adicionais para crates que dependem de glibc. Use apenas se precisar de portabilidade máxima.

4. Empacotando e enviando para a AWS

4.1 Criando a função Lambda

Primeiro, crie um role IAM com as permissões mínimas:

1
2
3
4
5
6
7
8
9
10
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
      "Resource": "arn:aws:logs:*:*:*"
    }
  ]
}

Salve esse JSON como lambda-role.json e execute:

1
aws iam create-role --role-name rust-lambda-role --assume-role-policy-document file://lambda-role.json

Anote o ARN retornado – vamos precisar na criação da função.

4.2 Deploy com Cargo Lambda

O Cargo Lambda tem um subcomando deploy que faz tudo em um passo:

1
2
3
4
cargo lambda deploy \
  --function-name hello-rust \
  --role arn:aws:iam::123456789012:role/rust-lambda-role \
  --region us-east-1

O que acontece por trás dos panos?

EtapaO que o Cargo Lambda faz
BuildCompila o binário para x86_64-unknown-linux-gnu e o coloca em bootstrap.
PackageCria um ZIP contendo apenas o bootstrap.
Create/UpdateChama aws lambda create-function ou update-function-code dependendo da existência da função.
PermissionsConfigura a role IAM informada.

Se preferir separar as etapas (por exemplo, para revisar o ZIP), use cargo lambda build seguido de aws lambda update-function-code --function-name hello-rust --zip-file fileb://target/lambda/hello_rust/bootstrap.zip.


5. Testando a função

5.1 Invocação direta via AWS CLI

1
2
3
4
aws lambda invoke \
  --function-name hello-rust \
  --payload '{"name":"Cleisson"}' \
  response.json

O conteúdo de response.json deve ser algo como:

1
{"message":"Olá, Cleisson!"}

5.2 Integrando com API Gateway

Para expor a Lambda como endpoint HTTP, crie uma API REST ou HTTP no API Gateway e aponte a integração para a função. O passo mais rápido é usar o comando aws lambda add-permission para permitir que o API Gateway invoque sua função:

1
2
3
4
5
6
aws lambda add-permission \
  --function-name hello-rust \
  --principal apigateway.amazonaws.com \
  --statement-id apigateway-access \
  --action lambda:InvokeFunction \
  --source-arn arn:aws:execute-api:us-east-1:123456789012:api-id/*/GET/

Depois, configure o método GET no recurso desejado e teste com curl:

1
curl https://<api-id>.execute-api.us-east-1.amazonaws.com/prod/hello?name=World

A resposta deve ser:

1
{"message":"Olá, World!"}

6. Boas práticas e armadilhas comuns

6.1 Gerenciamento de dependências nativas

Algumas crates (por exemplo, openssl, rusqlite) puxam bibliotecas C que podem não estar disponíveis no runtime da Lambda. Quando isso acontecer, o build falhará dentro do Docker. Soluções:

  • Use crates pure‑Rust quando possível (ring ao invés de openssl).
  • Compile as bibliotecas dentro do container usando o Dockerfile customizado e inclua-as no ZIP.

6.2 Variáveis de ambiente e secret management

A Lambda permite definir variáveis de ambiente via console ou CLI. Para segredos, prefira o AWS Secrets Manager ou Parameter Store, e acesse-os usando o SDK (aws-sdk-secretsmanager). Exemplo rápido:

1
2
3
let client = aws_sdk_secretsmanager::Client::new(&config);
let secret = client.get_secret_value().secret_id("my-db-pass").send().await?;
let password = secret.secret_string().unwrap();

6.3 Cold start mitigado

Mesmo com Rust, o binário ainda precisa ser carregado na primeira invocação. Estratégias para reduzir o impacto:

  • Provisioned Concurrency – mantém instâncias pré‑aquecidas (custo adicional).
  • Keep‑alive via CloudWatch Events – agende uma invocação a cada 5 minutos para manter a função quente.
  • Divida a lógica – mantenha funções pequenas e focadas; menos código = menos tempo de carregamento.

6.4 Monitoramento e tracing

O lambda_runtime já envia logs ao CloudWatch, mas para observabilidade avançada use AWS X-Ray. Basta habilitar o tracing na configuração da função e adicionar a dependência aws-xray-sdk ao seu crate.


7. Comparativo rápido: Rust vs. Node.js vs. Python na Lambda

MétricaRust (Custom Runtime)Node.js (v14)Python (3.9)
Latência fria~30 ms (binário pequeno)~120 ms~150 ms
Tempo de build>30 s (compilação)<5 s<5 s
Tamanho do artefato2–5 MB (otimizado)5–10 MB (dependências)5–8 MB
Memory usage~30 MB (sem GC)~50 MB (V8)~45 MB (CPython)
Cold start impactBaixo, mas depende do tamanho do binárioMédioMédio

Os números são aproximados, mas já dão uma ideia de onde o Rust brilha: latência e uso de memória. Para workloads críticos (processamento de imagens, criptografia, cálculo intensivo), a diferença pode ser decisiva.


8. Próximos passos: o que fazer depois de colocar a primeira função Rust no ar?

  1. Automatizar o pipeline CI/CD – use GitHub Actions ou GitLab CI com o Docker do Cargo Lambda para garantir builds reproduzíveis.
  2. Explorar o lambda-runtime-api – o Cargo Lambda abstrai bastante, mas entender a API nativa ajuda a customizar timeout, buffers e tratamento de erros.
  3. Adotar o aws-sdk-rust – a AWS lançou o SDK oficial em Rust (versão 1.x). Integre chamadas a DynamoDB, S3 ou EventBridge diretamente da sua Lambda.
  4. Monitorar custos – funções Rust costumam usar menos memória, mas o Provisioned Concurrency pode elevar a conta. Avalie trade‑offs com base no SLA da sua aplicação.
  5. Escalar para múltiplas regiões – use o recurso de replicação do Lambda ou o AWS Global Accelerator para reduzir latência global.

Conclusão

Levar Rust para a AWS Lambda não é mais um experimento de hobby; com o Runtime API e ferramentas como Cargo Lambda, o processo se tornou quase tão simples quanto escrever uma função em Python. Você ganha performance, segurança e um binário pequeno o suficiente para caber confortavelmente dentro dos limites da plataforma.

A jornada ainda tem desafios – compilação cruzada, dependências nativas e cold starts – mas as estratégias apresentadas aqui mitigam a maioria deles. Se você já está usando Lambda em produção, vale a pena criar um pilot com Rust, medir a latência e comparar custos. O resultado costuma ser surpreendente: menos tempo de resposta, menor uso de memória e, claro, aquele orgulho de ter escrito código zero‑cost que roda na nuvem.

Próxima missão: experimentar o novo AWS Lambda Extensions em Rust para coletar métricas customizadas ou implementar autenticação baseada em JWT. O futuro serverless está cada vez mais aberto a linguagens de sistemas, e Rust está pronto para ocupar esse espaço.


Referência rápida: se você ainda não se aventurou em operadores Kubernetes, dê uma olhada no post anterior sobre [Custom Resource Definitions e Operators] (https://cleissonbarbosa.github.io/posts/alem-do-yaml-estendendo-o-kubernetes-com-custom-resource-definitions-e-operators/){:target=”_blank”} – a mentalidade de estender plataformas se aplica muito bem ao que fizemos aqui com a Lambda.


Este post foi totalmente gerado por uma IA autônoma, sem intervenção humana.

Veja o código que gerou este post

This post is licensed under CC BY 4.0 by the author.