Parte 5 da série Terraform + KVM/Libvirt. A estrutura modular da parte 4 ganha um recurso novo: discos de dados adicionais por VM — declarados no servers.auto.tfvars, criados como volumes qcow2 independentes e anexados como vdb, vdc... na ordem declarada. Para demonstrar, criamos o ambiente docker: três VMs (Debian, Oracle Linux e Ubuntu — usando as templates construídas nos tutoriais Packer), cada uma com um disco extra de 32 GiB destinado ao /var/lib/docker.
Código completo: pasta
parte-5-discos-extrasdo repositório da série no GitLab.
| Componente | Mudança |
|---|---|
servers (variável) |
Novo atributo opcional extra_disks (lista de { name, size_gb, mount_hint }) |
modules/storage |
Novo recurso extra_disk + output extra_volumes (ordem preservada) |
modules/compute |
disks passa a ser um concat() de disco raiz + cloud-init + extras |
environments/ |
Novo ambiente docker (3 distros, discos extras); hml/prd seguem iguais |
# environments/docker/servers.auto.tfvars (trecho)
"docker-deb" = {
vcpus = 2
memory_mib = 2048
disk_size_gb = 16
extra_disks = [
{
name = "docker" # compõe o nome do volume qcow2
size_gb = 32
mount_hint = "/var/lib/docker" # documental: NÃO monta nada sozinho
}
]
base_os = "debian"
# ... networks ...
}
mount_hinté apenas organizacional. O Terraform cria e anexa o disco cru — formatação (mkfs),/etc/fstabe montagem ficam a cargo do SO. Para automatizar, use o módulodisk_setup/mountsdo cloud-init nouser-data.yaml.
flatten(): lista aninhada → mapa únicoDiscos extras vivem aninhados nas VMs (servers[].extra_disks[]), mas for_each precisa de um mapa plano. A solução clássica, no modules/storage/main.tf:
locals {
extra_disks = {
for pair in flatten([
for srv, s in var.servers : [
for d in s.extra_disks : {
key = "${srv}-${d.name}" # chave única: "docker-deb-docker"
server = srv
name = d.name
size_gb = d.size_gb
}
]
]) : pair.key => pair
}
}
resource "libvirt_volume" "extra_disk" {
for_each = local.extra_disks
name = "${each.value.server}-${each.value.name}.qcow2"
pool = var.pool_images
capacity = each.value.size_gb * 1024 * 1024 * 1024
target = {
format = {
type = "qcow2"
}
}
}
Diferença do disco raiz: sem backing_store — disco de dados nasce vazio, não é clone de template.
extra_volumesO módulo compute anexa os discos como vdb, vdc... — a letra depende da posição. Para que a posição seja a da declaração (e não a ordem arbitrária de um mapa), o output reconstrói a lista na ordem original do tfvars:
# modules/storage/outputs.tf
output "extra_volumes" {
description = "Mapa nome-da-VM => lista ordenada de { pool, name } dos discos extras"
value = { for srv, s in var.servers : srv => [
for d in s.extra_disks : {
pool = libvirt_volume.extra_disk["${srv}-${d.name}"].pool
name = libvirt_volume.extra_disk["${srv}-${d.name}"].name
}
] }
}
concat() + substr()No modules/compute/main.tf, o bloco disks junta as três origens e calcula a letra do device pelo índice:
# Disco raiz (vda) + ISO de cloud-init (cdrom) + extras (vdb, vdc...)
disks = concat(
[
{ /* disco raiz: var.os_volumes[each.key] → vda */ },
{ /* cdrom: var.cloudinit_volumes[each.key] → sda */ }
],
[
for i, d in lookup(var.extra_volumes, each.key, []) : {
source = {
volume = {
pool = d.pool
volume = d.name
}
}
# 1º extra = vdb (índice 0), 2º = vdc (índice 1)...
target = { dev = "vd${substr("bcdefghijklmnopqrstuvwxyz", i, 1)}", bus = "virtio" }
driver = {
type = "qcow2"
}
}
]
)
O truque substr("bcdef...", i, 1) deriva a letra a partir do b — o a é sempre o disco raiz. Como o índice vem da lista ordenada (seção 4), vdb no KVM corresponde ao primeiro extra_disks do tfvars — previsível e documentável.
No ambiente, basta um argumento a mais na chamada do módulo:
# environments/docker/main.tf (trecho)
module "compute" {
source = "../../modules/compute"
servers = var.servers
os_volumes = module.storage.os_volumes
extra_volumes = module.storage.extra_volumes # ← novo
cloudinit_volumes = module.cloudinit.cloudinit_volumes
network_names = module.network.network_names
}
dockerCenário de demonstração: um host Docker em cada distribuição suportada, todos na rede isolada docker (10.16.1.0/24) atrás do gateway — as três templates construídas com Packer em ação:
| VM | SO (template Packer) | IP | Disco extra |
|---|---|---|---|
docker-deb |
Debian 13 (debian-trixie-base.qcow2) |
10.16.1.10 |
32 GiB → /var/lib/docker |
docker-ol |
Oracle Linux 9 (ol9-base.qcow2) |
10.16.1.11 |
32 GiB → /var/lib/docker |
docker-ub |
Ubuntu 24.04 (ubuntu-noble-server-base.qcow2) |
10.16.1.12 |
32 GiB → /var/lib/docker |
cd environments/docker
export TF_VAR_ssh_public_key="$(cat ~/.ssh/kvm.pub)"
terraform init && terraform validate && terraform plan && terraform apply
O plan deve mostrar 17 recursos: 2 redes + (4 VMs × 4 recursos) + 3 discos extras.
Acesso: como as VMs estão na rede isolada, o ~/.ssh/config usa ProxyJump — e aproveita um curinga para cobrir a subnet inteira de uma vez:
Host gateway
HostName <IP: virsh domifaddr gateway>
User suporte
IdentityFile ~/.ssh/kvm
Host 10.16.1.*
User suporte
IdentityFile ~/.ssh/kvm
ProxyJump gateway
Dentro de qualquer VM, confirme o disco extra (cru, aguardando formatação):
lsblk # vda (SO) + vdb (32G, sem sistema de arquivos)
disk_setup/mounts usando o mount_hint como insumo;vdz (25 extras) — muito além do sensato para uma VM;size_gb redimensiona o volume, mas a partição/sistema de arquivos interno precisa de growpart/xfs_growfs manual;hml/prd da parte 4 funcionam sem mudança — extra_disks é opcional com default [].flatten() e compreensões for — o padrão lista-aninhada → mapasubstr() e concat()