Pular para o conteúdo
, , , ,

Construindo um nano-Lambda: como serverless funciona por dentro [pt.2]

Construa um mini AWS Lambda com Firecracker e Python: execute funções em microVMs isoladas e entenda como serverless funciona por dentro.

Avatar de DK
DKTrabalha com Linux e Unix a mais de 23 anos e possui as certificações LPI 3, RHCE, AIX e VIO.

08 dez, 2025
12 min de leitura

Série Firecracker:


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:

Como o Lambda funciona

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:

  1. Recebe o caminho de uma função Python
  2. Configura e inicia uma microVM Firecracker
  3. Copia a função pra dentro da VM (colocando no “disco” dela)
  4. Executa a função
  5. Captura o que a função imprimiu na tela
  6. Para e limpa a VM
  7. 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 requests do 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:

  1. Lê um texto de /functions/input.txt
  2. Gera um QR Code
  3. Salva em /output/qrcode.png
  4. Imprime a imagem codificada em texto (Base64) pra gente conseguir capturar de fora da VM

O nano-Lambda: controlando Firecracker com Python

Diagrama do fluxo do nano-Lambda

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 tmp e desmonte manualmente com sudo 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.

Avatar de DK

Comentários

Deixe um comentário

Seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Ir para