Para de configurar VM na mão

Para de configurar VM na mão

Você acabou de criar sua décima VM do mês. Instalou o sistema, configurou o usuário, copiou as chaves SSH, instalou os pacotes... tudo na mão. De novo. Se essa cena te parece familiar, você precisa conhecer duas ferramentas que vão mudar sua relação com máquinas virtuais: Cloud-init e Virt-customize.

São ferramentas diferentes, com momentos diferentes, mas que juntas formam um combo poderoso pra quem trabalha com KVM e Proxmox. Esse post cobre as duas do começo ao fim: do conceito ao comando que você vai copiar e adaptar.

Antes de começar: o básico que ninguém explica

Pra usar as duas ferramentas desse post, você vai trabalhar com imagens cloud-ready: arquivos .qcow2 que são distribuições Linux pré-instaladas, prontas pra rodar em ambientes de virtualização. A Debian, Ubuntu, Rocky Linux e outras disponibilizam essas imagens oficialmente. Elas são leves (geralmente 500MB a 2GB), vêm com cloud-init instalado e partição mínima que você expande depois.

O qcow2 é simplesamente o formato de disco virtual do QEMU. Pensa nele como um .iso, mas que já é o disco pronto, não um instalador.

💡 Por que não usar ISO normal? Dá pra usar, mas aí você precisa passar pela instalação manual toda vez. Com imagem cloud-ready, você pula direto pra configuração. Em escala, essa diferença importa muito.

Cloud-init: o bilhete dentro da encomenda

Pensa assim: você encomenda um produto e dentro da caixa tem um bilhete de instruções: "ligue aqui, configure assim, coloque isso na tomada". O Cloud-init é esse bilhete pra sua VM.

Ele roda dentro da máquina virtual, na primeira initializção, e executa tudo que você definiu antes: cria usuários, instala pacotes, aplica configurações de rede, executa scripts. É o padrão da indústria: AWS, GCP, Azure e o Proxmox com templates cloud-ready todos falam a mesma língua.

As fases do cloud-init

O cloud-init não roda tudo de uma vez. Ele tem fases, e entender isso resolve metade dos bugs:

Fase Quando roda O que faz
init Cedo no boot, sem rede Detecta datasource, monta discos
config Com rede disponível Instala pacotes, cria usuários, executa módulos
final No final do boot Executa runcmd, phone_home, scripts finais

Isso explica por que um runcmd que faz curl pra um serviço externo pode falhar se a rede não subiu ainda. Ele roda na fase final, mas dependências de rede podem não estar 100% prontas. Solução: usar --wait ou colocar um sleep 5 antes do comando crítico no runcmd. Feio, mas funciona.

O arquivo cloud-config

O formato mais comum de userdata é o cloud-config. A primeira linha tem que ser exatamente #cloud-config; não é comentário, é a assinatura que diz ao cloud-init o que ele está recebendo.

#cloud-config
# Sem essa primeira linha, o arquivo inteiro é ignorado.
# Sem erro. Sem aviso. Sem misericórdia.

# --- Pacotes ---
package_update: true
package_upgrade: true
packages:
  - nginx
  - git
  - curl
  - htop
  - fail2ban

# --- Usuário ---
users:
  - name: rapha
    groups: sudo
    shell: /bin/bash
    sudo: ALL=(ALL) NOPASSWD:ALL
    ssh_authorized_keys:
      - ssh-ed25519 AAAA... sua_chave_publica_aqui

# --- Comandos pós-boot ---
runcmd:
  - systemctl enable --now nginx
  - systemctl enable --now fail2ban
  - echo "Configurado em $(date)" >> /var/log/cloud-init-custom.log

# --- Arquivos que você quer criar ---
write_files:
  - path: /etc/motd
    content: |
      Acesso autorizado somente.
      Configurado automaticamente via cloud-init.
  - path: /etc/ssh/sshd_config.d/hardening.conf
    content: |
      PermitRootLogin no
      PasswordAuthentication no
      MaxAuthTries 3

