Atualizado em 08/06/2026
Série Firecracker:
- Parte 01: Firecracker
- Parte 02: Construindo um nano-Lambda
- Parte 03: Redes no Firecracker
- Parte 04: Snapshots
- Parte 05: Firecracker em produção (você está aqui)
Nos artigos anteriores, a gente construiu um nano-Lambda funcional. Sobe microVM, executa função, derruba microVM. Com rede. Com snapshots pra performance. Tudo lindo.
Mas tem um problema: você tá rodando tudo na mão. sudo python3 nano-lambda.py toda vez. Se o servidor reiniciar às 3h da manhã? A microVM sumiu. Se o processo morrer por falta de memória? Ninguém vai saber. Se você precisar rodar 5 VMs diferentes? Abre 5 terminais.
Isso não é produção. Isso é um demo.
Hoje a gente transforma o demo em serviço de verdade. Vamos usar o systemd, o gerenciador de serviços que já vem no seu Linux, pra fazer as microVMs:
- Iniciarem automaticamente no boot do servidor
- Reiniciarem se morrerem
- Terem rede configurada automaticamente
- Registrarem logs consultáveis
- Rodarem com isolamento de segurança
Por que systemd?
Se você já trabalhou com servidores Linux, provavelmente já usou systemd sem perceber. systemctl start nginx, systemctl enable postgresql… tudo isso é systemd.
O systemd é o “init system” da maioria das distros modernas. Ele é o primeiro processo que roda quando o Linux inicia (PID 1), e é responsável por subir todos os outros serviços na ordem certa.
Pra nossa microVM, o systemd resolve vários problemas de uma vez:
| Problema | Solução systemd |
|---|---|
| “Esqueci de iniciar a VM” | systemctl enable, inicia no boot |
| “A VM morreu e ninguém viu” | Restart=on-failure, reinicia automático |
| “Cadê os logs?” | journalctl -u, logs centralizados |
| “A rede não subiu antes da VM” | After=network.target, dependências |
| “Preciso de 5 VMs iguais” | Templates com @, uma unit, N instâncias |
O que vamos construir
Vamos pegar a microVM com rede do artigo 03 e transformar ela num serviço que fica no ar. Atenção a essa diferença, porque ela muda tudo: no artigo 03 a VM rodava a função uma vez e se desligava (modelo Lambda). Um serviço de verdade precisa ficar rodando, então aqui a VM sobe, configura a rede e continua viva, pronta pra receber trabalho. O resultado final:
# Inicia a microVM como serviço
sudo systemctl start nano-lambda
# Verifica status
sudo systemctl status nano-lambda
# Vê os logs
sudo journalctl -u nano-lambda -f
# Habilita pra iniciar no boot
sudo systemctl enable nano-lambda
A rede (interface TAP, NAT, regras de firewall) vai ser configurada automaticamente quando o serviço iniciar, e limpa quando parar.
Pré-requisito: Se você não leu o artigo 03 sobre redes, leia antes de continuar. Lá a gente explica por que a configuração de rede funciona assim. Aqui a gente só vai automatizar o que você já aprendeu.
Anatomia de uma unit file
Antes de meter a mão na massa, um glossário rápido. O systemd usa arquivos .service (chamados “unit files”) pra definir como um serviço funciona. A estrutura básica:
[Unit]
Description=Minha MicroVM
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/firecracker --api-sock /run/firecracker/vm.socket
Restart=on-failure
[Install]
WantedBy=multi-user.target
Três seções:
- [Unit]: Metadados e dependências. “Quem sou eu” e “de quem dependo”
- [Service]: Como rodar. O executável, variáveis de ambiente, política de restart
- [Install]: Como instalar. Em qual “target” (modo de operação) esse serviço deve rodar
Os campos mais importantes pra gente:
| Campo | O que faz |
|---|---|
After= |
Espera outros serviços iniciarem primeiro |
Type=simple |
O processo principal é o próprio ExecStart |
ExecStartPre= |
Comandos pra rodar ANTES do serviço principal |
ExecStopPost= |
Comandos pra rodar DEPOIS do serviço parar |
Restart=on-failure |
Reinicia se o processo morrer com erro |
RuntimeDirectory= |
Cria diretório em /run/ com permissões corretas |
Passo 1: O script de setup de rede
Primeiro, vamos criar um script que configura toda a rede necessária pra microVM. Isso inclui:
- Criar a interface TAP
- Configurar IP
- Habilitar IP forwarding
- Configurar NAT/masquerading
- Bloquear acesso à rede local (segurança)
Salve como /usr/local/bin/firecracker-network-setup.sh:
#!/bin/bash
# firecracker-network-setup.sh
# Configura rede para microVM Firecracker
#
# Uso:
# ./firecracker-network-setup.sh up # Cria interface TAP e configura NAT
# ./firecracker-network-setup.sh down # Remove interface TAP
#
# Variáveis de ambiente (opcionais):
# TAP_DEV - Nome da interface TAP (default: tap0)
# TAP_IP - IP do host na interface TAP (default: 172.16.0.1)
# TAP_CIDR - Máscara CIDR (default: 24)
set -e
TAP_DEV="${TAP_DEV:-tap0}"
TAP_IP="${TAP_IP:-172.16.0.1}"
TAP_CIDR="${TAP_CIDR:-24}"
# Subnet da VM derivada do TAP_IP (172.16.0.1 -> 172.16.0.0/24)
GUEST_NETWORK="${TAP_IP%.*}.0/${TAP_CIDR}"
ACTION="${1:-up}"
# Cores para output (desabilita se não for terminal)
if [ -t 1 ]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
else
RED=''
GREEN=''
YELLOW=''
NC=''
fi
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
check_root() {
if [ "$EUID" -ne 0 ]; then
log_error "Este script precisa ser executado como root"
exit 1
fi
}
setup_network() {
log_info "Configurando rede para microVM..."
log_info " TAP Device: $TAP_DEV"
log_info " TAP IP: $TAP_IP/$TAP_CIDR"
# Verifica se já existe
if ip link show "$TAP_DEV" &>/dev/null; then
log_warn "Interface $TAP_DEV ja existe, pulando criacao"
else
# Cria interface TAP
ip tuntap add dev "$TAP_DEV" mode tap
ip addr add "${TAP_IP}/${TAP_CIDR}" dev "$TAP_DEV"
ip link set "$TAP_DEV" up
log_info "Interface $TAP_DEV criada"
fi
# Habilita IP forwarding
sysctl -w net.ipv4.ip_forward=1 > /dev/null
log_info "IP forwarding habilitado"
# Configura firewall (detecta firewalld ou iptables)
if command -v firewall-cmd &>/dev/null && systemctl is-active firewalld &>/dev/null; then
setup_firewalld
else
setup_iptables
fi
log_info "Rede configurada com sucesso"
}
setup_firewalld() {
log_info "Configurando firewalld..."
# Adiciona TAP à zona trusted
firewall-cmd --zone=trusted --add-interface="$TAP_DEV" 2>/dev/null || true
# Habilita masquerading (NAT)
firewall-cmd --add-masquerade 2>/dev/null || true
# Isolamento - parte 1: rich rules (cadeia INPUT, tráfego destinado ao host)
firewall-cmd --zone=trusted --remove-rich-rule="rule family=ipv4 source address=${GUEST_NETWORK} destination address=10.0.0.0/8 drop" 2>/dev/null || true
firewall-cmd --zone=trusted --remove-rich-rule="rule family=ipv4 source address=${GUEST_NETWORK} destination address=192.168.0.0/16 drop" 2>/dev/null || true
firewall-cmd --zone=trusted --add-rich-rule="rule family=ipv4 source address=${GUEST_NETWORK} destination address=10.0.0.0/8 drop" 2>/dev/null || true
firewall-cmd --zone=trusted --add-rich-rule="rule family=ipv4 source address=${GUEST_NETWORK} destination address=192.168.0.0/16 drop" 2>/dev/null || true
# Isolamento - parte 2: direct rules na cadeia FORWARD (tráfego roteado pra LAN).
# As rich rules acima só pegam tráfego pro próprio host. O tráfego que a VM
# tenta encaminhar pra outros hosts da rede local passa pela cadeia FORWARD,
# então sem estas regras a VM ainda consegue escanear a rede local.
firewall-cmd --direct --add-rule ipv4 filter FORWARD 0 -i "$TAP_DEV" -d "$GUEST_NETWORK" -j ACCEPT 2>/dev/null || true
firewall-cmd --direct --add-rule ipv4 filter FORWARD 1 -i "$TAP_DEV" -d 10.0.0.0/8 -j DROP 2>/dev/null || true
firewall-cmd --direct --add-rule ipv4 filter FORWARD 1 -i "$TAP_DEV" -d 172.16.0.0/12 -j DROP 2>/dev/null || true
firewall-cmd --direct --add-rule ipv4 filter FORWARD 1 -i "$TAP_DEV" -d 192.168.0.0/16 -j DROP 2>/dev/null || true
log_info "firewalld configurado (NAT + isolamento INPUT/FORWARD)"
}
setup_iptables() {
log_info "Configurando iptables..."
# Detecta interface de saída
DEFAULT_IFACE=$(ip route | grep default | awk '{print $5}' | head -1)
if [ -z "$DEFAULT_IFACE" ]; then
log_error "Nao foi possivel detectar interface de saida"
exit 1
fi
log_info "Interface de saida: $DEFAULT_IFACE"
# NAT/Masquerading
iptables -t nat -C POSTROUTING -o "$DEFAULT_IFACE" -j MASQUERADE 2>/dev/null || \
iptables -t nat -A POSTROUTING -o "$DEFAULT_IFACE" -j MASQUERADE
# Forward da TAP para interface de saída
iptables -C FORWARD -i "$TAP_DEV" -o "$DEFAULT_IFACE" -j ACCEPT 2>/dev/null || \
iptables -A FORWARD -i "$TAP_DEV" -o "$DEFAULT_IFACE" -j ACCEPT
# Forward de pacotes de resposta
iptables -C FORWARD -i "$DEFAULT_IFACE" -o "$TAP_DEV" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \
iptables -A FORWARD -i "$DEFAULT_IFACE" -o "$TAP_DEV" -m state --state RELATED,ESTABLISHED -j ACCEPT
# Isolamento: a VM acessa a internet, mas não a rede local do host.
# Permite a própria subnet da VM antes dos blocos (regra entra no topo).
iptables -C FORWARD -i "$TAP_DEV" -d "$GUEST_NETWORK" -j ACCEPT 2>/dev/null || \
iptables -I FORWARD -i "$TAP_DEV" -d "$GUEST_NETWORK" -j ACCEPT
iptables -C FORWARD -i "$TAP_DEV" -d 10.0.0.0/8 -j DROP 2>/dev/null || \
iptables -I FORWARD 2 -i "$TAP_DEV" -d 10.0.0.0/8 -j DROP
iptables -C FORWARD -i "$TAP_DEV" -d 172.16.0.0/12 -j DROP 2>/dev/null || \
iptables -I FORWARD 3 -i "$TAP_DEV" -d 172.16.0.0/12 -j DROP
iptables -C FORWARD -i "$TAP_DEV" -d 192.168.0.0/16 -j DROP 2>/dev/null || \
iptables -I FORWARD 4 -i "$TAP_DEV" -d 192.168.0.0/16 -j DROP
log_info "iptables configurado (NAT + isolamento FORWARD)"
}
teardown_network() {
log_info "Removendo configuracao de rede..."
# Remove as regras de firewall desta TAP (mantém masquerading, que pode
# estar sendo usado por outras VMs).
if command -v firewall-cmd &>/dev/null && systemctl is-active firewalld &>/dev/null; then
firewall-cmd --zone=trusted --remove-interface="$TAP_DEV" 2>/dev/null || true
firewall-cmd --zone=trusted --remove-rich-rule="rule family=ipv4 source address=${GUEST_NETWORK} destination address=10.0.0.0/8 drop" 2>/dev/null || true
firewall-cmd --zone=trusted --remove-rich-rule="rule family=ipv4 source address=${GUEST_NETWORK} destination address=192.168.0.0/16 drop" 2>/dev/null || true
firewall-cmd --direct --remove-rule ipv4 filter FORWARD 0 -i "$TAP_DEV" -d "$GUEST_NETWORK" -j ACCEPT 2>/dev/null || true
firewall-cmd --direct --remove-rule ipv4 filter FORWARD 1 -i "$TAP_DEV" -d 10.0.0.0/8 -j DROP 2>/dev/null || true
firewall-cmd --direct --remove-rule ipv4 filter FORWARD 1 -i "$TAP_DEV" -d 172.16.0.0/12 -j DROP 2>/dev/null || true
firewall-cmd --direct --remove-rule ipv4 filter FORWARD 1 -i "$TAP_DEV" -d 192.168.0.0/16 -j DROP 2>/dev/null || true
fi
if ip link show "$TAP_DEV" &>/dev/null; then
ip link set "$TAP_DEV" down
ip tuntap del dev "$TAP_DEV" mode tap
log_info "Interface $TAP_DEV removida"
else
log_warn "Interface $TAP_DEV nao existe"
fi
log_info "Limpeza concluida"
}
show_status() {
echo ""
echo "Status da rede:"
echo ""
if ip link show "$TAP_DEV" &>/dev/null; then
echo "Interface $TAP_DEV:"
ip addr show "$TAP_DEV" | grep -E "inet|state"
echo ""
else
echo "Interface $TAP_DEV: NAO EXISTE"
echo ""
fi
echo "IP Forwarding:"
sysctl net.ipv4.ip_forward
echo ""
echo "Masquerading:"
if command -v firewall-cmd &>/dev/null && systemctl is-active firewalld &>/dev/null; then
firewall-cmd --query-masquerade && echo " firewalld: ATIVO" || echo " firewalld: INATIVO"
else
iptables -t nat -L POSTROUTING -n | grep -q MASQUERADE && echo " iptables: ATIVO" || echo " iptables: INATIVO"
fi
}
show_usage() {
echo "Uso: $0 {up|down|status}"
echo ""
echo "Comandos:"
echo " up Cria interface TAP e configura NAT"
echo " down Remove interface TAP"
echo " status Mostra status da configuracao"
echo ""
echo "Variaveis de ambiente:"
echo " TAP_DEV=$TAP_DEV"
echo " TAP_IP=$TAP_IP"
echo " TAP_CIDR=$TAP_CIDR"
}
# Main
check_root
case "$ACTION" in
up|start)
setup_network
;;
down|stop)
teardown_network
;;
status)
show_status
;;
*)
show_usage
exit 1
;;
esac
Torne executável:
sudo chmod +x /usr/local/bin/firecracker-network-setup.sh
O script detecta automaticamente se você usa firewalld (Fedora, RHEL) ou iptables (Ubuntu, Debian) e configura apropriadamente.
Por que duas camadas de regra de firewall? O bloqueio da rede local precisa cobrir dois caminhos diferentes. As rich rules da zona
trustedpegam o tráfego destinado ao próprio host (cadeia INPUT). Só que o tráfego que a VM tenta encaminhar pra outros hosts da rede local passa pela cadeia FORWARD, que as rich rules não tocam. Por isso o script adiciona também direct rules na FORWARD. Sem essa segunda camada, a VM ainda conseguiria escanear a sua rede interna, mesmo com as rich rules no lugar. É a mesma armadilha que a gente desarmou no artigo 03.
Passo 2: O script de inicialização da microVM
Agora precisamos de um script que inicia o Firecracker e configura a microVM via API. Esse script vai ser chamado pelo systemd.
Salve como /usr/local/bin/firecracker-vm-start.sh:
#!/bin/bash
# firecracker-vm-start.sh
# Inicia uma microVM Firecracker e configura via API
#
# Este script:
# 1. Inicia o processo Firecracker
# 2. Aguarda o socket API ficar disponível
# 3. Configura kernel, rootfs, recursos e rede via API
# 4. Inicia a microVM
# 5. Aguarda o processo (mantendo o serviço rodando)
#
# Variáveis de ambiente:
# VM_NAME - Nome da VM (default: default)
# SOCKET_PATH - Caminho do socket API (default: /run/firecracker/${VM_NAME}.socket)
# KERNEL_PATH - Caminho do kernel (default: /var/lib/firecracker/vmlinux.bin)
# ROOTFS_PATH - Caminho do rootfs (default: /var/lib/firecracker/rootfs-${VM_NAME}.ext4)
# VCPU_COUNT - Número de vCPUs (default: 1)
# MEM_SIZE_MIB - Memória em MiB (default: 256)
# TAP_DEV - Interface TAP (default: tap0)
# GUEST_MAC - MAC address do guest (default: AA:FC:00:00:00:01)
# BOOT_ARGS - Argumentos de boot do kernel (default: console=ttyS0 reboot=k panic=1 pci=off quiet)
set -e
# Configurações (podem ser sobrescritas por variáveis de ambiente)
VM_NAME="${VM_NAME:-default}"
SOCKET_PATH="${SOCKET_PATH:-/run/firecracker/${VM_NAME}.socket}"
KERNEL_PATH="${KERNEL_PATH:-/var/lib/firecracker/vmlinux.bin}"
ROOTFS_PATH="${ROOTFS_PATH:-/var/lib/firecracker/rootfs-${VM_NAME}.ext4}"
VCPU_COUNT="${VCPU_COUNT:-1}"
MEM_SIZE_MIB="${MEM_SIZE_MIB:-256}"
TAP_DEV="${TAP_DEV:-tap0}"
GUEST_MAC="${GUEST_MAC:-AA:FC:00:00:00:01}"
BOOT_ARGS="${BOOT_ARGS:-console=ttyS0 reboot=k panic=1 pci=off quiet}"
FIRECRACKER_BIN="${FIRECRACKER_BIN:-/usr/local/bin/firecracker}"
# Cores para output
if [ -t 1 ]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
else
RED=''
GREEN=''
YELLOW=''
NC=''
fi
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
cleanup() {
log_info "Recebido sinal de parada, limpando..."
if [ -n "$FC_PID" ] && kill -0 "$FC_PID" 2>/dev/null; then
kill "$FC_PID" 2>/dev/null || true
fi
rm -f "$SOCKET_PATH"
}
trap cleanup EXIT SIGTERM SIGINT
# Validações
validate_files() {
if [ ! -x "$FIRECRACKER_BIN" ]; then
log_error "Firecracker nao encontrado ou nao executavel: $FIRECRACKER_BIN"
exit 1
fi
if [ ! -f "$KERNEL_PATH" ]; then
log_error "Kernel nao encontrado: $KERNEL_PATH"
exit 1
fi
if [ ! -f "$ROOTFS_PATH" ]; then
log_error "Rootfs nao encontrado: $ROOTFS_PATH"
exit 1
fi
if ! ip link show "$TAP_DEV" &>/dev/null; then
log_error "Interface TAP nao existe: $TAP_DEV"
log_error "Execute primeiro: firecracker-network-setup.sh up"
exit 1
fi
}
call_api() {
local method="$1"
local path="$2"
local data="$3"
local response
response=$(curl --unix-socket "$SOCKET_PATH" -s -w "\n%{http_code}" -X "$method" \
"http://localhost$path" \
-H "Content-Type: application/json" \
-d "$data" 2>&1)
local http_code
http_code=$(echo "$response" | tail -n1)
local body
body=$(echo "$response" | sed '$d')
if [ "$http_code" -ge 400 ]; then
log_error "API error ($http_code): $body"
return 1
fi
}
start_firecracker() {
# Garante que o diretório do socket existe
mkdir -p "$(dirname "$SOCKET_PATH")"
# Remove socket antigo se existir
rm -f "$SOCKET_PATH"
log_info "Iniciando Firecracker..."
log_info " VM Name: $VM_NAME"
log_info " Socket: $SOCKET_PATH"
log_info " Kernel: $KERNEL_PATH"
log_info " Rootfs: $ROOTFS_PATH"
log_info " vCPUs: $VCPU_COUNT"
log_info " Memory: ${MEM_SIZE_MIB}MB"
log_info " TAP: $TAP_DEV"
log_info " MAC: $GUEST_MAC"
# Inicia Firecracker em background
$FIRECRACKER_BIN --api-sock "$SOCKET_PATH" &
FC_PID=$!
# Aguarda socket ficar disponível
local attempts=0
local max_attempts=50
while [ $attempts -lt $max_attempts ]; do
if [ -S "$SOCKET_PATH" ]; then
break
fi
sleep 0.1
attempts=$((attempts + 1))
done
if [ ! -S "$SOCKET_PATH" ]; then
log_error "Timeout esperando socket do Firecracker"
exit 1
fi
# Pequena pausa pra garantir que o socket está pronto
sleep 0.2
log_info "Firecracker iniciado (PID: $FC_PID)"
}
configure_vm() {
log_info "Configurando VM via API..."
# Configura kernel
log_info " Configurando kernel..."
call_api "PUT" "/boot-source" "{
\"kernel_image_path\": \"$KERNEL_PATH\",
\"boot_args\": \"$BOOT_ARGS\"
}"
# Configura rootfs
log_info " Configurando rootfs..."
call_api "PUT" "/drives/rootfs" "{
\"drive_id\": \"rootfs\",
\"path_on_host\": \"$ROOTFS_PATH\",
\"is_root_device\": true,
\"is_read_only\": false
}"
# Configura recursos
log_info " Configurando recursos..."
call_api "PUT" "/machine-config" "{
\"vcpu_count\": $VCPU_COUNT,
\"mem_size_mib\": $MEM_SIZE_MIB
}"
# Configura rede
log_info " Configurando rede..."
call_api "PUT" "/network-interfaces/eth0" "{
\"iface_id\": \"eth0\",
\"guest_mac\": \"$GUEST_MAC\",
\"host_dev_name\": \"$TAP_DEV\"
}"
log_info "VM configurada"
}
start_vm() {
log_info "Iniciando microVM..."
call_api "PUT" "/actions" '{"action_type": "InstanceStart"}'
log_info "MicroVM iniciada com sucesso"
}
# Main
validate_files
start_firecracker
configure_vm
start_vm
log_info "Aguardando processo Firecracker..."
# Aguarda o processo Firecracker (isso mantém o serviço "rodando")
wait $FC_PID
exit_code=$?
log_info "Firecracker encerrou com codigo: $exit_code"
exit $exit_code
Torne executável:
sudo chmod +x /usr/local/bin/firecracker-vm-start.sh
Passo 3: A unit file do systemd
Agora o coração da coisa: a unit file que junta tudo.
Salve como /etc/systemd/system/nano-lambda.service:
[Unit]
Description=Nano-Lambda MicroVM (Firecracker)
Documentation=https://github.com/firecracker-microvm/firecracker
After=network.target
Wants=network.target
# Limite de restarts: no máximo 3 em 60s (StartLimit* vão na seção [Unit]).
StartLimitBurst=3
StartLimitIntervalSec=60
[Service]
Type=simple
# Variáveis de ambiente para os scripts
Environment=VM_NAME=nano-lambda
Environment=SOCKET_PATH=/run/firecracker/nano-lambda.socket
Environment=KERNEL_PATH=/var/lib/firecracker/vmlinux.bin
Environment=ROOTFS_PATH=/var/lib/firecracker/rootfs-service.ext4
Environment=VCPU_COUNT=1
Environment=MEM_SIZE_MIB=256
Environment=TAP_DEV=tap0
Environment=TAP_IP=172.16.0.1
Environment=GUEST_MAC=AA:FC:00:00:00:01
# Cria diretório em /run para o socket
RuntimeDirectory=firecracker
RuntimeDirectoryMode=0755
# Antes de iniciar: configura rede
ExecStartPre=/usr/local/bin/firecracker-network-setup.sh up
# Comando principal
ExecStart=/usr/local/bin/firecracker-vm-start.sh
# Depois de parar: limpa rede
ExecStopPost=/usr/local/bin/firecracker-network-setup.sh down
# Política de restart
Restart=on-failure
RestartSec=5
# Timeout
TimeoutStartSec=30
TimeoutStopSec=10
# Logs
StandardOutput=journal
StandardError=journal
SyslogIdentifier=nano-lambda
[Install]
WantedBy=multi-user.target
Vamos entender os campos importantes:
Ciclo de vida:
ExecStartPre=, Roda ANTES do serviço principal. Aqui a gente configura a rede.ExecStart=, O serviço principal. O Firecracker rodando.ExecStopPost=, Roda DEPOIS do serviço parar. Limpeza da rede.
Diretórios:
RuntimeDirectory=firecracker, Cria/run/firecracker/automaticamente com permissões certas. Esse diretório é limpo no reboot (é um tmpfs), então o systemd recria toda vez.
Restart:
Restart=on-failure, Reinicia se o processo morrer com código de erro (não se for parado manualmente)RestartSec=5, Espera 5 segundos antes de reiniciarStartLimitBurst=3eStartLimitIntervalSec=60, No máximo 3 restarts em 60 segundos. Se falhar mais que isso, desiste (evita loop infinito de crash). Repare que essas duas linhas ficam na seção[Unit], não na[Service]. Se você colocar em[Service], o systemd ignora elas com um aviso (Unknown key 'StartLimitIntervalSec' in section [Service], ignoring) e o limite não tem efeito nenhum.
Logs:
StandardOutput=journal, Manda stdout pro journaldSyslogIdentifier=nano-lambda, Nome que aparece nos logs
Passo 4: Construindo o rootfs de serviço
Antes de testar, precisamos do rootfs da VM. E aqui está a diferença que comentamos lá no começo do artigo.
O rootfs do artigo 03 roda a função uma vez e dá reboot, igual a um Lambda. Pra um serviço que precisa ficar no ar, isso não funciona: a VM ia subir, rodar e morrer em poucos segundos, e o systemd ia marcar o serviço como parado (sem reiniciar, porque sair com código 0 não conta como falha). Então a gente constrói um rootfs que sobe a rede e continua vivo.
A mudança está no /etc/inittab e no vm-service.sh de dentro da VM: em vez de rodar a função e chamar reboot -f, o init configura a rede e entra num while true; do sleep 3600; done. A VM fica no ar até alguém parar o serviço. Em produção, é nesse while que você colocaria seu worker ou servidor de verdade.
Salve como build-rootfs-service.sh:
#!/usr/bin/env bash
#
# build-rootfs-service.sh
# Constrói um rootfs Alpine que sobe a rede e fica vivo como serviço.
#
# Diferença para o rootfs do artigo 03: aquele rodava a função uma vez e
# dava reboot (modelo Lambda). Um serviço de verdade precisa ficar no ar,
# então aqui o init configura a rede e depois mantém a VM rodando.
#
set -e
ROOTFS_FILE="rootfs-service.ext4"
ROOTFS_SIZE_MB=500
MOUNT_POINT="/tmp/rootfs-mount-$$"
ALPINE_VERSION="3.21"
echo "Construindo rootfs de servico persistente para Firecracker"
echo
if command -v docker &> /dev/null; then
CONTAINER_CMD="docker"
elif command -v podman &> /dev/null; then
CONTAINER_CMD="podman"
else
echo "[ERRO] Docker ou Podman nao encontrado!"
exit 1
fi
echo "[INFO] Usando ${CONTAINER_CMD}"
echo
if [ "${EUID}" -ne 0 ]; then
echo "[ERRO] Este script precisa ser executado como root ou com sudo"
exit 1
fi
if ! command -v mkfs.ext4 &> /dev/null; then
echo "[ERRO] mkfs.ext4 nao encontrado (instale e2fsprogs)"
exit 1
fi
echo "[1/6] Criando imagem de disco (${ROOTFS_SIZE_MB}MB)..."
dd if=/dev/zero of="${ROOTFS_FILE}" bs=1M count="${ROOTFS_SIZE_MB}" status=progress
mkfs.ext4 -F "${ROOTFS_FILE}"
echo "[2/6] Montando..."
mkdir -p "${MOUNT_POINT}"
mount "${ROOTFS_FILE}" "${MOUNT_POINT}"
cleanup() {
echo "[*] Limpando..."
umount "${MOUNT_POINT}" 2>/dev/null || true
rmdir "${MOUNT_POINT}" 2>/dev/null || true
}
trap cleanup EXIT
echo "[3/6] Instalando Alpine Linux ${ALPINE_VERSION} com Python e rede..."
${CONTAINER_CMD} run --rm -v "${MOUNT_POINT}:/rootfs:Z" "alpine:${ALPINE_VERSION}" sh -c '
mkdir -p /rootfs/etc/apk
cp -a /etc/apk/keys /rootfs/etc/apk/
cp /etc/apk/repositories /rootfs/etc/apk/
apk add --root /rootfs --initdb --no-cache \
alpine-base \
openrc \
python3 \
py3-requests \
py3-urllib3 \
ca-certificates
'
echo "[4/6] Configurando sistema..."
chroot "${MOUNT_POINT}" /bin/sh -c '
echo "nano-lambda" > /etc/hostname
echo "127.0.0.1 localhost" > /etc/hosts
echo "::1 localhost" >> /etc/hosts
cat > /etc/inittab << "INITTAB"
# /etc/inittab - microVM de serviço persistente
::sysinit:/sbin/openrc sysinit
::sysinit:/sbin/openrc boot
::wait:/sbin/openrc default
# Sobe a rede e mantém a VM viva (serviço, não Lambda)
::wait:/usr/local/bin/vm-service.sh
::ctrlaltdel:/sbin/reboot
::shutdown:/sbin/openrc shutdown
INITTAB
# DNS
cat > /etc/resolv.conf << "DNS"
nameserver 8.8.8.8
nameserver 1.1.1.1
DNS
mkdir -p /usr/local/bin
# Script de serviço: configura rede e fica no ar
cat > /usr/local/bin/vm-service.sh << "SCRIPT"
#!/bin/sh
echo ""
echo "=== nano-lambda: configurando rede ==="
ip link set eth0 up
ip addr add 172.16.0.2/24 dev eth0
ip route add default via 172.16.0.1
# Relatório de conectividade (aparece no journal do host)
if ping -c 1 -W 2 8.8.8.8 > /dev/null 2>&1; then
echo "[net] internet: OK"
else
echo "[net] internet: FALHOU"
fi
echo "=== nano-lambda: servico no ar ==="
echo "READY"
# Mantém a microVM viva. Em produção, aqui rodaria seu worker ou servidor.
while true; do
sleep 3600
done
SCRIPT
chmod +x /usr/local/bin/vm-service.sh
'
echo "[5/6] Limpando caches..."
chroot "${MOUNT_POINT}" /bin/sh -c '
rm -rf /var/cache/apk/*
rm -rf /tmp/*
'
echo "[6/6] Finalizando..."
umount "${MOUNT_POINT}"
rmdir "${MOUNT_POINT}"
trap - EXIT
echo
echo "${ROOTFS_FILE} criado com sucesso!"
echo " Tamanho: $(du -h ${ROOTFS_FILE} | cut -f1)"
Esse script precisa de Docker ou Podman (pra montar o Alpine) e roda como root. Construa o rootfs:
sudo ./build-rootfs-service.sh
Saída esperada (as últimas linhas):
[6/6] Finalizando...
rootfs-service.ext4 criado com sucesso!
Tamanho: 61M
Agora copie tudo pros lugares certos:
# Cria o diretorio de arquivos do Firecracker
sudo mkdir -p /var/lib/firecracker
# Copia o binario do Firecracker
sudo cp ./firecracker /usr/local/bin/firecracker
# Copia o kernel
sudo cp ./vmlinux.bin /var/lib/firecracker/vmlinux.bin
# Copia o rootfs de servico
sudo cp ./rootfs-service.ext4 /var/lib/firecracker/rootfs-service.ext4
A rede da VM já vem configurada no rootfs. O
vm-service.shsobe aeth0com IP172.16.0.2/24, gateway172.16.0.1(o IP do host na interface TAP) e DNS em/etc/resolv.conf. Esse é o outro lado da rede que ofirecracker-network-setup.shmonta no host. Os dois lados precisam combinar pra VM ter internet.
Passo 5: Testando
Recarregue o systemd e inicie o serviço:
# Recarrega as unit files
sudo systemctl daemon-reload
# Inicia o servico
sudo systemctl start nano-lambda
# Verifica status
sudo systemctl status nano-lambda
Se tudo der certo, você verá algo assim:
? nano-lambda.service - Nano-Lambda MicroVM (Firecracker)
Loaded: loaded (/etc/systemd/system/nano-lambda.service; disabled; preset: disabled)
Drop-In: /usr/lib/systemd/system/service.d
??10-timeout-abort.conf
Active: active (running) since Tue 2026-06-09 03:06:00 -03; 23s ago
Invocation: 5fc6f1c2e5874933ba04f31143236508
Docs: https://github.com/firecracker-microvm/firecracker
Process: 9607 ExecStartPre=/usr/local/bin/firecracker-network-setup.sh up (code=exited, status=0/SUCCESS)
Main PID: 9651 (firecracker-vm-)
Tasks: 5 (limit: 9394)
Memory: 49.1M (peak: 49.1M)
CPU: 1.977s
CGroup: /system.slice/nano-lambda.service
??9651 /bin/bash /usr/local/bin/firecracker-vm-start.sh
??9657 /usr/local/bin/firecracker --api-sock /run/firecracker/nano-lambda.socket
Pra ver os logs em tempo real:
sudo journalctl -u nano-lambda -f
Pra habilitar no boot:
sudo systemctl enable nano-lambda
Passo 6: Verificando a rede
Pra confirmar que a rede está funcionando:
# Verifica se a interface TAP existe
ip addr show tap0
# Verifica se o NAT está configurado
sudo iptables -t nat -L -n | grep MASQUERADE
# ou
sudo firewall-cmd --query-masquerade
A própria VM já reporta a conectividade no boot. O vm-service.sh testa a internet e imprime o resultado no console serial, que o systemd manda pro journald. O console serial da VM (console=ttyS0) vira stdout do Firecracker, então o boot inteiro fica no journal. Confira:
sudo journalctl -u nano-lambda -o cat | grep "\[net\]"
Saída esperada:
[net] internet: OK
Se apareceu [net] internet: OK, a VM saiu pra internet pela TAP, passou pelo NAT e a resposta voltou. A rede ponta a ponta está de pé.
E o isolamento? A VM acessa a internet, mas não pode varrer a sua rede local. Pra conferir, vale a mesma verificação do artigo 03: de dentro da VM, um host da LAN tem que dar timeout enquanto o 8.8.8.8 responde. Quem montou as direct rules na cadeia FORWARD lá em cima é justamente quem garante isso.
Logging e observabilidade
Com o systemd, os logs vão todos pro journald. Isso significa que você pode:
# Ver todos os logs do servico
sudo journalctl -u nano-lambda
# Logs em tempo real
sudo journalctl -u nano-lambda -f
# Logs desde o ultimo boot
sudo journalctl -u nano-lambda -b
# Logs das ultimas 2 horas
sudo journalctl -u nano-lambda --since "2 hours ago"
# Logs com prioridade de erro ou acima
sudo journalctl -u nano-lambda -p err
Isso é muito melhor do que caçar arquivos de log espalhados pelo sistema.
Restart automático e health checks
A configuração que fizemos já reinicia automaticamente em caso de falha. Mas você pode ajustar o comportamento:
# Reinicia sempre (inclusive se parar normalmente)
Restart=always
# Reinicia só em falha (codigo de saida != 0)
Restart=on-failure
# Nunca reinicia
Restart=no
O RestartSec=5 evita que o systemd fique tentando reiniciar freneticamente. Se a VM morrer, espera 5 segundos antes de tentar de novo.
Os limites StartLimitBurst=3 e StartLimitIntervalSec=60 previnem loops infinitos: se a VM crashar 3 vezes em menos de 1 minuto, o systemd desiste e marca o serviço como “failed”. Isso evita consumir CPU tentando subir algo que claramente está quebrado.
Quer ver na prática? Com o serviço rodando, mate o Firecracker na marra e veja o systemd trazer ele de volta:
# Mata o processo do Firecracker (simula um crash)
sudo pkill -9 -x firecracker
# Espera o RestartSec=5 e confere
sleep 8
systemctl show -p NRestarts,ActiveState nano-lambda
Saída esperada:
NRestarts=1
ActiveState=active
O NRestarts=1 confirma que o systemd percebeu a morte e subiu a VM de novo, sozinho.
Hardening: segurança via systemd
Até aqui, o Firecracker roda como root. Funciona, mas não é ideal. O systemd oferece várias diretivas pra isolar o processo e reduzir a superfície de ataque.
Tem uma pegadinha aqui: o processo principal (o Firecracker) a gente quer trancar, mas o setup de rede do ExecStartPre precisa de root pleno pra criar a TAP e mexer no firewall. A saída é o prefixo + nos comandos de rede: ele roda aquele comando com privilégio total, fora do sandbox, enquanto o sandbox vale só pro Firecracker.
Troque as linhas de ExecStartPre/ExecStopPost pra usar o + e adicione o bloco de hardening na seção [Service]:
[Service]
# ... configurações anteriores ...
# Setup/teardown de rede com privilégio total (prefixo +), fora do sandbox
ExecStartPre=+/usr/local/bin/firecracker-network-setup.sh up
ExecStopPost=+/usr/local/bin/firecracker-network-setup.sh down
# Isolamento de filesystem
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/var/lib/firecracker /run/firecracker
# Restrições de processo
NoNewPrivileges=yes
# Restringe dispositivos: nega tudo, libera só /dev/kvm e /dev/net/tun
DevicePolicy=closed
DeviceAllow=/dev/kvm rw
DeviceAllow=/dev/net/tun rw
# Capabilities mínimas
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW
# Limites de recursos
MemoryMax=512M
TasksMax=10
O que cada um faz:
Filesystem:
ProtectSystem=strict, Filesystem do host fica read-only (exceto /dev, /proc, /sys)ProtectHome=yes, Sem acesso ao /homePrivateTmp=yes, /tmp isolado (cada serviço vê um /tmp diferente)ReadWritePaths=, Exceções: onde o Firecracker precisa escrever
Processo:
NoNewPrivileges=yes, Previne escalação de privilégios via setuidDevicePolicy=closed, Nega acesso a todos os dispositivos, menos os liberados abaixoDeviceAllow=, Libera só /dev/kvm (virtualização) e /dev/net/tun (rede)
Cuidado: não troque isso por
PrivateDevices=yes. Parece o caminho óbvio pra restringir o/dev, mas ele esconde o/dev/net/tun, e o Firecracker precisa desse device pra anexar a interface TAP. ComPrivateDevices=yeso serviço nem sobe: o setup de rede falha na hora de criar a TAP, comopen: No such file or directory. Por isso usamosDevicePolicy=closed+DeviceAllow, que restringe via cgroup mas mantém os devices visíveis.
Capabilities:
CapabilityBoundingSet=, Dropa todas as capabilities exceto as listadasCAP_NET_ADMIN, Necessário pra anexar a interface TAPCAP_NET_RAW, Necessário pra alguns tipos de operação de rede
Recursos:
MemoryMax=512M, Limite de memória pro processo (além da RAM da VM)TasksMax=10, Limite de threads/processos filhos
A versão completa já trancada está no repositório, em nano-lambda-hardened.service. Com essas linhas, você reduz bastante o que um possível atacante conseguiria fazer mesmo se escapasse da microVM.
Quer ir além? Rode o Firecracker como usuário não-privilegiado. O hardening acima ainda roda o Firecracker como root, só que trancado. O próximo passo seria um
User=firecrackerna unit. O setup de rede continua precisando de root, mas isso a gente já resolveu com o prefixo+noExecStartPre. Fica como exercício pra quem vai pra produção de verdade.
Templates: múltiplas VMs com uma unit file
E se você precisar rodar 5 microVMs diferentes? Criar 5 unit files quase iguais seria tedioso.
O systemd tem um recurso chamado “templates” (ou “instanced units”). Você cria uma unit file com @ no nome, e ela pode ser instanciada várias vezes com parâmetros diferentes.
Salve como /etc/systemd/system/[email protected]:
[Unit]
Description=Firecracker MicroVM (%i)
Documentation=https://github.com/firecracker-microvm/firecracker
After=network.target
Wants=network.target
# Limite de restarts: no máximo 3 em 60s (StartLimit* vão na seção [Unit]).
StartLimitBurst=3
StartLimitIntervalSec=60
[Service]
Type=simple
# %i é substituído pelo nome da instância
# Exemplo: [email protected] -> %i = web
Environment=VM_NAME=%i
Environment=SOCKET_PATH=/run/firecracker/%i.socket
Environment=KERNEL_PATH=/var/lib/firecracker/vmlinux.bin
Environment=ROOTFS_PATH=/var/lib/firecracker/rootfs-%i.ext4
# Carrega configurações específicas da VM (se existir)
# Arquivo: /etc/firecracker/
<nome>.conf
# Formato: VAR=valor (uma por linha)
EnvironmentFile=-/etc/firecracker/%i.conf
# Cria diretório em /run para o socket
RuntimeDirectory=firecracker
RuntimeDirectoryMode=0755
# Antes de iniciar: configura rede
ExecStartPre=/usr/local/bin/firecracker-network-setup.sh up
# Comando principal
ExecStart=/usr/local/bin/firecracker-vm-start.sh
# Depois de parar: limpa rede
ExecStopPost=/usr/local/bin/firecracker-network-setup.sh down
# Política de restart
Restart=on-failure
RestartSec=5
# Timeout
TimeoutStartSec=30
TimeoutStopSec=10
# Logs (usa nome da instância como identificador)
StandardOutput=journal
StandardError=journal
SyslogIdentifier=firecracker-%i
[Install]
WantedBy=multi-user.target
O %i é substituído pelo nome da instância. O hífen em EnvironmentFile=- significa que o serviço não falha se o arquivo de configuração não existir, usa os defaults definidos nas linhas Environment=.
Agora crie arquivos de configuração:
# /etc/firecracker/web.conf
TAP_DEV=tap0
TAP_IP=172.16.0.1
GUEST_MAC=AA:FC:00:00:00:01
VCPU_COUNT=2
MEM_SIZE_MIB=512
# /etc/firecracker/worker.conf
TAP_DEV=tap1
TAP_IP=172.16.1.1
GUEST_MAC=AA:FC:00:00:00:02
VCPU_COUNT=1
MEM_SIZE_MIB=256
E use assim:
# Inicia a VM "web"
sudo systemctl start firecracker@web
# Inicia a VM "worker"
sudo systemctl start firecracker@worker
# Status de todas
sudo systemctl status 'firecracker@*'
# Logs da VM web
sudo journalctl -u firecracker@web -f
Cada instância roda independente, com sua própria rede, seus próprios logs, seu próprio ciclo de vida.
Atenção ao IP de cada VM. O
workerdo exemplo usa a TAPtap1na subnet172.16.1.0/24, mas o rootfs que a gente construiu fixa o IP do guest em172.16.0.2. Resultado: oworkersobe e fica no ar como serviço, só que sem internet, porque o IP de dentro da VM não bate com a subnet da TAP dele. Cada instância numa subnet diferente precisa de um rootfs com o IP correspondente (troque o172.16.0.2novm-service.shantes de construir). O mecanismo de templates do systemd é o mesmo. O que precisa casar aqui é a rede de dentro da VM.
Exemplo prático: o ciclo de vida completo
Vamos juntar tudo e rodar o serviço do começo ao fim. A essa altura você já tem os arquivos no lugar:
# Estrutura esperada
/var/lib/firecracker/
??? vmlinux.bin # Kernel
??? rootfs-service.ext4 # Rootfs que sobe a rede e fica no ar
A unit file já está pronta (nano-lambda.service) e o rootfs de serviço já tem tudo que precisa: rede configurada (IP 172.16.0.2, gateway 172.16.0.1, DNS) e o vm-service.sh que mantém a VM viva.
Agora o ciclo completo:
# Inicia
sudo systemctl start nano-lambda
# Verifica que esta rodando (e continua rodando)
sudo systemctl status nano-lambda
# Acompanha o boot da VM no journal
sudo journalctl -u nano-lambda -f
# Para
sudo systemctl stop nano-lambda
# Verifica que a rede foi limpa
ip link show tap0 # Deve dar erro "does not exist"
Ao parar o serviço, o ExecStopPost derruba a interface TAP e remove as regras de firewall daquela VM. O último comando confirma:
Device "tap0" does not exist.
Se a TAP sumiu, a limpeza funcionou.
Troubleshooting
Serviço não inicia:
# Veja os logs detalhados
sudo journalctl -u nano-lambda -b --no-pager
# Verifique a sintaxe da unit file
sudo systemd-analyze verify /etc/systemd/system/nano-lambda.service
Rede não funciona:
# Verifique se a TAP existe
ip addr show tap0
# Verifique IP forwarding
sysctl net.ipv4.ip_forward
# Verifique masquerading
sudo iptables -t nat -L -n | grep MASQUERADE
Firecracker não encontra o socket:
# Verifique se o diretório existe
ls -la /run/firecracker/
# Verifique permissões
stat /run/firecracker/
VM crashando em loop:
# Veja o status detalhado
systemctl status nano-lambda
# Se atingiu o limite de restarts, resete
sudo systemctl reset-failed nano-lambda
sudo systemctl start nano-lambda
Indo além: menção ao firectl
Se você quiser ir pra produção de verdade, existe o firectl, uma ferramenta oficial que abstrai muito do que fizemos manualmente.
Com firectl, você faz:
firectl \
--kernel=/var/lib/firecracker/vmlinux.bin \
--root-drive=/var/lib/firecracker/rootfs.ext4 \
--tap-device=tap0/AA:FC:00:00:00:01
E ele cuida de criar o socket, configurar via API, etc.
Mas agora você entende o que o firectl faz por baixo. Quando algo quebrar, e vai quebrar, você sabe onde olhar.
Conclusão
Pronto. Sua microVM agora é um serviço de verdade:
- Inicia no boot
- Reinicia se morrer
- Tem rede configurada automaticamente
- Logs centralizados no journald
- Isolamento de segurança via systemd
A diferença entre “funciona no meu terminal” e “funciona em produção” é exatamente isso: automação, resiliência, observabilidade.
No próximo artigo… bom, a série pode crescer. Orquestração com múltiplas VMs? Integração com containerd? Métricas com Prometheus? Me conta o que te interessa.
Até lá!
Arquivos deste artigo:
build-rootfs-service.sh, Constrói o rootfs de serviço persistentefirecracker-network-setup.sh, Script de setup/teardown de redefirecracker-vm-start.sh, Script de inicialização da VMnano-lambda.service, Unit file básicanano-lambda-hardened.service, Unit file com hardening de segurança[email protected], Template para múltiplas VMs
Este artigo faz parte da série “Firecracker: MicroVMs na Prática”. Todo o código está disponível em GitHub.





Comentários
Comentários fechados para visitantes. Entre ou registre-se para comentar.