Pular para o conteúdo
, , , ,

Firecracker em produção: systemd, restart automático e a diferença entre demo e serviço de verdade [pt.5]

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

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

10 dez, 2025
31 min de leitura

Atualizado em 08/06/2026

Série Firecracker:


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:

  1. Criar a interface TAP
  2. Configurar IP
  3. Habilitar IP forwarding
  4. Configurar NAT/masquerading
  5. 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 trusted pegam 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 reiniciar
  • StartLimitBurst=3 e StartLimitIntervalSec=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 journald
  • SyslogIdentifier=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.sh sobe a eth0 com IP 172.16.0.2/24, gateway 172.16.0.1 (o IP do host na interface TAP) e DNS em /etc/resolv.conf. Esse é o outro lado da rede que o firecracker-network-setup.sh monta 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 /home
  • PrivateTmp=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 setuid
  • DevicePolicy=closed, Nega acesso a todos os dispositivos, menos os liberados abaixo
  • DeviceAllow=, 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. Com PrivateDevices=yes o serviço nem sobe: o setup de rede falha na hora de criar a TAP, com open: No such file or directory. Por isso usamos DevicePolicy=closed + DeviceAllow, que restringe via cgroup mas mantém os devices visíveis.

Capabilities:

  • CapabilityBoundingSet=, Dropa todas as capabilities exceto as listadas
  • CAP_NET_ADMIN, Necessário pra anexar a interface TAP
  • CAP_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=firecracker na unit. O setup de rede continua precisando de root, mas isso a gente já resolveu com o prefixo + no ExecStartPre. 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 worker do exemplo usa a TAP tap1 na subnet 172.16.1.0/24, mas o rootfs que a gente construiu fixa o IP do guest em 172.16.0.2. Resultado: o worker sobe 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 o 172.16.0.2 no vm-service.sh antes 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 persistente
  • firecracker-network-setup.sh, Script de setup/teardown de rede
  • firecracker-vm-start.sh, Script de inicialização da VM
  • nano-lambda.service, Unit file básica
  • nano-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.

Avatar de DK

Comentários

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

Ir para