Przejdź do treści
Lekcja 96 z 117

Kurs › Moduł 10 › Lekcja 96

Maszyny wirtualne: KVM i libvirt

⏱ ok. 12 min czytania· moduł 10: Usługi i serwery

Po tej lekcji: kursant sprawdza, czy serwer nadaje się do wirtualizacji, zakłada dyski qemu-img, stawia maszynę virt-install, zarządza nią przez virsh (start, wyłączanie, konsola, migawki), rozumie różnicę między siecią NAT a mostkiem i konfiguruje nową maszynę automatycznie przez cloud-init.

Wstęp

Kontener jest świetny, dopóki wystarcza Ci jądro serwera. Ale chcesz przetestować aktualizację jądra. Albo uczysz się do RHCSA i potrzebujesz maszyny, którą można bezkarnie zepsuć, a potem w trzy sekundy cofnąć do stanu sprzed katastrofy. Albo szef chce Windowsa obok Linuksa. Kontener tu nie pomoże.

Potrzebny jest cały komputer udawany w komputerze. Maszyna wirtualna. Dziś postawimy ją bez klikania, z linii komend, tak jak na prawdziwym serwerze bez monitora. A na końcu sprawimy, że nowa maszyna przy pierwszym starcie sama ustawi sobie nazwę, użytkownika i klucz SSH.

Dom z własną elektrownią

Kontener to kontener mieszkalny na budowie, który bierze prąd z placu. Maszyna wirtualna to osobny dom z własną elektrownią, własną kuchnią i własnym zarządcą. Ma własne jądro, własny system, własny wirtualny dysk i kartę sieciową. Myśli, że jest prawdziwym komputerem.

Kto udaje dla niej sprzęt? Trzech pomocników:

  • KVM — moduł jądra Linuksa, który zamienia cały system w hiperwizor, czyli zarządcę osiedla domów. Daje maszynom prawdziwy procesor na wyłączność na krótkie chwile, dzięki czemu działają prawie tak szybko jak sprzęt.
  • QEMU — program, który udaje resztę sprzętu: dysk, kartę sieciową, ekran.
  • libvirt — biuro zarządcy. Pamięta, jakie masz maszyny, jak są skonfigurowane, i daje jedno narzędzie do wszystkiego: virsh.

Najpierw sprawdzamy, czy procesor w ogóle to potrafi:

grep -cE 'vmx|svm' /proc/cpuinfo
lsmod | grep kvm

vmx to wsparcie wirtualizacji w procesorach Intela, svm w AMD. Wynik 0 znaczy, że wirtualizacja jest wyłączona w BIOS-ie albo jesteś już w maszynie wirtualnej, która nie przekazuje jej dalej. Pełną diagnozę robi:

sudo virt-host-validate qemu
  QEMU: Checking for hardware virtualization                                 : FAIL (Host not compatible with KVM; HW virtualization CPU features not found. Only emulated CPUs are available; performance will be significantly limited)
  QEMU: Checking if device /dev/net/tun exists                               : PASS
  QEMU: Checking for cgroup 'cpu' controller support                         : PASS

Tak wygląda, kiedy się nie da. Maszyna wciąż ruszy, ale QEMU będzie emulować procesor programowo, jak symulator lotu na kalkulatorze. Na dobrze przygotowanym serwerze w każdej linijce chcesz widzieć PASS.

Instalacja

[RHEL] Według dokumentacji Red Hata:

sudo dnf install qemu-kvm libvirt virt-install virt-viewer
for drv in qemu network nodedev nwfilter secret storage interface; do sudo systemctl start virt${drv}d{,-ro,-admin}.socket; done

Nowsze RHEL-e nie mają jednego wielkiego libvirtd, tylko osobne małe usługi do maszyn, sieci, dysków. Pętla z lekcji o pętlach uruchamia je wszystkie naraz.

sudo apt install qemu-system-x86 libvirt-daemon-system virtinst
sudo usermod -aG libvirt $USER

Grupa libvirt pozwala zarządzać maszynami systemowymi bez sudo. Po dopisaniu do grupy wyloguj się i zaloguj ponownie.

Uwaga na jedną rzecz: libvirt ma dwa osobne biura. qemu:///system to biuro systemowe, z uprawnieniami roota, do maszyn serwerowych. qemu:///session to prywatne biurko zwykłego użytkownika. To osobne instancje z osobnymi listami maszyn. Jak „maszyna zniknęła”, sprawdź najpierw, do którego biura dzwonisz:

virsh uri
qemu:///system

Dyski: qemu-img

Wirtualny dysk to zwykły plik. Najpopularniejszy format to qcow2, „QEMU copy on write”. To jak nadmuchiwany materac: mówisz, że ma 10 GB, ale zajmuje tyle, ile w niego naprawdę włożysz.

qemu-img create -f qcow2 dysk.qcow2 5G
ls -lh dysk.qcow2
qemu-img resize dysk.qcow2 +5G
qemu-img info dysk.qcow2
Formatting 'dysk.qcow2', fmt=qcow2 cluster_size=65536 extended_l2=off compression_type=zlib size=5368709120 lazy_refcounts=off refcount_bits=16
-rw-r--r-- 1 root root 193K Nov  3 16:37 dysk.qcow2
Image resized.
image: dysk.qcow2
file format: qcow2
virtual size: 10 GiB (10737418240 bytes)
disk size: 200 KiB

Dysk na 10 GB, a na serwerze zajmuje 200 KB. virtual size to obietnica, disk size to rzeczywistość. Uwaga: dysk można powiększyć także w drugim kierunku, ale qemu-img zażąda wtedy --shrink i ostrzeże, że wszystko za nową granicą przepadnie. Zmniejszanie dysku z danymi to jak skracanie spodni na człowieku, który w nich stoi.

Konwersja między formatami, na przykład gdy przenosisz maszynę z VMware albo do niego:

qemu-img convert -p -f qcow2 -O raw dysk.qcow2 dysk.raw
qemu-img convert -f raw -O vmdk dysk.raw dysk.vmdk

I najlepsza sztuczka: dysk z podkładem (backing file). Masz jeden czysty obraz systemu i chcesz z niego dziesięć maszyn. Zamiast kopiować go dziesięć razy, robisz cienkie nakładki. Jak kalka na rysunku: oryginał pod spodem zostaje nietknięty, a każda maszyna rysuje tylko na swojej kalce.

cd /var/lib/libvirt/images
sudo qemu-img create -f qcow2 -F qcow2 -b /var/lib/libvirt/images/noble-server-cloudimg-amd64.img web01.qcow2 20G
sudo qemu-img info --backing-chain web01.qcow2 | grep -E 'image:|backing file|virtual size'
image: web01.qcow2
virtual size: 20 GiB (21474836480 bytes)
backing file: /var/lib/libvirt/images/noble-server-cloudimg-amd64.img
backing file format: qcow2
image: /var/lib/libvirt/images/noble-server-cloudimg-amd64.img

-b to podkład, -F jego format. Ważne: podkładu nie wolno potem zmieniać ani przesuwać, bo wszystkie kalki się rozjadą.

virt-install: nowa maszyna jedną komendą

Klasyczna instalacja z płyty ISO, z instalatorem w konsoli tekstowej:

sudo virt-install \\
  --name rocky01 \\
  --memory 2048 --vcpus 2 \\
  --disk size=20 \\
  --location /var/lib/libvirt/images/Rocky-10-x86_64-dvd.iso \\
  --osinfo rocky10 \\
  --network network=default \\
  --graphics none \\
  --extra-args 'console=ttyS0'

Czytamy od góry: nazwa, pamięć w megabajtach, liczba procesorów, nowy dysk 20 GB, skąd instalator, jaki to system, do jakiej sieci, bez okienka graficznego, a instalator ma mówić przez port szeregowy, żebyś go widział w terminalu. Nazwę systemu dla --osinfo znajdziesz tak:

virt-install --osinfo list | grep -E 'rocky|rhel10|ubuntu24'

Nazwa ma znaczenie: dla znanego systemu virt-install dobiera szybkie wirtualne urządzenia virtio. Dla nieznanego daje stare, wolne, za to bezpieczne. Ja na pierwszej próbie dałem generic i dysk dostał nazwę hda, jak w komputerze z poprzedniej epoki.

virsh: pilot do maszyn

virsh list --all
virsh dominfo test94
 Id   Name     State
------------------------
 1    test94   running

Id:             1
Name:           test94
State:          running
CPU(s):         1
Max memory:     524288 KiB
Persistent:     yes
Autostart:      disable

list bez --all pokazuje tylko włączone. Reszta pilota:

virsh shutdown test94
virsh destroy test94
virsh start test94
virsh autostart test94
virsh console test94
Domain 'test94' destroyed
Domain 'test94' started
Domain 'test94' marked as autostarted
Connected to domain 'test94'
Escape character is ^] (Ctrl + ])