⚠️ Sobre sudo: ALL=(ALL) NOPASSWD:ALL: isso é cômodo, mas pense bem antes de usar em produção. Em homelab tá ótimo. Em servidor exposto, prefira sudo: ALL=(ALL:ALL) ALL e use senha ou autenticação por chave com sudo limitado por comando.

Além do cloud-config: outros formatos de userdata

O cloud-config é o mais comum, mas não é o único formato que o cloud-init aceita. Vale saber que existem outros:

Shell script: se a primeira linha for #!/bin/bash (ou qualquer shebang), o cloud-init executa como script normal:

#!/bin/bash
set -euo pipefail

apt-get update -q
apt-get install -y nginx curl htop

systemctl enable --now nginx
echo "Script executado em $(date)" >> /tmp/setup.log

Mais simples pra quem já conhece bash. A desvantagem é que você perde os módulos específicos do cloud-init (como write_files e gestão de usuários padronizada).

MIME multipart: pra passar vários tipos de conteúdo juntos (um cloud-config + um script, por exemplo). Raramente necessário em homelab, mas existe.

💡 Na dúvida, use cloud-config. É o formato mais documentado, com validação embutida e comportamento previsível entre distribuições.

Debugando quando dá errado

Porque vai dar errado na primeira vez. Sempre dá.

# Ver o log principal — aqui fica o erro real
cat /var/log/cloud-init-output.log

# Status detalhado de cada fase
cloud-init status --long

# Ver exatamente o que foi passado como userdata
cloud-init query userdata

# Validar um arquivo cloud-config antes de usar
cloud-init schema --config-file meu-cloud-config.yaml

# Forçar re-execução completa (SÓ EM AMBIENTE DE TESTE)
cloud-init clean --logs && cloud-init init

⚠️ cloud-init clean em produção: apaga o estado que indica que o cloud-init já rodou. Na próxima inicialização, ele executa tudo de novo, inclusive sobrescrever configurações que você fez depois. Use só em VMs de teste.

A maioria dos problemas cai em três categorias:

  1. #cloud-config faltando: arquivo ignorado silenciosamente
  2. Indentação errada no YAML: YAML é sensível a tabs vs espaços; use sempre 2 espaços
  3. runcmd que depende de algo que ainda não existe: rede, serviço, arquivo criado por outro módulo

Pra categoria 2, valide o YAML antes de passar pra VM:

python3 -c "import yaml, sys; yaml.safe_load(sys.stdin)" < meu-cloud-config.yaml
echo $?  # 0 = YAML válido, qualquer outro = problema de sintaxe

Cloud-init no Proxmox

No Proxmox, o cloud-init funciona como um drive especial adicionado à VM, aparecendo como ide2 ou scsi1 dependendo da config. Você pode configurar pelo painel web (aba "Cloud-Init" da VM) ou pela CLI:

# Adicionar drive cloud-init a uma VM existente
qm set 100 --ide2 local-lvm:cloudinit

# Configurar usuário e chave SSH
qm set 100 --ciuser rapha --sshkeys ~/.ssh/id_ed25519.pub

# IP estático
qm set 100 --ipconfig0 ip=192.168.1.100/24,gw=192.168.1.1

# DHCP (se preferir deixar o roteador decidir)
qm set 100 --ipconfig0 ip=dhcp

# Ver o cloud-config que o Proxmox vai passar pra VM
qm cloudinit dump 100 user

O qm cloudinit dump é subestimado: mostra exatamente o que vai ser injetado, antes de iniciar a VM. Economiza tempo de debug.

💡 vmbr0 é o nome padrão da bridge de rede que o Proxmox cria durante a instalação. Se você tiver mais de uma interface de rede ou uma config customizada, o nome pode ser diferente. Veja em Datacenter → Node → Network.

Virt-customize: recheando a caixa antes de selar

