No dia 22/07/2026 a AWS anunciou o AWS Lambda MicroVMs, um recurso novo dentro do Lambda, com API própria, separado das Functions que a gente já conhece. E não é só mais uma opção de runtime. É um primitivo de computação novo, pensado pra um problema que vinha incomodando bastante gente: como dar a cada usuário (ou sessão, ou agente de IA) um ambiente isolado de verdade, que suba rápido e mantenha estado, sem você ter que construir sua própria camada de virtualização do zero.
Antes de ir pro exemplo prático vale entender por que isso existe. Se você constrói algo como um assistente de código com IA, um ambiente de execução interativo, uma plataforma de análise de dados ou um scanner de vulnerabilidades, você provavelmente já bateu de frente com esse trilema: VM tradicional isola bem mas demora minutos pra subir, container sobe rápido mas compartilha kernel com o host (o que exige um trabalho de hardening considerável pra rodar código não confiável), e Function as a Service é ótimo pra evento-resposta mas não guarda estado entre interações de uma sessão longa. O MicroVMs foi desenhado exatamente pra esse buraco no meio.
Um aviso rápido antes de continuar: nos últimos artigos eu vim criando todos os templates utilizando o AWS SAM. Desta vez troquei pra Terraform, e vale explicar o porquê. SAM/CloudFormation continuam cobrindo bem o pedaço nativo de MicroVM Image e Network Connector. Isso não mudou. Mas o resto do laboratório (VPC, buckets, S3 Files, IAM) é onde eu realmente vivo no dia a dia, e o provider hashicorp/aws (a partir da v6.40.0) já tem recursos nativos pra Amazon S3 Files (aws_s3files_*). Pra imagem e o network connector, enquanto o provider não tem recurso nativo pra isso (issue #48526 ainda aberta), uso null_resource + local-exec como ponte pro AWS CLI.
1.0 O que muda em relação ao Lambda que você já usa
O motor por baixo é o mesmo Firecracker que já roda as Functions, então o isolamento de VM (kernel próprio, sem recurso compartilhado entre sessões) é a mesma garantia de sempre. A diferença está no modelo de uso: em vez de deploy-then-invoke, aqui é imagem-então-lançamento. Você empacota uma aplicação com Dockerfile, a AWS builda essa imagem, inicializa a aplicação dentro dela e tira um snapshot Firecracker da memória e do disco naquele estado pronto. Todo MicroVM lançado a partir dessa imagem depois disso não faz boot frio: ele resume direto do snapshot.
Na prática isso significa que sua API já está de pé no instante em que o lançamento termina. E tem outra coisa que o Lambda tradicional não te dá: controle explícito de ciclo de vida. Você define uma política de idle (quanto tempo sem tráfego até suspender, quanto tempo suspenso antes de encerrar de vez, se retoma automaticamente ou não), e o MicroVM pode ficar suspenso com estado intacto por até 8 horas de runtime total. Por padrão, o container roda com um conjunto restrito de capabilities do Linux, e é isso que vai importar no exemplo a seguir.
2.0 Colocando isso pra rodar
Montei uma API de workspace de arquivos com FastAPI, com o objetivo de: provar que o estado sobrevive não só a uma suspensão, mas ao fim completo do MicroVM. O disco não é local, que é parte do snapshot e some quando o MicroVM é terminado. É o Amazon S3 Files montado dentro da imagem, um sistema de arquivos que expõe objetos do S3 como se fossem arquivos comuns, via NFS, no mesmo caminho local. O workspace passa a viver fora do ciclo de vida de qualquer MicroVM individual, e cada arquivo escrito vira um objeto de verdade no bucket por trás.
O repositório completo está em lambda-microvms-workspace-demo.
2.1 A API: app.py
import time
from pathlib import Path
from aws_lambda_powertools import Logger
from fastapi import FastAPI, HTTPException, UploadFile
from fastapi.responses import FileResponse
app = FastAPI()
# Sem inject_lambda_context aqui: esse decorator injeta cold_start,
# function_request_id etc. a partir do objeto context de uma invocação
# Lambda, e esse FastAPI roda como servidor de longa duração dentro do
# MicroVM, sem um context por request. O que eu ganho de graça mesmo
# assim é o log em JSON estruturado, sem escrever formatter na mão.
logger = Logger(service="workspace-microvm")
# Esse caminho é o ponto de montagem do Amazon S3 Files, não disco
# local do MicroVM. Cada arquivo aqui é, por baixo, um objeto no S3,
# então sobrevive à suspensão E à terminação do MicroVM.
WORKSPACE_DIR = Path("/mnt/workspace")
@app.get("/health")
def health():
mounted = WORKSPACE_DIR.exists() and WORKSPACE_DIR.is_mount()
return {"status": "ok", "workspace_mounted": mounted}
@app.post("/files")
async def upload_file(file: UploadFile):
destination = WORKSPACE_DIR / file.filename
content = await file.read()
destination.write_bytes(content)
logger.info("Arquivo salvo via S3 Files", filename=file.filename, size_bytes=len(content))
return {"filename": file.filename, "size_bytes": len(content), "stored_at": str(destination)}
@app.get("/files")
def list_files():
files = [
{
"filename": f.name,
"size_bytes": f.stat().st_size,
"modified_at": time.ctime(f.stat().st_mtime),
}
for f in WORKSPACE_DIR.iterdir()
if f.is_file()
]
logger.info("Listagem de workspace solicitada", total_arquivos=len(files))
return {"workspace_dir": str(WORKSPACE_DIR), "files": files}
@app.get("/files/{filename}")
def download_file(filename: str):
file_path = WORKSPACE_DIR / filename
if not file_path.exists():
logger.warning("Download solicitado para arquivo inexistente", filename=filename)
raise HTTPException(status_code=404, detail="Arquivo não encontrado no workspace")
return FileResponse(file_path)
2.2 Montando o S3 Files: entrypoint.sh
#!/bin/sh
set -e
mkdir -p /mnt/workspace
if [ -z "${S3FILES_MOUNT_TARGET}" ]; then
echo "ERRO: variável S3FILES_MOUNT_TARGET não definida." >&2
exit 1
fi
# S3FILES_MOUNT_TARGET chega aqui como variável de ambiente definida
# na CRIAÇÃO DA IMAGEM, não no lançamento. Mais sobre esse detalhe
# na seção 2.4.
mount -t nfs4 -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2 \
"${S3FILES_MOUNT_TARGET}:/lambda" /mnt/workspace
exec uvicorn app:app --host 0.0.0.0 --port 5000
2.3 Estrutura do repositório: da imagem ao Terraform
lambda-microvms-workspace-demo/
├── app/ # FastAPI + entrypoint.sh + Dockerfile
├── infra/ # Terraform "flat": vpc, s3, s3files, iam, microvm, outputs
├── scripts/ # só helpers chamados pelo apply/destroy (create_*, delete_*)
├── docs/
│ └── MANUAL.md # guia manual passo a passo pra lançar, testar e provar durabilidade
└── Makefile # só init / plan / apply / destroy
Reparem numa coisa, o repositório não tem make run, make test nem make proof. Isso é proposital. O Makefile só cobre init, plan, apply e destroy: tudo que é infraestrutura versionada. Lançar um MicroVM, gerar token, testar com curl e reproduzir a prova de durabilidade viraram uma sequência de comandos documentada em docs/MANUAL.md, não um script automatizado. O motivo é o mesmo que já defendi antes: imagem é infraestrutura versionada, MicroVM em execução é sessão efêmera, e sessão efêmera não deveria ter um make escondendo o que está de fato acontecendo, e muito menos ir parar no state do Terraform.
"Sempre que trabalho com Makefiles, me vem a memória do tempo da graduação, quando criava os arquivos de build para rodar meus programas em C."
2.4 Provisionando com Terraform
infra/ fica flat de propósito: vpc.tf, s3.tf, s3files.tf, iam.tf, microvm.tf, outputs.tf. A VPC é mínima, sem NAT gateway: nem o MicroVM nem os mount targets do S3 Files precisam de saída pra internet, só de rota entre si na porta 2049.
Uma peça de IAM que não tinha aparecido antes: o S3 Files usa um papel próprio pra sincronizar o que é escrito via NFS de volta pro bucket, e esse papel assume o principal elasticfilesystem.amazonaws.com. O S3 Files compartilha parte da arquitetura de serviço com o EFS por baixo, o que explica o principal emprestado. Esse papel de sync precisa de s3:GetObject, s3:PutObject, s3:ListBucket e s3:DeleteObject no bucket do workspace, e o bucket precisa ter versionamento habilitado. Sem isso a sincronização não funciona.
# infra/iam.tf (trecho)
resource "aws_iam_role" "s3files_sync" {
name = "${var.project_name}-s3files-sync-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "elasticfilesystem.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy" "s3files_sync_bucket_access" {
name = "${var.project_name}-s3files-sync-bucket-access"
role = aws_iam_role.s3files_sync.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Sid = "SyncWorkspaceBucket"
Effect = "Allow"
Action = ["s3:GetObject", "s3:PutObject", "s3:ListBucket", "s3:DeleteObject"]
Resource = [aws_s3_bucket.workspace.arn, "${aws_s3_bucket.workspace.arn}/*"]
}]
})
}
microvm.tf é onde fica a ponte via CLI, porque o provider ainda não tem aws_lambda_microvm_image nem equivalente pro network connector. O detalhe que mudou desde a última vez: o S3FILES_MOUNT_TARGET entra como variável de ambiente na criação da imagem, não no lançamento. Variáveis de ambiente do MicroVM são resolvidas em build time e congelam no snapshot. Se eu passasse isso no run-microvm, o entrypoint.sh simplesmente não veria o valor, porque ele já rodou antes de qualquer parâmetro de lançamento existir.
Outro ajuste: o provider do Terraform não expõe o DNS do mount target do S3 Files (diferente do EFS, que tem um DNS previsível por AZ). O jeito que encontrei foi ler o IP privado da ENI do mount target direto do Terraform e passar esse IP como valor da variável de ambiente:
# infra/microvm.tf (trecho)
data "aws_network_interface" "s3files_mount_target" {
filter {
name = "description"
values = ["EFS mount target for fs-*"] # o S3 Files reaproveita o padrão de descrição do EFS na ENI
}
filter {
name = "subnet-id"
values = [aws_subnet.private[0].id]
}
}
resource "null_resource" "microvm_image" {
triggers = {
artifact_etag = aws_s3_object.app_artifact.etag
mount_ip = data.aws_network_interface.s3files_mount_target.private_ip
}
provisioner "local-exec" {
command = "${path.module}/../scripts/create_microvm_image.sh"
environment = {
IMAGE_NAME = var.project_name
BASE_IMAGE_ARN = "arn:aws:lambda:${var.aws_region}:aws:microvm-image:al2023-1"
BUILD_ROLE_ARN = aws_iam_role.microvm_build.arn
ARTIFACT_URI = "s3://${aws_s3_bucket.artifact.id}/${aws_s3_object.app_artifact.key}"
MOUNT_TARGET_IP = data.aws_network_interface.s3files_mount_target.private_ip
MIN_MEMORY_MIB = var.microvm_memory_mb
AWS_REGION = var.aws_region
OUTPUT_FILE = "${path.module}/../.build/image_arn.txt"
}
}
}
O scripts/create_microvm_image.sh monta o comando de fato:
aws lambda-microvms create-microvm-image \
--name "${IMAGE_NAME}" \
--base-image-arn "${BASE_IMAGE_ARN}" \
--build-role-arn "${BUILD_ROLE_ARN}" \
--code-artifact "{\"uri\":\"${ARTIFACT_URI}\"}" \
--additional-os-capabilities '["ALL"]' \
--environment-variables "{\"S3FILES_MOUNT_TARGET\":\"${MOUNT_TARGET_IP}\"}" \
--resources "{\"minimumMemoryInMiB\":${MIN_MEMORY_MIB}}"
Reparei que a memória se divide em dois lugares diferentes do ciclo de vida: minimumMemoryInMiB na criação da imagem define o piso (o mesmo piso de 512 MB que o S3 Files exige pra habilitar leitura direta do S3), e a política de idle, que é por lançamento, só aparece depois, no run-microvm. Faz sentido: memória é característica da imagem (do jeito que a aplicação foi construída e testada), idle é característica de cada sessão.
scripts/ ficou só com os helpers que o Terraform chama (create_microvm_image.sh, create_network_connector.sh) e os de limpeza (delete_microvm_image.sh, delete_network_connector.sh, chamados no terraform destroy). Nada de script de teste ali. Isso é o próximo passo, na mão.
Pra provisionar tudo:
make init && make apply
# equivalente a:
# cd infra && terraform init && terraform apply
2.5 Rodando na mão: do lançamento ao teste
O docs/MANUAL.md documenta essa sequência inteira. Primeiro, pego os outputs do Terraform e lanço o MicroVM:
cd infra
IMAGE_ARN=$(terraform output -raw microvm_image_arn)
EXECUTION_ROLE_ARN=$(terraform output -raw microvm_execution_role_arn)
CONNECTOR_ARN=$(terraform output -raw network_connector_arn)
aws lambda-microvms run-microvm \
--image-identifier "${IMAGE_ARN}" \
--image-version "1.0" \
--execution-role-arn "${EXECUTION_ROLE_ARN}" \
--egress-network-connectors "[\"${CONNECTOR_ARN}\"]" \
--idle-policy '{"maxIdleDurationSeconds":300,"suspendedDurationSeconds":1800,"autoResumeEnabled":true}'
Sem variável de ambiente aqui: ela já foi resolvida lá atrás, na imagem. O comando devolve microvmId e endpointUrl. Com isso, gero um token de curta duração:
TOKEN=$(aws lambda-microvms create-microvm-auth-token \
--microvm-identifier <microvmId> \
--expiration-in-minutes 30 \
--allowed-ports '[{"port":5000}]' \
--query 'authToken."X-aws-proxy-auth"' --output text)
E testo o fluxo básico:
curl "<endpointUrl>/health" -H "X-aws-proxy-auth: ${TOKEN}"
curl -X POST "<endpointUrl>/files" -H "X-aws-proxy-auth: ${TOKEN}" -F "file=@notas.txt"
curl "<endpointUrl>/files" -H "X-aws-proxy-auth: ${TOKEN}"
2.6 Provando a durabilidade: terminate e relança
Até aqui, nada muito diferente do disco local. A prova de verdade vem agora: em vez de deixar o MicroVM só suspender, eu termino ele por completo e lanço um MicroVM novo, do zero, a partir da mesma imagem.
aws lambda-microvms terminate-microvm --microvm-identifier <microvmId-original>
# lança um MicroVM completamente novo, com um microvmId diferente
aws lambda-microvms run-microvm \
--image-identifier "${IMAGE_ARN}" \
--image-version "1.0" \
--execution-role-arn "${EXECUTION_ROLE_ARN}" \
--egress-network-connectors "[\"${CONNECTOR_ARN}\"]" \
--idle-policy '{"maxIdleDurationSeconds":300,"suspendedDurationSeconds":1800,"autoResumeEnabled":true}'
Gero um token novo pro microvmId novo, e chamo GET /files de novo:
notas.txt aparece no GET /files do MicroVM novo, com um microvmId que nunca existiu antes. O snapshot desse MicroVM não tinha nenhuma referência a esse arquivo, porque o arquivo nunca esteve no disco local pra começo de conversa. Ele sempre foi um objeto no bucket S3 por trás, só sendo lido através do mount do S3 Files.
2.7 Limpando
Antes do terraform destroy, preciso terminar qualquer MicroVM ativo na mão:
aws lambda-microvms terminate-microvm --microvm-identifier <microvmId-atual>
O motivo é de rede, não de custo: um MicroVM em execução mantém uma interface de rede anexada às subnets privadas via egress network connector, e o destroy da VPC falha enquanto essa ENI ainda existir. Só depois disso:
make destroy
# equivalente a:
# cd infra && terraform destroy
3.0 O que eu levo em conta ao usar isso
3.1 Durabilidade e estado
S3 Files resolve um problema que nem suspensão longa nem export manual resolvem bem sozinhos. Disco local sobrevive suspensão mas está preso ao teto de 8 horas e ao ciclo de vida daquele MicroVM específico. Escrever direto no bucket via boto3 funciona, mas exige um passo explícito de upload que é fácil de esquecer. Montar S3 Files tira essa escolha do meio: a escrita já cai como objeto no bucket, sem passo extra, e ainda por cima falando com o S3, que é o storage que eu já confio.
512 MB de memória é o corte pra leitura direta do S3, e agora isso vive na imagem, não no lançamento. Abaixo desse valor o mount ainda funciona, mas sem o caminho otimizado de leitura direta do S3 Files. Definir minimumMemoryInMiB na criação da imagem em vez de deixar solto no run-microvm evita que alguém lance um MicroVM subdimensionado sem perceber.
3.2 Segurança e acesso
A capability de montagem é opt-in por um motivo, e eu penso duas vezes antes de habilitar. --additional-os-capabilities '["ALL"]' amplia bastante o que o container consegue fazer dentro do MicroVM, não é só liberação pontual pra mount. Vale pesar isso contra o quanto de isolamento eu realmente preciso pro caso de uso. Pra rodar código gerado por IA de origem desconhecida, por exemplo, eu penso duas vezes antes de dar essa capability extra.
Permissão de S3 Files é separada de permissão de S3 comum, e agora percebi que são três papéis, não dois. s3files:ClientMount e s3files:ClientWrite na role de execução do MicroVM cobrem o acesso via NFS. Mas o próprio S3 Files também precisa de uma role de sync (assumindo elasticfilesystem.amazonaws.com) pra escrever de volta no bucket, com versionamento obrigatório. Três papéis diferentes fazendo três coisas diferentes, fácil de confundir se você já tem uma policy de S3 pronta e assume que ela cobre tudo.
Rede não é implícita, é uma peça que eu declaro. Sem o egress network connector apontando pra minha VPC, o MicroVM simplesmente não alcança o mount target do S3 Files. Isso é bom: rede que só existe porque eu declarei explicitamente é mais fácil de auditar do que rede que "só funciona" por padrão.
3.3 Padrões de implementação
Variável de ambiente pro mount vai na imagem, não no lançamento, e isso é fácil de errar uma vez. Da primeira vez que tentei passar S3FILES_MOUNT_TARGET no run-microvm, o entrypoint.sh simplesmente não via o valor. Variáveis de ambiente do MicroVM são resolvidas em build time, junto com o resto do snapshot. Isso é consistente com a lição do post anterior sobre estado congelado, só que na direção contrária: aqui eu preciso que congele, porque é informação de infraestrutura, não de sessão.
O provider do Terraform não expõe DNS do mount target do S3 Files, só a ENI. Diferente do EFS, que tem um endpoint DNS previsível, tive que resolver o IP privado da interface de rede via data "aws_network_interface" filtrando pela descrição e pela subnet. Funciona, mas é um detalhe frágil: se a AWS mudar o padrão de descrição da ENI, esse filtro quebra.
Montar em runtime (dentro do container), não em build time (na instrução RUN do Dockerfile), continua valendo. O mount em si ainda acontece no entrypoint.sh quando o container sobe, porque o mount target só é alcançável quando a rede da imagem já está configurada. A variável de ambiente pode congelar no build, a chamada de mount não pode.
Múltiplos MicroVMs podem montar o mesmo file system ao mesmo tempo, e isso muda o desenho. Diferente do disco local, que é exclusivo daquele MicroVM, o S3 Files permite compartilhar workspace entre sessões diferentes se fizer sentido pro seu caso. Vale filtrar por sessão dentro da aplicação (um prefixo de diretório por usuário, por exemplo) pra não misturar dado de gente diferente sem querer.
NFS tem custo de latência que disco local não tem. Toda leitura e escrita passa pela rede até o mount target. Pra arquivo pequeno e acesso frequente isso raramente é perceptível, mas pra workload de I/O intenso vale medir antes de assumir que dá na mesma.
3.4 Observabilidade
Powertools funciona em MicroVMs, mas nem todo pedaço faz sentido do mesmo jeito. O Logger estruturado em JSON é ganho direto, sem precisar escrever formatter na mão. Mas inject_lambda_context, que injeta cold_start e function_request_id automaticamente, foi desenhado pra um lambda_handler(event, context) que roda uma vez por invocação. Aqui é um servidor FastAPI de longa duração, sem um context novo a cada request, então esse pedaço específico eu deixei de fora. Vale essa checagem sempre que trouxer uma lib pensada pra Function pra dentro de um MicroVM: parte do contrato pode não existir mais.
3.5 Arquitetura e deploy
Terraform e SAM/CloudFormation não competem aqui, cada um cobre uma parte diferente do mesmo problema. SAM tem recurso nativo pra AWS::Lambda::MicrovmImage desde o lançamento, o provider Terraform da AWS, não. Daí o null_resource + local-exec como ponte enquanto o issue #48526 não fecha. Em compensação, o Terraform já tem recursos nativos pra S3 Files (aws_s3files_*) desde a v6.40.0, o que deixa o resto do laboratório mais direto de escrever do que ficaria em CloudFormation puro. Escolher a stack de IaC também é escolher onde você aceita workaround (palavra chique para gambiarra).
Imagem entra no pipeline de IaC, lançamento não, e isso ficou ainda mais explícito nessa versão do repositório. O Makefile só tem init, plan, apply e destroy, de propósito, sem run, sem test. Lançar um MicroVM é decisão de runtime, documentada em docs/MANUAL.md como sequência manual de comandos, não escondida atrás de um alvo de Makefile que faria parecer parte do provisionamento.
MicroVMs e Functions não competem, se complementam. O próprio anúncio da AWS deixa isso explícito: Functions seguem sendo a escolha certa pro backbone evento-resposta da aplicação, e MicroVMs entra especificamente na etapa que precisa rodar código não confiável (gerado por usuário ou por IA) isolado, com estado que precisa sobreviver além de uma única execução.
3.6 Limites honestos deste laboratório
Não tem CI rodando isso. Os nomes de alguns parâmetros de CLI e os principals de trust policy (elasticfilesystem.amazonaws.com pro papel de sync, por exemplo) eu confirmei o suficiente pra funcionar no meu teste, mas não bati contra a referência oficial linha por linha. Vale conferir antes de replicar em produção. A criação de imagem não é totalmente idempotente: rodar create_microvm_image.sh duas vezes cria uma versão nova em vez de atualizar in-place. E a ponte via null_resource + local-exec pro image/connector é workaround assumido, não o caminho idiomático. Se seu time prioriza Terraform 100% nativo pra esse pedaço específico, SAM ainda é a opção mais direta hoje.
4.0 Conclusão
O que mais me chamou atenção nesse anúncio foi perceber que a AWS não criou uma tecnologia nova, ela deu acesso direto a uma peça que já sustentava o Lambda por trás dos panos há anos. Isso é coerente quando se entende a forma que o Firecracker funciona: entender a engenharia por baixo ajuda a prever pra onde o produto vai. E o Powertools que usei aqui pro Logger é o mesmo que detalhei naquele post sobre observabilidade serverless. O Logger continua valendo em MicroVM, só o contrato de invocação que muda. Trocar SAM por Terraform pra montar esse laboratório não mudou a história que o artigo conta, só me obrigou a ser mais explícito sobre o que cada stack cobre nativamente e onde ainda falta peça. Pensei em testar isso e ver até onde dá pra empurrar: rodar um agente de IA gerando e executando código de verdade dentro de um MicroVM com esse mesmo workspace compartilhado via S3 Files, e medir o custo real de manter várias sessões vivas ao mesmo tempo, isso pode gerar um novo artigo pra compartilhar.




Top comments (0)