Série Firecracker:
- Parte 01: Firecracker
- Parte 02: Construindo um nano-Lambda (você está aqui)
- Parte 03: Redes no Firecracker
- Parte 04: Snapshots
- Parte 05: Firecracker em produção
No artigo anterior, você subiu sua primeira microVM com Firecracker. Viu o Linux iniciar, entrou pelo terminal, explorou o sistema. Legal, né?
Mas você também deve ter pensado: “Tá, e daí? Subi uma VM mínima… e agora?”
Agora a gente vai fazer algo útil. Vamos construir — em Python, com menos de 150 linhas de código — um sistema que funciona parecido com o AWS Lambda.
Você passa uma função. O sistema sobe uma microVM isolada. Executa a função lá dentro. Retorna o resultado. Destrói a VM.
É basicamente isso que o Lambda faz. Claro, o Lambda real é muito mais sofisticado (mantém VMs prontas pra reusar, tira “fotos” da memória pra restaurar rápido, milhares de otimizações). Mas o conceito central? É esse.
Como o Lambda funciona — versão honesta
Antes de construir, vamos entender o que estamos imitando.
Quando você invoca uma função Lambda, mais ou menos isso acontece:

O pulo do gato é que a AWS mantém várias microVMs prontas esperando. Então quando sua requisição chega, geralmente já tem uma VM “quente” disponível. Por isso Lambda consegue responder em milissegundos mesmo sendo VM.
A gente não vai fazer esse esquema de manter VMs prontas. Seria complexo demais pra um artigo. Nosso nano-Lambda vai ser mais simples: uma VM por execução, criada na hora, destruída depois.
Ineficiente? Sim. Didático? Muito.
O que vamos construir
Nosso nano-Lambda vai ser um script Python que:
- Recebe o caminho de uma função Python
- Configura e inicia uma microVM Firecracker
- Copia a função pra dentro da VM (colocando no “disco” dela)
- Executa a função
- Captura o que a função imprimiu na tela
- Para e limpa a VM
- Retorna o resultado
E pra exemplo prático, nossa função vai gerar QR Codes. Você passa um texto, recebe uma imagem PNG. Algo visual, útil, que você pode testar com o celular.
Preparando o terreno
Vou assumir que você já tem o Firecracker funcionando do artigo anterior. Se não, volta lá primeiro.
Você vai precisar de:
- Firecracker binário (já tem)
- Kernel Linux (já tem)
- Rootfs customizado (vamos criar) — o “disco” da VM com Python instalado
- Python 3 no seu computador (não na VM, no seu mesmo)
- Biblioteca
requestsdo Python
Instala as dependências se não tiver:
pip install requests requests-unixsocket
Customizando o rootfs
Lembra que usamos um rootfs pronto da AWS no Hello World? Aquele era um Alpine Linux pelado — não tinha nem Python.
Pra rodar funções Python, precisamos de um rootfs com Python instalado. Vamos criar um.
O processo em alto nível
A ideia é simples: criar uma imagem de disco vazia, instalar um Linux mínimo nela, adicionar Python e as bibliotecas que precisamos, e configurar pra executar nossa função no boot.
Passo 1: Criar a imagem de disco
Precisamos de um arquivo que vai funcionar como o “HD” da microVM. Criamos um arquivo vazio e formatamos ele pra virar um disco:
dd if=/dev/zero of=rootfs-python.ext4 bs=1M count=500
mkfs.ext4 rootfs-python.ext4
500MB é mais que suficiente pro nosso caso.
Passo 2: Montar e instalar o sistema base
Montamos a imagem como um disco normal e usamos Docker ou Podman pra “extrair” um Alpine Linux pra dentro dela:
sudo mount rootfs-python.ext4 /mnt/rootfs
# Se o mount reclamar "not a block device", adicione: sudo mount -o loop rootfs-python.ext4 /mnt/rootfs
# Com Docker:
sudo docker run --rm -v /mnt/rootfs:/rootfs alpine:3.21 sh -c \
'apk add --root /rootfs --initdb alpine-base openrc python3 py3-pillow py3-qrcode'
# Ou com Podman:
sudo podman run --rm -v /mnt/rootfs:/rootfs alpine:3.21 sh -c \
'apk add --root /rootfs --initdb alpine-base openrc python3 py3-pillow py3-qrcode'
Por que Docker/Podman? Porque o apk (o gerenciador de pacotes do Alpine, tipo apt do Debian ou dnf do Fedora) consegue instalar pacotes direto numa pasta que você escolher. Muito mais simples que baixar e configurar tudo na mão.
Passo 3: Configurar o sistema
Agora precisamos configurar algumas coisas dentro do rootfs. Pra isso usamos chroot — um comando que “finge” que um diretório é a raiz do sistema. Quando você executa chroot /mnt/rootfs /bin/sh, você abre um shell onde /mnt/rootfs vira /. É como se você tivesse “entrado” no Alpine que acabou de instalar.
sudo chroot /mnt/rootfs /bin/sh
Lá dentro, configuramos algumas coisas. O Alpine mínimo vem com o vi pra edição de arquivos, mas se preferir algo mais amigável, pode instalar o nano:
apk add nano
Hostname e rede básica:
echo "nano-lambda" > /etc/hostname
echo "127.0.0.1 localhost" > /etc/hosts
echo "::1 localhost" >> /etc/hosts
Inittab para Lambda-style — o Alpine usa o arquivo /etc/inittab pra definir o que roda no boot. Por padrão ele abre um terminal de login (getty) que ficaria esperando alguém digitar. Pra nosso caso de Lambda, queremos que a VM rode a função e desligue automaticamente, sem ficar esperando. Então substituímos o inittab padrão por uma versao que executa diretamente nossa funcao via ::wait:/run-function.sh:
cat > /etc/inittab << 'EOF'
# Configurado para microVM Lambda-style
::sysinit:/sbin/openrc sysinit
::sysinit:/sbin/openrc boot
::wait:/sbin/openrc default
::wait:/run-function.sh
::ctrlaltdel:/sbin/reboot
::shutdown:/sbin/openrc shutdown
EOF
A linha ::wait:/run-function.sh e o segredo: ela diz pro init “depois de rodar o runlevel default, execute esse script e espere ele terminar”. Como o script termina com poweroff -f, a VM desliga automaticamente apos executar a funcao.
Diretórios de trabalho — criamos duas pastas: /functions vai receber o código Python que queremos executar (nosso handler.py), e /output é onde a função pode salvar resultados (como o QR Code que vamos gerar):
mkdir -p /functions /output
Script de execução — esse é o coração do nano-Lambda. Criamos um script que roda no boot, executa nossa função Python, e desliga a VM:
cat > /run-function.sh << 'EOF'
#!/bin/sh
echo "=== nano-Lambda executando... ==="
if [ -f /functions/handler.py ]; then
cd /functions
python3 handler.py
echo "=== Execucao finalizada ==="
else
echo "ERRO: handler.py nao encontrado"
fi
sync
sleep 1
poweroff -f
EOF
chmod +x /run-function.sh
Pronto! Agora saimos do chroot com exit.
Passo 4: Limpar e desmontar
Removemos arquivos temporários de instalação (economiza espaço) e “ejetamos” o disco:
sudo umount /mnt/rootfs
O script pronto
Os passos acima mostram o que está acontecendo por baixo dos panos. Se quiser automatizar, no repositório do artigo tem um script build-rootfs.sh que faz tudo isso. Ele detecta se você tem Docker ou Podman e usa o que estiver disponível.
# Baixa o script
curl -LO https://raw.githubusercontent.com/dklima/firecracker-na-pratica/main/02-nano-lambda/build-rootfs.sh
chmod +x build-rootfs.sh
# Executa (precisa de sudo)
sudo ./build-rootfs.sh
Em alguns minutos você terá um rootfs-python.ext4 com ~150MB contendo:
- Alpine Linux 3.21
- Python 3
- Pillow (processamento de imagem)
- qrcode (geração de QR codes)
A função de exemplo: gerador de QR Code
Vamos criar uma função simples que lê um texto de um arquivo e gera um QR Code. O código está comentado explicando o que cada bloco faz.
Salva como exemplo-qrcode/handler.py:
#!/usr/bin/env python3
"""
Função nano-Lambda: Gerador de QR Code
Lê o texto de /functions/input.txt e gera um QR Code em /output/qrcode.png
"""
import qrcode
import sys
import base64
def main():
try:
with open('/functions/input.txt', 'r') as f:
text = f.read().strip()
except FileNotFoundError:
print("ERRO: /functions/input.txt não encontrado")
sys.exit(1)
if not text:
print("ERRO: input.txt está vazio")
sys.exit(1)
print(f"Gerando QR Code para: {text}")
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=4,
)
qr.add_data(text)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
output_path = '/output/qrcode.png'
img.save(output_path)
# Também imprime em base64 pro stdout (pra capturar fora da VM)
with open(output_path, 'rb') as f:
img_base64 = base64.b64encode(f.read()).decode('utf-8')
print(f"QR Code gerado com sucesso!")
print(f"BASE64_IMAGE_START")
print(img_base64)
print(f"BASE64_IMAGE_END")
if __name__ == '__main__':
main()
Essa função:
- Lê um texto de
/functions/input.txt - Gera um QR Code
- Salva em
/output/qrcode.png - Imprime a imagem codificada em texto (Base64) pra gente conseguir capturar de fora da VM
O nano-Lambda: controlando Firecracker com Python

