libvirt로 가상머신 관리 자동화하기

virsh 명령을 손으로 하나씩 치면서 VM을 만들고 지우는 건 한두 대일 때는 괜찮지만, 스토리지 풀 준비·도메인 정의·스냅샷·정리까지 반복해야 하는 상황이라면 스크립트로 묶는 게 낫다. 이 글에서는 virt-install로 도메인 XML을 자동 생성하고, virsh로 정의·기동·스냅샷·해제까지 이어지는 흐름을 실제로 실행해 정리한다. 게스트 OS 설치 자체보다는 VM 생명주기 관리를 스크립트 하나로 묶는 자동화에 초점을 맞춘다.

스토리지 풀 준비

VM 디스크를 담을 스토리지 풀부터 정의한다. 로컬 디렉터리 기반 dir 타입이 가장 단순하다.

$ mkdir -p ~/vmpool
$ virsh pool-define-as demo-pool dir --target ~/vmpool
Pool demo-pool defined

$ virsh pool-start demo-pool
Pool demo-pool started

$ virsh pool-autostart demo-pool
Pool demo-pool marked as autostarted

$ virsh pool-list --all
 Name        State    Autostart
---------------------------------
 demo-pool   active   yes

볼륨 생성과 도메인 XML 자동 생성

디스크 볼륨을 만들고, virt-install --print-xml로 실제 정의 없이 도메인 XML만 뽑아낸다 — 이 옵션이 자동화의 핵심이다. 대화형 설치 마법사 없이, 파라미터만 바꿔가며 XML을 스크립트로 찍어낼 수 있다.

$ virsh vol-create-as demo-pool demo-vm.qcow2 1G --format qcow2
Vol demo-vm.qcow2 created

$ virt-install \
  --name demo-vm --memory 512 --vcpus 1 \
  --disk vol=demo-pool/demo-vm.qcow2 \
  --os-variant generic --network none --graphics none \
  --print-xml 1 > demo-vm.xml

$ head -8 demo-vm.xml
<domain type="qemu">
  <name>demo-vm</name>
  <uuid>fb08b1f2-1574-4de7-aafc-a56a9501c316</uuid>
  <memory>524288</memory>
  <currentMemory>524288</currentMemory>
  <vcpu>1</vcpu>
  <os>
    <type arch="x86_64" machine="pc-i440fx-noble-v2">hvm</type>

--graphics none --network none은 이 실습을 최소 구성으로 유지하려는 선택이다. 실제 운영에서는 --network bridge=br0처럼 브리지를 지정하면 된다.

정의 → 기동 → 상태 확인

$ virsh define demo-vm.xml
Domain 'demo-vm' defined from demo-vm.xml

$ virsh list --all
 Id   Name      State
--------------------------
 -    demo-vm   shut off

$ virsh start demo-vm
Domain 'demo-vm' started

$ virsh list --all
 Id   Name      State
-------------------------
 1    demo-vm   running

부팅 가능한 디스크 이미지가 없어도 QEMU 프로세스 자체는 정상적으로 running 상태가 된다 — 이 실습은 게스트 OS 설치 여부와 무관하게 도메인 생명주기가 자동화 스크립트로 관리 가능함을 보여주는 데 목적이 있다.

스냅샷

실행 중인 VM 상태 그대로 스냅샷을 뜰 수 있다. 위험한 변경 전에 자동으로 스냅샷을 찍어두는 배치 스크립트에 쓰기 좋다.

$ virsh snapshot-create-as demo-vm snap1 --description "before risky change"
Domain snapshot snap1 created

$ virsh snapshot-list demo-vm
 Name    Creation Time               State
----------------------------------------------
 snap1   2026-08-02 23:00:39 +0900   running

정리

--remove-all-storage를 붙이면 도메인 정의를 해제하면서 연결된 볼륨까지 함께 지운다.

$ virsh destroy demo-vm
Domain 'demo-vm' destroyed

$ virsh undefine demo-vm --remove-all-storage --snapshots-metadata
Domain 'demo-vm' has been undefined
Volume 'hda'(/home/noble/vmpool/demo-vm.qcow2) removed.

$ virsh list --all
 Id   Name   State
--------------------
$ virsh vol-list demo-pool
 Name   Path
--------------

하나의 스크립트로 묶기

지금까지의 명령을 이름·메모리·vCPU·디스크 크기를 인자로 받는 스크립트 하나로 합치면, VM을 만들고 스냅샷을 찍고 지우는 과정 전체를 재현 가능하게 반복할 수 있다.

#!/usr/bin/env bash
set -euo pipefail

NAME=${1:-demo-vm}
MEM=${2:-512}
VCPUS=${3:-1}
DISK_SIZE=${4:-1G}
POOL=demo-pool

virsh pool-info "$POOL" >/dev/null 2>&1 || {
	mkdir -p ~/vmpool
	virsh pool-define-as "$POOL" dir --target ~/vmpool
	virsh pool-start "$POOL"
	virsh pool-autostart "$POOL"
}

virsh vol-create-as "$POOL" "${NAME}.qcow2" "$DISK_SIZE" --format qcow2

virt-install \
	--name "$NAME" --memory "$MEM" --vcpus "$VCPUS" \
	--disk "vol=${POOL}/${NAME}.qcow2" \
	--os-variant generic --network none --graphics none \
	--print-xml 1 > "${NAME}.xml"

virsh define "${NAME}.xml"
virsh start "$NAME"
virsh dominfo "$NAME"

스토리지 풀이 이미 있으면 건너뛰도록 pool-info로 존재 여부부터 확인해, 여러 번 실행해도 안전하게(idempotent) 동작하도록 했다.

주의사항

  • KVM 가속 여부 확인: /dev/kvm이 없는 환경(중첩 가상화 미지원 등)에서는 QEMU가 소프트웨어 에뮬레이션(TCG)으로 동작해 실제 OS 부팅은 크게 느려진다. ls /dev/kvm으로 먼저 확인할 것.
  • dir 타입 풀의 한계: 로컬 디렉터리 기반 풀은 호스트 하나에 묶여 있다. 여러 호스트 간 마이그레이션이 필요하면 NFS나 Ceph 같은 공유 스토리지 풀을 써야 한다.
  • undefine –remove-all-storage는 되돌릴 수 없다: 볼륨까지 함께 삭제되므로, 실제 데이터가 있는 VM에는 스냅샷이나 백업 없이 쓰지 않는다.
  • 대규모 자동화라면 API 바인딩 고려: VM 수가 많아지고 조건 분기가 복잡해지면 virsh 쉘 스크립트보다 libvirt-python 바인딩으로 에러 처리를 명시적으로 하는 편이 안정적이다.

마무리

virt-install --print-xmlvirsh만으로도 VM 생성부터 스냅샷, 정리까지 전 과정을 스크립트로 재현할 수 있다. 여기서는 로컬 dir 풀과 최소 구성으로 흐름만 다뤘지만, 같은 패턴에 브리지 네트워크와 실제 설치 미디어를 끼워 넣으면 그대로 운영 환경의 VM 프로비저닝 스크립트가 된다.

참고

답글 남기기