vSphere Hypervisor (ESXi)でカジュアルにホットバックアップするスクリプト

# 2020/02/12追記

復元するときにシンプルで、仕組みをわかっている方が安全だろうと自作してみましたが、結局こちらに落ち着きました。
https://github.com/lamw/ghettoVCB

似たような処理を行っており、使い勝手や安定性も遥かに上です。


# ESXiのバックアップに苦労した過去

久々にESXiを触りました。名前がvSphere Hypervisorになっていたとは。

以前バージョン5のときに社内でサーバーを運用するのに使っていましたが、バックアップの運用に苦労しました。

色々探してもちょうどいいソリューションがなく、諦めてバックアップにはActonis製のソフトウェアを使っていました。
当時はもうちょっと安かったですが、それでもいい値段しました。
https://www.acronis.com/ja-jp/business/backup/virtual-machine/

でもこれが、まあ安定して動いてくれない。動かしてた環境がしょぼかったのもあると思いますが。

この前、VirtualBoxをハイパーバイザーとして使う際のバックアップスクリプトを作ったので、そのvSphere版を作ってみました。

**VirtualBoxのVMをライブ/オンライン/無停止バックアップするスクリプト**
https://qiita.com/miyanaga/items/39c19f1aa1171b2b402e

個人用なので作りが甘く、もっとよいやり方があると思いますが、プライベートな仮想環境のバックアップに参考ください。

# やってること

各VMについて、`.vmx`ファイル、`.vmdk`ファイル、`.log`ファイルをバックアップします。
バックアップの直前にスナップショットを作成し、バックアップ後にそのスナップショットを削除します。

起動中のVMは、ブロックデバイスのイメージファイルがロックされるのでコピーできません。仮にコピーできても整合性が保たれないので危険です。

そこでスナップショットをとると、イメージファイルが一度凍結され、差分が別のファイルに書き込まれます。その差分ファイルのみロックされるので、ファイルコピーで安全にイメージファイルをバックアップできます。

最初はVMのディレクトリにある全ファイルをコピーしていたのですが、メタデータとの不整合のためかVMを起動できませんでした。それで上記の拡張子のみコピーしています。

有償版のvSphereではライブマイグレーションができたと記憶していますが、それに比べると貧弱です。
スナップショットの作成と削除で謎の標準エラーが出ます。
スナップショットのIDはインクリメントされるのでどんどん増えていきます(特に不都合はないですが)。

まあ個人用なのでよしとします。

# 使い方

vSphere Hypervisor上で実行します。
`backup=""`を実際のバックアップ先に変更してください。
各VMには便宜的にでも一つ以上のスナップショットを作っておくと、高速な差分バックアップが実現します。

```bash
#!/bin/sh

# vsphare Hypervisor(6.7で動作確認)のホットバックアップスクリプト

# TODO
# * スナップショットの作成と削除で'Create Snapshot:: not found'という標準エラー
# * スペースを含むVMへの対応
# * 名称が重複するVMへの対処
# * バックアップ先の同期削除(rsync的な動作)

# datastoreが存在するvolumesパスとバックアップ先
volumes="/vmfs/volumes"
backup="/backup"

completed=""

# VMをリストアップ
vms=$(vim-cmd vmsvc/getallvms | tail -n +2)

# スナップショットを作成する関数
# 引数は整数によるVMのID、戻り値はスナップショットID(失敗した場合は""か0)
create_snapshot() {
  vmid="$1"

  # 日付付きスナップショットラベル
  ts=$(date '+%Y-%m-%d %H:%M:%S')
  label="backup $ts"

  # スナップショットを作成
  $(vim-cmd vmsvc/snapshot.create "$vmid" "$label" 2>/dev/null)

  # スナップショットIDはインクリメントされるので数値として最も大きなIDを取得
  # 正常に取得できない場合は初期値0のまま
  snapshotid=$(vim-cmd vmsvc/snapshot.get $vmid | grep 'Snapshot Id' | awk 'BEGIN{ id=0 } { if ($4>id) id=$4 } END{ print id }')

  echo $snapshotid
}

# スナップショットを削除する関数
# 引数は整数によるVMのIDとスナップショットID
remove_snapshot() {
  vmid="$1"
  snapshotid="$2"

  # スナップショットを削除
  $(vim-cmd vmsvc/snapshot.remove "$vmid" "$snapshotid" 2>/dev/null)
}

# VMレコードを解析する正規表現
re_vm_record='^([0-9]+) +([^ ]+) +\[([^\]+)\]\s+([^ ]+).+'

# リストアップしたVMを1件ずつ処理
IFS=$'\n'
for record in $vms; do
  # レコードを解析して整数のID、データストア、.vmxファイルパス、vmパスを取得
  vmid=$(echo "$record" | sed -r "s!$re_vm_record!\1!")
  datastore=$(echo "$record" | sed -r "s!$re_vm_record!\3!")
  vmx=$(echo "$record" | sed -r "s!$re_vm_record!\4!")
  vm=$(dirname "$vmx")

  # 意図しないレコードフォーマットなどで.vmxファイルパスを取得できなかった場合は次のVMへ
  if [ "$vmx" = "" ]; then
    echo "Could not parse vmx in '$record'"
    continue
  fi

  # .vmxファイルのフルパス
  path="$volumes/$datastore/$vmx"
  file=$(basename "$path")
  dir=$(dirname "$path")

  # .vmxファイルがもし存在しない場合は次のVMへ
  if [ ! -f "$path" ]; then
    echo "'$path' does not exists"
    continue
  fi

  # バックアップ先ディレクトリ
  dest="$backup/$vm"
  mkdir -p "$dest"

  # 一旦、現時点のvmxをコピーしておく
  cp -au "$path" "$dest/$file.tmp"

  # バックアップ用のスナップショット
  snapshot=$(create_snapshot "$vmid")

  # スナップショットIDが""または0の場合は作成に失敗したため次のVMへ
  if [ "$snapshot" = "" -o "$snapshot" = "0" ]; then
    echo "Failed to create snapshot of '$vmx'"
    continue
  fi

  # .vmemと.vmsnはスナップショットごとに追加されるので蓄積しないように削除
  # rm -f "$dest/"*.vmem "$dest/"*.vmsn
  # cp -au "$dir/"* "$dest"

  # vmdkファイルのコピーだけでも復元する
  cp -au "$dir/"*.vmdk "$dir/"*.log "$dest"

  # 一時ファイルからスナップショット作成まえの.vmxファイルを復元
  mv -f "$dest/$file.tmp" "$dest/$file"

  # バックアップ用のスナップショットを削除
  $(remove_snapshot "$vmid" "$snapshot")

  # 完了したVMを記録
  completed="$completed $vm"
done

echo "Backup complated:$completed"
```