MinecraftサーバーのワールドをGCSへ自動バックアップしてみた

Minecraft

はじめに

Minecraftサーバーを長く運用していると、ワールドデータのバックアップをどうするかは悩みどころです。

僕の環境では、

  • 日次でバックアップしたい
  • 半年前の状態まで遡れるようにしたい
  • VPS本体のストレージはあまり消費したくない

というような要件(欲求)がありました。

そして、今回構築したのはGoogle Cloud Storage(GCS)への自動バックアップです。個人のMinecraftサーバーでここまでやる必要があるかというと正直微妙ですが、自分のサーバーではこういう構成でバックアップしているという一例として紹介しようと思います。

バックアップの全体構成

全体の構成は次のようになっています。

VPS上でバックアップスクリプトを実行して、ワールドデータを圧縮、GCSへアップロードしています。

GCS上の古いバックアップデータはGCS側のLifecycle Ruleで削除することで、バックアップスクリプトをシンプルにできています。

Daily・Weekly・Monthlyの3世代で管理する

バックアップは次の3種類に分けています。

種類保存頻度保存期間ストレージクラス
Daily毎日7日STANDARD
Weekly毎週30日NEARLINE
Monthly毎月180日COLDLINE

直近は細かく復元できるようにDailyを残し、時間が経ったものはWeekly・Monthlyだけを残す形です。
GCSのStorage Classを保存期間に合わせて分けています。

これによって、すべてのバックアップを180日保存するより、ストレージ使用量を抑えながら長期間の復元ポイントを確保できます。

Daily・Weekly・Monthlyでは保存期間が異なるため、最低保存期間と保管コストを考慮してストレージクラスを使い分けています。短期間で削除するDailyにはSTANDARD、30日程度保持するWeeklyにはNEARLINE、長期間保持するMonthlyにはCOLDLINEを使用しています。

GCSのバックアップ環境構築

GCSのバケットやService AccountはTerraformで管理しています。

3つのGCSバケットを作成する

Daily・Weekly・Monthly用に、それぞれバケットを作成します。

例えばDailyは次のような設定です。

resource "google_storage_bucket" "daily" {
  name          = var.bucket_name_daily
  location      = var.bucket_location
  storage_class = "STANDARD"

  uniform_bucket_level_access = true
  public_access_prevention    = "enforced"

  lifecycle_rule {
    condition {
      age = 7
    }

    action {
      type = "Delete"
    }
  }
}

WeeklyとMonthlyもほぼ同じで、Storage Classと保存期間だけ変えています。

バックアップ用Service Accountを作成する

VPSからGCSへアクセスするため、専用のService Accountを作成します。

resource "google_service_account" "backup_uploader" {
  account_id   = "minecraft-backup-uploader"
  display_name = "Minecraft Backup Uploader"
}

Service Accountには、

roles/storage.objectCreator

のみを付与しています。

必要なのはバックアップファイルのアップロードだけなので、Storage Adminのような強い権限は与えていません。

Minecraftのバックアップスクリプト

バックアップ処理全体の流れ

バックアップスクリプトは次の順番で動きます。

1. 稼働中にワールドをrsync
2. save-off
3. save-all flush
4. 保存完了を待つ
5. もう一度rsync
6. save-on
7. zstdで圧縮
8. GCSへアップロード
9. Discordへ通知

VPS から GCS へのアップロードには Google Cloud CLI を使用しています。
ここでは省略しますが、あらかじめ Service Account で認証し、対象のバケットへアップロードできる状態にしておきます。

稼働中にワールドを事前同期する

まずMinecraftサーバーを動かしたまま、ワールドデータを一時ディレクトリへコピーします。

rsync -a --delete \
  "$SERVER_DIR/$world/" \
  "$SNAPSHOT_DIR/$world/"

この時点ではまだMinecraft側でファイルが変更されるため、これだけでは最終的なバックアップにはしません。

ここではデータの大部分を先にコピーしておくことが目的です。

save-offで自動保存を一時停止する

次にMinecraftへ、

save-off

を送信します。

これによってMinecraftサーバーのファイルへのデータ自動保存がオフになります。

save-all flushの完了を待つ

続けて、

save-all flush

を実行します。

コマンドを送るだけではなく、ServerのLogに、

Saved the game

が出たことを確認してから次へ進みます。
このログが出るまではワールドデータの書き込みが完了しておらず、ファイルが書き換わる可能性があるためです。

また、このとき過去のログを誤検知しないように、コマンド送信後に追加された行だけを見るようにしています。

もう一度rsyncして差分を同期する

保存が完了したら、もう一度同じrsyncを実行します。

1回目の同期から変更されたファイルだけがコピーされるため、ここでは比較的短時間で処理できます。

save-onで自動保存を再開する

2回目の同期が終わったら、

save-on

を送信して自動保存を再開します。

ここまで終われば、あとはMinecraft Server本体とは切り離して、同期先のデータでバックアップ処理を続けられます。

zstdでワールドデータを圧縮する

一時ディレクトリに作成したワールドデータを、tar + zstdで圧縮します。

