KVM のディスクイメージファイルの無停止バックアップ

# はじめに
オンプレミス環境で仮想マシーンを運用するにあたって、ディスクイメージファイルのバックアップをどのように取るか、という問題があるわけです。
当然、仮想マシーンを停止させてしまえば確実にバックアップを取得できるわけですが、サービス断は避けたい。
仮想化基盤の製品なら無停止でバックアップを取る機能があるのでしょうが、それを無料で手軽にすぐに使えるKVM で行う方法を書きます。

稼働中の仮想マシーンのディスクイメージファイルは常々書き換わっているので、普通にコピーしたのでは正常に動作するディスクイメージファイルのコピーは取れません。
そこで使うのが、LVM のスナップショットです。
LVM スナップショットからディスクイメージファイルの静止点をコピーできます。
今まで、この手法で取得したディスクイメージファイルで起動できなかったことはありません。

私の知る範囲で、この手法でバックアップを取得している人を見たことが無く、口で説明してもKVM のスナップショットと混同されてなかなか伝わらないので記事にすることにしました。

# 構成
ホストマシーン(物理サーバー) にCentOS7 を最小構成でインストールする。
その時ディスク構成を以下のようにする。
/var/lib/libvirt/images 用のVG とLV の名前は何でも良いが、ここではそれぞれserver-vg, image-lv とする。

<table>
<tr>
<th>dev</th>
<th>Physical Volume<br>(pv)</th>
<th>システム</th>
<th>Volume Group<br>(vg)</th>
<th>Logical Volume<br>(lv)</th>
<th>mount</th>
</tr>
<tr>
<td rowspan='4'>sda</td>
<td>sda1</td>
<td>Linux<br>(83)</td>
<td>-</td>
<td>-</td>
<td>/boot</td>
</tr>
<tr>
<td>sda2</td>
<td>Linux LVM<br>(8e)</td>
<td>centos</td>
<td>root<br>swap</td>
<td>/<br>swap</td>
</tr>
<tr>
<td>sda3</td>
<td>Linux LVM<br>(8e)</td>
<td>server-vg</td>
<td>image-lv</td>
<td>/var/lib/libvirt/images</td>
</tr>
<tr>
<td colspan='5' style='text-align:center;'>空き</td>
</tr>
</table>

次の項のVG 拡張で以下のように構成変更し、sda4 のサイズがsda3 のサイズの1/4 以上になるようにしたいので
sda3 と空きスペースのサイズの割り振りをOS インストール前に予め決めておく。

<table>
<tr>
<th>dev</th>
<th>Physical Volume<br>(pv)</th>
<th>システム</th>
<th>Volume Group<br>(vg)</th>
<th>Logical Volume<br>(lv)</th>
<th>mount</th>
</tr>
<tr>
<td rowspan='4'>sda</td>
<td>sda1</td>
<td>Linux<br>(83)</td>
<td>-</td>
<td>-</td>
<td>/boot</td>
</tr>
<tr>
<td>sda2</td>
<td>Linux LVM<br>(8e)</td>
<td>centos</td>
<td>root<br>swap</td>
<td>/<br>swap</td>
</tr>
<tr>
<td>sda3</td>
<td>Linux LVM<br>(8e)</td>
<td rowspan='2'>server-vg</td>
<td>image-lv</td>
<td>/var/lib/libvirt/images</td>
</tr>
<tr>
<td>sda4</td>
<td>Linux<br>(83)</td>
<td>-</td>
<td>-</td>
</tr>
</table>

このsda4 をimage-lv のスナップショットの保存領域として使う。

# VG 拡張
sda4 の作成。

```
sudo su -
echo -e "n\np\n\n\nw\n" | fdisk /dev/sda
reboot
```

sda4 をserver-vg に加える。
server-vg のFree  PE / Size がsda4 の容量分増えていれば良い。

```
sudo vgextend server-vg /dev/sda4
  Physical volume "/dev/sda4" successfully created.
  Volume group "server-vg" successfully extended

sudo vgdisplay
--- Volume group ---
VG Name               server-vg
System ID
Format                lvm2
Metadata Areas        2
Metadata Sequence No  3
VG Access             read/write
VG Status             resizable
MAX LV                0
Cur LV                1
Open LV               1
Max PV                0
Cur PV                2
Act PV                2
VG Size               408.12 GiB
PE Size               4.00 MiB
Total PE              104480
Alloc PE / Size       76800 / 300.00 GiB
Free  PE / Size       27680 / 108.12 GiB
VG UUID               eUT9O4-CiSA-3a27-jfsJ-VNqE-v0ML-5yYLWZ
```