Pamiętasz sygnały? shutdown to grzeczna prośba o zamknięcie lokalu: wirtualne naciśnięcie przycisku zasilania, system sam się zamyka. destroy to wyłączenie prądu. Nazwa jest straszna, ale nic nie kasuje: maszyna i jej dysk zostają, tylko gwałtownie gasną, z ryzykiem uszkodzenia otwartych plików. autostart włącza maszynę razem z serwerem. console to wejście przez kabel szeregowy, kiedy SSH nie działa. Wyjście z konsoli to Ctrl+]. Zapisz to sobie, bo exit wyloguje Cię z systemu w maszynie, ale z konsoli nie wyjdziesz.

A co z usuwaniem maszyny na dobre?

virsh undefine rocky01 --remove-all-storage

To już kasuje naprawdę, razem z dyskiem. Dwa razy sprawdź nazwę.

Migawki, czyli zapisz grę

Największa zaleta maszyny do nauki. Przed ryzykowną zmianą robisz migawkę, jak zapis stanu gry przed walką z bossem:

virsh snapshot-create-as test94 --name przed-aktualizacja --description "czysta instalacja"
virsh snapshot-list test94
Domain snapshot przed-aktualizacja created
 Name                 Creation Time               State
-----------------------------------------------------------
 po-starcie           2026-11-03 16:36:24 +0100   running
 przed-aktualizacja   2026-11-03 16:36:23 +0100   shutoff

Kolumna State mówi, w jakim stanie była maszyna: migawka włączonej maszyny zapisuje też pamięć, więc po powrocie programy działają dalej, jakby nic się nie stało. Zepsułeś? Wracasz:

virsh snapshot-revert test94 przed-aktualizacja
virsh snapshot-delete test94 po-starcie

Te migawki żyją wewnątrz pliku qcow2, widać je też w qemu-img info. Ważne: migawka to nie kopia zapasowa. Leży w tym samym pliku co maszyna. Padł dysk, padły migawki. O prawdziwych kopiach jest osobna lekcja.

Sieć: NAT kontra mostek

Maszyna potrzebuje sieci. Są dwie główne drogi.

NAT to osiedle zamknięte z jedną bramą. Maszyny mają swoje wewnętrzne adresy, wychodzą do internetu przez serwer, ale z zewnątrz nikt do nich nie zapuka. Sieć default w libvirt działa właśnie tak:

sudo virsh net-list --all
sudo virsh net-dumpxml default | grep -E 'forward|ip address|range'
  <forward mode='nat'/>
  <ip address='192.168.122.1' netmask='255.255.255.0'>
      <range start='192.168.122.2' end='192.168.122.254'/>

Serwer to brama 192.168.122.1, maszyny dostają adresy z DHCP z puli. Świetne do nauki i testów.

Mostek (bridge) to wpuszczenie maszyn prosto na ulicę. Maszyna dostaje adres z tej samej sieci co serwer, jak kolejny komputer podłączony kablem do switcha. Tego chcesz, gdy maszyna ma być normalnym serwerem w firmie.

Mostek tworzysz w systemie serwera. [Ubuntu] przez netplan, plik /etc/netplan/01-most.yaml:

network:
  version: 2
  ethernets:
    enp1s0:
      dhcp4: false
  bridges:
    br0:
      interfaces: [enp1s0]
      dhcp4: true
sudo netplan try

[RHEL] przez nmcli:

sudo nmcli connection add type bridge con-name br0 ifname br0
sudo nmcli connection add type ethernet port-type bridge con-name br0-port1 ifname enp1s0 controller br0
sudo nmcli connection up br0

Ostrzeżenie: robisz to na karcie sieciowej, przez którą jesteś zalogowany. Adres przenosi się z karty na mostek i przez SSH możesz stracić połączenie w połowie operacji. Miej dostęp do konsoli serwera albo ćwicz na maszynie wirtualnej. netplan try po to właśnie istnieje: jak nie potwierdzisz zmian w 120 sekund, wraca do starej konfiguracji.

Potem w virt-install zamiast --network network=default piszesz --network bridge=br0.

cloud-init: maszyna, która sama się urządza

Instalacja z ISO to klikanie przez instalator. W chmurze nikt tego nie robi. Bierze się gotowy obraz systemu, tak zwany cloud image, i przy pierwszym starcie podaje mu kartkę z instrukcjami: jak się nazywasz, kto ma konto, jaki klucz SSH, co zainstalować. Program w maszynie, który czyta tę kartkę, to cloud-init. Jak nowy pracownik, który pierwszego dnia znajduje na biurku list powitalny z loginem, hasłem do Wi-Fi i listą zadań.