Se o Cloud-init é o bilhete dentro da caixa, o Virt-customize é o que você faz com a caixa antes de enviar: instalar, modificar, configurar enquanto a VM ainda está desligada.

Ele faz parte da biblioteca libguestfs e acessa diretamente o sistema de arquivos da imagem .qcow2 sem precisar ligar a VM. Isso tem uma vantagem enorme: qualquer coisa que você faz aqui fica em todos os clones: você modifica o template uma vez, herda pra sempre.

Instalando

# Debian/Ubuntu
apt install libguestfs-tools

# RHEL/Rocky/Alma/Fedora
dnf install guestfs-tools

⚠️ No Proxmox host: instale libguestfs-tools diretamente no host se quiser rodar virt-customize lá. Mas atenção: o Proxmox é Debian, então apt install libguestfs-tools funciona. Só não sai instalando pacotes no host à toa; prefira rodar virt-customize numa máquina separada e copiar a imagem depois.

As flags mais úteis

# Preparar uma imagem Debian 12 cloud pra virar template
virt-customize -a debian-12-genericcloud-amd64.qcow2 \
  --update \
  --install "qemu-guest-agent,curl,htop,vim,fail2ban,sudo" \
  --run-command "systemctl enable qemu-guest-agent" \
  --run-command "systemctl enable fail2ban" \
  --timezone "America/Sao_Paulo" \
  --hostname "template-base"

⚠️ --selinux-relabel só em distros com SELinux. Em Debian e Ubuntu, essa flag não faz nada útil, pois o SELinux não é padrão nelas. Use --selinux-relabel só com Rocky Linux, AlmaLinux, RHEL e derivados.

Outras situações em que o virt-customize salva:

# Redefinir senha de root quando você esqueceu (acontece)
virt-customize -a imagem.qcow2 --root-password password:nova-senha-aqui

# Injetar chave SSH diretamente na imagem
# (Prefira fazer via cloud-init, mas às vezes não dá)
virt-customize -a imagem.qcow2 \
  --ssh-inject "rapha:file:/home/rapha/.ssh/id_ed25519.pub"

# Expandir partição root antes de importar (economiza um passo depois)
virt-resize --expand /dev/sda1 imagem-original.qcow2 imagem-expandida.qcow2

# Inspecionar o conteúdo de uma imagem sem modificar nada
virt-ls -a imagem.qcow2 /etc/
virt-cat -a imagem.qcow2 /etc/os-release

💡 virt-resize antes de importar: as imagens cloud geralmente vêm com 2-4GB de disco, o mínimo pra rodar, mas pequeno pra usar. Você pode expandir antes de importar no Proxmox com virt-resize, ou usar qm resize depois. O segundo é mais fácil, mas o primeiro evita que a VM suba com disco quase cheio.

O que o virt-customize NÃO faz

Não confunda com virt-install (que cria VMs) ou virt-manager (interface gráfica). O virt-customize só modifica arquivos dentro de imagens existentes. Ele não inicializa serviços, não tem acesso à rede e não sabe o que vai acontecer quando a VM ligar. Tudo que você configura aqui é estático. Para comportamento dinâmico (como hostname por instância, IP, usuário específico), use cloud-init.

Quando usar cada um

Situação Ferramenta
Configurar IP, usuário, SSH na criação da VM Cloud-init
Preparar um template base pra clonagem em massa Virt-customize
Instalar pacotes comuns a todas as VMs do parque Virt-customize
Instalar pacotes que variam por instância Cloud-init
Executar scripts que dependem da rede estar ativa Cloud-init (fase final)
Redefinir senha de root numa imagem de emergência Virt-customize
Expandir disco de uma imagem antes de importar virt-resize
A VM já está rodando e precisa de ajustes Nenhuma; use Ansible

O combo perfeito no Proxmox

O fluxo que definitvamente uso aqui no homelab, e que parei de alterar porque simplesmente funciona:

1. Baixar a imagem cloud oficial

# Debian 12 (Bookworm)
wget https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2

# Ubuntu 24.04 LTS
wget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img