Agora o coração do sistema. Vamos criar um script Python que:
- Copia a função pro rootfs
- Inicia o Firecracker
- Captura o que a VM imprimiu
- Extrai o resultado e salva
O código está comentado explicando o que cada bloco faz.
Salva como nano-lambda.py:
#!/usr/bin/env python3
"""
nano-Lambda: Um Lambda caseiro usando Firecracker
Executa funções Python em microVMs isoladas.
"""
import subprocess
import requests
import requests_unixsocket
import time
import shutil
import tempfile
import base64
import sys
import os
from pathlib import Path
# Configurações
FIRECRACKER_BIN = "./firecracker"
KERNEL_PATH = "./vmlinux.bin"
ROOTFS_TEMPLATE = "./rootfs-python.ext4"
SOCKET_PATH = "/tmp/firecracker-nanolambda.socket"
VCPU_COUNT = 1
MEM_SIZE_MIB = 256
class NanoLambda:
def __init__(self):
self.socket_path = SOCKET_PATH
self.fc_process = None
self.temp_rootfs = None
self.output_file = "/tmp/firecracker-output.log"
def _api_url(self, path):
"""Converte path pra URL do socket Unix"""
encoded_socket = self.socket_path.replace("/", "%2F")
return f"http+unix://{encoded_socket}{path}"
def _call_api(self, method, path, data=None):
"""Faz chamada pra API do Firecracker"""
session = requests_unixsocket.Session()
url = self._api_url(path)
if method == "PUT":
resp = session.put(url, json=data)
elif method == "GET":
resp = session.get(url)
else:
raise ValueError(f"Método não suportado: {method}")
if resp.status_code >= 400:
raise Exception(f"API error {resp.status_code}: {resp.text}")
return resp
def prepare_rootfs(self, function_path, input_data):
"""Prepara o rootfs com a função e input"""
self.temp_rootfs = tempfile.NamedTemporaryFile(
suffix='.ext4',
delete=False
).name
print(f"[*] Copiando rootfs template...")
shutil.copy(ROOTFS_TEMPLATE, self.temp_rootfs)
mount_point = tempfile.mkdtemp()
try:
print(f"[*] Montando rootfs temporário...")
subprocess.run(
["sudo", "mount", self.temp_rootfs, mount_point],
check=True
)
print(f"[*] Copiando função: {function_path}")
func_dest = os.path.join(mount_point, "functions", "handler.py")
subprocess.run(
["sudo", "cp", function_path, func_dest],
check=True
)
input_dest = os.path.join(mount_point, "functions", "input.txt")
subprocess.run(
["sudo", "sh", "-c", f"echo '{input_data}' > {input_dest}"],
check=True
)
finally:
subprocess.run(["sudo", "umount", mount_point], check=True)
os.rmdir(mount_point)
def start_firecracker(self):
"""Inicia o processo Firecracker"""
if os.path.exists(self.socket_path):
os.remove(self.socket_path)
if os.path.exists(self.output_file):
os.remove(self.output_file)
print(f"[*] Iniciando Firecracker...")
self.output_handle = open(self.output_file, 'w')
self.fc_process = subprocess.Popen(
["sudo", FIRECRACKER_BIN, "--api-sock", self.socket_path],
stdout=self.output_handle,
stderr=subprocess.STDOUT
)
for _ in range(50):
if os.path.exists(self.socket_path):
break
time.sleep(0.1)
else:
raise Exception("Timeout esperando socket do Firecracker")
time.sleep(0.2)
def configure_vm(self):
"""Configura a microVM via API"""
print(f"[*] Configurando kernel...")
self._call_api("PUT", "/boot-source", {
"kernel_image_path": KERNEL_PATH,
"boot_args": "console=ttyS0 reboot=k panic=1 pci=off quiet"
})
print(f"[*] Configurando rootfs...")
self._call_api("PUT", "/drives/rootfs", {
"drive_id": "rootfs",
"path_on_host": self.temp_rootfs,
"is_root_device": True,
"is_read_only": False
})
print(f"[*] Configurando recursos ({VCPU_COUNT} vCPU, {MEM_SIZE_MIB}MB RAM)...")
self._call_api("PUT", "/machine-config", {
"vcpu_count": VCPU_COUNT,
"mem_size_mib": MEM_SIZE_MIB
})
def run_vm(self, timeout=30):
"""Inicia a VM e aguarda execução"""
print(f"[*] Iniciando microVM...")
self._call_api("PUT", "/actions", {"action_type": "InstanceStart"})
print(f"[*] Aguardando execução (timeout: {timeout}s)...")
start_time = time.time()
while time.time() - start_time < timeout:
if self.fc_process.poll() is not None:
break
time.sleep(0.5)
self.output_handle.close()
if self.fc_process.poll() is None:
self.fc_process.terminate()
try:
self.fc_process.wait(timeout=5)
except subprocess.TimeoutExpired:
self.fc_process.kill()
self.fc_process.wait()
if os.path.exists(self.output_file):
with open(self.output_file, 'r') as f:
output = f.read()
else:
output = ""
return output
def cleanup(self):
"""Limpa recursos"""
print(f"[*] Limpando...")
if hasattr(self, 'output_handle') and not self.output_handle.closed:
self.output_handle.close()
if self.fc_process and self.fc_process.poll() is None:
self.fc_process.terminate()
try:
self.fc_process.wait(timeout=5)
except subprocess.TimeoutExpired:
self.fc_process.kill()
if self.temp_rootfs and os.path.exists(self.temp_rootfs):
os.remove(self.temp_rootfs)
if os.path.exists(self.socket_path):
os.remove(self.socket_path)
if os.path.exists(self.output_file):
os.remove(self.output_file)
def invoke(self, function_path, input_data):
"""Invoca uma função Lambda-style"""
try:
self.prepare_rootfs(function_path, input_data)
self.start_firecracker()
self.configure_vm()
output = self.run_vm()
return self.parse_output(output)
finally:
self.cleanup()
def parse_output(self, raw_output):
"""Extrai resultado do output bruto"""
# Procura pelo marcador de imagem base64
if "BASE64_IMAGE_START" in raw_output and "BASE64_IMAGE_END" in raw_output:
start = raw_output.find("BASE64_IMAGE_START") + len("BASE64_IMAGE_START")
end = raw_output.find("BASE64_IMAGE_END")
base64_data = raw_output[start:end].strip()
return {
"success": True,
"type": "image",
"data": base64_data
}
return {
"success": False,
"type": "text",
"data": raw_output
}
def main():
if len(sys.argv) < 3:
print("Uso: python nano-lambda.py <função.py> <input />")
print("Exemplo: python nano-lambda.py exemplo-qrcode/handler.py 'https://meusite.com'")
sys.exit(1)
function_path = sys.argv[1]
input_data = sys.argv[2]
if not os.path.exists(function_path):
print(f"Erro: função não encontrada: {function_path}")
sys.exit(1)
print("=" * 50)
print("nano-Lambda: Executando função em microVM isolada")
print("=" * 50)
print(f"Função: {function_path}")
print(f"Input: {input_data}")
print()
lambda_runner = NanoLambda()
result = lambda_runner.invoke(function_path, input_data)
print()
print("=" * 50)
print("Resultado:")
print("=" * 50)
if result["success"] and result["type"] == "image":
output_file = "resultado-qrcode.png"
img_data = base64.b64decode(result["data"])
with open(output_file, "wb") as f:
f.write(img_data)
print(f"QR Code salvo em: {output_file}")
print(f"Tamanho: {len(img_data)} bytes")
print()
print("Escaneie com seu celular para testar!")
else:
print("Output bruto da VM:")
print(result["data"])
if __name__ == "__main__":
main()
Testando!
Agora é a hora da verdade. Com tudo preparado:
# Estrutura esperada no diretório:
# .
# ??? firecracker (binário)
# ??? vmlinux.bin (kernel)
# ??? rootfs-python.ext4 (rootfs customizado)
# ??? nano-lambda.py (nosso script)
# ??? exemplo-qrcode/
# ??? handler.py (função de exemplo)
# Executando
sudo python3 nano-lambda.py exemplo-qrcode/handler.py "https://fogonacaixadagua.com.br"
Se tudo der certo, você vai ver algo assim:
==================================================
nano-Lambda: Executando função em microVM isolada
==================================================
Função: exemplo-qrcode/handler.py
Input: https://fogonacaixadagua.com.br
[*] Copiando rootfs template...
[*] Montando rootfs temporário...
[*] Copiando função: exemplo-qrcode/handler.py
[*] Iniciando Firecracker...
[*] Configurando kernel...
[*] Configurando rootfs...
[*] Configurando recursos (1 vCPU, 256MB RAM)...
[*] Iniciando microVM...
[*] Aguardando execução (timeout: 30s)...
[*] Limpando...
==================================================
Resultado:
==================================================
QR Code salvo em: resultado-qrcode.png
Tamanho: 1847 bytes
Escaneie com seu celular para testar!
Abre o arquivo resultado-qrcode.png. Aponta a câmera do celular. Se abrir o site, funcionou!
Troubleshooting: Se o script der erro no meio da execução, pode ser que tenha ficado uma montagem presa. Verifique com
mount | grep tmpe desmonte manualmente comsudo umount /tmp/tmp.*se necessário.
Você acabou de executar código Python dentro de uma microVM completamente isolada, que você controlou do início ao fim. Isso é, em essência, o que o AWS Lambda faz.
O que simplificamos vs Lambda real
Pra ser honesto, nosso nano-Lambda é um brinquedo comparado com o Lambda de verdade. Algumas diferenças:
VMs prontas pra reusar: Lambda mantém VMs “quentes” esperando. A gente cria uma nova toda vez. Muito mais lento.
Fotos da memória: Lambda tira “screenshots” do estado da VM pra restaurar instantaneamente depois. A gente não fez isso.
Internet: Nossas microVMs não têm acesso à internet. Lambda configura rede completa.
Várias linguagens: Lambda suporta Node, Python, Java, Go, .NET, Ruby… A gente só fez Python.
Escala: Lambda aguenta milhares de execuções ao mesmo tempo. A gente roda uma por vez.
Logs e monitoramento: Lambda tem CloudWatch integrado. A gente só vê o que aparece na tela.
Mas… o conceito central tá ali. Você entendeu como funciona. Agora quando usar Lambda, vai saber o que acontece por baixo.
Experimentando mais
Algumas ideias se quiser continuar explorando:
Outras funções: Tenta criar uma função que faz algo diferente. Processamento de texto, cálculos, validações. Só precisa ler de /functions/input.txt e imprimir o resultado na tela.
Medição de tempo: Adicione marcações de tempo no nano-lambda.py pra ver quanto cada etapa demora. Você vai notar que a maior parte do tempo é pra ligar a VM, não pra rodar a função.
Várias execuções: Modifique pra rodar a mesma função várias vezes e veja a média de tempo.
Internet na VM: Se quiser que a VM acesse a internet, tem que configurar rede virtual no seu computador. Mais complexo, mas possível — e é o tema do próximo artigo!
Conclusão
Você construiu um Lambda caseiro. Não vai substituir o Lambda da AWS (óbvio), mas agora você entende o que acontece quando faz aws lambda invoke.
Firecracker é uma tecnologia fascinante. Ela resolve um problema real — isolamento com velocidade — de forma elegante. E o fato de ser open source significa que você pode estudar, experimentar, e até contribuir.
No próximo artigo, vamos configurar rede no Firecracker — fazer a VM ter acesso à internet de verdade. Aí sim as possibilidades ficam interessantes.
Até lá!
Este artigo faz parte da série “Firecracker: MicroVMs na Prática”. Todo o código está disponível em GitHub.





Deixe um comentário