Este guia detalha o processo de criação automatizada de imagens do sistema operacional Ubuntu Server 24.04 LTS utilizando o Packer e o QEMU. Diferente do Debian e do Oracle Linux, o Ubuntu moderno utiliza o sistema Subiquity e Cloud-Init para instalações autônomas, dispensando o uso de preseed ou kickstart tradicionais.
As imagens geradas servirão como templates para a criação de infraestrutura KVM/libvirt via Terraform, complementando o ambiente já estruturado.
O Packer já deve ter sido instalado no passo anterior. Caso não tenha feito, consulte o Guia de Criação de Imagem QEMU com Packer: Debian 13 (Trixie).
O projeto segue uma estrutura modular. Note a presença do diretório http contendo os arquivos user-data e meta-data, que são servidos via HTTP para o instalador Subiquity do Ubuntu.
.
├── http
│ ├── meta-data # Arquivo vazio necessário para o cloud-init (NoCloud data source)
│ └── user-data # Configurações do autoinstall (Subiquity/Cloud-init)
├── scripts
│ └── customizing-image.sh # Script bash para limpeza profunda e otimização pós-instalação
├── packer.pkr.hcl # Declaração de plugins necessários (QEMU)
├── provisioners.pkr.hcl # Etapas de provisionamento (cópia e execução de scripts)
├── secrets.pkrvars.hcl # Variáveis sensíveis (senhas) - NÃO comitar no Git
├── ubuntu-template.pkr.hcl # Definição principal da VM e comandos de boot
└── variables.pkr.hcl # Declaração e valores padrão de todas as variáveis
variables.pkr.hcl)Este arquivo centraliza as configurações. Foram adicionadas descrições detalhadas para compreender o impacto de cada parâmetro no build.
# variables.pkr.hcl
#############################################
# Configuração de ISO e Repositórios
#############################################
variable "iso_url" {
type = string
description = "URL direta para download da imagem ISO do Ubuntu Server 24.04 LTS."
default = "https://www.releases.ubuntu.com/24.04/ubuntu-24.04.4-live-server-amd64.iso"
}
variable "iso_checksum" {
type = string
description = "Hash SHA256 para validar a integridade da ISO. Obtido do arquivo SHA256SUMS oficial."
default = "e907d92eeec9df64163a7e454cbc8d7755e8ddc7ed42f99dbc80c40f1a138433"
}
#############################################
# Hardware da VM de Build
#############################################
variable "cpus" {
type = number
description = "Número de núcleos de CPU alocados temporariamente para acelerar o processo de instalação."
default = 2
}
variable "memory" {
type = number
description = "Quantidade de memória RAM (em MB) alocada para a VM durante o build."
default = 2048
}
variable "disk_size" {
type = string
description = "Tamanho máximo do disco virtual (em MB). O formato qcow2 ocupará apenas o espaço real utilizado."
default = "16384M"
}
variable "disk_format" {
type = string
description = "Formato do disco virtual. qcow2 é o padrão para KVM, permitindo compressão e snapshots."
default = "qcow2"
}
variable "vm_name" {
type = string
description = "Prefixo usado para nomear o diretório de saída e o arquivo final da imagem qcow2."
default = "noble-server-amd64"
}
variable "headless" {
type = bool
description = "Se 'true', oculta a interface gráfica do QEMU. Ideal para servidores CI/CD."
default = false
}
#############################################
# Acesso SSH e Credenciais
#############################################
variable "hostname" {
type = string
description = "Hostname configurado internamente na VM durante a instalação."
default = "ubuntu"
}
variable "ssh_username" {
type = string
description = "Nome do usuário temporário criado durante a instalação via cloud-init."
default = "packer"
}
variable "realname" {
type = string
description = "Nome completo associado ao usuário temporário."
default = "Packer Build User"
}
variable "ssh_password_hash" {
type = string
sensitive = true
description = "Hash SHA-512 da senha (gerada com 'openssl passwd -6'). Necessário para o cloud-init configurar o usuário. Não será usada para login automatizado do Packer."
default = ""
}
variable "ssh_password" {
type = string
sensitive = true
description = "Senha do usuário packer em texto plano para login SSH e comandos sudo durante o build."
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 = "45m"
}
#############################################
# Localização e Sistema
#############################################
variable "locale" {
type = string
description = "Configuração regional completa (Locale)."
default = "pt_BR.UTF-8"
}
variable "keyboard" {
type = string
description = "Layout do teclado."
default = "br"
}
variable "timezone" {
type = string
description = "Fuso horário do sistema."
default = "America/Sao_Paulo"
}
#############################################
# Firmware UEFI (OVMF) e Tempos
#############################################
variable "boot_wait" {
type = string
description = "Tempo de espera antes de enviar os comandos de boot para o instalador do Ubuntu."
default = "5s"
}
variable "shutdown_timeout" {
type = string
description = "Tempo máximo de espera para o desligamento gracioso da VM."
default = "5m"
}
variable "ovmf_code" {
type = string
description = "Caminho para o firmware OVMF_CODE (UEFI). 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 a NVRAM UEFI."
default = "/usr/share/OVMF/OVMF_VARS_4M.fd"
}
# Lógica condicional para lidar com diferentes distribuições Linux no host
locals {
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"
output_directory = "build/${var.vm_name}-${formatdate("YYYY-MM-DD", timestamp())}"
}
secrets.pkrvars.hcl)Este arquivo contém as senhas. Lembre-se de adicioná-lo ao .gitignore. O Ubuntu Subiquity exige o hash da senha para configurar o usuário.
# secrets.pkrvars.hcl
# =============================================================================
# ARQUIVO SENSÍVEL - Adicione ao .gitignore!
# =============================================================================
# Gere o hash com o comando: openssl passwd -6 "sua_senha_aqui"
ssh_password_hash = "$6$i.TCUE3.nAeTg4aj$hLYA20eaF.1dHRqE7.EVgRcsm2PdCGpuikG.gVkk9c4REI.D3MooIJqQOl2GRbvNardmLGOtB2xSeH5bE127M0"
# Senha em texto plano (usada pelo Packer para logar via SSH)
ssh_password = "packer"
packer.pkr.hcl)# 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"
}
}
}
ubuntu-template.pkr.hcl)Este arquivo configura o QEMU e injeta os comandos de boot para iniciar o instalador Subiquity apontando para os arquivos user-data e meta-data.
# ubuntu-template.pkr.hcl
source "qemu" "ubuntu" {
# Sequência de teclas enviadas ao GRUB do instalador da ISO do Ubuntu
# Injeta a configuração do cloud-init (nocloud-net) apontando para o servidor HTTP interno do Packer
boot_command = [
"e<wait>", # Edita a entrada atual do GRUB
"<down><down><down><end>", # Vai para o final da linha do kernel linux
" autoinstall ds=\"nocloud-net;s=http://{{.HTTPIP}}:{{.HTTPPort}}/\" ", # Aponta para o diretório http/
"<f10>" # Pressiona F10 para fazer o boot
]
boot_wait = var.boot_wait
accelerator = "kvm" # Aceleração de hardware nativa
cpus = var.cpus
memory = var.memory
machine_type = "q35" # Chipset moderno PCIe
efi_boot = true # Força boot UEFI
efi_firmware_code = local.ovmf_code_fallback
efi_firmware_vars = local.ovmf_vars_fallback
# Configurações de Disco
disk_interface = "virtio-scsi" # Controladora SCSI paravirtualizada
disk_size = var.disk_size
disk_compression = true # Habilita compressão qcow2 nativa
disk_discard = "unmap" # Suporte a TRIM
skip_compaction = false
format = var.disk_format
iso_checksum = var.iso_checksum
iso_url = var.iso_url
net_device = "virtio-net" # Placa de rede virtio
# Servidor HTTP interno do Packer
# Serve os arquivos meta-data e user-data renderizados com as variáveis
http_content = {
"/meta-data" = file("http/meta-data")
"/user-data" = templatefile("http/user-data", var)
}
ssh_username = var.ssh_username
ssh_password = var.ssh_password
ssh_timeout = var.ssh_timeout
headless = var.headless
vm_name = var.vm_name
output_directory = local.output_directory
# Desligamento seguro
shutdown_command = "echo '${var.ssh_password}' | sudo -S poweroff"
shutdown_timeout = var.shutdown_timeout
}
provisioners.pkr.hcl)# provisioners.pkr.hcl
build {
sources = ["source.qemu.ubuntu"]
# ========================================================================
# Provisioner 1: Copiar script de customização para a VM
# ========================================================================
provisioner "file" {
source = "scripts/customizing-image.sh"
destination = "/tmp/customizing-image.sh"
}
# ========================================================================
# Provisioner 2: Executar script de limpeza e generalização
# ========================================================================
provisioner "shell" {
inline = [
"chmod +x /tmp/customizing-image.sh",
"echo '${var.ssh_password}' | sudo -S /tmp/customizing-image.sh"
]
}
}
O Ubuntu utiliza o cloud-init via autoinstall (Subiquity). Para que funcione, é necessário um arquivo meta-data vazio e um arquivo user-data contendo as configurações.
Crie o arquivo meta-data vazio:
touch http/meta-data
Arquivo user-data:
# http/user-data
#cloud-config
autoinstall:
version: 1
# Configurações do APT (Repositórios e Espelhos)
apt:
disable_components: []
fallback: abort
geoip: true
mirror-selection:
primary:
- uri: http://archive.ubuntu.com/ubuntu/
arches: [amd64]
preserve_sources_list: false
# Identidade (Usuário e Hostname)
identity:
hostname: ${hostname}
username: ${ssh_username}
realname: ${realname}
password: ${ssh_password_hash}
# Kernel a ser instalado (generic para VMs comuns, kvm para VMs otimizadas)
kernel:
package: linux-generic
# Configurações Regionais
keyboard:
layout: ${keyboard}
toggle: null
variant: ''
locale: ${locale}
timezone: ${timezone}
# Fonte de Instalação (Usa a versão mínima do Ubuntu Server para reduzir tamanho)
source:
id: ubuntu-server-minimal
search_drivers: false
# Configuração SSH
ssh:
allow-pw: true
authorized-keys: []
install-server: true
# Particionamento de Disco (Otimizado para UEFI)
storage:
swap:
size: 0 # Desabilita Swap (recomendado para Kubernetes/Cloud)
config:
- id: sda
type: disk
ptable: gpt
path: /dev/sda
wipe: superblock
grub_device: true
- id: efi-part # Partição EFI
type: partition
device: sda
size: 256M
flag: boot
grub_device: true
- id: efi-format
type: format
fstype: vfat
volume: efi-part
label: UEFI
- id: efi-mount
type: mount
device: efi-format
path: /boot/efi
- id: root-part # Partição Raiz
type: partition
device: sda
size: -1 # Ocupa todo o espaço restante
- id: root-format
type: format
fstype: ext4
volume: root-part
label: cloudimg-rootfs
- id: root-mount
type: mount
device: root-format
path: /
# Pacotes Adicionais
packages:
- qemu-guest-agent # Essencial para comunicação com o hypervisor KVM
# Atualizações de Segurança Automáticas
updates: security
Nota: Diferente de outras distribuições, o Ubuntu Server já traz o
cloud-initinstalado por padrão, não sendo necessário declará-lo na seçãopackages.
O script customizing-image.sh alinha a imagem com as práticas da Canonical para imagens cloud minimalistas.
#!/usr/bin/env bash
# scripts/customizing-image.sh
set -euo pipefail
# -----------------------------------------------------------------------------
# Script de Otimização para Imagens Ubuntu Cloud
# Alinhado com as práticas da Canonical para imagens minimalistas
# -----------------------------------------------------------------------------
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 "Este script deve ser executado como root (sudo)."
exit 1
fi
log_info "Iniciando otimização da imagem Ubuntu..."
# -----------------------------------------------------------------------------
# 1. KERNEL E MÓDULOS – LIMPEZA PROFUNDA
# -----------------------------------------------------------------------------
log_info "Limpando kernels e módulos desnecessários..."
current_kernel=$(uname -r)
# Remove kernels antigos que possam ter sido instalados durante atualizações
kernel_pkgs=$(dpkg-query -W -f='${Package}\n' 'linux-image-*' 'linux-modules-*' 'linux-headers-*' | grep -v "$current_kernel" || true)
if [ -n "$kernel_pkgs" ]; then
apt-get purge -y $kernel_pkgs
fi
# Remove módulos extras e ferramentas de profiling (Economiza mais de 100MB)
apt-get purge -y linux-tools-$(uname -r) linux-tools-generic || true
# -----------------------------------------------------------------------------
# 2. FIRMWARE E MICROCODE – REDUÇÃO DE FOOTPRINT
# -----------------------------------------------------------------------------
# VMs não gerenciam hardware físico, logo, firmwares são inúteis.
log_info "Removendo firmware e microcode..."
apt-get purge -y linux-firmware 'firmware-*' intel-microcode amd64-microcode || true
# -----------------------------------------------------------------------------
# 3. PACOTES NÃO ESSENCIAIS
# -----------------------------------------------------------------------------
log_info "Removendo pacotes não essenciais..."
apt-get purge -y command-not-found command-not-found-data friendly-recovery iso-codes lupin-support python-babel-localedata ubuntu-release-upgrader-core python3-pygments ncurses-term libintl-perl || true
# -----------------------------------------------------------------------------
# 4. LOCALIZAÇÃO E DOCUMENTAÇÃO
# -----------------------------------------------------------------------------
log_info "Limpando dados de localização e documentação..."
# Mantém apenas os locales pt_BR e en_US
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
# -----------------------------------------------------------------------------
# 5. GRUB – ALINHADO COM A IMAGEM OFICIAL
# -----------------------------------------------------------------------------
log_info "Configurando GRUB..."
sed -i 's/^GRUB_TIMEOUT=.*/GRUB_TIMEOUT=0/' /etc/default/grub
sed -i 's/^GRUB_CMDLINE_LINUX_DEFAULT=.*/GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"/' /etc/default/grub
sed -i 's/^GRUB_CMDLINE_LINUX=.*/GRUB_CMDLINE_LINUX=""/' /etc/default/grub
update-grub
# -----------------------------------------------------------------------------
# 6. REDE E CLOUD-INIT
# -----------------------------------------------------------------------------
# Remove configurações estáticas de rede para que o cloud-init as recrie no boot
log_info "Limpando configurações de rede e cloud-init..."
rm -f /etc/netplan/*.yaml
cloud-init clean --logs || true
# -----------------------------------------------------------------------------
# 7. APT – LIMPEZA DE CACHE E ÓRFÃOS
# -----------------------------------------------------------------------------
log_info "Limpando cache do APT e pacotes órfãos..."
apt-get autoremove --purge -y
apt-get autoclean
apt-get clean
rm -rf /var/lib/apt/lists/*
# -----------------------------------------------------------------------------
# 8. HIGIENE DO SISTEMA (Sysprep)
# -----------------------------------------------------------------------------
# Remove identificadores únicos (machine-id) e logs para evitar conflitos em clones
log_info "Executando limpeza de higiene..."
# Logs
journalctl --rotate || true
journalctl --vacuum-time=1s || true
find /var/log -type f -exec truncate -s 0 {} \;
# Temporários e Crash dumps
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
rm -rf /var/crash/*
# Machine-ID
truncate -s 0 /etc/machine-id
[ -f /var/lib/dbus/machine-id ] && truncate -s 0 /var/lib/dbus/machine-id || true
# Hostname
echo "localhost" > /etc/hostname
cat > /etc/hosts <<'EOF'
127.0.0.1 localhost localhost.localdomain
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
EOF
# DNS e SSH Host Keys
> /etc/resolv.conf
rm -f /etc/ssh/ssh_host_*
# -----------------------------------------------------------------------------
# 9. COMPACTAÇÃO DE DISCO (ZERO FILL)
# -----------------------------------------------------------------------------
# Preenche o espaço livre com zeros para que a compressão qcow2 seja máxima
log_info "Compactando espaço livre no disco..."
sync
dd if=/dev/zero of=/zero bs=1M status=progress || true
sync
rm -f /zero
fstrim -av 2>/dev/null || true
# Auto-remoção do script
rm -f "$(readlink -f "$0")"
log_info "Otimização concluída com sucesso!"
Inicialize, formate e valide o projeto:
packer init .
packer fmt .
packer validate .
Inicie o processo de build (lembre-se de criar o arquivo secrets.pkrvars.hcl antes):
PACKER_LOG=1 packer build -var-file=secrets.pkrvars.hcl .
Após a conclusão, o arquivo gerado estará na pasta configurada (ex: build/noble-server-amd64-2026-04-03/noble-server-amd64).
Copie para o servidor KVM:
scp build/noble-server-amd64-2026-04-03/noble-server-amd64 gean@192.168.0.251:
Acesse o servidor KVM e mova a imagem para o diretório de templates:
sudo mv noble-server-amd64 /datastore/templates/noble-server-base.qcow2
Verifique o tamanho incrivelmente otimizado da imagem final:
qemu-img info /datastore/templates/noble-server-base.qcow2
Saída esperada:
image: /datastore/templates/noble-server-base.qcow2
file format: qcow2
virtual size: 16 GiB (17179869184 bytes)
disk size: 437 MiB <-- Tamanho real extremamente reduzido!
cluster_size: 65536
Format specific information:
compat: 1.1
compression type: zlib
...
A imagem do Ubuntu 24.04 LTS está pronta, minimalista e otimizada para uso em seus projetos com Terraform!