Este guia detalha o processo de criação automatizada de imagens de sistema operacional utilizando o Packer e o QEMU. As imagens geradas por este processo servirão como base (templates) para a criação de infraestrutura KVM/libvirt via Terraform, complementando o ambiente configurado no Guia Completo: KVM com Debian 13 para Homelab.
Ao automatizar a criação da imagem com o Packer, garantimos que todas as máquinas virtuais baseadas nela sejam idênticas, seguras e otimizadas para ambientes de nuvem/homelab.
O processo de build da imagem ocorrerá em uma estação de trabalho local (neste exemplo, Ubuntu 24.04). Após a criação, a imagem final será transferida para o servidor hypervisor KVM.
Para instalar a versão oficial mais recente do Packer, adicione o repositório da HashiCorp:
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
Atualize a lista de pacotes e instale o Packer:
sudo apt update
sudo apt install packer
Verifique a instalação:
packer -v
# Saída esperada: Packer v1.15.1 ou superior
Para manter a organização, o projeto é dividido em múltiplos arquivos modulares de configuração (HCL) e scripts de provisionamento.
.
├── http
│ └── preseed.cfg # Configurações de instalação autônoma do Debian
├── scripts
│ └── customizing-image.sh # Script executado dentro da VM para limpeza e otimização
├── debian-template.pkr.hcl # Definição principal da máquina virtual (QEMU)
├── locals.pkr.hcl # Variáveis locais e lógicas condicionais
├── packer.pkr.hcl # Declaração de plugins necessários
├── provisioners.pkr.hcl # Definição das etapas pós-instalação (scripts)
├── secrets.pkrvars.hcl # Variáveis sensíveis (senhas) - NÃO COmitar no Git
└── variables.pkr.hcl # Declaração e valores padrão de todas as variáveis
variables.pkr.hcl)Este arquivo centraliza todas as variáveis customizáveis do projeto. Foram adicionadas descrições detalhadas para compreender o impacto de cada configuração.
# variables.pkr.hcl
# ==========================================
# Configurações da Imagem ISO
# ==========================================
variable "iso_url" {
type = string
description = "URL direta para download da imagem ISO de instalação (netinst) do Debian."
default = "http://cdimage.debian.org/cdimage/release/current/amd64/iso-cd/debian-13.4.0-amd64-netinst.iso"
}
variable "iso_checksum" {
type = string
description = "Caminho ou URL contendo o hash (SHA512) para validar a integridade da ISO baixada."
default = "file:http://cdimage.debian.org/cdimage/release/current/amd64/iso-cd/SHA512SUMS"
}
# ==========================================
# Configurações de Hardware da VM de Build
# ==========================================
variable "memory" {
type = number
description = "Quantidade de memória RAM (em MB) alocada para a VM apenas durante o processo de build."
default = 2048
}
variable "cpus" {
type = number
description = "Número de núcleos de CPU alocados para acelerar a compilação/instalação."
default = 2
}
variable "disk_size" {
type = number
description = "Tamanho do disco virtual primário (em MB). A imagem final (qcow2) ocupará apenas o espaço real utilizado."
default = 16384
}
# ==========================================
# Configurações do QEMU e Boot
# ==========================================
variable "qemu_binary" {
type = string
description = "Nome do executável do QEMU no sistema host."
default = "qemu-system-x86_64"
}
variable "boot_wait" {
type = string
description = "Tempo de espera antes de enviar os comandos de boot para o instalador. Ajuste se o menu do GRUB demorar a aparecer."
default = "3s"
}
variable "start_retry_timeout" {
type = string
description = "Tempo máximo que o Packer tentará iniciar a VM antes de falhar."
default = "5m"
}
variable "ovmf_code" {
type = string
description = "Caminho para o firmware OVMF_CODE (UEFI). Essencial para VMs modernas. Em Debian/Ubuntu: /usr/share/OVMF/OVMF_CODE_4M.fd."
default = "/usr/share/OVMF/OVMF_CODE_4M.fd"
}
variable "ovmf_vars" {
type = string
description = "Caminho para o firmware OVMF_VARS (NVRAM UEFI), onde variáveis de boot são armazenadas."
default = "/usr/share/OVMF/OVMF_VARS_4M.fd"
}
# ==========================================
# Identificação e Acesso
# ==========================================
variable "vm_name" {
type = string
description = "Prefixo usado para nomear o diretório de saída e o arquivo final da imagem."
default = "base-trixie"
}
variable "hostname" {
type = string
description = "Hostname configurado internamente na VM durante a instalação."
default = "packer"
}
variable "domain" {
type = string
description = "Sufixo de domínio DNS para a VM."
default = ""
}
variable "ssh_timeout" {
type = string
description = "Tempo máximo que o Packer aguardará o SSH ficar disponível após a instalação do SO."
default = "20m"
}
variable "ssh_username" {
type = string
description = "Nome do usuário temporário criado para o Packer acessar e provisionar a VM."
default = "packer"
}
variable "ssh_fullname" {
type = string
description = "Nome completo associado ao usuário temporário."
default = "Packer Build User"
}
variable "ssh_password" {
type = string
sensitive = true
description = "Senha do usuário temporário (usada para login SSH e comandos sudo). Definida no arquivo secrets."
default = ""
}
# ==========================================
# Localização (Preseed)
# ==========================================
variable "preseed_file" {
type = string
description = "Nome do arquivo de respostas (preseed) servido via HTTP para automatizar a instalação do Debian."
default = "preseed.cfg"
}
variable "language" {
type = string
description = "Idioma base do sistema operacional."
default = "pt_BR"
}
variable "country" {
type = string
description = "País para configuração de espelhos (mirrors) do apt."
default = "BR"
}
variable "locale" {
type = string
description = "Configuração regional completa (Locale)."
default = "pt_BR.UTF-8"
}
variable "keyboard" {
type = string
description = "Layout do teclado (ex: 'br' para ABNT2)."
default = "br"
}
variable "timezone" {
type = string
description = "Fuso horário do sistema."
default = "America/Sao_Paulo"
}
secrets.pkrvars.hcl)Este arquivo contém senhas e chaves. Ele deve ser adicionado ao .gitignore para evitar vazamento de credenciais.
# secrets.pkrvars.hcl
# =============================================================================
# ARQUIVO SENSÍVEL - Adicione ao .gitignore!
# =============================================================================
# Senha em texto plano usada exclusivamente durante o build.
# O script de customização pode (e deve) remover ou bloquear este usuário posteriormente.
ssh_password = "packer"
locals.pkr.hcl)Variáveis locais são usadas para criar lógicas baseadas em outras variáveis, como formatar caminhos ou definir fallbacks (caminhos alternativos).
# locals.pkr.hcl
locals {
# Fallback automático: verifica se o arquivo UEFI existe no caminho fornecido.
# Se não existir, tenta o caminho padrão de distribuições baseadas em RedHat/Arch.
ovmf_code_fallback = fileexists(var.ovmf_code) ? var.ovmf_code : "/usr/share/edk2/ovmf/OVMF_CODE.fd"
ovmf_vars_fallback = fileexists(var.ovmf_vars) ? var.ovmf_vars : "/usr/share/edk2/ovmf/OVMF_VARS.fd"
# Gera um nome de diretório de saída dinâmico incluindo a data do build (ex: build/base-trixie-2026-04-03)
output_directory = "build/${var.vm_name}-${formatdate("YYYY-MM-DD", timestamp())}"
}
packer.pkr.hcl)Declara a versão do Packer suportada e o plugin oficial do QEMU necessário para o build.
# packer.pkr.hcl
packer {
required_version = ">= 1.7.0, < 2.0.0"
required_plugins {
qemu = {
source = "github.com/hashicorp/qemu"
version = ">= 1.0.0, < 2.0.0"
}
}
}
debian-template.pkr.hcl)Este é o arquivo principal que orquestra a máquina virtual, configurando o QEMU e simulando a digitação no teclado (boot_command) para iniciar a instalação automatizada.
# debian-template.pkr.hcl
source "qemu" "debian" {
vm_name = var.vm_name
iso_url = var.iso_url
iso_checksum = var.iso_checksum
cpus = var.cpus
memory = var.memory
# Configurações de Disco
disk_image = false # False indica que vamos instalar a partir de uma ISO, não de um disco existente
disk_size = var.disk_size
disk_interface = "virtio-scsi" # Interface de alta performance para discos virtuais
format = "qcow2" # Formato padrão e recomendado para KVM
disk_cache = "writeback"
disk_compression = true # Comprime a imagem final para economizar espaço
disk_discard = "unmap" # Permite recuperar espaço de blocos deletados (TRIM)
skip_compaction = false
# Configurações de Sistema e Boot
machine_type = "q35" # Chipset moderno (recomendado para PCIe/PCIe passthrough)
accelerator = "kvm" # Usa aceleração de hardware nativa (essencial para velocidade)
efi_boot = true # Força boot UEFI ao invés de BIOS legado
efi_firmware_code = local.ovmf_code_fallback
efi_firmware_vars = local.ovmf_vars_fallback
# Rede e Acesso
net_device = "virtio-net" # Placa de rede paravirtualizada de alta performance
ssh_username = var.ssh_username
ssh_password = var.ssh_password
ssh_timeout = var.ssh_timeout
# Saída e Comportamento
output_directory = local.output_directory
headless = false # Se true, não exibe a janela do QEMU (útil em servidores CI/CD)
# Comando de desligamento seguro após o provisionamento
shutdown_command = "echo '${var.ssh_password}' | sudo -E -S poweroff"
# Servidor HTTP interno do Packer para fornecer o arquivo preseed.cfg
http_content = { "/${var.preseed_file}" = templatefile("http/${var.preseed_file}", { var = var }) }
boot_wait = var.boot_wait
# Simulação de teclado no GRUB da ISO de instalação do Debian
boot_command = [
"<wait><wait><wait>c<wait><wait><wait>", # Entra no modo de comando do GRUB
"linux /install.amd/vmlinuz ", # Carrega o kernel de instalação
"auto=true ", # Modo de instalação automática
"url=http://{{ .HTTPIP }}:{{ .HTTPPort }}/preseed.cfg ", # Aponta para o preseed servido pelo Packer
"hostname=${var.hostname} ",
"domain=${var.domain} ",
"interface=auto ",
"vga=normal noprompt quiet --<enter>", # Oculta logs desnecessários na tela
"initrd /install.amd/initrd.gz<enter>", # Carrega o disco de RAM inicial
"boot<enter>" # Inicia o processo
]
}
provisioners.pkr.hcl)Após o Debian ser instalado e reiniciar, o Packer acessa a VM via SSH e executa estas etapas.
# provisioners.pkr.hcl
build {
sources = ["source.qemu.debian"]
# 1. Copia o script de otimização do host para dentro da VM recém-criada
provisioner "file" {
source = "scripts/customizing-image.sh"
destination = "/tmp/customizing-image.sh"
}
# 2. Executa o script como root (usando sudo)
provisioner "shell" {
inline = [
"chmod +x /tmp/customizing-image.sh",
"echo '${var.ssh_password}' | sudo -S /tmp/customizing-image.sh"
]
}
}
O arquivo preseed.cfg responde automaticamente a todas as perguntas que o instalador do Debian faria na tela.
# http/preseed.cfg
# Debian 13 (Trixie) Preseed para Packer/QEMU
# Locale Setup (Linguagem e Região)
d-i debian-installer/locale string ${var.locale}
d-i debian-installer/country string ${var.country}
d-i debian-installer/language string ${var.language}
# Keyboard Setup (Teclado)
d-i keyboard-configuration/xkb-keymap select ${var.keyboard}
# Clock Setup (Fuso Horário e Relógio)
d-i time/zone string ${var.timezone}
d-i clock-setup/utc boolean true
# Network Setup (Rede)
d-i netcfg/get_hostname string ${var.hostname}
d-i netcfg/get_domain string
d-i netcfg/choose_interface select auto
# User Setup (Criação do usuário temporário do Packer)
d-i passwd/user-fullname string ${var.ssh_fullname}
d-i passwd/username string ${var.ssh_username}
d-i passwd/user-password password ${var.ssh_password}
d-i passwd/user-password-again password ${var.ssh_password}
d-i user-setup/allow-password-weak boolean true
d-i user-setup/encrypt-home boolean false
d-i passwd/root-login boolean false
# Package Setup (Configuração de repositórios e pacotes)
d-i hw-detect/load_firmware boolean false
d-i hw-detect/load_media boolean false
apt-cdrom-setup apt-setup/cdrom/set-first boolean false
d-i mirror/country string manual
d-i mirror/http/hostname string deb.debian.org
d-i mirror/http/directory string /debian
d-i mirror/http/proxy string
d-i apt-setup/contrib boolean true
d-i apt-setup/non-free boolean true
# Instala SSH Server e pacotes essenciais para KVM/Cloud
tasksel tasksel/first multiselect ssh-server
d-i pkgsel/include string cloud-init, qemu-guest-agent, netplan.io netplan-generator systemd-resolved
popularity-contest popularity-contest/participate boolean false
# Drive Setup (Particionamento de Disco - UEFI)
d-i partman-auto/method string regular
d-i partman-auto/choose_recipe select boot-root-only
d-i partman-auto/expert_recipe string \
boot-root-only :: \
256 256 256 fat32 \
$primary{ } \
$iflabel{ gpt } \
method{ efi } format{ } \
. \
1024 1024000 -1 ext4 \
method{ format } format{ } \
use_filesystem{ } filesystem{ ext4 } \
mountpoint{ / } \
.
d-i partman-partitioning/confirm_write_new_label boolean true
d-i partman/choose_partition select finish
d-i partman/confirm boolean true
d-i partman/confirm_nooverwrite boolean true
d-i partman-basicfilesystems/no_swap boolean false
# Final Setup (Reboot automático ao final)
d-i finish-install/reboot_in_progress note
Nota: Foi incluído o pacote
qemu-guest-agent, vital para que o hypervisor (KVM) possa comunicar-se com a VM (ex: para realizar shutdown gracioso ou consultar IPs). Ocloud-inittambém é instalado para permitir a injeção de dados via Terraform posteriormente.
O script customizing-image.sh é o coração da otimização. Ele limpa o sistema para que a imagem final seja o menor e mais genérica possível, removendo logs, históricos e identificadores únicos.
#!/usr/bin/env bash
# scripts/customizing-image.sh
set -euo pipefail
log_info() { echo -e "\033[0;32m[INFO]\033[0m $1"; }
log_warn() { echo -e "\033[0;33m[WARN]\033[0m $1" >&2; }
log_error() { echo -e "\033[0;31m[ERROR]\033[0m $1" >&2; }
if [ "$EUID" -ne 0 ]; then
log_error "Execute como root"
exit 1
fi
log_info "Iniciando otimização da imagem Debian (alinhada com oficial)..."
# -----------------------------------------------------------------------------
# 1. KERNEL E FIRMWARE
# -----------------------------------------------------------------------------
log_info "Removendo kernels antigos e firmwares desnecessários..."
current_kernel=$(uname -r)
kernel_packages=$(dpkg-query -W -f='${Package}\n' 'linux-image-*' | grep -v "$current_kernel" || true)
if [ -n "$kernel_packages" ]; then
apt-get purge -y $kernel_packages
fi
# Remove microcodes de CPU física (VMs não gerenciam isso)
apt-get purge -y 'firmware-*' 'intel-microcode' 'amd64-microcode' || true
# -----------------------------------------------------------------------------
# 2. LOCALIZAÇÃO E DOCUMENTAÇÃO
# -----------------------------------------------------------------------------
log_info "Removendo pacotes de localização e documentação para poupar espaço..."
apt-get purge -y iso-codes python-babel-localedata util-linux-locales libglib2.0-data console-setup-linux xkb-data || true
find /usr/share/locale -mindepth 1 -maxdepth 1 ! -name 'pt_BR*' ! -name 'en_US*' ! -name 'C*' -exec rm -rf {} + 2>/dev/null || true
rm -rf /usr/share/doc/* /usr/share/man/* /usr/share/info/*
mkdir -p /usr/share/doc /usr/share/man /usr/share/info
# -----------------------------------------------------------------------------
# 3. REDE (Forçar Netplan)
# -----------------------------------------------------------------------------
log_info "Forçando Netplan como renderer de rede..."
apt-get purge -y ifupdown || true
# Prepara o cloud-init para usar Netplan
cat > /etc/cloud/cloud.cfg.d/99-netplan.cfg <<'EOF'
system_info:
network:
renderers: ['netplan', 'eni']
EOF
systemctl enable systemd-networkd
rm -f /etc/network/interfaces.d/50-cloud-init
# -----------------------------------------------------------------------------
# 4. LIMPEZA DE PACOTES
# -----------------------------------------------------------------------------
log_info "Removendo pacotes órfãos e limpando cache..."
apt-get autoremove --purge -y
apt-get autoclean
apt-get clean
rm -rf /var/cache/apt/* /var/lib/apt/lists/*
# -----------------------------------------------------------------------------
# 5. CONFIGURAÇÃO DO GRUB (Modo Serial para Cloud)
# -----------------------------------------------------------------------------
log_info "Configurando GRUB para console serial..."
sed -i 's/^GRUB_TIMEOUT=.*/GRUB_TIMEOUT=5/' /etc/default/grub
sed -i '/^GRUB_TIMEOUT_STYLE=/d' /etc/default/grub
sed -i 's/^GRUB_CMDLINE_LINUX=.*/GRUB_CMDLINE_LINUX="console=tty0 console=ttyS0,115200 earlyprintk=ttyS0,115200 consoleblank=0"/' /etc/default/grub
sed -i 's/^GRUB_TERMINAL_OUTPUT=.*/GRUB_TERMINAL_OUTPUT="gfxterm serial"/' /etc/default/grub
sed -i 's/^GRUB_SERIAL_COMMAND=.*/GRUB_SERIAL_COMMAND="serial --speed=115200"/' /etc/default/grub
update-grub
# -----------------------------------------------------------------------------
# 6. GENERALIZAÇÃO DA IMAGEM (Sysprep manual)
# -----------------------------------------------------------------------------
log_info "Aplicando limpezas finais (Logs, Machine-ID, Hostname)..."
find /var/log -type f -exec truncate -s 0 {} \;
journalctl --rotate || true
journalctl --vacuum-time=1s || true
find /tmp -mindepth 1 -maxdepth 1 ! -name 'cleanup-image.sh' -exec rm -rf {} + 2>/dev/null || true
find /var/tmp -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true
# Zera o machine-id. Isso força a VM a gerar um novo ID (e novo MAC address/IP DHCP) no primeiro boot
truncate -s 0 /etc/machine-id
truncate -s 0 /etc/hostname
hostnamectl set-hostname localhost || true
> /etc/resolv.conf
# -----------------------------------------------------------------------------
# 7. COMPACTAÇÃO DO DISCO (Zero Fill)
# -----------------------------------------------------------------------------
log_info "Preenchendo espaço livre com zeros para otimizar compressão do qcow2..."
sync
dd if=/dev/zero of=/zero bs=1M status=progress || true
sync
rm -f /zero
fstrim -av 2>/dev/null || true
rm -f "$(readlink -f "$0")"
log_info "Otimização concluída!"
Com todos os arquivos no lugar, inicialize o Packer para baixar os plugins necessários:
packer init .
Formate e valide os arquivos de configuração para garantir que não há erros de sintaxe:
packer fmt .
packer validate .
Execute o build (passando o arquivo de senhas):
PACKER_LOG=1 packer build -var-file=secrets.pkrvars.hcl .
Dica: O parâmetro
PACKER_LOG=1ativa logs detalhados, o que é extremamente útil caso a instalação falhe no meio do processo.
Após o sucesso do build, o arquivo gerado estará no diretório configurado em locals.pkr.hcl.
Transfira a imagem gerada para o servidor hypervisor KVM via SCP:
scp build/base-trixie-2026-04-03/base-trixie gean@192.168.0.251:
Acesse o servidor KVM e mova a imagem para o pool de templates configurado anteriormente:
sudo mv base-trixie /datastore/templates/debian-trixie-base.qcow2
Verifique as informações da imagem final para confirmar seu tamanho reduzido graças à compressão:
qemu-img info /datastore/templates/debian-trixie-base.qcow2
Saída esperada:
image: /datastore/templates/debian-trixie-base.qcow2
file format: qcow2
virtual size: 16 GiB (17179869184 bytes)
disk size: 288 MiB <-- Tamanho real incrivelmente pequeno!
cluster_size: 65536
Format specific information:
compat: 1.1
compression type: zlib
...
A imagem agora está pronta para ser clonada instantaneamente via Terraform!