Bir önceki seride Proxmox VE cluster ve Corosync'i kurmuştuk; üç node (pve-a, pve-b, pve-c), bir QDevice, quorum dersleri, split-brain denemesi ve NFS shared storage ile bitirmiştik.
O lab hâlâ ayakta ve bu yeni seri tam olarak o lab'ın üzerine kuruluyor. Cluster kısmı bitti; şimdi asıl konu, o cluster'dan verimli VM üretmek.
Başlangıç noktası hepimizin bildiği o manuel rutin: yeni bir sunucu lazım olur, konsolu açarsın, template'den klonlarsın, IP'yi, maskeyi, ağ geçidini elle yazarsın, her agent kurulumu için ayrı ayrı SSH girersin, bir dosyayı elinle düzenlersin, sonunda reboot atarsın. Günde yirmi kurulumda bu sıkıcı; doksan kurulumda yönetilemez. Bu serinin ilk fazı, o işin en can yakıcı katmanını kaldırıyor: işletim sistemi kurulumunun kendisi. Faz 1'in sonunda elimizde tek komutla çalışan bir pipeline olacak; altın kalıbı (golden template) Terraform kuracak, workload VM'i cloud-init ile klonlayacak ve makineyi Ansible'a devredecek.
Hedef: tek komut, üç kaynak
Faz 1'in tamamı aslında şu cümleye indirgenebilir: terraform apply yazdığımda Proxmox üç kaynak oluşturmalı ve Ansible kalanını halletmeli. Üç kaynak, sırayla şunlar:
- Ubuntu 26.04 LTS cloud imajının NFS storage'a indirilmesi
- Bu imajdan VMID 9000 numaralı, Terraform tarafından yönetilen altın kalıp
- VMID 200 numaralı, statik IP'li, cloud-init'li tam klon (full clone)
Klon ayağa kalkıp ağa girdikten sonra sıra Ansible'a geçiyor; ilk temas (first contact) playbook'u qemu-guest-agent'ı kuruyor ve VM'e "buradan yönetiliyorsun" damgasını basıyor. Sonrası, ileriki fazların konusu: veri diskleri (Faz 2), monitoring ve güvenlik agent'ları (Faz 3), form tabanlı self-servis akış (Faz 4).
Kullanılan araçların doğrulanmış sürümleri şöyle; hepsi yazım anında (Ağustos 2026) upstream kaynaklardan kontrol edildi. Güncelliğini yitirmiş tutorial'lar, lab'da boşa geçen saatlerin bir numaralı sebebi:
| Bileşen | Sürüm | Not |
|---|---|---|
| Proxmox VE | 9.2 | 8.4 dalı 2026-08-31'te EOL; hard minimum 8.4 (Import içerik türü 8.4'le geldi) |
| Terraform | 1.15.9 | 1.16 hâlâ release-candidate aşamasında |
| bpg/proxmox provider | 0.111.1 | Eski proxmox_virtual_environment_download_file adı deprecated |
| Ubuntu imajı | 26.04 LTS (Resolute) | İki varyant: amd64 ve amd64v3 (x86-64-v3 CPU ister) |
| ansible-core | 2.20.1 | Ubuntu deposundan; Faz 1 için builtin modüller yeterli |
Ön kontrol: dört kapı, on dakika
İlk apply'dan önce dört kapıyı geçmek gerekiyor: cluster sürümü, CPU komut seti, NFS storage ve ağ planı. Cluster serisindeki disiplin burada da geçerli: her iddia, komut çıktısıyla kanıtlanır.
root@pve-a:~# pveversion
pve-manager/9.2.2/b9984c6d90a4bd80 (running kernel: 7.0.2-6-pve)
root@pve-a:~# hostname -I
192.168.122.11
root@pve-a:~# pvesm status
Name Type Status Total(KiB) Used(KiB) Available(KiB) %
local dir active 17687844 4986852 11777140 28.19%
local-lvm lvmthin active 10940416 0 10940416 0.00%
nfs-shared nfs active 486256640 197097472 286114816 40.53%
root@pve-a:~# ping -c2 192.168.122.1
PING 192.168.122.1 (192.168.122.1) 56(84) bytes of data.
64 bytes from 192.168.122.1: icmp_seq=1 ttl=64 time=0.312 ms
64 bytes from 192.168.122.1: icmp_seq=2 ttl=64 time=0.298 ms
--- 192.168.122.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
hostname -I çıktısındaki adres, https:// öneki ve :8006 portuyla birlikte sonradan pve_endpoint değişkeninin değerini oluşturacak. NFS tarafında 486 GB'lık paylaşımda yer bol; imaj, kalıp ve ilk VM için en az 25 GB gerekiyor. Paket kaybı sıfır olan ping de ağ tarafını kanıtlıyor.
Çoğu tutorial'ın atladığı bir kapı daha var: CPU komut seti. Ubuntu 26.04'ün iki cloud imajı var; baseline amd64 her x86-64 CPU'da çalışır, amd64v3 ise x86-64-v3 mikro mimari seviyesini (AVX2, BMI2, FMA, MOVBE) ister ve modern donanımda belirgin şekilde daha hızlıdır. v3 imajını bu özelliklerden yoksun bir CPU'da açarsanız boot'ta kernel panic alırsınız; fiziksel CPU yeni olsa bile Proxmox'un VM'e gösterdiği sanal CPU tipi eskiyse aynı sonuç çıkar. Benim lab'ım nested-virt olduğu için bu kontrolü fiziksel makinede, yani laptop üzerinde çalıştırdım:
# laptop üzerinde, scripts/check-cpu-v3.sh
OK: CPU exposes all x86-64-v3 features (avx2 bmi2 fma movbe).
You can use the amd64v3 cloud image.
Storage: content type'ların sessiz tuzağı
PVE node'larının yerel diskleri küçük olduğu için her şey NFS'te duruyor: indirilen cloud imajı, altın kalıp, VM diskleri. Proxmox storage'ları hangi içerik türlerini kabul ettiklerini bildirir; yeni oluşturulmuş bir NFS storage genelde sadece disk imajlarını kabul eder. Faz 1 için kritik olan tür Import; bu tür kapalıysa Terraform'un indirme kaynağı HTTP 500 ile ölüyor ve hata mesajı storage ayarına hiç değinmiyor. Snippets de şimdiden açılırsa Faz 4'te custom user-data dosyaları için bu ayara geri dönmek zorunda kalmayız.
Web arayüzünde Datacenter > Storage > nfs-shared > Edit yolunu izleyip Content listesindeki her kutuyu tiklemek yeterli.
CLI'deki karşılığı tek satır; lab günlüğüne yazdığım da bu:
root@pve-a:~# pvesm set nfs-shared -content images,rootdir,vztmpl,snippets,import
Import türü çoğu tutorial'dan yeni olduğu için bir cümlelik açıklamayı hak ediyor. Proxmox'un download-url adında bir API ucu var; bir node bu uç sayesinde dosyayı HTTPS üzerinden doğrudan storage'a çekiyor. bpg provider bu ucu proxmox_download_file kaynağıyla kullanıyor; bu yüzden kontrol düğümünde wget de, qm importdisk da yok. Uç, import içerik türünü bildirmeyen bir storage'a yazmayı reddediyor ve bu ret Terraform tarafına jenerik bir 500 olarak yansıyor.
Terraform için API token'ı
Provider'ın cluster'a erişmesi için üç yol var: kullanıcı adı ve şifre, auth ticket ya da API token'ı. Otomasyon için doğru cevap token: tek tek iptal edilebilir, kendi izin sınırını taşır ve bir insanın şifresini CI değişkenine gömmenize gerek kalmaz. Token'ı üç komutla, idempotent şekilde oluşturuyorum; kullanıcı ve izin nesneleri cluster genelindeki yapılandırma veritabanında tutulduğu için komutu tek bir node'da çalıştırmak yeterli, diğer node'lara kendiliğinden replike oluyor:
root@pve-a:~# pveum user add terraform@pve --comment "Terraform automation"
root@pve-a:~# pveum acl modify / --users terraform@pve --roles Administrator
root@pve-a:~# pveum user token add terraform@pve provider --privsep 0
┌────────────┬──────────────────────┬────────┬─────────┐
│ token-name │ value │ expire │ privsep │
├────────────┼──────────────────────┼────────┼─────────┤
│ provider │ 1a2b3c4d-... │ never │ 1 │
└────────────┴──────────────────────┴────────┴─────────┘
Web arayüzünde Datacenter > Users > add yolunu izleyip yeni kullanıcı eklenebilir. Önemli olan kısım, Linux PAM standard authentication yerine Proxmox VE authentication server seçilmesi:
Ardından Web arayüzünde Datacenter > API Tokens > add yolunu izleyip yeni bir token eklenir:
Token değeri, tam token kimliği ile UUID'nin eşittirle birleşimi: terraform@pve!provider=1a2b3c4d-.... Kopyalarken dizgenin başına ya da sonuna karışan boşluklar, yeni üretilmiş bir token'ın reddedilmesinin en yaygın sebebi; o yüzden tamını tamına alın, ne eksiği ne fazlası. Açık konuşmak gerekirse: Administrator rolü ve privsep 0 bir lab kısayolu. Production'da custom bir role geçilip minimum yetkiyle yetinilmeli (indirme kaynağı için Sys.Audit, Sys.Modify, Datastore.AllocateTemplate; VM kurulumu için VM.Allocate, VM.Clone ve benzerleri) ve privsep 1 yapılmalı. Bu, serinin sonundaki hardening maddelerinden biri.
Kontrol düğümü: küçük bir LXC
Terraform ve Ansible'ın çalışacağı kontrol düğümü, NFS üzerinde küçük bir LXC konteyneri: ctrl-01. Konteyner olması bilinçli bir tercih; saniyeler içinde açılıyor, boştayken kabaca 100 MB RAM yiyor ve root filesystem'i zaten Faz 1'de açtığımız rootdir içerik türünün kapsamına giriyor. Faz 1'de kernel düzeyinde yetki gerektiren hiçbir şey yok, o yüzden unprivileged konteyner sorunsuz.
Önce Ubuntu LXC template'ini NFS'e indirip konteyneri oluşturuyorum:
root@pve-a:~# pveam available --section system | grep ubuntu
root@pve-a:~# pveam download nfs-shared ubuntu-26.04-standard_26.04-1_amd64.tar.zst
downloading ubuntu-26.04-standard_26.04-1_amd64.tar.zst from
download finished
root@pve-a:~# pct create 900 nfs-shared:vztmpl/ubuntu-26.04-standard_26.04-1_amd64.tar.zst \
--hostname ctrl-01 --memory 2048 --cores 2 --rootfs nfs-shared:8 \
--net0 name=eth0,bridge=vmbr0,ip=192.168.122.10/24,gw=192.168.122.1 \
--unprivileged 1 --features nesting=1 --start 1
root@pve-a:~# pct list
VMID Status Lock Name
900 running ctrl-01
Template'in tam sürüm dizgesi zamanla değişir; önemli olan üç şey: template'in NFS'e indirilmesi (böylece migration sonrası her node konteyneri başlatabilir), konteynere gelecekteki VM'lerle aynı ağdan statik adres verilmesi ve ağ tarafının aynı vmbr0 köprüsüne takılması. 2 GB RAM, Terraform state işlemlerine rahat rahat yetiyor.
İlk giriş: konsolda şifre yok
Konteyner ayağa kalktı, noVNC konsolunu açtım, root yazdım ve şunu aldım:
Login incorrect
Ubuntu LXC template'inde root hesabı kilitli geliyor ve hiçbir varsayılan şifresi yok; template, yapılandırmanın konsoldan değil host'tan yapılacağını varsayıyor. Kapının kendisi pct enter; kimlik doğrulaması olmadan, kernel namespace'i üzerinden konteynerin içine root shell açıyor:
root@pve-a:~# pct enter 900
root@ctrl-01:~#
Konsoldan tty1'e doğrudan girmek isterseniz içeride passwd root çalıştırmak yeterli. Node tarafından tek satırlık, etkileşimsiz alternatif de var:
root@pve-a:~# pct exec 900 -- bash -c 'echo "root:changeme" | chpasswd'
Bu yazıda "ctrl-01 içinde" etiketli komutların tamamı bu kapıdan girilerek çalıştırıldı.
Aynı shell'e web arayüzünden de girilebilir:
Minimal konteynerde gpg yok
Terraform'u HashiCorp deposundan kurmak istedim; çünkü Ubuntu'nun kendi deposundaki sürüm geride kalıyor. Elinizdeki tutorial'ların hepsi şöyle bir blok gösterir: anahtarı indir, gpg --dearmor'dan geçir, keyring'e yaz. Ben de öyle yaptım ve şunu aldım:
sudo: 'gpg': command not found
Bu sadece ilk halka. Minimal Ubuntu LXC template'inde gnupg yok, o yüzden dearmor adımı keyring'e hiçbir şey yazmıyor ve apt-get update bir sonraki adımda HashiCorp deposunu imzasız reddediyor:
NO_PUBKEY AA16FCBCA621E701
çünkü signed-by referansı, hiç var olmamış bir .gpg dosyasına bakıyor. Çözüm, gpg'yi tamamen atlamak: HashiCorp'ın sunduğu anahtar ASCII-armored ve modern apt (2.4 ve üzeri; Ubuntu 26.04'te apt 3.x var) armored anahtarları .asc uzantısıyla doğrudan okuyor:
root@ctrl-01:~# wget -qO /usr/share/keyrings/hashicorp.asc \
https://apt.releases.hashicorp.com/gpg
root@ctrl-01:~# echo "deb [signed-by=/usr/share/keyrings/hashicorp.asc] \
https://apt.releases.hashicorp.com $(lsb_release -cs) main" \
> /etc/apt/sources.list.d/hashicorp.list
root@ctrl-01:~# apt-get update && apt-get install -y terraform
root@ctrl-01:~# apt-get install -y ansible
Ayrıca doğruladım: Ubuntu 26.04'ün kod adı resolute ve HashiCorp mirror'ında kendi dağıtımı var; lsb_release -cs hiçbir özel işlem gerektirmiyor.
Üretilmemiş locale
Terraform kuruldu, sıra Ansible'da. ansible --version ise version banner'ı yerine bunu bastı:
ERROR: Ansible could not initialize the preferred locale: unsupported locale setting
Ubuntu template'i LANG=en_US.UTF-8 set ediyor ama o locale'i hiç üretmiyor; aynı eksiklik, paket kurulumları sırasında perl'den gelen uyarıların da sebebiydi. Ansible, perl kadar hoşgörülü değil; başlamayı tamamen reddediyor. Locale'i bir kez üretmek ikisini de tek hamlede çözüyor ve update-locale bu ayarı /etc/default/locale'a taşıdığı için reboot sonrası da kalıcı:
root@ctrl-01:~# apt-get install -y locales
root@ctrl-01:~# locale-gen en_US.UTF-8
root@ctrl-01:~# update-locale LANG=en_US.UTF-8
Araç kapıları da artık kanıtla kapandı; her iki banner'ı da gerçek çalıştırmadaki haliyle buraya bırakıyorum:
root@ctrl-01:~# terraform -version
Terraform v1.15.9
on linux_amd64
root@ctrl-01:~# ansible --version
ansible-core 2.20.1
config file = /etc/ansible/ansible.cfg
executable location = /usr/bin/ansible
python version = 3.13.2 (/usr/bin/python3)
jinja version = 3.1.6
libyaml = True
Bir küçük sürüm gerçeğini de buraya not edeyim: araştırmam sırasında "Ansible core 2.21" yazmıştım ama Ubuntu deposu ansible-core 2.20.1 + ansible 13.1.0 meta paketini veriyor. Plan 2.21'di, saha 2.20.1'di; saha kazanır.
SSH anahtarı ve repo'nun üç aktarmalı yolculuğu
Kontrol düğümü, kurduğu her VM'e anahtar enjekte edecek; önce o anahtar çifti:
root@ctrl-01:~# ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N ""
Generating public/private ed25519 key pair.
Your identification has been saved in /root/.ssh/id_ed25519
Your public key has been saved in /root/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:1Zc5k... root@ctrl-01
cat ~/.ssh/id_ed25519.pub ile okunan satır, Terraform tarafında her klonlanan VM'e cloud-init user_account bloğuyla enjekte edilecek olan dizgi. Aynı anahtar malzemesini üç yerde görmek (kontrol düğümü, hypervisor'da qm cloudinit çıktısı, guest içinde authorized_keys), Faz 1'in en hızlı uçtan uca sağlaması.
Repo zip'i laptop'ta duruyor. Konteynere taşımak üç aktarmada tamamlanıyor ve her aktarma, o ayağın elinde zaten olan yolu kullanıyor:
# laptop üzerinde: cluster'a (laptop aynı zamanda lab'ın NAT ağ geçidi, 192.168.122.1)
laptop$ scp ~/Downloads/proxmox-automation.zip root@192.168.122.11:/root/
proxmox-automation.zip 100% 452KB 9.1MB/s 00:00
# pve-a üzerinde: dosyayı konteynerin filesystem'ine doğrudan yaz; SSH'a gerek yok
root@pve-a:~# pct push 900 /root/proxmox-automation.zip /root/proxmox-automation.zip
# ctrl-01 içinde
root@ctrl-01:~# apt-get install -y unzip
root@ctrl-01:~# unzip ~/proxmox-automation.zip -d ~/
root@ctrl-01:~# ls ~/proxmox-automation
ansible docs README.md scripts terraform
Laptop'tan konteynere doğrudan scp neden çalışmaz? Çünkü yeni kurulmuş Ubuntu konteynerinde kilitli bir root var, şifre yok, authorized_keys yok; SSH'ın doğrulayacağı hiçbir şey yok. pct push bu engeli tasarım gereği bypass ediyor, dosyayı konteyner filesystem'ine node üzerinden yazıyor. Her aktarmanın başarı işareti farklı: scp ilerleme satırı basıp sessizce çıkıyor, pct push hiçbir şey basmıyor (Proxmox'un "başarı sessizdir" geleneği) ve ls nihai makbuz. Bu zincir, repo bir git remote'a taşındığı anda tek bir git clone'a indirgeniyor; o zaman bu bölümün tamamı tarih oluyor.
Terraform tarafı: kritik kararlar
Repo'daki Terraform konfigürasyonunun tamamına girmeyeceğim; sadece beklenmedik bir davranışla karşılaştığınızda açıklaması elinizin altında olsun diye kritik kararları ve iki dosyanın can alıcı kısımlarını buraya bırakıyorum.
İlki, imaj indirme ve altın kalıp:
resource "proxmox_download_file" "ubuntu_image" {
content_type = "import" # PVE 8.4+ ve storage'da Import açık olmalı
datastore_id = var.storage_id
node_name = var.template_node
url = var.cloud_image_url
file_name = "ubuntu-2604-cloudimg.qcow2"
upload_timeout = 3600 # provider varsayılanı 600 s, dar geliyor
overwrite_unmanaged = true
}
resource "proxmox_virtual_environment_vm" "ubuntu_template" {
name = var.template_name
node_name = var.template_node
vm_id = var.template_vmid # sabit 9000
template = true
agent { enabled = true }
cpu {
cores = 2
type = var.cpu_type # x86-64-v3, aşağıda hikayesi var
}
scsi_hardware = "virtio-scsi-pci"
disk {
datastore_id = var.storage_id
import_from = proxmox_download_file.ubuntu_image.id
interface = "scsi0"
size = var.template_disk_size_gb
}
serial_device {}
vga { type = "serial0" }
boot_order = ["scsi0"]
}
Buradaki en önemli satır import_from. file_id kullanırsanız provider sessizce PVE node'una SSH açıp qm disk import çalıştırıyor; API token'ıyla giden yolda ise SSH yok ve apply template adımında düşüyor. import_from ise PVE'nin yerel import-from= disk seçeneğini API üzerinden kullanıyor; kimse hiçbir node'a SSH yapmıyor. İkinci kritik karar: template'te initialization bloğu olmamalı; cloud-init ayarları klonun işidir, öyle ki tek kalıp, birbirinden tamamen farklı ağ kimlikleriyle workload'lara hizmet verebilsin. Kalıp bir kalıp, makine değil.
İkinci dosya, workload klonu:
resource "proxmox_virtual_environment_vm" "test_vm" {
name = var.vm_name
node_name = var.vm_node
vm_id = var.workload_vmid # sabit 200
clone {
vm_id = proxmox_virtual_environment_vm.ubuntu_template.vm_id
full = true
retries = 3
}
agent {
enabled = true
wait_for_ip { disabled = true } # bu satırın 15 dakikalık hikayesi aşağıda
}
disk { # klonun root diskini büyütür
datastore_id = var.storage_id
interface = "scsi0"
size = var.vm_disk_size_gb
}
initialization {
datastore_id = var.storage_id
dns {
domain = var.search_domain
servers = var.dns_servers
}
ip_config {
ipv4 {
address = "${var.vm_ip_address}/${var.vm_ip_cidr}"
gateway = var.vm_ip_gateway
}
}
user_account {
keys = [trimspace(file(pathexpand(var.ssh_public_key_path)))]
username = var.vm_username
}
}
}
Kararların gerekçeleri, kısaca:
| Karar | Gerekçe |
|---|---|
proxmox_download_file (yeni ad) |
proxmox_virtual_environment_download_file deprecated; v1.0'da kalkacak |
scsi_hardware = virtio-scsi-pci |
Ubuntu cloud imajlarının istediği kontrolcü; online disk resize'ı yalnızca bu destekliyor, Faz 2 bunun üzerine kurulu |
CPU tipi x86-64-v3
|
PVE varsayılanı x86-64-v2-Auto AVX2 taşımıyor; amd64v3 guest boot'ta kernel panic yiyor |
Seri konsol (serial_device + vga serial0) |
Cloud imaj konvansiyonu; standart VGA'da noVNC'de görülen boş ekran belirtisini önler |
| Full clone | NFS üzerinde bağımsız diskler; production DB sunucusu pratiğinin aynısı |
| Guest agent cloud-init yerine Ansible'dan | user-data sade kalıyor, paket yapılandırması yönetim altına giriyor; Faz 3'te drift tespiti de yanında geliyor |
init, validate ve plan: iki schema hatası üst üste
Değişkenleri doldurup (pve_endpoint, token, storage_id = nfs-shared, VM adı ve IP'si) provider'ı çekiyorum:
root@ctrl-01:~# terraform init
Initializing the backend...
Initializing provider plugins...
- Finding bpg/proxmox versions matching "~> 0.111.1"...
- Installing bpg/proxmox v0.111.1...
- Installed bpg/proxmox v0.111.1 (signed, key ID F0582AD6AE97C188)
Terraform has been successfully initialized!
vlan_tag artık yok
İlk terraform plan tam da burada, schema hatasıyla düştü:
╷
│ Error: Unsupported argument
│
│ on template.tf line 65, in resource "proxmox_virtual_environment_vm" "ubuntu_template":
│ 65: vlan_tag = var.vlan_tag
│
│ An argument named "vlan_tag" is not expected here.
╵
Provider 0.111 hattında network_device.vlan_tag özniteliğini vlan_id olarak yeniden adlandırmış. Eski tutorial'lar vlan_tag kullanıyor. Düzeltme tek satır; değişken adını koruyup değeri vlan_id'ye bağlıyorum, böylece tfvars dosyası değişmiyor
Bu arada, VLAN-aware köprülerdeki trunk özelliği ayrı bir öznitelik (trunks = "10;20;30"); vlan_id ile karıştırmamak gerek.
hostname kalkmış, CPU tipinde -Auto yok
Plan bu kez başka iki hatayla düştü. İlki initialization.hostname özniteliği; provider 0.111'de kaldırılmış. cloud-init hostname'i artık VM'in adından geliyor, o yüzden VM adı geçerli bir DNS ismi olmalı:
│ Error: Unsupported argument
│
│ on vm.tf line 90, in resource "proxmox_virtual_environment_vm" "test_vm":
│ 90: hostname = var.vm_name
│
│ An argument named "hostname" is not expected here.
╵
İkincisi daha gizliydi: x86-64-v3-Auto diye bir CPU tipi provider validator'ının listesinde yok. PVE arayüzünde gördüğünüz -Auto varyantları, provider tarafında geçersiz:
│ Error: expected cpu.0.type to be one of [x86-64-v2 x86-64-v3 ... host ...],
│ got x86-64-v3-Auto
Çözüm: hostname satırını silmek ve cpu_type'ı düz x86-64-v3 yapmak. Buradan çıkan ders, terraform validate'ı alışkanlık haline getirmek oldu; init ile plan arasına sıkışan bu tek komut, tüm öznitelik ve blokları gerçek provider şemasına karşı, cluster'a tek istek atmadan kontrol ediyor. İki schema hatasının ikisini de, kimlik bilgilerine hiç dokunmadan, birer saniyenin altında yakalayabilirdi.
root@ctrl-01:~# terraform validate
Success! The configuration is valid.
Ve nihayet sağlıklı bir plan:
Plan: 3 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ ssh_command = "ssh ubuntu@192.168.122.50"
+ template_vm_id = 9000
+ test_vm_id = 200
+ test_vm_ip = "192.168.122.50"
+ test_vm_name = "vm-test-01"
İlk çalıştırmada üç ekleme, sıfır değişiklik, sıfır silme; olması gereken tablo bu. Outputs bloğu da anahtarların devrini gösteriyor.
İlk apply: 4 dakika 25 saniye, 11 saniye, 15 dakika 5 saniye
Konfigürasyon düzeltmeleri sonrası apply. Süreler ölçüldü; işte gerçek değerler:
proxmox_download_file.ubuntu_image: Creating...
proxmox_download_file.ubuntu_image: Still creating... [4m25s elapsed]
proxmox_download_file.ubuntu_image: Creation complete after 4m25s
proxmox_virtual_environment_vm.ubuntu_template: Creating...
proxmox_virtual_environment_vm.ubuntu_template: Creation complete after 11s
proxmox_virtual_environment_vm.test_vm: Creating...
proxmox_virtual_environment_vm.test_vm: Still creating... [15m00s elapsed]
proxmox_virtual_environment_vm.test_vm: Creation complete after 15m5s
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
Outputs:
ssh_command = "ssh ubuntu@192.168.122.50"
template_vm_id = 9000
test_vm_id = 200
test_vm_ip = "192.168.122.50"
test_vm_name = "vm-test-01"
Süreyi belirleyen indirme (4,5 dakika) ve saniyeler içinde ayaklanan kalıp (11 saniye) normal; VM'e bağlanan 15 dakikalık kısım ise asıl sorun işareti.
Hata olmayan on beş dakikalık uyarı
Apply'ın kuyruğunda şu uyarı vardı:
╷
│ Warning: error waiting for network interfaces from QEMU agent
│
│ with proxmox_virtual_environment_vm.test_vm,
│ on vm.tf line 7, in resource "proxmox_virtual_environment_vm" "test_vm":
│ 7: resource "proxmox_virtual_environment_vm" "test_vm" {
│
│ timeout while waiting for the QEMU agent on VM "200" to publish the
│ network interfaces
╵
15 dakika 5 saniyelik sürenin kendisi ipucu. Klon saniyeler içinde bitti, VM neredeyse anında açıldı; beklemin tamamı, provider'ın guest agent'ı yoklamasından ibaretti. Mekanizmayı sonradan provider'ın kaynak kodundan doğruladım: VM konfigürasyonunda agent açık olduğu sürece provider, VM'i başlattıktan sonra ve sonraki her plan/apply yenilemesinde agent'a ağ arayüzlerini soruyor ve "henüz IP yok" cevabını agent.timeout (varsayılan 15 dakika) bitene kadar tekrarlıyor. Ubuntu cloud imajları qemu-guest-agent ile gelmiyor; o yüzden her yoklama boşa çıkıyor, 15 dakika dolunca provider bir Warning ile pes ediyor. Warning, Error değil: VM çalışıyor, state temiz, tek kayıp state'te ipv4_addresses'ın boş kalması; statik IP tasarımında zaten ihtiyacım yok.
Üç makul tepki var; repoya işlenen, birincisi:
-
agent { wait_for_ip { disabled = true } }ile sorguyu tamamen atlamak; statik IP'li bu pipeline için doğru varsayılan. -
agent.timeout'ı kısaltmak (örneğin 2m); bekleme küçülür ama kalkmaz. Agent tabanlı IP keşfini ileride isterseniz makul orta yol. - Hiçbir şey yapmamak; guest agent kurulunca yoklamalar saniyeler içinde başarıya döner ve uyarı kendiliğinden kaybolur.
Asla doğru olmayan dördüncü yol ise apply'ı Ctrl-C ile kesmek; oluşturulması yarıda kesilen kaynak tainted olur ve arkasında yarım kalmış bir temizlik bırakır. Ben kesmedim, bekledim; 15 dakikanın bedeli bu yazı oldu.
Düzeltmeyi uygulayıp tekrar plan çektiğimde fark tek bir bloğa iniyordu (agent), apply "0 to add, 2 changed" ile bir saniyede tamamlandı ve sonraki tüm yenilemeler saniye sürdü. Yani wait_for_ip satırı, bu yazıyı okuyan herkesin konfigürasyonuna eklemesi gereken tek satırlık sigorta.
Doğrulama: üç bağımsız katman hemfikir olana kadar
Terraform "tamam" dedikten sonra güven meselesi başlıyor. Hipervizör, guest ve ağ katmanlarının üçü de aynı şeyi söyleyene kadar hiçbir şeye inanmıyorum.
Hipervizör tarafı:
root@pve-a:~# qm list
VMID Name Status MEM(MB) BOOTDISK(GB) PID
9000 ubuntu-2604-tpl stopped 2048 10.00 0
200 vm-test-01 running 2048 10.00 318234
root@pve-a:~# qm config 200 | grep -E "scsi0|net0|agent|serial"
agent: enabled=1
net0: virtio=BC:24:11:2E:AA:F7,bridge=vmbr0
scsi0: nfs-shared:vm-200-disk-0,size=10G
serial0: socket
vga: serial0
root@ctrl-01:~# terraform state list
proxmox_download_file.ubuntu_image
proxmox_virtual_environment_vm.ubuntu_template
proxmox_virtual_environment_vm.test_vm
Kalıbın stopped ve PID 0 görünmesi tam olması gerektiği gibi; bir kalıbın çalışıyor görünmesi, birilerinin yanlışlıkla kalıbı başlattığı anlamına gelir ve asla olmamalıdır. qm config çıktısı ise altıncı bölümdeki dört donanım kararını tek bakışta doğruluyor: agent açık, virtio ağ, NFS'te root diski, seri konsol çifti.
Guest ve ağ tarafı:
root@ctrl-01:~# ssh ubuntu@192.168.122.50
The authenticity of host '192.168.122.50' can't be established.
ED25519 key fingerprint is SHA256:dG9w...DzKw.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Welcome to Ubuntu 26.04 LTS (Resolute) (GNU/Linux 6.17.0-25-generic...)
ubuntu@vm-test-01:~$ cloud-init status --wait
status: done
ubuntu@vm-test-01:~$ grep Datasource /var/log/cloud-init-output.log
Cloud-init v. 25.1 finished at ... Datasource DataSourceNoCloud
seeddev=/dev/sr0. Up 14.32 seconds
Prompt'taki vm-test-01, kaldırılan initialization.hostname özniteliğine rağmen hostname'in cloud-init tarafından VM adından alındığının kanıtı. status: done ve DataSourceNoCloud seeddev=/dev/sr0 ise tohum (seed) diskının bulunduğunun ve ilk boot'ta tamamen tüketildiğinin makbuzu. Bu ikisi olmadan, VM göründüğü gibi çalışsa bile cloud-init payload'ı hiç almamış olabilir; belirtisi de şu olur: statik adres yerine imajın DHCP kimliğiyle ağa çıkar. Bir not: VM'i destroy/apply döngüsüne sokarsanız yeni makine yeni host anahtarıyla gelir; yeniden bağlanmadan önce ssh-keygen -R 192.168.122.50 şart. Bunu sonradan, Faz 2 rebuild'i sırasında öğrendim.
Ansible ilk temas: pipeline'ın kapanışı
Terraform, açılmış ve ağa bağlanmış bir VM'e kadar götürüyor. İşletim sisteminin içindeki her şey artık Ansible'ın sahası ve Faz 3'te bu ilk playbook'un üzerine tüm agent yığını (Zabbix Agent 2, Trend Micro, ManageEngine, QRadar'a rsyslog forward'u) gelecek. Faz 1 playbook'u bilinçli olarak minik; görevi yönetim kanalını kanıtlamak ve hypervisor'ın tek umursadığı paketi, qemu-guest-agent'ı kurmak.
Envanter tek VM'lik:
all:
children:
provisioned:
hosts:
vm-test-01:
ansible_host: 192.168.122.50
ansible_user: ubuntu
root@ctrl-01:~/proxmox-automation/ansible# ansible provisioned -m ping
vm-test-01 | SUCCESS => {
"ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" },
"changed": false,
"ping": "pong"
}
Playbook dört görevden ibaret: kimlik göster, agent'ı kur, servisi başlat, /etc/provisioned-by damgasını yaz. (Playbook'un tamamı repoda; burada çalıştırmanın çıktısı daha değerli.)
root@ctrl-01:~# ansible-playbook playbooks/01_first_contact.yml
PLAY [First contact - verify a freshly provisioned VM] ***********
TASK [Gathering Facts] ******************************************
ok: [vm-test-01]
TASK [Show system identity] *************************************
ok: [vm-test-01] => {
"msg": "vm-test-01 is up, Ubuntu 26.04"
}
TASK [Install QEMU guest agent] *********************************
changed: [vm-test-01]
TASK [Enable and start the guest agent] *************************
ok: [vm-test-01]
TASK [Mark the VM as managed by this pipeline] ******************
changed: [vm-test-01]
PLAY RECAP *******************************************************
vm-test-01 : ok=5 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ok=5 changed=2 ilk bakışta yanlış görünür; aritmetiği şöyle: ok sayacı changed olan görevleri de içerir. Beş görevin beş sonucu var; üçü normalde changed raporlardı ama service görevi fresh VM'de ok rapor etti, çünkü paketin postinst'i, apt görevinin hemen ardından agent'ı zaten başlatıp enable etmişti. İki changed; paket kurulumu ve damga dosyası. Bir dakika sonra ikinci çalıştırma, idempotans makbuzu:
vm-test-01 : ok=5 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
changed'in sıfıra düşmesi bu playbook'u tek seferlik kurulumdan güvenli, tekrar çalıştırılabilir bir operasyona çevirir; Faz 3'te aynı örüntü her provision edilmiş VM'e karşı çalışacak ve sıfırın üstündeki changed, rutin bir yan etki değil, incelenmeye değer bir drift sinyali olacak.
Facts enjeksiyonu 2.24'te kalkıyor
İlk çalıştırmada iki görevden şu uyarı düştü:
[DEPRECATION WARNING]: The INJECT_FACTS_AS_VARS setting is scheduled to be
removed in version 2.24. ...
Playbook, ansible_distribution tarzı çıplak fact değişkenleri kullanıyordu. ansible-core 2.20 otomatik fact enjeksiyonunu deprecate etmiş; 2.24'te tamamen kalkıyor. Düzeltme, parantez sözdizimine geçmek:
# eski:
msg: "{{ inventory_hostname }} is up, {{ ansible_distribution }} {{ ansible_distribution_version }}"
# yeni:
msg: "{{ inventory_hostname }} is up, {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }}"
2.24 çıktığında bu uyarı, kırılan playbook'ların başındaki hata mesajına dönüşecek; düzeltmesi de gösterildiği kadar küçük.
Agent kurulduktan sonra hypervisor tarafındaki son kapı da sessizce açılıyor:
root@pve-a:~# qm guest cmd 200 ping
root@pve-a:~# echo $?
0
qm guest cmd ping başarılı olunca hiçbir şey basmaz; sessizlik ve sıfır çıkış kodu, testin geçtiğinin işareti. Bu tek satır, 15 dakikalık uyarıyla başlayan döngüyü kapatıyor: agent artık var, yoklamalar saniyeler içinde cevap buluyor ve Proxmox arayüzü VM'in IP adresini kendisi gösteriyor.
Lab aniden dondu
Faz 1'i kapatırken bir de gizemli olay yaşadım; kaydetmemek olmaz. Oturum ortasında pve-a'ya açık SSH bağlantım aniden Connection reset by peer ile gitti. Yeniden girdiğimde CT 900 önce not running gösterdi, saniyeler sonra yeniden running oldu; VM 200 ise durmuştu ve sonraki plan started: false -> true değişikliği gösterdi. Ansible, guest'in uptime'ını 2.2 dakika raporladı; yani makine kısa süre önce yeniden başlamıştı. Üç belirti de aynı anda bir pve-a reboot'una işaret ediyordu ama hiçbir crash kanıtı yoktu.
Cevap, utanılacak kadar basitti: laptop öğle yemeğinde uykuya girmiş. Bu lab nested-virt; pve-a ve arkadaşları o laptop'un libvirt guest'leri. Host uykuya girince tüm lab bir anda askıya alınıyor; SSH kopuyor, konteyner duruyor, VM kapanıyor. Hiçbir şey çökmedi; onboot=1 ayarları her şeyi kendisi toparladı. Son ders donanımda değil, lab tasarımında: nested-virt lab'ın tek bir fiziksel host'u var ve onun güç yönetimi, cluster'ın beşinci düğümüdür. Lab saatlerinde host'un otomatik uykuya dalmasını kapatmak ve bunu uptime ile journalctl --list-boots üzerinden doğrulamak yeterli.
Hızlı başvuru: belirti, neden, çözüm
Yazı boyunca geçen belirti ve çözümler, hepsini baştan okumak istemeyenler için tek tabloda:
| Belirti | Neden | Çözüm |
|---|---|---|
Konsolda Login incorrect
|
Ubuntu LXC template'i şifresiz ve root kilitli |
pct enter 900, gerekiyorsa passwd
|
gpg: command not found + NO_PUBKEY
|
Minimal template'te gnupg yok; keyring hiç yazılmadı | Anahtarı .asc olarak sakla; gpg'ye hiç ihtiyaç yok |
Ansible could not initialize the preferred locale |
LANG set edilmiş ama locale üretilmemiş |
locale-gen en_US.UTF-8 + update-locale
|
Plan: vlan_tag is not expected here
|
Provider 0.111 özniteliği vlan_id yaptı |
network_device içinde vlan_id = N
|
Plan: hostname yok + x86-64-v3-Auto geçersiz |
initialization.hostname kaldırıldı; -Auto CPU tipleri validatörde yok |
hostname satırını sil (VM adı DNS ismi), cpu_type = x86-64-v3
|
VM 15 dk Still creating + Warning |
Agent açık ama imajda qemu-guest-agent yok; agent.timeout 15m |
wait_for_ip { disabled = true }; asla Ctrl-C değil |
INJECT_FACTS_AS_VARS deprecation uyarısı |
ansible-core 2.24'te çıplak fact değişkenleri kalkıyor |
ansible_facts['...'] parantez sözdizimi |
| SSH reset, CT durdu, VM kapandı; kanıt yok | Laptop uykusu; nested-virt lab'ın tamamı host'a bağlı | Lab saatlerinde host uykusunu kapat; journalctl --list-boots
|
Sırada ne var?
Faz 1 tek diskli bir VM üretiyor; web sunucusu ve jump host için yeterli, veritabanı sunucusu için değil. Faz 2'nin konusu çoklu disk örüntüsü: /var, /log, /data ve /backup için ayrı SCSI cihazlar, her diske Terraform serial özniteliğiyle kalıcı kimlik ve konuk içinde LVM + XFS zinciri. Buraya çekilen temeller tam olarak Faz 2'nin ihtiyacı olanlar; virtio-scsi-pci kontrolcüsü online disk resize'ı destekleyen tek kontrolcü ve serial etiketi, /dev/sdb harflerinin konuk içinde kayması problemine kökten çözüm. Müşteri bir terabayt ek data isterse cevap bir satırlık değişken düzenlemesi ve bir re-apply olacak.
Companion repo, seri boyunca her fazda biraz daha büyüyor:
📌 Link: GitHub repo; fazlarla eşleşen dizin yapısı, playbook'lar ve Terraform dosyaları
Bu fazı siz de kendi lab'ınızda denerseniz, 15 dakikalık uyarı sizin de karşınıza çıkacaktır; çözümü, yazının yukarısındaki o tek satırlık sigorta. Sorularınız, düzeltmeleriniz ve kendi karşılaştığınız tuzaklar için yorumlarda bekliyorum.





Top comments (0)