# LVM スナップショットの取得試験
準備として、ディスクイメージファイルを作成したり/var/lib/libvirt/images に適当なファイルを作成しておく。  

現状の論理ボリューム一覧を確認する。

```
sudo lvscan
  ACTIVE            '/dev/centos/root' [30.00 GiB] inherit
  ACTIVE            '/dev/centos/swap' [<8.00 GiB] inherit
  ACTIVE            '/dev/server-vg/image-lv' [300.00 GiB] inherit
```

仮想マシーンディスクイメージファイル保存ディレクトリのサイズを確認する。

```
sudo du -h /var/lib/libvirt/images
25G     /var/lib/libvirt/images/
```

images-snap という名前のLVM のスナップショットを作成して一覧で確認する。
サイズは上で確認したサイズの1/4 以上。

```
sudo lvcreate -s -L 7G -n images-snap /dev/server-vg/image-lv
  Logical volume "images-snap" created.

sudo lvscan
  ACTIVE            '/dev/centos/root' [30.00 GiB] inherit
  ACTIVE            '/dev/centos/swap' [<8.00 GiB] inherit
  ACTIVE   Original '/dev/server-vg/image-lv' [300.00 GiB] inherit
  ACTIVE   Snapshot '/dev/server-vg/images-snap' [7.00 GiB] inherit
```

適当にマウント用ディレクトリを作成してLVM スナップショットをマウントして中身を確認する。
この時、元の/var/lib/libvirt/images 以下に変更をかけても/mnt/snap 以下には変化が無いことを確認する。
また、/mnt/snap 以下のファイルをホームディレクトリなどにコピーできることを確認する。

```
sudo mkdir /mnt/snap
sudo mount -o nouuid -o ro /dev/server-vg/images-snap /mnt/snap
sudo ls /mnt/snap
 XXXX.qcow2
```

アンマウントしてLVM スナップショットを削除する。

```
sudo umount /mnt/snap
sudo lvremove -f /dev/server-vg/images-snap
  Logical volume "images-snap" successfully removed
```

論理ボリューム一覧でLVM スナップショットが削除されたことを確認する。

```
sudo lvscan
  ACTIVE            '/dev/centos/root' [30.00 GiB] inherit
  ACTIVE            '/dev/centos/swap' [<8.00 GiB] inherit
  ACTIVE            '/dev/server-vg/image-lv' [300.00 GiB] inherit
```

以上の流れをスクリプトにしてCron に登録すればディスクイメージファイルのバックアップを自動化できる。

# バックアップの自動化
例として、LVM のスナップショットを作成して、そこからディスクイメージファイルを取り出してNFS サーバーにコピーするスクリプトを作成する。
おおよその流れは前項の試験内容と同じ。

スクリプトで使用するコマンドをインストール。

```
sudo yum -y install bc
```

スクリプトの作成。
環境に合わせて適宜編集する。

