Este guia detalha o processo de provisionamento de uma infraestrutura completa de laboratório (homelab) baseada em KVM/Libvirt utilizando o Terraform. A abordagem utilizada é modular e orientada a ambientes (environments), permitindo a criação de múltiplas topologias (como um cluster Kubernetes em Alta Disponibilidade) reaproveitando o mesmo código base.
Esta infraestrutura fará uso das imagens (templates) criadas nos guias anteriores com o Packer (Debian, Oracle Linux e Ubuntu).
O Terraform é a ferramenta de Infraestrutura como Código (IaC) que orquestrará a criação das redes, discos, injeção de cloud-init e inicialização das máquinas virtuais.
Instale o Terraform em sua estação de trabalho:
sudo apt install terraform
Verifique a versão instalada:
terraform version
# Saída esperada: Terraform v1.14.8 (ou superior)
Nota: O repositório de instalação do Terraform é o mesmo utilizado para o Packer. Consulte o Guia de Criação de Imagem QEMU com Packer: Debian 13 caso não o tenha configurado.
A arquitetura do projeto foi desenhada para separar o código reutilizável (Módulos) das configurações específicas de cada topologia (Environments).
.
├── cloud-init # Templates base para injeção de dados nas VMs
│ └── user-data.yaml # Configuração de usuários, senhas e chaves SSH
├── environments # Diferentes topologias de infraestrutura
│ ├── homolog # Ambiente de homologação simples
│ ├── kube-cluster # Cluster Kubernetes padrão
│ └── kube-cluster-ha # Cluster Kubernetes em Alta Disponibilidade (Foco deste guia)
└── modules # Módulos reutilizáveis do Terraform
├── cloudinit # Gera ISOs de configuração (user-data/network-data)
├── compute # Cria as Máquinas Virtuais (Domains)
├── network # Gerencia as redes virtuais e DHCP
└── storage # Gerencia os discos virtuais (Volumes)
O Cloud-Init é o padrão da indústria para inicialização de instâncias em nuvem. O Terraform usará este template para gerar um disco ISO virtual anexado a cada VM no primeiro boot, configurando usuários e acessos.
cloud-init/user-data.yaml#cloud-config
manage_etc_hosts: true # Permite que o cloud-init gerencie o /etc/hosts
hostname: ${hostname} # Injetado dinamicamente pelo Terraform
users:
- name: ${username} # Nome do usuário padrão (ex: suporte)
gecos: "${user_gecos}" # Nome completo/descrição do usuário
sudo: "ALL=(ALL) NOPASSWD:ALL"# Permite executar sudo sem solicitar senha
groups: ${user_groups} # Grupos adicionais (ex: wheel, sudo, users)
shell: /bin/bash
lock_passwd: true # Desabilita login por senha (força o uso de chaves SSH)
ssh_authorized_keys:
- ${ssh_key} # Chave pública SSH injetada pelo Terraform
kube-cluster-ha)O ambiente kube-cluster-ha define a topologia de um cluster Kubernetes robusto, incluindo redes isoladas para banco de dados, infraestrutura, autenticação e os nós do cluster (Control Plane e Workers).
providers.tf)# environments/kube-cluster-ha/providers.tf
terraform {
required_version = "~> 1.14"
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
version = "~> 0.9"
}
}
}
provider "libvirt" {
uri = var.libvirt_uri
}
main.tf)O arquivo principal chama os módulos locais, passando as variáveis definidas para este ambiente específico. A ordem de dependência é resolvida automaticamente pelo Terraform.
# environments/kube-cluster-ha/main.tf
# 1. Criação das Redes Virtuais
module "network" {
source = "../../modules/network"
networks = var.networks
}
# 2. Criação dos Discos (Baseados nos templates do Packer)
module "storage" {
source = "../../modules/storage"
servers = var.servers
images_pool = var.pool_images
path_templates = var.path_templates
os_profiles = var.os_profiles
}
# 3. Geração das ISOs do Cloud-Init (Configuração de SO e Rede)
module "cloudinit" {
source = "../../modules/cloudinit"
servers = var.servers
ssh_public_key = var.ssh_public_key
images_pool = var.pool_images
user_data_template_path = "../../cloud-init/user-data.yaml"
os_profiles = var.os_profiles
default_vm_user = var.default_vm_user
all_networks = var.networks
}
# 4. Criação das Máquinas Virtuais (Vinculando Rede, Storage e Cloud-Init)
module "compute" {
source = "../../modules/compute"
servers = var.servers
vm_volumes = module.storage.vm_volumes
extra_volumes = module.storage.vm_extra_volumes
cloud_init_volumes = module.cloudinit.cloud_init_volumes
network_names = module.network.network_names
}
variables.tf)# environments/kube-cluster-ha/variables.tf
variable "libvirt_uri" {
description = "URI de conexão com o daemon libvirt. Ex: qemu+ssh://user@host/system"
type = string
sensitive = true
}
variable "ssh_public_key" {
description = "Chave pública SSH para acesso às VMs"
type = string
sensitive = true
}
variable "pool_images" {
description = "Pool de armazenamento para os discos das VMs"
type = string
default = "default"
}
variable "pool_templates" {
description = "Pool de armazenamento onde estão os templates base"
type = string
default = "templates"
}
variable "path_images" {
description = "Caminho físico no host para o pool de imagens"
type = string
default = "/datastore/images/"
}
variable "path_templates" {
description = "Caminho físico no host para os templates de OS"
type = string
default = "/datastore/templates/"
}
variable "networks" {
description = "Mapa de redes a serem criadas"
type = map(object({
forward_mode = string
ipv4_cidr = string
ipv6_cidr = string
dhcp_ipv4_start = optional(string)
dhcp_ipv4_end = optional(string)
dhcp_ipv6_start = optional(string)
dhcp_ipv6_end = optional(string)
gateway_ipv4 = optional(string)
gateway_ipv6 = optional(string)
dns_servers = optional(list(string))
}))
}
variable "servers" {
description = "Mapa de servidores a serem criados"
type = map(object({
vcpus = number
memory_mib = number
disk_size_gb = number
disk_extra_gb = optional(number)
base_os = string
networks = list(object({
network_name = string
ipv4_address = string
ipv4_prefix = optional(number)
ipv6_address = string
ipv6_prefix = optional(number)
interface_name = string
is_default_gateway = bool
is_default_dns = optional(bool, true)
}))
}))
}
variable "os_profiles" {
description = "Perfis de sistema operacional com nome do template e grupos padrão."
type = map(object({
template_name = string
default_groups = list(string)
supports_dhcp6 = optional(bool, false)
}))
}
variable "default_vm_user" {
description = "Configurações do usuário padrão para todas as VMs."
type = object({
name = string
gecos = string
})
}
networks.auto.tfvars)Este arquivo define todas as redes lógicas do ambiente. O parâmetro forward_mode = "none" cria redes totalmente isoladas (Host-Only). A rede external (forward_mode = "nat") é a única com saída para a internet real.
# environments/kube-cluster-ha/networks.auto.tfvars
networks = {
"external" = {
forward_mode = "nat" # Acesso à internet via NAT do Host KVM
ipv4_cidr = "10.48.0.0/24"
ipv6_cidr = "fd00:0:b::/64"
dhcp_ipv4_start = "10.48.0.128" # Libvirt gerenciará IPs dinâmicos neste range
dhcp_ipv4_end = "10.48.0.254"
dhcp_ipv6_start = "fd00:0:b::100"
dhcp_ipv6_end = "fd00:0:b::1ff"
}
"infra-net" = {
forward_mode = "none" # Rede isolada (Sem NAT)
ipv4_cidr = "10.48.1.0/24"
ipv6_cidr = "fd00:0:b:1::/64"
gateway_ipv4 = "10.48.1.1" # IP que será assumido pela VM Gateway
gateway_ipv6 = "fd00:0:b:1::1"
dns_servers = ["10.48.1.2", "fd00:0:b:1::2"] # Aponta para a VM 'ns1'
}
"auth-net" = {
forward_mode = "none"
ipv4_cidr = "10.48.3.0/24"
ipv6_cidr = "fd00:0:b:3::/64"
gateway_ipv4 = "10.48.3.1"
gateway_ipv6 = "fd00:0:b:3::1"
dns_servers = ["10.48.1.2", "fd00:0:b:1::2"]
}
"db-net" = {
forward_mode = "none"
ipv4_cidr = "10.48.5.0/24"
ipv6_cidr = "fd00:0:b:5::/64"
gateway_ipv4 = "10.48.5.1"
gateway_ipv6 = "fd00:0:b:5::1"
dns_servers = ["10.48.1.2", "fd00:0:b:1::2"]
}
"dhcp-net" = {
forward_mode = "none"
ipv4_cidr = "10.48.7.0/24"
ipv6_cidr = "fd00:0:b:7::/64"
gateway_ipv4 = "10.48.7.1"
gateway_ipv6 = "fd00:0:b:7::1"
dns_servers = ["10.48.1.2", "fd00:0:b:1::2"]
}
"kube-net" = {
forward_mode = "none"
ipv4_cidr = "10.48.9.0/24"
ipv6_cidr = "fd00:0:b:9::/64"
gateway_ipv4 = "10.48.9.1"
gateway_ipv6 = "fd00:0:b:9::1"
dns_servers = ["10.48.1.2", "fd00:0:b:1::2"]
}
}
Nota de Arquitetura: As redes isoladas (
none) não possuem saída para a internet nativamente. O acesso externo será provido de forma centralizada pela VMgateway, que atuará como roteador (pfSense/OPNsense ou iptables) possuindo interfaces virtuais conectadas a todas as redes simultaneamente.
servers.auto.tfvars)Este é o arquivo mais extenso, pois descreve cada máquina virtual, seus recursos (CPU, RAM, Discos) e a quais redes ela pertence.
# environments/kube-cluster-ha/servers.auto.tfvars
servers = {
# ==========================================
# Roteador Central (Gateway)
# ==========================================
"gateway" = {
vcpus = 2
memory_mib = 1024
disk_size_gb = 16
base_os = "debian"
networks = [
{
network_name = "external" # Interface WAN (Conecta à internet)
ipv4_address = "dhcp"
ipv6_address = "dhcp"
interface_name = "enp1s0"
is_default_gateway = true # O tráfego padrão do Gateway sai por aqui
},
{
network_name = "infra-net" # Interface LAN 1
ipv4_address = "10.48.1.1" # Atua como gateway para esta rede
ipv4_prefix = 24
ipv6_address = "fd00:0:b:1::1"
ipv6_prefix = 64
interface_name = "enp2s0"
is_default_gateway = false
is_default_dns = false # Não usa o DNS desta rede, usa o da external
},
{
network_name = "auth-net"
ipv4_address = "10.48.3.1"
ipv4_prefix = 24
ipv6_address = "fd00:0:b:3::1"
ipv6_prefix = 64
interface_name = "enp3s0"
is_default_gateway = false
is_default_dns = false
},
{
network_name = "db-net"
ipv4_address = "10.48.5.1"
ipv4_prefix = 24
ipv6_address = "fd00:0:b:5::1"
ipv6_prefix = 64
interface_name = "enp4s0"
is_default_gateway = false
is_default_dns = false
},
{
network_name = "dhcp-net"
ipv4_address = "10.48.7.1"
ipv4_prefix = 24
ipv6_address = "fd00:0:b:7::1"
ipv6_prefix = 64
interface_name = "enp5s0"
is_default_gateway = false
is_default_dns = false
},
{
network_name = "kube-net"
ipv4_address = "10.48.9.1"
ipv4_prefix = 24
ipv6_address = "fd00:0:b:9::1"
ipv6_prefix = 64
interface_name = "enp6s0"
is_default_gateway = false
is_default_dns = false
}
]
}
# ==========================================
# Infraestrutura Base
# ==========================================
"ns1" = {
vcpus = 2
memory_mib = 1024
disk_size_gb = 16
base_os = "debian"
networks = [
{
network_name = "infra-net"
ipv4_address = "10.48.1.2" # Servidor DNS (Bind9/CoreDNS)
ipv4_prefix = 24
ipv6_address = "fd00:0:b:1::2"
ipv6_prefix = 64
interface_name = "enp1s0"
is_default_gateway = true # Envia tráfego externo para o 10.48.1.1 (Gateway)
is_default_dns = false # Evita loop de DNS em si mesmo durante o boot
}
]
}
"storage" = {
vcpus = 2
memory_mib = 2048
disk_size_gb = 16
disk_extra_gb = 512 # Disco secundário para dados NFS (Kubernetes PVs)
base_os = "oracle"
networks = [
{
network_name = "infra-net"
ipv4_address = "10.48.1.10"
ipv4_prefix = 24
ipv6_address = "fd00:0:b:1::a"
ipv6_prefix = 64
interface_name = "enp1s0"
is_default_gateway = true
is_default_dns = true # Usa o DNS da rede (10.48.1.2)
}
]
}
# ==========================================
# Serviços de Autenticação e Banco de Dados
# ==========================================
"adds-01" = {
vcpus = 2
memory_mib = 2048
disk_size_gb = 16
base_os = "ubuntu"
networks = [
{
network_name = "auth-net"
ipv4_address = "10.48.3.2"
ipv4_prefix = 24
ipv6_address = "fd00:0:b:3::2"
ipv6_prefix = 64
interface_name = "enp1s0"
is_default_gateway = true
is_default_dns = true
}
]
}
"idm-01" = {
vcpus = 2
memory_mib = 2048
disk_size_gb = 16
base_os = "oracle"
networks = [
{
network_name = "auth-net"
ipv4_address = "10.48.3.20"
ipv4_prefix = 24
ipv6_address = "fd00:0:b:3::14"
ipv6_prefix = 64
interface_name = "enp1s0"
is_default_gateway = true
is_default_dns = true
}
]
}
"mysql" = {
vcpus = 2
memory_mib = 2048
disk_size_gb = 16
disk_extra_gb = 64
base_os = "oracle"
networks = [
{
network_name = "db-net"
ipv4_address = "10.48.5.2"
ipv4_prefix = 24
ipv6_address = "fd00:0:b:5::2"
ipv6_prefix = 64
interface_name = "enp1s0"
is_default_gateway = true
is_default_dns = true
}
]
}
"postgres" = {
vcpus = 2
memory_mib = 2048
disk_size_gb = 16
disk_extra_gb = 64
base_os = "debian"
networks = [
{
network_name = "db-net"
ipv4_address = "10.48.5.20"
ipv4_prefix = 24
ipv6_address = "fd00:0:b:5::14"
ipv6_prefix = 64
interface_name = "enp1s0"
is_default_gateway = true
is_default_dns = true
}
]
}
# ==========================================
# Kubernetes Cluster (Control Plane HA)
# ==========================================
"kube-lb" = {
vcpus = 2
memory_mib = 1024
disk_size_gb = 16
base_os = "debian"
networks = [
{
network_name = "kube-net"
ipv4_address = "10.48.9.100" # HAProxy: Balanceador de carga para a API do K8s
ipv4_prefix = 24
ipv6_address = "fd00:0:b:9::63"
ipv6_prefix = 64
interface_name = "enp1s0"
is_default_gateway = true
is_default_dns = true
}
]
}
"kube-ctrl-01" = {
vcpus = 2
memory_mib = 4096
disk_size_gb = 16
disk_extra_gb = 32 # Disco secundário para o Container Runtime (CRI-O/Containerd)
base_os = "debian"
networks = [
{
network_name = "kube-net"
ipv4_address = "10.48.9.2"
ipv4_prefix = 24
ipv6_address = "fd00:0:b:9::2"
ipv6_prefix = 64
interface_name = "enp1s0"
is_default_gateway = true
is_default_dns = true
}
]
}
"kube-ctrl-02" = {
vcpus = 2
memory_mib = 4096
disk_size_gb = 16
disk_extra_gb = 32
base_os = "debian"
networks = [
{
network_name = "kube-net"
ipv4_address = "10.48.9.3"
ipv4_prefix = 24
ipv6_address = "fd00:0:b:9::3"
ipv6_prefix = 64
interface_name = "enp1s0"
is_default_gateway = true
is_default_dns = true
}
]
}
"kube-ctrl-03" = {
vcpus = 2
memory_mib = 4096
disk_size_gb = 16
disk_extra_gb = 32
base_os = "debian"
networks = [
{
network_name = "kube-net"
ipv4_address = "10.48.9.4"
ipv4_prefix = 24
ipv6_address = "fd00:0:b:9::4"
ipv6_prefix = 64
interface_name = "enp1s0"
is_default_gateway = true
is_default_dns = true
}
]
}
# ==========================================
# Kubernetes Cluster (Worker Nodes)
# ==========================================
"kube-worker-01" = {
vcpus = 4
memory_mib = 16384 # 16GB RAM para cargas de trabalho pesadas
disk_size_gb = 16
disk_extra_gb = 32 # Armazenamento local efêmero de containers
base_os = "debian"
networks = [
{
network_name = "kube-net"
ipv4_address = "10.48.9.20"
ipv4_prefix = 24
ipv6_address = "fd00:0:b:9::14"
ipv6_prefix = 64
interface_name = "enp1s0"
is_default_gateway = true
is_default_dns = true
}
]
}
"kube-worker-02" = {
vcpus = 4
memory_mib = 16384
disk_size_gb = 16
disk_extra_gb = 32
base_os = "debian"
networks = [
{
network_name = "kube-net"
ipv4_address = "10.48.9.21"
ipv4_prefix = 24
ipv6_address = "fd00:0:b:9::15"
ipv6_prefix = 64
interface_name = "enp1s0"
is_default_gateway = true
is_default_dns = true
}
]
}
"kube-worker-03" = {
vcpus = 4
memory_mib = 16384
disk_size_gb = 16
disk_extra_gb = 32
base_os = "debian"
networks = [
{
network_name = "kube-net"
ipv4_address = "10.48.9.22"
ipv4_prefix = 24
ipv6_address = "fd00:0:b:9::16"
ipv6_prefix = 64
interface_name = "enp1s0"
is_default_gateway = true
is_default_dns = true
}
]
}
}
terraform.tfvars)Este arquivo mapeia as imagens geradas pelo Packer aos seus respectivos sistemas operacionais e define parâmetros globais.
# environments/kube-cluster-ha/terraform.tfvars
# =========================================================================
# SEGURANÇA: Credenciais Sensíveis
# =========================================================================
# NUNCA commite a chave SSH ou o URI do Libvirt neste arquivo.
# Passe-os via variáveis de ambiente antes de executar o Terraform:
# export TF_VAR_ssh_public_key="$(cat ~/.ssh/kvm.pub)"
# export TF_VAR_libvirt_uri="qemu+ssh://gean@192.168.0.251/system"
# =========================================================================
pool_images = "default"
pool_templates = "templates"
path_images = "/datastore/images"
path_templates = "/datastore/templates"
# Mapeamento dos perfis de SO para as imagens geradas pelo Packer
os_profiles = {
"debian" = {
template_name = "debian-trixie-base.qcow2",
default_groups = ["users", "sudo"],
supports_dhcp6 = false # O Debian (ifupdown) não precisa de DHCPv6 explícito, usa SLAAC nativo
},
"oracle" = {
template_name = "ol9-base.qcow2",
default_groups = ["users", "wheel"],
supports_dhcp6 = false
},
"ubuntu" = {
template_name = "noble-server-base.qcow2",
default_groups = ["users", "sudo"],
supports_dhcp6 = true # O Netplan do Ubuntu requer declaração explícita
}
}
default_vm_user = {
name = "suporte"
gecos = "Suporte User"
}
Os módulos abstraem a complexidade do provider do Libvirt. Abaixo estão os códigos completos de cada módulo.
modules/network)Cria as redes virtuais. Lida com a diferença entre redes NAT (com DHCP do KVM) e redes Isoladas.
modules/network/main.tfresource "libvirt_network" "network" {
for_each = var.networks
name = each.key
autostart = true
# Omite o bloco forward completamente para mode "none"
forward = each.value.forward_mode != "none" ? {
mode = each.value.forward_mode
} : null
ips = each.value.forward_mode == "nat" ? [
{
family = "ipv4"
address = cidrhost(each.value.ipv4_cidr, 1)
prefix = tonumber(split("/", each.value.ipv4_cidr)[1])
dhcp = {
ranges = lookup(each.value, "dhcp_ipv4_start", null) != null ? [
{
start = each.value.dhcp_ipv4_start
end = each.value.dhcp_ipv4_end
}
] : []
}
},
{
family = "ipv6"
address = cidrhost(each.value.ipv6_cidr, 1)
prefix = tonumber(split("/", each.value.ipv6_cidr)[1])
dhcp = {
ranges = lookup(each.value, "dhcp_ipv6_start", null) != null ? [
{
start = each.value.dhcp_ipv6_start
end = each.value.dhcp_ipv6_end
}
] : []
}
}
] : []
# NUNCA use dns = {} — deixe null para evitar o bug do provider
dns = null
}
modules/network/variables.tfvariable "networks" {
description = "Mapa de redes a serem criadas"
type = map(object({
forward_mode = string
ipv4_cidr = string
ipv6_cidr = string
dhcp_ipv4_start = optional(string)
dhcp_ipv4_end = optional(string)
dhcp_ipv6_start = optional(string)
dhcp_ipv6_end = optional(string)
gateway_ipv4 = optional(string)
gateway_ipv6 = optional(string)
dns_servers = optional(list(string))
}))
}
modules/network/outputs.tfoutput "network_names" {
description = "Nomes das redes libvirt criadas."
value = { for k, v in libvirt_network.network : k => v.name }
}
output "network_ids" {
description = "IDs das redes libvirt criadas."
value = { for k, v in libvirt_network.network : k => v.id }
}
modules/network/version.tfterraform {
required_version = "~> 1.14"
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
version = "~> 0.9"
}
}
}
modules/storage)Gerencia a clonagem rápida dos templates (backing_store) e a criação de discos secundários.
modules/storage/main.tfresource "libvirt_volume" "vm_volume" {
for_each = var.servers
name = "${each.key}.qcow2"
pool = var.images_pool
backing_store = {
path = "${var.path_templates}/${var.os_profiles[each.value.base_os].template_name}"
format = { type = "qcow2" }
}
target = {
format = { type = "qcow2" }
}
capacity = each.value.disk_size_gb * 1024 * 1024 * 1024
}
# --- Volume extra (dados) ---
locals {
servers_with_extra_disk = {
for name, srv in var.servers : name => srv
if srv.disk_extra_gb != null
}
}
resource "libvirt_volume" "vm_extra_volume" {
for_each = local.servers_with_extra_disk
name = "${each.key}-extra.qcow2"
pool = var.images_pool
target = {
format = { type = "qcow2" }
}
capacity = each.value.disk_extra_gb * 1024 * 1024 * 1024
}
modules/storage/variables.tfvariable "servers" {
description = "Mapa de servidores a serem criados para definir os volumes."
type = map(object({
disk_size_gb = number
disk_extra_gb = optional(number)
base_os = string
}))
}
variable "images_pool" {
description = "Pool de armazenamento para os discos das VMs."
type = string
}
variable "path_templates" {
description = "Caminho físico no host para os templates de OS."
type = string
}
variable "os_profiles" {
description = "Perfis de sistema operacional com nome do template."
type = map(object({
template_name = string
}))
}
modules/storage/outputs.tfoutput "vm_volumes" {
description = "Mapa completo dos volumes das VMs criados."
value = libvirt_volume.vm_volume
}
output "vm_extra_volumes" {
description = "Mapa dos volumes extras criados para VMs que definiram disk_extra_gb."
value = libvirt_volume.vm_extra_volume
}
modules/storage/version.tfterraform {
required_version = "~> 1.14"
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
version = "~> 0.9"
}
}
}
modules/cloudinit)Gera o arquivo de rede (Network-Data v2) dinamicamente, injetando IPs estáticos, rotas (gateways) e servidores DNS, de acordo com as especificações do servers.auto.tfvars.
modules/cloudinit/main.tflocals {
# Operador de coalescência explícito para null:
dns_servers_per_network = {
for net_name, net in var.all_networks : net_name => net.dns_servers != null ? net.dns_servers : []
}
should_include_dns = {
for vm_name, vm in var.servers : vm_name => {
for net in vm.networks : net.interface_name => (
net.is_default_dns && length(local.dns_servers_per_network[net.network_name]) > 0
)
}
}
# Gateway IPv4: usa o valor explícito ou calcula o primeiro IP do CIDR.
gateway_ipv4_per_network = {
for net_name, net in var.all_networks : net_name => (
try(net.gateway_ipv4, cidrhost(net.ipv4_cidr, 1))
)
}
# Redes sem ipv6_cidr nem gateway_ipv6 retornam null
gateway_ipv6_per_network = {
for net_name, net in var.all_networks : net_name => (
net.ipv6_cidr != null || net.gateway_ipv6 != null
? try(net.gateway_ipv6, cidrhost(net.ipv6_cidr, 1))
: null
)
}
}
resource "libvirt_cloudinit_disk" "common_init" {
for_each = var.servers
name = "${each.key}-init.iso"
user_data = templatefile(var.user_data_template_path, {
ssh_key = var.ssh_public_key
hostname = each.key
username = var.default_vm_user.name
user_gecos = var.default_vm_user.gecos
user_groups = join(", ", var.os_profiles[each.value.base_os].default_groups)
})
network_config = yamlencode({
version = 2
ethernets = {
for net_config in each.value.networks : net_config.interface_name => merge(
# DHCP IPv4
net_config.ipv4_address == "dhcp" ? { dhcp4 = true } : {},
# DHCPv6 — apenas quando o OS suporta e o endereço é dinâmico.
net_config.ipv6_address == "dhcp" && var.os_profiles[each.value.base_os].supports_dhcp6
? { dhcp6 = true } : {},
# Desabilita Router Advertisements apenas em interfaces com IPv6 estático
net_config.ipv6_address != "dhcp" ? { "accept-ra" = false } : {},
# Endereços estáticos.
net_config.ipv4_address != "dhcp" || net_config.ipv6_address != "dhcp" ? {
addresses = [
for addr in [
net_config.ipv4_address != "dhcp" && net_config.ipv4_prefix != null
? "${net_config.ipv4_address}/${net_config.ipv4_prefix}" : null,
net_config.ipv6_address != "dhcp" && net_config.ipv6_prefix != null
? "${net_config.ipv6_address}/${net_config.ipv6_prefix}" : null
] : addr if addr != null
]
} : {},
# Rotas padrão — apenas para interfaces com endereço estático.
net_config.is_default_gateway && (
net_config.ipv4_address != "dhcp" || net_config.ipv6_address != "dhcp"
) ? {
routes = concat(
net_config.ipv4_address != "dhcp" ? [{
to = "0.0.0.0/0"
via = local.gateway_ipv4_per_network[net_config.network_name]
}] : [],
net_config.ipv6_address != "dhcp" &&
local.gateway_ipv6_per_network[net_config.network_name] != null ? [{
to = "::/0"
via = local.gateway_ipv6_per_network[net_config.network_name]
}] : []
)
} : {},
# DNS
local.should_include_dns[each.key][net_config.interface_name] ? {
nameservers = {
addresses = local.dns_servers_per_network[net_config.network_name]
}
} : {}
)
}
})
meta_data = yamlencode({
instance-id = each.key
local-hostname = each.key
})
}
# Importa o ISO gerado pelo libvirt_cloudinit_disk para o pool de imagens.
# Isso permite referenciar o CDROM como source.volume no libvirt_domain.
resource "libvirt_volume" "cloud_init_volume" {
for_each = var.servers
name = "${each.key}-init.iso"
pool = var.images_pool
create = {
content = {
url = libvirt_cloudinit_disk.common_init[each.key].path
}
}
}
modules/cloudinit/variables.tfvariable "servers" {
description = "Mapa de servidores para os quais o cloud-init será gerado."
type = map(object({
base_os = string
networks = list(object({
network_name = string
ipv4_address = string
ipv4_prefix = optional(number)
ipv6_address = string
ipv6_prefix = optional(number)
interface_name = string
is_default_gateway = bool
is_default_dns = optional(bool, true)
}))
}))
}
variable "ssh_public_key" {
description = "Chave pública SSH para acesso às VMs."
type = string
sensitive = true
}
variable "images_pool" {
description = "Pool de armazenamento para importar os ISOs de cloud-init."
type = string
}
variable "user_data_template_path" {
description = "Caminho para o arquivo de template user-data.yaml."
type = string
}
variable "os_profiles" {
description = "Perfis de sistema operacional com nome do template e grupos padrão."
type = map(object({
template_name = string
default_groups = list(string)
supports_dhcp6 = optional(bool, false)
}))
}
variable "default_vm_user" {
description = "Configurações do usuário padrão para todas as VMs."
type = object({
name = string
gecos = string
})
}
variable "all_networks" {
description = "Mapa completo de todas as redes configuradas, para lookup de gateways e DNS."
type = map(object({
forward_mode = string
ipv4_cidr = string
ipv6_cidr = string
dhcp_ipv4_start = optional(string)
dhcp_ipv4_end = optional(string)
dhcp_ipv6_start = optional(string)
dhcp_ipv6_end = optional(string)
gateway_ipv4 = optional(string)
gateway_ipv6 = optional(string)
dns_servers = optional(list(string))
}))
}
modules/cloudinit/outputs.tfoutput "cloud_init_volumes" {
description = "Mapa dos volumes de cloud-init importados para o pool (libvirt_volume)."
value = libvirt_volume.cloud_init_volume
}
modules/cloudinit/version.tfterraform {
required_version = "~> 1.14"
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
version = "~> 0.9"
}
}
}
modules/compute)Une todos os recursos criando o Domínio (Máquina Virtual) no Libvirt.
modules/compute/main.tfresource "libvirt_domain" "domain" {
for_each = var.servers
name = each.key
memory = each.value.memory_mib
memory_unit = "MiB"
vcpu = each.value.vcpus
type = "kvm"
cpu = {
mode = "host-passthrough"
}
features = {
acpi = true
}
os = {
type = "hvm"
type_arch = "x86_64"
type_machine = "q35"
firmware = "efi"
}
devices = {
disks = concat(
[
# Disco do sistema (vda)
{
source = {
volume = {
pool = var.vm_volumes[each.key].pool
volume = var.vm_volumes[each.key].name
}
}
target = { dev = "vda", bus = "virtio" }
driver = { type = "qcow2" }
},
# CDROM com cloud-init (sda)
{
device = "cdrom"
source = {
volume = {
pool = var.cloud_init_volumes[each.key].pool
volume = var.cloud_init_volumes[each.key].name
}
}
target = { dev = "sda", bus = "sata" }
}
],
# Disco extra de dados (vdb) — só incluído se disk_extra_gb foi definido
contains(keys(var.extra_volumes), each.key) ? [
{
source = {
volume = {
pool = var.extra_volumes[each.key].pool
volume = var.extra_volumes[each.key].name
}
}
target = { dev = "vdb", bus = "virtio" }
driver = { type = "qcow2" }
}
] : []
)
interfaces = [
for net in each.value.networks : {
type = "network"
model = { type = "virtio" }
source = {
network = {
network = var.network_names[net.network_name]
}
}
}
]
graphics = [{
vnc = {
auto_port = true
listen = "127.0.0.1"
}
}]
}
running = true
}
modules/compute/variables.tfvariable "servers" {
description = "Mapa de servidores a serem criados, com suas configurações de CPU, memória e rede."
type = map(object({
vcpus = number
memory_mib = number
networks = list(object({
network_name = string
}))
}))
}
variable "vm_volumes" {
description = "Mapa dos volumes das VMs criados pelo módulo de storage."
type = map(any)
}
variable "extra_volumes" {
description = "Mapa dos volumes extras criados pelo módulo de storage."
type = map(any)
default = {}
}
variable "cloud_init_volumes" {
description = "Mapa dos volumes de cloud-init importados para o pool pelo módulo de cloudinit."
type = map(any)
}
variable "network_names" {
description = "Nomes das redes libvirt criadas pelo módulo de network."
type = map(string)
}
modules/compute/outputs.tfoutput "domain_names" {
description = "Nomes dos domínios (VMs) criados."
value = { for k, v in libvirt_domain.domain : k => v.name }
}
output "domain_ids" {
description = "IDs dos domínios (VMs) criados."
value = { for k, v in libvirt_domain.domain : k => v.id }
}
modules/compute/version.tfterraform {
required_version = "~> 1.14"
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
version = "~> 0.9"
}
}
}
Com os arquivos configurados, navegue até o ambiente desejado e exporte as variáveis sensíveis de ambiente.
cd environments/kube-cluster-ha/
# Define a chave SSH que será injetada nas VMs
export TF_VAR_ssh_public_key="$(cat ~/.ssh/kvm.pub)"
# Define a string de conexão remota ao servidor KVM
export TF_VAR_libvirt_uri="qemu+ssh://gean@192.168.0.251/system"
Inicialize o Terraform para baixar o provider do Libvirt e carregar os módulos locais:
terraform init
Valide se a sintaxe e as referências estão corretas:
terraform validate
# Saída esperada: Success! The configuration is valid.
Gere o plano de execução para visualizar o que será criado (Redes, Volumes, ISOs e VMs):
terraform plan
Aplique as mudanças (confirme com yes quando solicitado):
terraform apply
Após o término do Terraform, conecte-se ao servidor KVM para validar se as VMs estão rodando.
export LIBVIRT_DEFAULT_URI='qemu+ssh://gean@192.168.0.251/system'
virsh list
Saída esperada:
Id Name State
--------------------------------
1 ns1 running
2 adds-01 running
3 kube-lb running
...
11 gateway running
A VM gateway recebe seu IP da rede external via DHCP do Libvirt. Descubra qual IP foi atribuído a ela:
virsh domifaddr gateway
Saída esperada:
Name MAC address Protocol Address
-------------------------------------------------------------------------------
vnet10 52:54:00:3c:a7:4e ipv4 10.48.0.221/24 <-- Este é o IP
Como as VMs das redes internas (10.48.x.x) não possuem acesso direto a partir da sua estação de trabalho, configure o SSH para usar o gateway como "ponte" (ProxyJump).
Edite seu arquivo ~/.ssh/config ou crie um arquivo dedicado (ex: ~/.ssh/config.d/homelab.conf):
# ~/.ssh/config.d/homelab.conf
# Conexão direta com o Host Físico KVM
Host kvm-01
HostName 192.168.0.251
User gean
IdentityFile ~/.ssh/kvm
# Conexão com a VM Gateway
Host gw-kvm-01
HostName 10.48.0.221
User suporte
IdentityFile ~/.ssh/kvm
ProxyJump kvm-01 # Pula através do host físico (Opcional dependendo da sua rede)
# Regras de roteamento SSH para as redes internas
# Qualquer conexão para 10.48.1.x passará automaticamente pelo Gateway
Host 10.48.1.*
User suporte
IdentityFile ~/.ssh/kvm
ProxyJump gw-kvm-01
Host 10.48.3.*
User suporte
IdentityFile ~/.ssh/kvm
ProxyJump gw-kvm-01
Host 10.48.5.*
User suporte
IdentityFile ~/.ssh/kvm
ProxyJump gw-kvm-01
Host 10.48.9.*
User suporte
IdentityFile ~/.ssh/kvm
ProxyJump gw-kvm-01
Agora, você pode conectar-se diretamente a qualquer nó do cluster Kubernetes (ex: kube-ctrl-01) de forma transparente:
ssh 10.48.9.2
O SSH cuidará automaticamente de estabelecer o túnel através do Gateway! A infraestrutura base está pronta para a instalação do Kubernetes e demais serviços.