Gotowe obrazy pobierasz od dystrybucji: Ubuntu z cloud-images.ubuntu.com, Rocky i Red Hat też udostępniają obrazy dla chmury (format sprawdź przez qemu-img info). Kartka składa się z dwóch plików. user-data:

#cloud-config
hostname: web01
users:
  - name: eryk
    groups: sudo
    shell: /bin/bash
    sudo: ALL=(ALL) NOPASSWD:ALL
    ssh_authorized_keys:
      - ssh-ed25519 AAAA... eryk@laptop
package_update: true
packages:
  - htop
runcmd:
  - echo "Postawione przez cloud-init" > /etc/motd

Pierwsza linijka #cloud-config jest obowiązkowa. Bez niej cloud-init uzna plik za coś innego. Na RHEL-u grupa adminów to wheel, nie sudo. Drugi plik, meta-data:

instance-id: web01
local-hostname: web01

Zanim użyjesz, sprawdź składnię. Literówka w YAML to maszyna bez Twojego konta:

cloud-init schema -c user-data
Valid schema user-data

Teraz dwie drogi. Nowszy virt-install sam spakuje te pliki:

sudo virt-install --name web01 --memory 2048 --vcpus 2 \\
  --disk path=/var/lib/libvirt/images/web01.qcow2 --import \\
  --osinfo ubuntu24.04 --network network=default \\
  --cloud-init user-data=user-data,meta-data=meta-data \\
  --graphics none --noautoconsole

--import znaczy: nie instaluj, dysk już ma system, po prostu uruchom. A dysk web01.qcow2 to nasza kalka na obrazie Ubuntu z poprzedniej części. Druga droga, działająca wszędzie: sam robisz małą płytę z kartką.

cloud-localds seed.iso user-data meta-data
file seed.iso
seed.iso: ISO 9660 CD-ROM filesystem data 'cidata'

Etykieta płyty cidata to sygnał dla cloud-init: „tu jest Twoja kartka”. Wkładasz ją jako drugi napęd: --disk path=seed.iso,device=cdrom. [Ubuntu] cloud-localds jest w paczce cloud-image-utils.

Po starcie wchodzisz ssh eryk@ADRES, bez hasła, z kluczem. W środku cloud-init status --wait czeka, aż skończy, a jak coś nie wyszło, zaglądasz do /var/log/cloud-init.log i /var/log/cloud-init-output.log.

Jak to działa pod spodem

Maszyna wirtualna z punktu widzenia serwera to… jeden proces qemu-system-x86_64. Każdy wirtualny procesor to wątek tego procesu. Kucharz udający całą restaurację. Kiedy maszyna wykonuje zwykłe instrukcje, KVM pozwala jej działać prosto na procesorze. Gdy maszyna chce dotknąć sprzętu, na przykład zapisać na dysk, procesor zatrzymuje ją i oddaje głos QEMU, które zapisuje dane do pliku qcow2.

libvirt przechowuje opis każdej maszyny jako XML. Systemowe maszyny leżą w /etc/libvirt/qemu/:

sudo ls /etc/libvirt/qemu/
virsh domblklist test94
autostart  networks  test94.xml
 Target   Source
------------------------------------------------
 hda      /var/lib/libvirt/images/test94.qcow2

Tego XML-a nie edytujesz ręcznie w edytorze. Używasz virsh edit test94, który sprawdzi poprawność przed zapisem, tak jak visudo dla sudoers. Podgląd to virsh dumpxml test94.

Ściąga flag

KomendaFlagaCo robi
qemu-img create-f qcow2 PLIK ROZMIARnowy dysk
qemu-img create-b PODKŁAD -F FORMATdysk z podkładem (kalka)
qemu-img info--backing-chaincały łańcuch dysków
qemu-img resize+5G, --shrinkpowiększa, zmniejsza
qemu-img convert-f Z -O NA -pzmiana formatu z paskiem postępu
virt-install--name --memory --vcpusnazwa, RAM w MB, procesory
virt-install--disk size=20 / path=...nowy dysk albo istniejący
virt-install--location, --cdrom, --importskąd system
virt-install--osinfo NAZWAtyp systemu, dobór urządzeń
virt-install--network network=default / bridge=br0NAT albo mostek
virt-install--cloud-init user-data=...kartka dla cloud-init
virt-install--graphics none --noautoconsolebez okna, bez łączenia z konsolą
virshlist --allwszystkie maszyny
virshstart, shutdown, destroywłącz, grzecznie wyłącz, wyłącz prąd
virshconsolekonsola szeregowa, wyjście Ctrl+]
virshautostartstart razem z serwerem
virshsnapshot-create-as, snapshot-list, snapshot-revertmigawki
virshundefine --remove-all-storageusuwa maszynę i jej dyski