```sh:img_backup.sh
#!/bin/sh
# 説明   : LVM スナップショットからディスクイメージファイルをNFS 先にコピーする。
# 作成者 : 
# 作成日 : 

date=`/usr/bin/date +"%Y/%m/%d %H:%M:%S"`
log="/var/log/img_backup.log"
vg="server-vg"
lv="image-lv"
snap_dir="/mnt/snap"
nfs_dir="/mnt/nfs"
images="/var/lib/libvirt/images"
nfs_server="10.253.3.20"
nfs_server_dir="/opt/images_backup"


remove_snapshot (){
 /usr/bin/umount ${snap_dir}
 local lvremove=`/usr/sbin/lvremove -f /dev/${vg}/${lv}.snap 2>&1`

 local exists_snapshot=`check_snapshot`

 if [ ${exists_snapshot} -eq 1 ]; then
  /usr/bin/echo "${date} ${lvremove}" >> ${log}
 fi
}


check_snapshot (){
 local lvscan=`/usr/sbin/lvscan | /usr/bin/grep ${lv}.snap | /usr/bin/wc -c`

 if [ ${lvscan} -gt 0 ]; then
  /usr/bin/echo 1
 else
  /usr/bin/echo 0
 fi
}


unmount_nfs (){
 local umount=`/usr/bin/umount ${nfs_dir} 2>&1`

 local mount=`check_mount`

 if [ ${mount} -eq 1 ]; then
  /usr/bin/echo "${date} ${umount}" >> ${log}
 fi
}

check_mount (){
 local mounted=`/usr/bin/mount | /usr/bin/grep ${nfs_server} | /usr/bin/wc -c`

 if [ ${mounted} -gt 0 ]; then
  /usr/bin/echo 1
 else
  /usr/bin/echo 0
 fi
}


#
# イメージファイルがあるか確認
#
exist_qcow2=0
for qcow2 in $( ls ${images} ); do
 if [[ ${qcow2} =~ .+\.qcow2 || ${qcow2} =~ .+\.img ]]; then
  exist_qcow2=1
  break
 fi
done

if [ ${exist_qcow2} -eq 0 ]; then
 /usr/bin/echo "${date} not exist image file." >> ${log}
 exit 1
fi


#
# ディレクトリの確認
#
if [ ! -e ${snap_dir} ]; then
 /bin/mkdir ${snap_dir}
fi

if [ ! -e ${nfs_dir} ]; then
 /bin/mkdir ${nfs_dir}
fi


#
# NFS マウント
#
mount=`check_mount`
if [ ${mount} -eq 0 ]; then
 
 mount_nfs=`/usr/bin/mount -t nfs -o tcp ${nfs_server}:${nfs_server_dir} ${nfs_dir} 2>&1`
 mount=`check_mount`
 
 if [ ${mount} -eq 0 ]; then
  /usr/bin/echo "${date} ${mount_nfs}" >> ${log}
  exit 1
 fi
fi


#
# ${images} のサイズの1/4 GB + 1GB をスナップショットのサイズにする。
#
img_size=`/usr/bin/du -b ${images} | /usr/bin/sed -r "s/^([0-9]+).+$/\1/"`
snap_size=`/usr/bin/echo "scale=2; ${img_size} / (4 * 1024 * 1024 * 1024) + 1.00" | /usr/bin/bc`


#
# LVM スナップショットの作成
#
exists_snapshot=`check_snapshot`

if [ ${exists_snapshot} -eq 1 ]; then
 `remove_snapshot`
fi

lvcreate=`/usr/sbin/lvcreate -s -L ${snap_size}GB -n ${lv}.snap /dev/${vg}/${lv} 2>&1`
exists_snapshot=`check_snapshot`

if [ ${exists_snapshot} -eq 0 ]; then
 /usr/bin/echo "${date} ${lvcreate}" >> ${log}
 exit 1
fi

/usr/bin/mount -o nouuid -o ro /dev/${vg}/${lv}.snap ${snap_dir}


#
# ディスクイメージファイルのコピー
# 前回のバックアップよりも更新日時が新しい場合のみ。
#
start_date=`/usr/bin/date +"%Y/%m/%d %H:%M:%S"`
/usr/bin/echo "${start_date} start copy." >> ${log}

for qcow2 in $( ls ${snap_dir} ); do
 if [[ ${qcow2} =~ .+\.qcow2 || ${qcow2} =~ .+\.img ]]; then
  if [ -e ${nfs_dir}/${qcow2} ]; then
   active_img=`/usr/bin/ls -l --time-style="+%s" ${snap_dir}/${qcow2} | /usr/bin/cut -d " " -f 6`
   backup_img=`/usr/bin/ls -l --time-style="+%s" ${nfs_dir}/${qcow2} | /usr/bin/cut -d " " -f 6`
   
   if [ ${backup_img} -ge ${active_img} ]; then
    skip_date=`/usr/bin/date +"%Y/%m/%d %H:%M:%S"`
    /usr/bin/echo "${skip_date} skip ${qcow2}." >> ${log}
    continue
   fi
  fi
  
  copy_date=`/usr/bin/date +"%Y/%m/%d %H:%M:%S"`
  /usr/bin/echo "${copy_date} copy ${qcow2}." >> ${log}

  cp ${snap_dir}/${qcow2} ${nfs_dir}/${qcow2}
 fi
done

end_date=`/usr/bin/date +"%Y/%m/%d %H:%M:%S"`
/usr/bin/echo "${end_date} end copy." >> ${log}


#
# XML 定義ファイルのコピー
#
for xml in $( ls /etc/libvirt/qemu ); do
 if [[ ${xml} =~ .+\.xml ]]; then
  /bin/cp /etc/libvirt/qemu/${xml} ${nfs_dir}/${xml}
 fi
done


#
# NFS アンマウントとLVM スナップショットの削除
#
`unmount_nfs`
`remove_snapshot`
```

作成したスクリプトを適当なディレクトリに保存してCron に登録する。
コピーにかかる時間を考慮して頻度を調節する。

```text:/etc/cron.d/img_backup.cron
1 * * * * root sh /usr/local/bin/img_backup.sh
```

/var/log にログを出力するようにしているのでlogrotate を定義する。

