Lab 21 — Primeira Lambda .NET 8, e o cold start
O problema, e a empresa que o tem
A Cadência é a mesma equipe de duas pessoas dos laboratórios L01 e L03: API de pedidos em .NET 8, ECS Fargate, banco em sub-rede privada. O que mudou foi um pedido pequeno do time financeiro: o bucket onde a API exporta CSV de pedidos para download estava acumulando arquivo antigo, e alguém precisava apagar o que passou de duas horas.
A solução saiu em uma tarde: um BackgroundService dentro do mesmo processo da API, com um temporizador que acorda a cada hora, lista o prefixo, apaga o vencido. Funcionou. O problema não é que funcionou — é que a task Fargate que hospeda a API já ficava ligada 24 horas por causa do tráfego HTTP, e agora ela também é dona de um job que usa 2 segundos dessas 24 horas, 24 vezes por dia.
Ninguém decidiu pagar por isso. A decisão foi "colocar em algum lugar que já existe", e o lugar que já existia cobra por hora ligada, não pelo que roda dentro dela. Este laboratório muda ONDE o job roda — não o que ele faz — e usa a mudança para explicar como a Lambda cobra, e por que o tempo de arranque dela não é acidente de implementação.
O que este laboratório NÃO é
Não é a reescrita da API em serverless — a API continua em ECS Fargate, exatamente como no L01. Também não é o laboratório de fila ou fanout: aqui há só um job, um agendamento, uma função. Absorver pico com fila é o L22, e ele depende deste para o modelo de execução já estar entendido.
O que você vai conseguir fazer
Objetivos verificáveis: cada um se prova com um número na seção de implantação, não com a sensação de ter entendido o cold start.
- Explicar por que pagar por invocação pode vencer pagar por hora ligada — e quando não vence.
- Nomear as três tarefas do Init (extensão, runtime, código estático) e dizer qual costuma dominar num handler .NET simples.
- Explicar por que aumentar a memória também aumenta a CPU, e medir o efeito no cold start.
- Comparar o cold start do runtime gerenciado dotnet8 com o mesmo código em Native AOT, com número.
- Decidir, com critério escrito, quando Provisioned Concurrency ou SnapStart valem o custo — e quando não valem.
- Publicar uma função .NET 8 sem VPC, disparada por um agendamento gerenciado.
- Medir, com CloudWatch Logs, a proporção de chamadas que pagam cold start neste padrão de tráfego esporádico.
- Desligar o job embutido na task Fargate e provar que o agendamento sobrevive sem ele.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| Modelo de execução da Lambda | DVA-C02, SAA-C03 | cobrança por invocação e duração, sem servidor dedicado | quando o padrão de uso favorece Lambda e quando favorece computação sempre ligada |
| Fases do ciclo de vida (Init/Invoke/Shutdown) | DVA-C02 | extensão, runtime, código estático, na ordem certa | que o limite de 10 s do Init é sobre as três tarefas juntas, não sobre o handler |
| Memória e CPU proporcionais | DVA-C02, SAA-C03 | 1.769 MB como o ponto de 1 vCPU | que memória é o único dial de CPU disponível, e por que subir memória pode encurtar cold start |
| Provisioned Concurrency vs SnapStart | DVA-C02 | as duas formas de eliminar cold start, e por que são mutuamente exclusivas na mesma versão | a diferença de cobrança: hora reservada de um lado, cache e restauração do outro |
| Native AOT no .NET 8 | DVA-C02 | mesmo runtime gerenciado dotnet8, binário compilado antes do deploy | que trimming pode quebrar biblioteca com reflection, e isso só aparece testando |
| Agendamento gerenciado | SOA-C02, DVA-C02 | EventBridge Scheduler substituindo um cron dentro de um processo sempre ligado | a diferença entre agendar um alvo e manter um servidor só para contar hora |
| Execution role vs resource policy | SAA-C03, DVA-C02 | duas roles, dois papéis: quem chama e o que a função pode fazer | por que dar ao agendamento permissão sobre o bucket é erro de desenho, não atalho |
| Reaproveitamento de ambiente de execução | DVA-C02 | variável fora do handler sobrevive entre chamadas — até o ambiente ser reciclado | por que confiar nisso para estado que precisa estar sempre certo é o erro mais comum da banda |
Onde isto costuma ser cobrado errado
A pergunta clássica dá uma função chamada raramente e pergunta por que ela "sempre parece lenta". A resposta certa liga baixa frequência a ALTA chance de cold start — o oposto do que a intuição de "pouco uso, pouco problema" sugere. Confundir isso é o erro mais comum em questão de cold start na prova.
Requisitos, e como cada um muda o desenho
Requisito não funcional que não aparece numa linha de configuração é intenção. A coluna da direita é onde cada um deixou marca.
| Requisito | Valor declarado | O que ele decide no desenho |
|---|---|---|
| Custo proporcional ao trabalho | zero cobrança quando não há job rodando | exclui manter qualquer processo sempre ligado só para o job; obriga computação sob demanda |
| Duração do job | ~2 s, 24 execuções por dia | volume baixo o bastante para nunca justificar Provisioned Concurrency por padrão |
| Ninguém aguarda a resposta em tempo real | confirmado com o time financeiro | remove o argumento de UX para eliminar cold start; a decisão vira só custo vs custo |
| Escopo de acesso | só o prefixo de exportações de um bucket | duas roles com permissão mínima, nenhuma delas ampla |
| Rastreabilidade de execução | log separado do tráfego da API | log group próprio da função, com retenção definida |
| Decisão de memória | baseada em medição, não em valor popular | seção de provas varia memória e registra Init Duration antes de fixar |
| Continuidade de stack | time só conhece .NET | runtime dotnet8 gerenciado, com Native AOT como variante avaliada, não obrigatória |
| Tolerância a reexecução | apagar de novo o que já foi apagado não quebra nada | job desenhado como idempotente, o que permite retry automático do agendamento |
Arquitetura mínima: o job embutido na API
Este é o desenho que a Cadência tem hoje, e ele é honesto como ponto de partida: zero infraestrutura nova, um `Timer` a mais numa classe que já existia. O laboratório começa por medir o desperdício — porque um número torna a decisão discutível, e "está gastando à toa" sozinho não convence ninguém a mexer em algo que funciona.
- → mantém 1 task viva o tempo todo
- → lista e apaga arquivo expirado, a cada hora
- → grava linha de log a cada disparo do temporizador
- Compute
- Armazenamento
- Gestão e governança
Este desenho é o que já existe: nenhuma linha de infraestrutura nova, porque o job foi embutido no processo que já estava de pé. O desperdício não é uma suposição — é aritmética entre o tempo que a task fica no ar e o tempo que ela realmente trabalha. Percorra os passos e repare que o acoplamento com a API é o preço extra que ninguém pediu.
- A task nunca dorme, porque é o mesmo processo da API. Não existe um "desligar entre execuções" aqui: o `BackgroundService` mora dentro do mesmo processo .NET que atende requisição HTTP o dia inteiro. Parar a task para economizar derrubaria a API junto.
- O trabalho real cabe em 2 segundos, 24 vezes por dia. Listar o prefixo de exportações e apagar o que passou da validade leva cerca de 2 s por execução. Em um dia inteiro, isso soma menos de um minuto de trabalho — contra 86.400 s em que a task simplesmente existe.
- O que sobra é pago do mesmo jeito. A Fargate cobra por vCPU e memória reservados enquanto a task existe, não pelo que ela processa. Os cerca de 48 s de trabalho útil por dia dividem a mesma fatura dos outros 86.352 s em que a task só está esperando a próxima hora.
- O log mistura o job com o tráfego da API. Sem um `namespace` ou log group próprios, uma linha de erro do job de limpeza aparece misturada com centenas de linhas de requisição HTTP. Achar o log certo é buscar agulha, não olhar painel.
- Desligar a task derruba o job junto com a API. Os dois trabalhos — servir requisição e limpar arquivo — ficam presos ao mesmo ciclo de vida. Um deploy da API reinicia o temporizador do job; um problema no job (uma exceção não tratada) pode derrubar a task e tirar a API do ar com ele.
- Por que alguém embutiu assim. Porque a task já existia, já tinha permissão para o bucket e já rodava 24 horas — acrescentar um `Timer` custa uma classe nova, não um recurso novo na conta. É o caminho de menor atrito, e é exatamente o que este laboratório desfaz.
A conta é simples e vale fazer antes de qualquer código: 24 execuções de 2 s somam 48 s de trabalho por dia. A task existe pelas 86.400 s do dia inteiro. A proporção de tempo realmente ocupado com este job é inferior a 0,06% — o resto é a task esperando a próxima hora virar, cobrando o mesmo vCPU e a mesma memória reservados o tempo todo.
O risco real da migração não é o código — é esquecer de desligar o antigo
Enquanto o `BackgroundService` continuar dentro da task e a função Lambda também estiver ativa, os dois vão limpar o mesmo bucket ao mesmo tempo. Isso não corrompe nada — o job é idempotente — mas significa pagar duas vezes pelo mesmo trabalho até alguém perceber. A seção de limpeza deste módulo tem o passo exato de remoção do código antigo, e ele não é opcional.
Arquitetura para produção
Cada peça nova abaixo rastreia a uma linha da tabela de requisitos. Se você não conseguir apontar o requisito, a peça é adorno — e este desenho não tem nenhuma.
- → invoke agendado, com role própria do schedule
- → autoriza só lambda:InvokeFunction neste ARN
- → autoriza só s3:ListBucket e s3:DeleteObject no prefixo
- → lista e apaga arquivo expirado
- → grava Init Duration e Billed Duration por chamada
- Integração de apps
- Compute
- Segurança e identidade
- Armazenamento
- Gestão e governança
Não existe mais processo esperando o relógio virar. O que muda no desenho é a ausência de qualquer coisa entre uma chamada e a próxima — e a existência de DUAS identidades com trabalhos diferentes. Percorra os passos: nenhuma peça aqui está dentro de uma VPC, porque nenhuma delas precisa estar.
- O agendamento substitui o laço dentro do processo. O `EventBridge Scheduler` chama a função pela API de invocação, do lado de fora — não existe mais nenhum `Timer` rodando dentro de um processo que precisa ficar de pé só para contar as horas.
- Duas roles fazem dois trabalhos diferentes. A role do agendamento só decide QUEM PODE CHAMAR a função; a role de execução só decide O QUE A FUNÇÃO PODE FAZER depois de chamada. Confundir as duas — dar ao agendamento permissão sobre o bucket, por exemplo — quebra o menor privilégio sem nenhum ganho funcional.
- Sem servidor entre uma chamada e a próxima. É a diferença estrutural em relação ao desenho anterior: aqui não há nada cobrando entre 13h00 e 14h00. A função é cobrada pela invocação e pela duração dela, ponto — e é também por isso que ela precisa reconstruir o próprio estado a cada vez.
- O toque no S3 continua igual, com escopo mais estreito. A operação — listar e apagar — não mudou. O que mudou foi a role que a executa: de uma task role compartilhada com a API inteira para uma role que só existe para este job e só alcança um prefixo.
- Cada chamada relata o próprio Init. A linha `REPORT` do CloudWatch Logs separa `Init Duration` (o cold start, quando existe) de `Duration` (o handler). É a prova deste laboratório: sem essa linha, "parece mais rápido" é opinião, não medição.
- Nenhuma VPC nesta topologia. O bucket S3 é regional e é alcançado pela rede da própria AWS, sem interface de rede nenhuma. Anexar a função a uma VPC sem necessidade só acrescentaria provisionamento de ENI ao Init — custo de latência sem contrapartida.
A diferença estrutural em relação ao desenho mínimo não é "trocar Fargate por Lambda" como troca de marca. É a ausência de qualquer coisa entre uma chamada e a próxima — e a separação de identidade que o desenho anterior nunca teve, porque a task role da API já bastava para tudo, inclusive para o que não devia.
O que este desenho custa quando ninguém chama a função
Nada. Não há task, não há processo, não há vCPU reservado entre 13h00 e 14h00. É a contrapartida exata do requisito "custo proporcional ao trabalho" — e é também por isso que o job precisa ser escrito assumindo que nenhum estado sobrevive de uma hora para a outra, o que o código da próxima seção torna explícito.
Como funciona ponta a ponta
Os nomes das fases não são jargão de documentação: são a prova de por que este job, chamado com pouca frequência, sofre cold start quase toda vez — e não raramente, como a intuição sugeriria.
A consequência contraintuitiva, e vale saber com precisão
Cold starts ocorrem, no agregado de toda a plataforma Lambda, em menos de 1% das invocações — mas esse número descreve tráfego constante, onde ambientes ficam quentes por reaproveitamento. Uma função chamada uma vez por hora não vive nesse regime: o intervalo costuma exceder o tempo que a Lambda mantém um ambiente ocioso vivo, então a estatística agregada não se aplica a ela. Meça a SUA função — não empreste o número geral.
O payload que a função recebe é o que o `EventBridge Scheduler` foi configurado para enviar, definido no atributo input do agendamento, não um envelope de evento genérico da AWS:
O EventBridge Scheduler entrega ESTE json diretamente como o parametro `event` do handler — nao existe camada extra de envelope quando o alvo e chamado direto.
{
"jobName": "limpeza-exportacoes",
"prefixo": "exportacoes/",
"idadeMaximaHoras": 2
}O payload é configuração, não segredo
O `input` do agendamento fica visível para quem tem permissão de leitura no recurso `aws_scheduler_schedule`. Nunca coloque credencial ou string de conexão aqui — o lugar certo continua sendo Secrets Manager, lido dentro do handler.
As decisões, e o que se perde em cada uma
📋 Migrar um job de limpeza de 2 s, chamado uma vez por hora, de dentro de uma task Fargate ligada 24 horas para algo que só existe enquanto roda — sem ninguém aguardando a resposta em tempo real.
A cobrança por invocação e duração vence de forma decisiva um recurso pago por hora ligada quando o trabalho real é uma fração pequena do tempo — 48 s de trabalho por dia contra 86.400 s de task existindo é o caso mais claro possível. Eliminar o cold start com Provisioned Concurrency ou SnapStart não se paga aqui: ninguém espera a resposta em tempo real, então um Init de menos de um segundo, uma vez por chamada, não tem custo de experiência a evitar — só teria custo de reservar capacidade que fica 59 minutos ociosa a cada hora. Native AOT é diferente: reduz o Init e a memória usada sem gerar NENHUMA cobrança recorrente — o preço é pago uma vez, em complexidade de build e teste, não a cada invocação nem a cada hora reservada. Por isso ele entra por padrão, e os outros dois ficam de fora.
Alt: Provisioned Concurrency — Cobra por ambiente reservado a cada hora, esteja ele em uso ou não. Para uma função chamada uma vez por hora, isso significa pagar por cerca de 59 minutos ociosos a cada 60 — o mesmo problema de fundo que fez o Fargate perder o desenho mínimo. Faz sentido para uma API síncrona com pico previsível, que é outro caso.
Alt: Lambda SnapStart — Cobra pelo cache do snapshot (mínimo de 3 horas por versão publicada) e por cada restauração, e é mutuamente exclusivo com Provisioned Concurrency na mesma versão. Também exige atenção à unicidade de qualquer valor gerado no código estático, que passa a ser compartilhado entre ambientes restaurados do mesmo snapshot.
Alt: Manter o job dentro do Fargate 24 h — É o desenho mínimo deste módulo, e é o problema declarado: paga pela existência da task, não pelo trabalho dela. Continua sendo a opção certa quando o job depende de um recurso que já exige um servidor sempre ligado por outro motivo — como uma conexão persistente que a própria API mantém.
Alt: Reescrever o job como Step Functions com espera — Resolve orquestração de múltiplos passos com retry declarado, não frequência de disparo de uma única etapa. Para um job de um passo só, é infraestrutura sem problema correspondente — fica para o L25, quando o fluxo tiver mais de uma etapa.
| Decisão | Escolha | Alternativas | Motivo | O que se perde |
|---|---|---|---|---|
| Onde o job roda | Lambda, sob demanda | Fargate 24h (desenho mínimo); EC2 com cron | cobrança proporcional ao trabalho, não à existência | perde o reaproveitamento natural de conexão que um processo sempre ligado tinha de graça |
| Eliminação de cold start | nenhuma, por padrão | Provisioned Concurrency; SnapStart | ninguém aguarda a resposta; o custo de reservar capacidade não tem contrapartida de UX | se o padrão de uso mudar para síncrono, esta decisão precisa ser revisitada — não é permanente |
| Compilação | Native AOT como padrão | runtime gerenciado dotnet8 sem AOT | reduz Init e memória sem cobrança recorrente; o custo é só de build e teste | perde reflection dinâmica; qualquer biblioteca nova entra sob suspeita até testada |
| Memória configurada | medida por experimento, não copiada | 1024 MB "porque é o que todo mundo usa" | evita pagar GB-segundo por capacidade que o job não usa | exige repetir a medição se o código do handler mudar de forma relevante |
| Identidade | duas roles, uma para chamar e outra para executar | uma role só, reaproveitada da task da API | menor privilégio real, não decorativo | mais um recurso Terraform para manter — o preço de não confiar numa role ampla |
A dívida que esta decisão não paga
O job continua sendo um passo só, sem retry declarativo além do que o `EventBridge Scheduler` oferece de forma genérica. Se um dia ele precisar de múltiplas etapas com compensação em caso de falha parcial, a resposta certa é Step Functions — assunto do L25, que depende deste laboratório para o modelo de execução da Lambda já estar entendido.
Construir: infraestrutura da função e o agendamento
O Terraform abaixo declara as duas roles separadas, o log group com retenção definida, a função sem `vpc_config`, e o agendamento com o payload que a seção anterior mostrou.
# lambda.tf — a funcao, as duas roles e o agendamento gerenciado
# ── Role de EXECUCAO: o que a funcao pode fazer depois de chamada ────────────
data "aws_iam_policy_document" "execucao_assume" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
}
}
resource "aws_iam_role" "execucao" {
name = "${var.projeto}-limpeza-execucao"
assume_role_policy = data.aws_iam_policy_document.execucao_assume.json
}
# Escopo estreito de proposito: so o prefixo de exportacoes, so listar e apagar.
# Nao e AmazonS3FullAccess "para nao travar" — e a lista exata do que o job faz.
data "aws_iam_policy_document" "execucao_permissoes" {
statement {
effect = "Allow"
actions = ["s3:ListBucket"]
resources = [aws_s3_bucket.exportacoes.arn]
condition {
test = "StringLike"
variable = "s3:prefix"
values = ["exportacoes/*"]
}
}
statement {
effect = "Allow"
actions = ["s3:DeleteObject"]
resources = ["${aws_s3_bucket.exportacoes.arn}/exportacoes/*"]
}
statement {
effect = "Allow"
actions = ["logs:CreateLogStream", "logs:PutLogEvents"]
resources = ["${aws_cloudwatch_log_group.funcao.arn}:*"]
}
}
resource "aws_iam_role_policy" "execucao" {
role = aws_iam_role.execucao.id
policy = data.aws_iam_policy_document.execucao_permissoes.json
}
resource "aws_cloudwatch_log_group" "funcao" {
name = "/aws/lambda/${var.projeto}-limpeza-exportacoes"
retention_in_days = 30 # sem isto, o log group fica indefinido e nunca para de cobrar
}
resource "aws_lambda_function" "limpeza" {
function_name = "${var.projeto}-limpeza-exportacoes"
role = aws_iam_role.execucao.arn
handler = "LimpezaExportacoes::LimpezaExportacoes.Function::FunctionHandler"
runtime = "dotnet8"
architectures = ["arm64"] # mesma proporcionalidade memoria x CPU; menor custo por GB-s no padrao
filename = data.archive_file.pacote.output_path
timeout = 30
# Comece baixo e MEÇA — 512 esta aqui como ponto de partida, nao como resposta.
# A secao de provas troca este valor e registra o efeito no Init Duration.
memory_size = 512
# NENHUM vpc_config: a funcao so toca S3, que e regional. Anexar VPC aqui so
# acrescentaria provisionamento de interface de rede ao Init, sem beneficio.
environment {
variables = {
BUCKET_EXPORTACOES = aws_s3_bucket.exportacoes.bucket
PREFIXO = "exportacoes/"
IDADE_MAXIMA_HORAS = "2"
}
}
tracing_config {
mode = "Active" # X-Ray no Init e no handler, para separar as duas fases no trace
}
}
# ── Role do AGENDAMENTO: o que pode CHAMAR a funcao, nada sobre o que ela faz ─
data "aws_iam_policy_document" "schedule_assume" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["scheduler.amazonaws.com"]
}
}
}
resource "aws_iam_role" "schedule" {
name = "${var.projeto}-limpeza-agendamento"
assume_role_policy = data.aws_iam_policy_document.schedule_assume.json
}
data "aws_iam_policy_document" "schedule_invoca" {
statement {
effect = "Allow"
actions = ["lambda:InvokeFunction"]
resources = [aws_lambda_function.limpeza.arn] # NUNCA "*": so esta funcao
}
}
resource "aws_iam_role_policy" "schedule" {
role = aws_iam_role.schedule.id
policy = data.aws_iam_policy_document.schedule_invoca.json
}
resource "aws_scheduler_schedule" "limpeza_horaria" {
name = "${var.projeto}-limpeza-horaria"
group_name = "default"
# Sem servidor nenhum entre um disparo e o proximo: e a AWS que garante a
# entrega, nao um processo que alguem precisa manter vivo.
schedule_expression = "rate(1 hour)"
flexible_time_window {
mode = "OFF" # previsibilidade do horario importa mais que economia de burst aqui
}
target {
arn = aws_lambda_function.limpeza.arn
role_arn = aws_iam_role.schedule.arn
# Este e o payload que o handler recebe como `event` — ver a secao de
# payload mais abaixo no modulo.
input = jsonencode({
jobName = "limpeza-exportacoes"
prefixo = "exportacoes/"
idadeMaximaHoras = 2
})
retry_policy {
maximum_retry_attempts = 2 # o job e idempotente: rodar de novo so apaga o que ja deveria estar apagado
}
}
}
output "nome_funcao" {
value = aws_lambda_function.limpeza.function_name
description = "usado nos comandos de prova, para filtrar log e trocar memoria"
}
Por que `retry_policy` com 2 tentativas é seguro aqui
O job é idempotente por construção: apagar um arquivo que já foi apagado não produz erro nem efeito colateral novo. Retry automático só é seguro quando essa propriedade é verdadeira — configurá-lo num job que NÃO é idempotente duplica o efeito, não só a tentativa.
Por que `architectures = ["arm64"]` entrou sem uma linha de requisito pedindo isso
A proporcionalidade memória×CPU vale nas duas arquiteturas; a diferença é o preço por GB-segundo, historicamente menor em arm64. Como o handler não usa nenhuma biblioteca nativa amarrada a x86_64, trocar de arquitetura não custa nada em compatibilidade — só exige compilar o pacote (ou o binário AOT) para o alvo certo.
Construir: o handler em .NET 8, runtime gerenciado
O comentário no código marca a fronteira que este laboratório existe para tornar visível: o que roda no código estático, uma vez por ambiente, e o que roda no handler, a cada chamada.
// Function.cs — runtime gerenciado dotnet8. Cada comentario marca onde o
// trabalho acontece: no codigo estatico (Init) ou dentro do handler (Invoke).
using Amazon.Lambda.Core;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
using Amazon.S3;
using Amazon.S3.Model;
using System.Text.Json.Serialization;
namespace LimpezaExportacoes;
public class EventoAgendamento
{
public string JobName { get; set; } = "";
public string Prefixo { get; set; } = "";
public int IdadeMaximaHoras { get; set; }
}
public class ResultadoLimpeza
{
public int ArquivosApagados { get; set; }
public string ExecutadoEm { get; set; } = "";
}
public class Function
{
// ── CODIGO ESTATICO: roda uma vez por ambiente novo, dentro do Init ──────
// O cliente do S3 e criado AQUI de proposito. E o mesmo objeto reaproveitado
// em toda chamada subsequente no mesmo ambiente — e e tambem o motivo de o
// cold start deste job ser dominado por Function init, nao por Runtime init:
// abrir o cliente do SDK e resolver credencial e o trabalho real aqui.
private static readonly AmazonS3Client _s3 = new();
private static async Task Main()
{
Func<EventoAgendamento, ILambdaContext, ResultadoLimpeza> handler = FunctionHandler;
await LambdaBootstrapBuilder.Create(handler,
new SourceGeneratorLambdaJsonSerializer<ContextoSerializacao>())
.Build()
.RunAsync();
}
// ── HANDLER: roda a cada invocacao, quente ou fria ────────────────────────
public static async Task<ResultadoLimpeza> FunctionHandler(EventoAgendamento evento, ILambdaContext ctx)
{
// Log deliberado para tornar o cold start OBSERVAVEL sem depender so da
// linha REPORT: se esta e a primeira invocacao do ambiente, o tempo
// decorrido desde o inicio do processo (nao da invocacao) e grande.
ctx.Logger.LogInformation(
$"inicio do handler para {evento.JobName}, ambiente ativo ha {Environment.TickCount64} ms");
var bucket = Environment.GetEnvironmentVariable("BUCKET_EXPORTACOES")!;
var idadeMaxima = TimeSpan.FromHours(evento.IdadeMaximaHoras);
var limite = DateTime.UtcNow - idadeMaxima;
// NUNCA guarde o contador em variavel estatica esperando que ele
// sobreviva entre chamadas: com uma hora de intervalo, o ambiente quase
// sempre foi reciclado, e o contador reiniciaria do zero, silenciosamente.
var apagados = 0;
string? continuationToken = null;
do
{
var resp = await _s3.ListObjectsV2Async(new ListObjectsV2Request
{
BucketName = bucket,
Prefix = evento.Prefixo,
ContinuationToken = continuationToken,
});
foreach (var obj in resp.S3Objects.Where(o => o.LastModified < limite))
{
await _s3.DeleteObjectAsync(bucket, obj.Key);
apagados++;
}
continuationToken = resp.IsTruncated == true ? resp.NextContinuationToken : null;
} while (continuationToken != null);
ctx.Logger.LogInformation($"apagados {apagados} arquivos com mais de {evento.IdadeMaximaHoras}h");
return new ResultadoLimpeza
{
ArquivosApagados = apagados,
ExecutadoEm = DateTime.UtcNow.ToString("O"),
};
}
}
[JsonSerializable(typeof(EventoAgendamento))]
[JsonSerializable(typeof(ResultadoLimpeza))]
public partial class ContextoSerializacao : JsonSerializerContext
{
}
O erro que este código evita de propósito
Guardar `apagados` como campo estático, esperando que ele acumule entre execuções, pareceria funcionar em teste local — onde o mesmo processo atende tudo. Em produção, com uma hora entre chamadas, o ambiente quase sempre é reciclado antes da próxima invocação, e o contador voltaria a zero silenciosamente. Estado que precisa estar sempre certo vai para fora do processo: S3, DynamoDB ou Parameter Store.
Construir: a mesma função, compilada em Native AOT
Nenhuma linha do handler muda. O que muda é o arquivo de projeto e o processo de build — e é exatamente essa separação que permite comparar as duas variantes com o mesmo código.
<!-- LimpezaExportacoes.csproj — variante Native AOT, para comparar com a
acima. O handler C# nao muda uma linha; o que muda e a compilacao. -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- A troca central: compila para binario nativo no momento do build, em
vez de gerar IL que o runtime da Lambda compilaria por JIT depois.
O build DEVE rodar em ambiente AL2023 (o `dotnet lambda` usa Docker
para isso) — compilar no seu Mac ou Windows e depois enviar o binario
para a Lambda NAO funciona: e cross-OS. -->
<PublishAot>true</PublishAot>
<!-- Reduz o binario removendo dado de globalizacao que este job nao usa
(datas em UTC, sem formatacao dependente de cultura). Nao ative isto
sem conferir se alguma dependencia realmente precisa de globalizacao. -->
<InvariantGlobalization>true</InvariantGlobalization>
<!-- Assemblies que usam reflection em algum ponto e que o trimming do AOT
nao consegue provar como seguras sozinho. Sem isto, um tipo usado so
indiretamente pelo SDK da AWS pode ser cortado e falhar em tempo de
execucao com "no metadata for type" — nunca em tempo de build. -->
<TrimmerRootAssembly Include="AWSSDK.Core" />
<TrimmerRootAssembly Include="AWSSDK.S3" />
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Amazon.Lambda.Core" Version="2.5.0" />
<PackageReference Include="Amazon.Lambda.RuntimeSupport" Version="1.12.0" />
<PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.4.3" />
<PackageReference Include="AWSSDK.S3" Version="3.7.400" />
</ItemGroup>
</Project>
Build que passa não é prova de que a função roda
Native AOT corta código que o compilador não consegue provar como alcançável — e SDKs que usam reflection internamente podem escapar dessa análise sem gerar aviso de build. Um "no metadata for type" só aparece em tempo de EXECUÇÃO, na Lambda publicada, nunca no seu terminal. Teste a versão publicada antes de assumir que compilar sem erro é suficiente.
Implantar, medir e comparar: o script de prova
O script abaixo publica uma versão nova — o que força um ambiente de execução novo — e lê a linha `REPORT` do CloudWatch Logs, onde `Init Duration` só aparece quando aquela chamada específica pagou cold start.
#!/usr/bin/env bash
# medir-cold-start.sh — publica as duas variantes, invoca cada uma N vezes e
# separa Init Duration de Duration a partir da linha REPORT do CloudWatch Logs.
set -euo pipefail
FUNCAO="${1:?uso: medir-cold-start.sh <nome-da-funcao> <runtime|aot>}"
VARIANTE="${2:?informe runtime ou aot}"
REGIAO="${REGIAO:-us-east-1}"
# Publicar uma VERSAO nova forca um ambiente completamente novo na proxima
# chamada — e o unico jeito confiavel de garantir que a medicao capture um
# cold start de verdade, em vez de reaproveitar um ambiente ja quente.
VERSAO=$(aws lambda publish-version --function-name "$FUNCAO" \
--region "$REGIAO" --query Version --output text)
echo "== variante: $VARIANTE | versao publicada: $VERSAO =="
for i in $(seq 1 5); do
aws lambda invoke --function-name "${FUNCAO}:${VERSAO}" \
--region "$REGIAO" --payload '{"jobName":"medicao","prefixo":"exportacoes/","idadeMaximaHoras":2}' \
--cli-binary-format raw-in-base64-out /tmp/saida.json > /tmp/invoke-meta.json
sleep 1
done
# A linha REPORT so aparece quando ha algo a reportar; Init Duration so existe
# quando aquela invocacao especifica pagou cold start.
aws logs filter-log-events \
--log-group-name "/aws/lambda/${FUNCA-$FUNCAO}" --region "$REGIAO" \
--filter-pattern "REPORT" --start-time "$(( $(date +%s%3N) - 300000 ))" \
--query 'events[].message' --output text
# Esperado: nas 5 chamadas, a PRIMEIRA traz "Init Duration"; as demais, se
# caírem no mesmo ambiente quente, não trazem — e é essa ausência, medida, que
# prova reaproveitamento. Repita depois de 70+ minutos parado para confirmar
# que a reciclagem por ociosidade faz TODA chamada voltar a ter Init Duration.
Implantar e provar
Cinco medições. Nenhuma conclusão deste laboratório vem de "parece mais rápido".
O que este módulo NÃO afirma sobre os números
A ordem de grandeza da redução com Native AOT varia com o que o código estático faz — aqui, principalmente criar um cliente do SDK da AWS. Um handler com mais dependências reflection-heavy pode reduzir menos, ou exigir mais trabalho de `TrimmerRootAssembly` antes de reduzir qualquer coisa. Trate o percentual que você medir como o dado real; trate qualquer percentual citado em blog post como ponto de partida, não como garantia.
Quebrar de propósito
| Falha provocada | Sintoma | Onde olhar | Correção |
|---|---|---|---|
| Mover uma leitura de configuração pesada (ex.: baixar um catálogo grande) para o código estático, fora do handler | `INIT_REPORT` com `Status: timeout` no CloudWatch Logs, sem nenhuma linha de erro do seu código | CloudWatch Logs Insights, filtrando por `INIT_REPORT` | mover o carregamento para dentro do handler com carga preguiçosa, ou usar Provisioned Concurrency/SnapStart, que estendem o limite de 10 s para até 130 s |
| Configurar a role do agendamento sem a ação `lambda:InvokeFunction` no ARN certo | nenhuma execução aparece nos logs da função, e nenhum erro é reportado no lado dela — a chamada nunca chega a acontecer | CloudTrail, buscando o evento `Invoke` tentado pela role do `scheduler.amazonaws.com` | corrigir a policy da role do agendamento para incluir o ARN exato da função |
| Guardar um contador de "arquivos processados hoje" em campo estático, assumindo reaproveitamento de ambiente entre as 24 chamadas diárias | o contador nunca passa de um valor baixo, mesmo depois de dias rodando — ele reinicia toda vez que o ambiente é reciclado | comparar o valor logado pelo handler com o valor esperado, chamada a chamada | mover o contador para fora do processo: um item em DynamoDB, ou um objeto em S3 lido e reescrito a cada execução |
A função de limpeza da Cadência é chamada uma vez por hora. Por que ela tende a sofrer cold start com MAIS frequência do que uma função chamada 50 vezes por segundo, mesmo sendo muito mais simples?
Segurança
| Risco | Probabilidade | Impacto | Prevenção | Detecção | Resposta |
|---|---|---|---|---|---|
| Role de execução com `s3:*` no bucket inteiro em vez do prefixo | Média | Alto — apagar exportação que ainda está dentro do prazo | policy com `condition` de prefixo, como no Terraform desta seção | IAM Access Analyzer sinalizando acesso mais amplo que o necessário | restringir a policy e restaurar do versionamento do bucket, se o versionamento estiver ativo |
| Resource policy da função permitindo `Principal: "*"` invocar | Baixa | Alto — execução por terceiro, custo e possível exfiltração via payload | nenhuma resource policy além da role específica do agendamento | CloudTrail mostrando `Invoke` fora da janela horária esperada | remover o statement amplo, girar qualquer segredo que a função possa ter exposto |
| Segredo em variável de ambiente em texto claro | Média | Médio — credencial legível por qualquer papel com permissão de leitura de configuração | ler segredo do Secrets Manager dentro do handler, nunca em `environment` | regra de AWS Config para variável de ambiente com padrão de credencial | rotacionar o segredo e mover a leitura para o gerenciador |
| Log group sem retenção definida | Alta | Baixo isolado, alto em agregado — custo e dado histórico acumulando sem prazo | `retention_in_days` explícito em todo log group criado | regra de AWS Config para log group sem retenção | aplicar retenção retroativamente; avaliar se o histórico acumulado deve ser expurgado |
| SnapStart reaproveitando valor "único" gerado no código estático entre ambientes restaurados | Baixa neste job (não usa SnapStart por padrão), alta se ativado sem revisão | Médio a alto, dependendo do que for gerado — ID, seed de aleatoriedade | gerar qualquer valor que precise ser único DEPOIS do restore, nunca no Init | valor duplicado aparecendo entre execuções que deveriam ser independentes | regenerar a partir de fonte de entropia pós-restore; revisar todo `static readonly` do código |
Observabilidade
O painel deste job responde a perguntas específicas — não "está tudo bem", que não é pergunta.
- A execução da última hora aconteceu? (métrica `Invocations`, período de 1 h, esperado ≥ 1)
- Quantas das últimas 24 execuções pagaram cold start? (`Init Duration` presente na linha REPORT)
- A duração faturada está subindo com o tempo, sem mudança de código? (`Duration`, tendência)
- Alguma execução falhou ou foi reprocessada pelo retry do agendamento? (`Errors`, `Throttles`)
- A role de execução tentou algo fora do escopo concedido? (CloudTrail, eventos negados)
| Alarme | Métrica | Limiar inicial | O que ele cobre |
|---|---|---|---|
| Job não rodou na última hora | Invocations (soma, período 1h) | < 1 | o agendamento parou de disparar, ou a função foi desabilitada |
| Erro na execução | Errors (soma, período 1h) | ≥ 1 | exceção não tratada no handler; a permissão do bucket é a primeira suspeita |
| Init Duration fora do padrão | Duration com filtro de Init (CloudWatch Logs Insights) | acima de 2× a mediana observada nas provas | regressão de desempenho — dependência nova pesada no código estático, por exemplo |
Alarme sem destinatário é decoração
Um alarme configurado sem ação associada — sem tópico SNS, sem integração com o canal do time — só existe no console. Ele passa em qualquer checklist de "observabilidade implementada" e não avisa ninguém quando o job de fato para de rodar.
Escala
| Ordem de grandeza | O que muda | O que continua igual |
|---|---|---|
| 10 arquivos por execução | praticamente todo o tempo é Init, não o loop de listagem | cold start em quase toda chamada, pelo intervalo de uma hora |
| 10 mil arquivos por execução | paginação do `ListObjectsV2` passa a importar de verdade; o tempo de handler cresce | ainda cabe dentro do timeout de 30 s configurado, com folga |
| 1 milhão de arquivos por execução | o job pode se aproximar do timeout máximo da função; um passo só deixa de ser suficiente | nada aqui muda o modelo de cobrança — continua por invocação e duração |
| Falha de zona de disponibilidade | nenhuma ação do cliente é necessária: a Lambda distribui execução entre AZs por trás da API | o que depende de outra AZ é o que a função TOCA, não a função — aqui, S3, que é regional por design |
Onde este desenho para de servir
Em 1 milhão de arquivos por execução, o problema deixa de ser cold start e passa a ser orquestração: listar, apagar e possivelmente falhar no meio do caminho exige checkpoint e retry por lote, não um handler de ponta a ponta. É o assunto do L25 (Step Functions), que evolui a partir deste laboratório quando um passo só deixa de bastar.
Custo
| Cenário | Perfil | O que domina o custo |
|---|---|---|
| Protótipo | algumas dezenas de invocações manuais durante o desenvolvimento | nenhuma das duas parcelas é relevante; o custo é, na prática, irrelevante |
| Produção pequena (a Cadência hoje) | ~720 invocações por mês, 2-3 s cada, memória medida | a duração faturada, mas em valor absoluto continua sendo a menor linha da fatura mensal da conta |
| Alta escala (hipotético) | um job por loja, em paralelo, a cada hora | a parcela de invocações passa a competir com a de duração — e concorrência reservada, se adotada, viraria a maior linha |
O custo oculto que este módulo evita, e o que ainda fica de fora
Versões antigas de uma função com SnapStart ativado continuam sendo cobradas pelo cache do snapshot enquanto existirem — apagar a função não basta se versões publicadas sobrarem. Este job não usa SnapStart, então este custo específico não se aplica aqui; ele é citado porque é o tipo de custo que só aparece na fatura, nunca no `terraform plan`.
Nenhum valor absoluto de preço aparece neste módulo por decisão: preço varia por região e envelhece mais rápido que o conteúdo. Calcule o cenário real da sua conta no AWS Pricing Calculator, com o `Init Duration` e a `Duration` que você mediu — não com um número de blog.
Well-Architected nos seis pilares
| Pilar | Situação | Risco | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | log do job separado do log da API, com REPORT legível | baixo — a causa de uma falha fica rastreável em minutos | alarme automático de "job não rodou" além da inspeção manual | Média |
| Segurança | duas roles com escopo mínimo declarado | baixo, desde que a condition de prefixo não seja removida por conveniência | revisão periódica da policy com IAM Access Analyzer | Média |
| Confiabilidade | retry automático do agendamento, job idempotente | médio — nenhum alarme cobre "falhou e o retry também falhou" | alarme de `Errors` com ação, não só métrica visível | Alta |
| Eficiência de performance | memória escolhida por medição, não por convenção | baixo hoje; cresce se o handler ganhar dependências sem repetir a medição | repetir a prova de memória a cada mudança relevante de código | Baixa |
| Otimização de custo | cobrança proporcional ao uso, sem capacidade reservada ociosa | baixo — a decisão de não usar Provisioned Concurrency está escrita e justificada | revisar a decisão se o padrão de uso mudar para síncrono | Baixa |
| Sustentabilidade | nenhum vCPU reservado ficando ocioso 99,94% do tempo | baixo — é justamente o que este laboratório eliminou | considerar arm64 (já adotado no Terraform) por eficiência de instrução | Baixa |
Confiabilidade puxa a maior prioridade, e não é acidente
Custo e performance já saíram resolvidos da decisão original — pagar por invocação e medir memória antes de fixar. O que ficou pendente é o que acontece quando o retry automático também falha, e é por isso que confiabilidade concentra a prioridade alta desta tabela, não segurança nem custo.
Evolução em níveis
uma função Lambda invocada manualmente pela CLI para validar a lógica de limpeza, sem agendamento nenhum. Novo risco: ninguém lembra de rodar — o job só existe quando alguém se lembra dele.EventBridge Scheduler dispara a cada hora; log básico no CloudWatch, sem alarme. Novo risco: falha silenciosa — se o job parar de funcionar, ninguém percebe até o bucket encher de novo.duas roles com escopo mínimo, memória medida, retry idempotente, alarmes de invocação e erro, Native AOT. Novo risco: um retry que também falha não tem escalonamento para humano.um job por loja, em paralelo, com concorrência reservada dimensionada por medição e tracing distribuído no laço inteiro. Novo risco: rajada de concorrência nova gera onda de cold start simultâneo se a reserva não acompanhar.a função vira módulo Terraform reutilizável; uma Lambda layer compartilhada entre os jobs agendados da Cadência; pipeline de deploy único para todos eles. Novo risco: mudança na layer compartilhada quebra vários jobs ao mesmo tempo, num só deploy.os metadados de cada limpeza alimentam um catálogo consultável (Glue/Athena); opcionalmente, um resumo em linguagem natural do que foi removido entra num relatório operacional semanal. Novo risco: usar um modelo de linguagem para resumir um número (quantos arquivos, de que idade) é latência e custo sem ganho — a próxima seção trata isso de frente.Onde IA entra, e onde não entra
Neste laboratório, IA não agrega — e a razão é específica, não genérica
O job decide "este arquivo passou de 2 horas? apague" — uma regra determinística, com uma entrada (a data de modificação) e uma saída binária. Nenhum modelo de linguagem decide isso melhor que uma comparação de `DateTime`, e trocar a comparação por uma chamada a um modelo trocaria uma operação de microssegundos por uma de centenas de milissegundos, com custo por token, para o mesmo resultado. Forçar IA aqui seria o hype que esta plataforma existe para não vender.
Onde IA entraria com propósito real é em cima do RESULTADO agregado deste tipo de job — por exemplo, um resumo semanal de operação que cruza vários jobs agendados e aponta anomalia em linguagem natural para quem não lê métrica bruta. Isso é o assunto das trilhas de Bedrock e RAG, não deste laboratório de modelo de execução.
Anti-padrões
| Antipadrão | Por que alguém faz isso | Sintoma em produção | Forma correta |
|---|---|---|---|
| Configurar 1.024 MB "porque é o valor que todo tutorial usa" | é o número mais citado, e citação repetida parece validação | ou desperdício de GB-segundo pago sem necessidade, ou Init mais lento do que precisava ser — sem saber qual dos dois | medir Init Duration em pelo menos três configurações antes de fixar um valor |
| Ativar Provisioned Concurrency num job em lote "para garantir performance" | medo de reclamação de lentidão, sem medir se a lentidão importa para alguém | fatura de capacidade reservada 24 h para um job que roda 24 vezes por dia em 2 s cada | medir se existe alguém esperando a resposta em tempo real antes de pagar para eliminar cold start |
| Guardar contador ou estado de negócio em variável estática fora do handler | funciona nos testes locais, onde o mesmo processo atende tudo, então parece correto | contador reseta silenciosamente após reciclagem de ambiente, e ninguém percebe até o número não bater | persistir estado que precisa estar sempre certo fora do processo — S3, DynamoDB, Parameter Store |
| Migrar tudo para Native AOT de uma vez, sem testar biblioteca por biblioteca | a promessa de redução de cold start nas notas de release soa boa demais para não aplicar em tudo | erro de "no metadata for type" em produção, nunca detectado em build local | migrar uma função por vez, testando a versão publicada antes de tornar AOT o padrão do time |
| Deixar a task Fargate antiga ligada "por precaução" depois de migrar o job | ninguém quer ser responsável se o caminho novo falhar e o antigo já tiver sido removido | os dois caminhos limpam o mesmo bucket ao mesmo tempo, pagando duas vezes pelo mesmo trabalho | remover o código antigo como parte da mesma entrega, não como tarefa "depois" |
| Conceder `AmazonS3FullAccess` à role de execução "para não travar por permissão" | menor atrito ao configurar, e a maioria nunca revisa depois de funcionar | blast radius de qualquer bug ou credencial vazada cobre o bucket inteiro, não só o prefixo do job | policy com `condition` de prefixo, como a desta seção — específica desde o primeiro deploy |
O padrão comum aos seis: nenhum é preguiça pontual
Todo antipadrão desta tabela é a escolha de menor atrito no momento em que foi feita. O defeito não é a pessoa — é a ausência de um critério escrito para quando o atalho deixa de valer a pena. É esse critério que este laboratório tentou deixar registrado em cada decisão, não só na tabela.
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Onde olhar | Correção |
|---|---|---|---|---|
| Função nunca é chamada | role do agendamento sem `lambda:InvokeFunction` no ARN certo | tentar `aws scheduler get-schedule` e conferir o `target.roleArn` | CloudTrail, evento `Invoke` tentado e negado | corrigir a policy da role do agendamento |
| `INIT_REPORT` com `Status: timeout` | trabalho pesado no código estático, fora do handler | medir tempo de cada linha do construtor estático isoladamente | CloudWatch Logs Insights, filtro `INIT_REPORT` | mover para carga preguiçosa dentro do handler, ou usar PC/SnapStart |
| Erro "no metadata for type X" só na versão Native AOT | tipo não registrado no `JsonSerializerContext` | ler a mensagem de exceção completa, que cita o tipo faltante | log de erro da invocação | adicionar `[JsonSerializable(typeof(X))]` ao contexto de serialização |
| Contador ou estado parece "esquecido" entre execuções | ambiente reciclado por ociosidade; nada estático sobrevive | comparar Init Duration das últimas execuções — presença dele indica ambiente novo | CloudWatch Logs Insights, campo `Init Duration` | mover o estado para fora do processo |
| `AccessDenied` ao apagar objeto fora do prefixo esperado | condition de prefixo funcionando como desenhado, não como bug | conferir se o objeto realmente está fora de `exportacoes/` | mensagem de erro do SDK, campo do recurso negado | nenhuma — é o menor privilégio fazendo o trabalho dele |
Limpeza
# A ordem importa: remova o AGENDAMENTO antes da FUNCAO, para nao deixar um
# schedule apontando para um ARN que nao existe mais.
terraform destroy -target=aws_scheduler_schedule.limpeza_horaria
terraform destroy
# Se alguma versao foi publicada manualmente durante as provas (fora do
# `terraform apply`), ela NAO e removida pelo destroy — confira e apague:
aws lambda list-versions-by-function --function-name <nome-da-funcao> \
--query "Versions[?Version!='\$LATEST'].Version" --output text
| O que o destroy leva | O que o destroy NÃO leva |
|---|---|
| Função Lambda, agendamento, as duas roles | versões publicadas manualmente fora do Terraform durante as provas |
| Log group da função (se declarado como recurso Terraform) | log já enviado antes da exclusão do log group, se ele estava fora do controle do Terraform |
| Policies inline anexadas às roles | nada em SnapStart — este job não o usa, mas se você testou SnapStart à parte, apague as versões com cache ativo, que continuam sendo cobradas |
O passo que não é opcional: remover o job embutido na API
Terraform destroy nesta seção só cuida da infraestrutura NOVA. O `BackgroundService` dentro da task Fargate da API continua existindo até alguém apagar aquele código e reimplantar a API — e enquanto ele existir, o bucket segue sendo limpo duas vezes, uma vez de graça (a antiga, que você já paga) e outra pela função nova.
Resumo
| Problema | Peça | Motivo |
|---|---|---|
| Task Fargate ligada 24h para 2s de trabalho | função Lambda sob demanda | cobrança por invocação e duração, não por existência |
| Relógio interno dentro do processo da API | EventBridge Scheduler | dispara o alvo sem nenhum servidor mantendo a hora |
| Uma role fazendo tudo | role do agendamento + role de execução, separadas | quem chama e o que a função pode fazer são decisões diferentes |
| Memória "que parece razoável" | memória medida em três configurações | CPU é proporcional à memória; o ponto certo se mede, não se adivinha |
| Cold start como incógnita | Init Duration medido, runtime gerenciado vs Native AOT | decisão de eliminar (ou não) cold start com número, não com opinião |
- 1. O job embutido na API paga por 24 horas para entregar 2 segundos de trabalho.
- 2. A conta do desperdício é medida, não estimada — menos de 0,06% do tempo é trabalho real.
- 3. A Lambda cobra por invocação e duração; o desenho de produção não tem servidor entre chamadas.
- 4. O Init tem três tarefas: extensão, runtime, código estático — e um limite de 10 s.
- 5. Uma chamada a cada hora quase sempre encontra um ambiente reciclado: cold start é a regra aqui, não a exceção.
- 6. Memória e CPU são o mesmo dial; 1.769 MB é o ponto de referência de 1 vCPU.
- 7. Native AOT reduz Init sem cobrança recorrente; Provisioned Concurrency e SnapStart cobram para eliminar o mesmo problema.
- 8. Como ninguém espera a resposta em tempo real, a decisão aqui é não pagar para eliminar cold start.
- 9. Duas roles — agendamento e execução — mantêm o menor privilégio real, não decorativo.
- 10. O job antigo, embutido na task da API, precisa ser removido — ou os dois cobram pelo mesmo trabalho.
Perguntas frequentes
❓ Lambda é sempre mais barato que manter um servidor ligado?
❓ O que exatamente causa o cold start de uma função Lambda?
❓ Aumentar a memória da função deixa o cold start mais rápido sem mudar código?
❓ Native AOT funciona com qualquer biblioteca .NET sem alteração?
❓ Provisioned Concurrency e SnapStart resolvem o mesmo problema do mesmo jeito?
❓ Preciso colocar a função Lambda dentro de uma VPC para ela funcionar?
❓ A função Lambda guarda variáveis e conexões entre uma execução e a próxima?
❓ EventBridge Scheduler é só um cron gerenciado, sem vantagem real sobre EC2?
Fixando
O time da Cadência quer encurtar o Init da função sem reescrever nenhuma linha de código. Qual mudança de configuração tem chance real de conseguir isso, e por quê?
Para o job de limpeza horário da Cadência, que ninguém espera em tempo real, qual é a decisão mais defensável entre as três formas de reduzir cold start — Native AOT, Provisioned Concurrency e SnapStart?
Conhecimentos, próximo módulo e documentação
| Item | Conteúdo |
|---|---|
| Conhecimentos anteriores necessários | L01 no ar (para o bucket de exportações e o contexto da Cadência); .NET 8, Docker e Terraform básicos |
| Conhecimentos adquiridos | as três tarefas do Init e o limite de 10 s; por que baixa frequência de chamada aumenta, não reduz, a chance de cold start; memória como o único dial de CPU; Native AOT no runtime gerenciado dotnet8; quando Provisioned Concurrency e SnapStart valem o custo — e quando não valem |
| Limitação que fica | o job continua sendo um passo só, sem compensação declarativa em caso de falha parcial; e a decisão de não usar SnapStart/PC precisa ser revisitada se o padrão de uso mudar para síncrono |
| Próximo exemplo recomendado | L22 — fila com DLQ e idempotência, para quando o gatilho deixa de ser um relógio e passa a ser um pico de eventos que a API não aguenta receber de forma síncrona |
| Também habilitado por este módulo | L26 (API 100% serverless) reutiliza o modelo de execução e o critério de memória medida deste laboratório; L27 (agendamento avançado) aprofunda janela flexível e fuso horário do EventBridge Scheduler |
| Data da última validação técnica | 7 de agosto de 2026 |
Documentação oficial consultada: Configure Lambda function memory — a proporcionalidade entre memória e CPU e o ponto de 1.769 MB para 1 vCPU; Understanding the Lambda execution environment lifecycle — as três tarefas do Init, o limite de 10 s e sua extensão para 130 s com Provisioned Concurrency, SnapStart ou Lambda Managed Instances; Improving startup performance with Lambda SnapStart — o suporte a .NET 8, a incompatibilidade com Provisioned Concurrency na mesma versão, o modelo de cobrança e a ressalva de unicidade de estado; e Compile .NET Lambda function code to a native runtime format — a confirmação de que Native AOT usa o runtime gerenciado dotnet8 (não um runtime customizado), a exigência de build em ambiente AL2023 e a dependência de serialização por source generator. Os valores de preço não aparecem neste módulo por decisão: use o AWS Pricing Calculator, porque preço varia por região e envelhece mais rápido que o conteúdo.
O que não foi verificado, e você deve conferir na sua conta
A documentação consultada não afirma, em nenhuma das páginas lidas, se SnapStart e Native AOT combinam na mesma função .NET 8 — ambos são descritos sob o mesmo runtime gerenciado dotnet8, mas nenhuma página trata explicitamente da combinação dos dois. Este módulo não ativa SnapStart por padrão (a decisão já não depende disso), mas se você for testar a combinação, trate o resultado como não documentado até confirmar na sua conta — e não generalize o que encontrar para outra versão do runtime sem repetir o teste. Os tempos de Init citados nas provas são os medidos na função de exemplo, com o código estático deste módulo; sirvam como ordem de grandeza, não como referência para um handler com dependências diferentes.
Terminou de ler?
Marcar como concluído registra o XP, mantém sua sequência e coloca 3 cartas deste módulo na fila de revisão espaçada.
Próximos passos sugeridos
Temas deste módulo
Discussão
Carregando comentários…