# Rocky Linux 9
wget https://dl.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud.latest.x86_64.qcow2

2. Preparar com virt-customize

virt-customize -a debian-12-genericcloud-amd64.qcow2 \
  --update \
  --install "qemu-guest-agent,curl,htop,fail2ban,sudo" \
  --run-command "systemctl enable qemu-guest-agent" \
  --run-command "systemctl enable fail2ban" \
  --timezone "America/Sao_Paulo"

Isso instala o qemu-guest-agent (essencial pro Proxmox enxergar IP e estado da VM), configura timezone e já coloca fail2ban em todas as VMs que vierem desse template.

3. Criar o template no Proxmox

# Criar a VM base (ID 9000 — uso IDs altos pra templates)
qm create 9000 --name "debian-12-template" \
  --memory 2048 --cores 2 \
  --net0 virtio,bridge=vmbr0 \
  --ostype l26

# Importar o disco
qm importdisk 9000 debian-12-genericcloud-amd64.qcow2 local-lvm

# Configurar o hardware da VM
qm set 9000 \
  --scsihw virtio-scsi-pci \
  --scsi0 local-lvm:vm-9000-disk-0 \
  --ide2 local-lvm:cloudinit \
  --boot order=scsi0 \
  --serial0 socket --vga serial0 \
  --agent enabled=1

# Converter em template (irreversível — teste antes de converter)
qm template 9000

💡 --agent enabled=1 ativa o suporte ao qemu-guest-agent no lado do Proxmox. Sem isso, mesmo com o agente instalado na VM, o Proxmox não consegue pegar o IP nem fazer shutdown gracioso pela interface. É o tipo de coisa que você esquece uma vez e fica se perguntando por que o IP não aparece no painel.

4. Clonar e configurar

# Clonar o template (--full cria um disco independente, não linked clone)
qm clone 9000 101 --name "minha-vm" --full

# Configurar via cloud-init
qm set 101 \
  --ciuser rapha \
  --sshkeys ~/.ssh/id_ed25519.pub \
  --ipconfig0 ip=192.168.1.101/24,gw=192.168.1.1

# Ampliar o disco pra algo utilizável (imagem base tem ~2GB)
qm resize 101 scsi0 +18G

# Ligar
qm start 101

Em menos de 2 minutos você tem uma VM nova, configurada, com chave SSH injetada e IP definido. Sem instalar nada, sem clicar em nada, sem digitar senha em nenhum momento.

5. Confirmar que funcionou

# Esperar o cloud-init terminar e verificar status
ssh rapha@192.168.1.101 "cloud-init status --wait && cloud-init status --long"

# Ver o log de execução
ssh rapha@192.168.1.101 "cat /var/log/cloud-init-output.log"

🔥 Na prática: quando tudo funciona, cloud-init status retorna status: done. Quando algo deu errado, o log em /var/log/cloud-init-output.log vai te contar exatamente onde travou. É raro chegar nisso sem saber por quê.

Pra onde ir depois

Com esse fluxo funcionando, o passo natural é Ansible, pra gerenciar o que acontece depois que a VM subiu, em escala, repetível. Mas isso é assunto pra outro post.

O que você tem agora é a base: templates que você cria uma vez e clona quantas vezes precisar, com configuração injetada automaticamente e imagem já preparada. Pra homelab, isso é definitivamente suficiente. Pra ambientes maiores, é exatamente o mesmo fluxo, só com mais VMs.

Conclusão

Cloud-init e Virt-customize resolvem o mesmo problema em momentos diferentes. Entender quando usar cada um é o que separa "copiei da documentação" de "entendi o que tá acontecendo". Combinados com o sistema de templates do Proxmox, eles são a base de qualquer infraestrutura que leva automação a sério, mesmo que seja simplesamente você, gerenciando tudo do sofá, às 23h, de pijama.

A diferença entre clicar 40 vezes pra subir uma VM e rodar 5 comandos é exatamente essa dupla. Usa.