```conf:/etc/logrotate.d/img_backup
/var/log/img_backup.log {
 monthly
 missingok
 rotate 12
 compress
}
```

# 応用
仮想マシーンディスクイメージファイル保存領域としてSSD やNVMe を使用する場合の例。
OS インストール時は以下のようなディスク構成にする。
この時、sda の空き領域の容量が増設デバイスの空き領域の容量の1/4 以上になるようにする。

<table>
<tr>
<th>dev</th>
<th>Physical Volume<br>(pv)</th>
<th>システム</th>
<th>Volume Group<br>(vg)</th>
<th>Logical Volume<br>(lv)</th>
<th>mount</th>
</tr>
<tr>
<td rowspan='3'>sda</td>
<td>sda1</td>
<td>Linux<br>(83)</td>
<td>-</td>
<td>-</td>
<td>/boot</td>
</tr>
<tr>
<td>sda2</td>
<td>Linux LVM<br>(8e)</td>
<td>centos</td>
<td>root<br>swap</td>
<td>/<br>swap</td>
</tr>
<tr>
<td colspan='5' style='text-align:center;'>空き</td>
</tr>
<tr>
<td>nvme0n1</td>
<td colspan='5' style='text-align:center;'>空き</td>
</tr>
</table>

以下のような構成になるように設定していく。

<table>
<tr>
<th>dev</th>
<th>Physical Volume<br>(pv)</th>
<th>システム</th>
<th>Volume Group<br>(vg)</th>
<th>Logical Volume<br>(lv)</th>
<th>mount</th>
</tr>
<tr>
<td rowspan='3'>sda</td>
<td>sda1</td>
<td>Linux<br>(83)</td>
<td>-</td>
<td>-</td>
<td>/boot</td>
</tr>
<tr>
<td>sda2</td>
<td>Linux LVM<br>(8e)</td>
<td>centos</td>
<td>root<br>swap</td>
<td>/<br>swap</td>
</tr>
<tr>
<td>sda3</td>
<td>Linux<br>(83)</td>
<td rowspan='2'>server-vg</td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td>nvme0n1</td>
<td>nvme0n1p1</td>
<td>Linux LVM<br>(8e)</td>
<td>image-lv</td>
<td>/var/lib/libvirt/images</td>
</tr>
</table>

増設デバイスにパーティションを作成する。

```
sudo su -
echo -e "n\np\n1\n\n\nt\n8e\nw\n" | fdisk /dev/nvme0n1
```

増設デバイスにPV とVG とLV を作成しxfs でフォーマットする。

```
pvcreate /dev/nvme0n1p1
  Physical volume "/dev/nvme0n1p1" successfully created.

vgcreate server-vg /dev/nvme0n1p1
  Volume group "server-vg" successfully created

lvcreate -n image-lv -l 100%FREE server-vg
  Logical volume "image-lv" created.

mkfs.xfs /dev/server-vg/image-lv
meta-data=/dev/server-vg/image-lv isize=512    agcount=4, agsize=97675520 blks
         =                       sectsz=512   attr=2, projid32bit=1
         =                       crc=1        finobt=0, sparse=0
data     =                       bsize=4096   blocks=390702080, imaxpct=5
         =                       sunit=0      swidth=0 blks
naming   =version 2              bsize=4096   ascii-ci=0 ftype=1
log      =internal log           bsize=4096   blocks=190772, version=2
         =                       sectsz=512   sunit=0 blks, lazy-count=1
realtime =none                   extsz=4096   blocks=0, rtextents=0
```

/var/lib/libvirt/images にマウントしてfstab に追記する。

```
mount /dev/server-vg/image-lv /var/lib/libvirt/images
echo "/dev/server-vg/image-lv /var/lib/libvirt/images xfs defaults 0 0" >> /etc/fstab
```

sda3 を作成してserver-vg に加える。

```
echo -e "n\np\n3\n\n\nw\n" | fdisk /dev/sda
reboot

sudo vgextend server-vg /dev/sda3
```

これで前項のスクリプトでバックアップを取得できる環境になった。

# まとめ
複数台の物理サーバーを置けるならGlusterfs で冗長化するのがスマートであるが、汎用サーバー1台しか置けない環境や、あまり難しいことをしたくない環境ならこの方法で仮想マシーンのバックアップを取るのが費用もかからず便利である。
また、LVM のスナップショットは様々なファイルのバックアップに使えるので、物理サーバーを構築するときはHDD を全部使わずに空き領域を残しておくと後からLVM のスナップショットを利用したバックアップの仕組みを作りやすい。