tar --use-compress-program="zstd -T0 -10" \
  -cf "$BACKUP_FILE" \
  -C "$SNAPSHOT_DIR" \
  "${WORLDS_TO_BACKUP[@]}"

GCSへアップロードする

完成したバックアップをGCSへアップロードします。

gcloud storage cp "$BACKUP_FILE" "$DAILY_BUCKET/"

必要な日であればWeeklyやMonthlyにも同じファイルをアップロードします。

以下は毎月1日にMonthlyへアップロードする例です。

if [ "$DAY_OF_MONTH" = "01" ]; then
  gcloud storage cp "$BACKUP_FILE" "$MONTHLY_BUCKET/"
fi

同じバックアップファイルを複数のバケットへ送っているだけなので、圧縮処理を何度も行うわけではありません。

save-offの時間を短くするために2段階でrsyncする

今回の構成で少し悩んだのが、圧縮前のローカルスナップショットをどう作るかでした。

本当はLVMやZFS、Btrfsなど、ボリュームやファイルシステム側が持つSnapshot機能を使いたいと考えていました。

それが使えれば、

save-off
↓
save-all flush
↓
Snapshot作成
↓
save-on

のように、かなり短時間である時点の状態を切り出せます。

あとはそのSnapshotをゆっくり圧縮してGCSへ送ればよいため、今回のバックアップ構成とも相性がよさそうでした。

ただ、現在のVPSは最初からその前提でストレージを構築していません。

既存環境へ導入するにはストレージ構成の変更やデータ移行が必要になるため、バックアップのためだけにそこまで変更するのは少し面倒だったため、今回はrsyncを2回行う方式で妥協しています。

Minecraft稼働中
↓
1回目のrsync
↓
save-off
↓
save-all flush
↓
2回目のrsync
↓
save-on

異常終了しても必ずsave-onする

バックアップスクリプトで特に避けたいのが、

save-off
↓
途中でエラー
↓
save-onされない

という状態です。

自動保存が停止したままサーバーが動き続けるのは危険なので、Bashのtrapを使っています。

スクリプト開始時に、

trap cleanup EXIT

を設定しています。

cleanup では、バックアップ処理の途中で異常終了した場合に備えて、自動保存の復旧( save-on )とDiscordへの失敗通知を行っています。

特に save-off 実行後にスクリプトが停止すると、Minecraftの自動保存が無効なまま残る可能性があるため save-off を実行したら必ず save-on が実行されるようにしているのです。

バックアップ結果をDiscordへ通知する

自動化すると、今度はバックアップが失敗していることに気づきにくくなります。

そこで実行結果をDiscordへ通知しています。

正常終了した場合は、

という通知を送って、途中でエラーになった場合は、

という通知を送るようにしています。

systemd timerでバックアップを自動実行する

バックアップはsystemd timerから毎日実行しています。

systemd serviceを作成する

Serviceはoneshotです。

[Service]
Type=oneshot
User=minecraft
Group=minecraft

ExecStart=/opt/minecraft-backup/backup.sh

StandardOutput=journal
StandardError=journal

毎日4時にtimerを実行する

Timerは次のようにしています。

[Timer]
OnCalendar=*-*-* 04:00:00
Persistent=true

これで毎日午前4時にバックアップが実行されます。

あとはこれらを有効化するだけです。

sudo systemctl daemon-reload
sudo systemctl enable --now minecraft-backup.timer

バックアップが正常に動いているか確認する

GCSに保存されたバックアップを確認する

Google Cloud Consoleから各バケットを開き、バックアップが作成されていることを確認します。

VPS側のService Accountにはroles/storage.objectCreatorしか付けていないため、VPSからはオブジェクト一覧を取得できない点には注意が必要です。

Discord通知を確認する

最後にDiscordにも成功通知が届いていれば、一連の処理が正常に動いていることを確認できます。

バックアップからワールドを復元する

復元するときは、まずGCSから対象のバックアップをダウンロードします。

gcloud storage cp \
  gs://YOUR_BUCKET/BACKUP_FILE \
  /tmp/minecraft-backup

その後、tar + zstdで展開します。

mkdir /tmp/minecraft-restore

tar --use-compress-program=zstd \
  -xf /tmp/minecraft-backup \
  -C /tmp/minecraft-restore

Minecraftサーバーを停止し、現在のワールドを退避してから復元したワールドへ置き換えます。

mv /opt/minecraft/world \
   /opt/minecraft/world.old
cp -a /tmp/minecraft-restore/world \
      /opt/minecraft/world

バックアップは取れているだけでは意味がないので、一度は実際に展開できることも確認しておくと安心です。

まとめ

今回は、自分のMinecraftサーバーで使っているGCSへの自動バックアップ構成を紹介しました。

GCSを使ってDaily・Weekly・Monthlyまで保存するのは、個人のMinecraftサーバーとしてはかなり手厚い構成だと思います。

あくまで、Minecraftサーバーのバックアップ構成を考える際の、一つの実例として参考になればと思います。