Post

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.

Guia Completo: Criando um Template Debian 13 (BIOS+LVM) com Packer e Terraform (Parte 2/2)

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:

  1. virt-install: Uma ferramenta de linha de comando ideal para scripts e provisionamento rápido e direto.
  2. 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:

  1. Gerar a ISO manualmente: Usar o null_resource para invocar o comando genisoimage e criar a ISO do cloud-init.
  2. Criar um Volume Libvirt para a ISO: Tratar a ISO gerada como um volume raw padrão dentro do pool do Libvirt.
  3. Anexar o Volume como um Disco SCSI: Na definição do libvirt_domain, anexar a ISO explicitamente como um disco scsi, o que é totalmente compatível com a máquina q35.

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.


This post is licensed under CC BY 4.0 by the author.