⚠ Typowy błąd

Wpisujesz virsh shutdown, a maszyna… dalej działa:

virsh shutdown test94
sleep 5
virsh list --all
Domain 'test94' is being shutdown

 Id   Name     State
------------------------
 1    test94   running

„Is being shutdown”, a stan running. Dlaczego? shutdown tylko naciska wirtualny przycisk zasilania. Jeśli system w środku jeszcze się uruchamia, wisi albo nie ma usługi, która ten przycisk obsługuje, to nikt go nie usłyszy. U mnie maszyna miała pusty dysk, więc nie było w niej nikogo, kto mógłby usłyszeć. Jak dzwonek do pustego mieszkania.

Co robić: poczekaj chwilę i sprawdź virsh console, co się dzieje w środku. W nowoczesnych obrazach chmurowych włączony jest też agent QEMU (qemu-guest-agent), z którym zadziała virsh shutdown --mode agent. Dopiero na końcu virsh destroy, świadomie, ze świadomością, że to wyjęcie wtyczki z gniazdka.

✎ Zadanie

  1. Utwórz dysk baza.qcow2 o rozmiarze 10 GB, a na nim dysk z podkładem klon.qcow2 o rozmiarze 20 GB. Sprawdź qemu-img info --backing-chain, ile każdy zajmuje naprawdę.
  2. Postaw maszynę z obrazu chmurowego przez virt-install --import z własnym user-data, które zakłada Twoje konto z kluczem SSH. Zaloguj się po SSH.
  3. Zrób migawkę czysta, usuń w maszynie plik /etc/motd, a potem przywróć migawkę i sprawdź, że plik wrócił.
Pokaż rozwiązanie

Część pierwsza: dysk z podkładem.

qemu-img create -f qcow2 baza.qcow2 10G
qemu-img create -f qcow2 -F qcow2 -b baza.qcow2 klon.qcow2 20G
qemu-img info --backing-chain klon.qcow2 | grep -E '^image:|virtual size|^disk size|backing file:'
image: klon.qcow2
virtual size: 20 GiB (21474836480 bytes)
disk size: 196 KiB
backing file: baza.qcow2
image: baza.qcow2
virtual size: 10 GiB (10737418240 bytes)
disk size: 196 KiB

Dwa dyski, razem 30 GB obietnic, a zajmują mniej niż zdjęcie kota z telefonu. Zwróć uwagę: kalka może być większa niż podkład.

Część druga: maszyna z cloud-init. Plik user-data z Twoim kontem i kluczem jak w lekcji, sprawdzony przez cloud-init schema -c user-data, i:

sudo virt-install --name lab01 --memory 2048 --vcpus 2 \\
  --disk path=/var/lib/libvirt/images/klon.qcow2 --import \\
  --osinfo ubuntu24.04 --network network=default \\
  --cloud-init user-data=user-data,meta-data=meta-data \\
  --graphics none --noautoconsole
sudo virsh domifaddr lab01
ssh eryk@192.168.122.X

domifaddr poda adres, który maszyna dostała z DHCP sieci default. Jak SSH pyta o hasło, to znaczy, że klucz nie dotarł: zajrzyj przez virsh console do /var/log/cloud-init.log. Najczęstszy powód to brak #cloud-config w pierwszej linijce.

Część trzecia: migawka.

sudo virsh snapshot-create-as lab01 czysta
ssh eryk@192.168.122.X 'sudo rm /etc/motd'
sudo virsh snapshot-revert lab01 czysta
ssh eryk@192.168.122.X 'cat /etc/motd'

Plik wrócił, razem z napisem „Postawione przez cloud-init”. Wehikuł czasu, który działa tylko w jedną stronę: do tyłu. Do przodu musisz iść sam.

Co dalej

Masz już kontenery, maszyny i usługi. Kto ich pilnuje w nocy? W następnej lekcji monitorowanie: historia obciążenia w sar, progi i alarmy, Zabbix z agentem, Netdata i Prometheus z node_exporter.