Guia Completo: Criando um Template Debian 13 (BIOS+LVM) com Packer e Terraform (Parte 2/2)
Aprenda a automatizar a criação de imagens Debian 13 com LVM usando Packer e a provisionar VMs com Terraform e virt-install. Um guia prático de ponta a ponta.
Introdução
Na Parte 1, Guia Completo: Criando um Template Debian 13 (BIOS+LVM) com Packer e Terraform (Parte 1/2), criamos uma imagem base do Debian 13 “Trixie” com o Packer. Agora, nesta segunda parte, vamos demonstrar como utilizar esse template para provisionar novas máquinas virtuais (VMs).
Abordaremos duas metodologias populares:
virt-install
: Uma ferramenta de linha de comando ideal para scripts e provisionamento rápido e direto.- Terraform: Uma abordagem de Infraestrutura como Código (IaC) para gerenciar o ciclo de vida da VM de forma declarativa e reprodutível.
Ambos os métodos utilizarão cloud-init para a configuração inicial da VM, como a criação de usuários e a injeção de chaves SSH.
1. Provisionando com virt-install
Esta abordagem é excelente para automação via scripts shell e para quem prefere ferramentas nativas do ecossistema Libvirt.
1.1. Preparação do Ambiente
Primeiro, vamos criar um diretório de trabalho e copiar a imagem base para o diretório de imagens do KVM, renomeando-a para a nossa nova VM.
1
2
3
4
5
6
7
# Crie um diretório para os arquivos de configuração da VM
mkdir -p ~/Workspace/libvirt/packer/debian13
cd ~/Workspace/libvirt/packer/debian13
# Copie a imagem base para o diretório de imagens de VMs ativas
# É uma boa prática não usar o template diretamente
cp ~/kvm/templates/debian-13.qcow2 ~/kvm/images/debian-trixie.qcow2
1.2. Configuração do cloud-init
O cloud-init
utiliza arquivos de metadados para configurar a VM no primeiro boot. Vamos criar os três arquivos necessários: user-data
, meta-data
e network-config
.
user-data
(Configuração do Usuário): Define o usuário, senha, grupos e chaves SSH autorizadas.
1
2
3
4
5
6
7
8
9
10
11
12
13
cat << EOF > user-data
#cloud-config
# Define o usuário 'gean', concede privilégios de sudo sem senha e
# adiciona a chave SSH pública para acesso remoto.
users:
- name: gean
sudo: ALL=(ALL) NOPASSWD:ALL
groups: users, sudo
shell: /bin/bash
lock_passwd: true
ssh_authorized_keys:
- $(cat ~/.ssh/kvm.pub)
EOF
meta-data
(Identidade da Instância): Define o hostname e o ID da instância.
1
2
3
4
cat << EOF > meta-data
instance-id: debian-trixie
local-hostname: debian-trixie
EOF
network-config
(Configuração de Rede): Instrui a VM a obter um endereço IP via DHCP.
1
2
3
4
5
6
cat << EOF > network-config
version: 2
ethernets:
enp1s0:
dhcp4: true
EOF
1.3. Criação da ISO de cloud-init
Agora, vamos empacotar esses três arquivos em uma imagem ISO, que será anexada à VM como um CD-ROM.
1
2
# O 'genisoimage' cria um ISO chamado cidata.iso com os arquivos de configuração
genisoimage -output cidata.iso -volid cidata -joliet -rock user-data meta-data network-config
1.4. Criação da VM com virt-install
Com a imagem e a ISO prontas, podemos criar a VM.
1
2
3
4
5
6
7
8
9
10
11
12
virt-install \
--name debian-trixie \
--memory 2048 \
--vcpus 2 \
--machine q35 \
--os-variant debian13 \
--network=default,model=virtio \
--disk path=/home/gean/kvm/images/debian-trixie.qcow2,bus=scsi \
--disk path=cidata.iso,device=cdrom,bus=scsi \
--graphics spice \
--noautoconsole \
--import # A flag --import é crucial, pois instrui o virt-install a usar o SO da imagem existente
1.5. Verificação e Acesso
Após alguns instantes, a VM estará em execução. Verifique seu status e obtenha seu endereço IP para acessá-la via SSH.
1
2
3
4
~/Workspace/libvirt/packer/debian13 → virsh list
Id Name State
-------------------------------
14 debian-trixie running
1
2
3
4
~/Workspace/libvirt/packer/debian13 → virsh domifaddr debian-trixie
Name MAC address Protocol Address
-------------------------------------------------------------------------------
vnet13 52:54:00:d4:26:fa ipv4 192.168.122.237/24
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
~/Workspace/libvirt/packer/debian13 → ssh -i ~/.ssh/kvm gean@192.168.122.237
The authenticity of host '192.168.122.237 (192.168.122.237)' can't be established.
ED25519 key fingerprint is SHA256:uRQ9A7d8qv6AG1fw9AJrmEJv077Q8OZefwDQk/O9i6c.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '192.168.122.237' (ED25519) to the list of known hosts.
Linux debian-trixie 6.12.41+deb13-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.41-1 (2025-08-12) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
gean@debian-trixie:~$
1
2
~/Workspace/libvirt/packer/debian13 → virsh undefine --domain debian-trixie
Domain 'debian-trixie' has been undefined
2. Provisionando com Terraform
Esta abordagem utiliza Infraestrutura como Código para um gerenciamento mais robusto, ideal para ambientes complexos e times que colaboram no gerenciamento da infraestrutura.
2.1. Estrutura do Projeto Terraform
Crie um novo diretório para o seu projeto Terraform.
1
2
mkdir -p ~/Workspace/terraform/providers/libvirt/debian13-trixie
cd ~/Workspace/terraform/providers/libvirt/debian13-trixie
2.2. Arquivos de Configuração (cloud-init
)
Assim como no método anterior, crie os arquivos user-data
, meta-data
e network-config
no diretório do projeto. Você pode usar os mesmos conteúdos da seção 1.2.
2.3. main.tf
(Configuração do Terraform)
Este arquivo define todos os recursos que o Terraform irá gerenciar: a cópia da imagem, a ISO do cloud-init
e a própria VM.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# main.tf
terraform {
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
}
}
}
provider "libvirt" {
uri = "qemu:///system"
}
resource "libvirt_volume" "os_image" {
name = "debpacker.qcow2"
pool = "default"
source = "/home/gean/kvm/templates/debian-13.qcow2"
format = "qcow2"
}
# Criar a ISO do cloud-init usando genisoimage
resource "null_resource" "create_cloud_init_iso" {
triggers = {
user_data = filemd5("${path.module}/user-data")
meta_data = filemd5("${path.module}/meta-data")
network-config = filemd5("${path.module}/network-config")
}
provisioner "local-exec" {
command = "genisoimage -output ${path.module}/cloud-init.iso -volid cidata -joliet -rock ${path.module}/user-data ${path.module}/meta-data ${path.module}/network-config"
}
}
# Volume para a ISO do cloud-init
resource "libvirt_volume" "cloud_init_iso" {
name = "cloud-init.iso"
pool = "default"
source = "${path.module}/cloud-init.iso"
format = "raw"
depends_on = [null_resource.create_cloud_init_iso]
}
resource "libvirt_domain" "debpacker" {
name = "debpacker"
memory = "2048"
vcpu = 2
machine = "q35"
cpu {
mode = "host-passthrough"
}
network_interface {
network_name = "default"
wait_for_lease = true
}
disk {
volume_id = libvirt_volume.os_image.id
scsi = true
}
disk {
volume_id = libvirt_volume.cloud_init_iso.id
scsi = true
}
console {
type = "pty"
target_port = "0"
target_type = "serial"
}
graphics {
type = "spice"
listen_type = "none"
}
depends_on = [libvirt_volume.cloud_init_iso]
}
output "ip" {
value = libvirt_domain.debpacker.network_interface[0].addresses[0]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cat << EOF > user-data
#cloud-config
manage_etc_hosts: true
users:
- name: gean
gecos: "Gean Martins"
sudo: ALL=(ALL) NOPASSWD:ALL
groups: users, sudo
shell: /bin/bash
lock_passwd: true
ssh_authorized_keys:
- $(cat ~/.ssh/kvm.pub)
runcmd:
- echo '127.0.1.1 debian-trixie' >> /etc/hosts
EOF
1
2
3
4
5
6
cat << EOF > network-config
version: 2
ethernets:
enp1s0:
dhcp4: true
EOF
1
2
3
4
cat << EOF > meta-data
instance-id: debian-trixie
local-hostname: debian-trixie
EOF
2.4. Execução do Terraform
Com os arquivos prontos, siga o fluxo de trabalho padrão do Terraform.
1
2
3
4
5
6
7
8
# Inicializa o projeto (baixa o provedor libvirt)
terraform init
# Valida a sintaxe dos arquivos
terraform validate
# (Opcional) Formata o código para o padrão
terraform fmt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# Gera e exibe um plano de execução
terraform plan
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# libvirt_domain.debpacker will be created
+ resource "libvirt_domain" "debpacker" {
+ arch = (known after apply)
+ autostart = (known after apply)
+ emulator = (known after apply)
+ fw_cfg_name = "opt/com.coreos/config"
+ id = (known after apply)
+ machine = "q35"
+ memory = 2048
+ name = "debpacker"
+ qemu_agent = false
+ running = true
+ type = "kvm"
+ vcpu = 2
+ console {
+ source_host = "127.0.0.1"
+ source_service = "0"
+ target_port = "0"
+ target_type = "serial"
+ type = "pty"
}
+ cpu {
+ mode = "host-passthrough"
}
+ disk {
+ scsi = true
+ volume_id = (known after apply)
+ wwn = (known after apply)
}
+ disk {
+ scsi = true
+ volume_id = (known after apply)
+ wwn = (known after apply)
}
+ graphics {
+ autoport = true
+ listen_address = "127.0.0.1"
+ listen_type = "none"
+ type = "spice"
}
+ network_interface {
+ addresses = (known after apply)
+ hostname = (known after apply)
+ mac = (known after apply)
+ network_id = (known after apply)
+ network_name = "default"
+ wait_for_lease = true
}
+ nvram (known after apply)
}
# libvirt_volume.cloud_init_iso will be created
+ resource "libvirt_volume" "cloud_init_iso" {
+ format = "raw"
+ id = (known after apply)
+ name = "cloud-init.iso"
+ pool = "default"
+ size = (known after apply)
+ source = "./cloud-init.iso"
}
# libvirt_volume.os_image will be created
+ resource "libvirt_volume" "os_image" {
+ format = "qcow2"
+ id = (known after apply)
+ name = "debpacker.qcow2"
+ pool = "default"
+ size = (known after apply)
+ source = "/home/gean/kvm/templates/debian-13.qcow2"
}
# null_resource.create_cloud_init_iso will be created
+ resource "null_resource" "create_cloud_init_iso" {
+ id = (known after apply)
+ triggers = {
+ "meta_data" = "a810370912b8804965df258628bb1f7a"
+ "network-config" = "739856b4831a24a6333014e36aec1070"
+ "user_data" = "68b572127f680168e8637cb7ee20d740"
}
}
Plan: 4 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ ip = (known after apply)
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
1
2
# Aplica o plano para criar os recursos
terraform apply
Após a confirmação, o Terraform provisionará a VM e, ao final, exibirá o endereço IP dela.
1
2
3
4
5
6
[...]
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
Outputs:
ip = "192.168.122.243"
2.5. Acesso e Destruição
Acesse a VM usando o IP fornecido pelo terraform apply
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
~/Workspace/terraform/providers/libvirt/debian13-trixie → ssh -i ~/.ssh/kvm gean@192.168.122.243
The authenticity of host '192.168.122.243 (192.168.122.243)' can't be established.
ED25519 key fingerprint is SHA256:NBALPYO1GomW0pjPzStomVd7wx7PC9kxDaFXGnqYnyU.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '192.168.122.243' (ED25519) to the list of known hosts.
Linux debian-trixie 6.12.41+deb13-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.41-1 (2025-08-12) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
gean@debian-trixie:~$
Quando não precisar mais da VM, você pode destruí-la facilmente com um único comando, removendo todos os recursos gerenciados pelo Terraform (a VM e os volumes).
1
terraform destroy
3. Desafios Enfrentados: A Jornada até a Solução
Durante o desenvolvimento deste guia, enfrentei vários desafios que enriqueceram o aprendizado. Um final de semana não foi o suficiente. Muitas pesquisas e uso de IAs foram necessários; ou seja, usei tudo que tinha à disposição e, ainda assim, encontrei dificuldades.
O principal desafio que destaco foi fazer o Terraform funcionar com o tipo de máquina q35
(machine = "q35"
) em conjunto com o cloud-init.
O Problema: q35
vs. cloudinit
Padrão do Terraform
O provedor libvirt
do Terraform oferece um recurso nativo, libvirt_cloudinit_disk
, que simplifica a criação da ISO de configuração. Normalmente, ele é usado assim:
1
2
3
4
5
6
7
8
9
10
11
12
# Abordagem padrão que NÃO funcionou com a máquina q35
resource "libvirt_cloudinit_disk" "cloudinit" {
name = "cloudinit.iso"
user_data = templatefile("${path.module}/user-data", {})
pool = "default"
}
resource "libvirt_domain" "debpacker" {
# ...
cloudinit = libvirt_cloudinit_disk.cloudinit.id
# ...
}
O problema é que este recurso, por padrão, tenta anexar a ISO a um controlador IDE, que é o padrão para tipos de máquina mais antigos como i440fx
. No entanto, ao usar o tipo de máquina moderno q35
, os controladores IDE não são suportados, resultando no seguinte erro:
Error: error defining libvirt domain: unsupported configuration: IDE controllers are unsupported for this QEMU binary or machine type
Tentei forçar a configuração via XML (usando XSLT para transformar o XML do domínio), mas sem sucesso. Confesso que, após esgotar as possibilidades que me eram familiares, precisei buscar uma solução alternativa.
A Solução: genisoimage
e Controle Manual
A solução que funcionou, e que está detalhada neste guia, foi contornar o recurso libvirt_cloudinit_disk
e assumir o controle total da criação e anexo da ISO.
A estratégia foi dividida em três passos:
- Gerar a ISO manualmente: Usar o
null_resource
para invocar o comandogenisoimage
e criar a ISO docloud-init
. - Criar um Volume Libvirt para a ISO: Tratar a ISO gerada como um volume
raw
padrão dentro do pool do Libvirt. - Anexar o Volume como um Disco SCSI: Na definição do
libvirt_domain
, anexar a ISO explicitamente como um discoscsi
, o que é totalmente compatível com a máquinaq35
.
Essa abordagem nos deu o controle granular necessário para resolver a incompatibilidade, garantindo que a VM pudesse ser criada com a arquitetura desejada.
4. Conclusão
Neste guia, demonstramos o poder de um fluxo de trabalho de imagens imutáveis. Ao criar uma imagem base com o Packer e provisioná-la com ferramentas como virt-install
ou Terraform, você estabelece um processo de implantação rápido, confiável e altamente reprodutível.
A jornada para encontrar a solução para o provisionamento com Terraform e q35
destaca a importância de entender as ferramentas subjacentes e ter a flexibilidade para combinar diferentes abordagens quando os métodos padrão não atendem a requisitos específicos.
A partir daqui, você pode expandir os provisionadores do Packer para incluir mais softwares na imagem base ou criar módulos Terraform mais complexos para orquestrar múltiplas VMs.