【GCP】Java版MinecraftサーバーをGCEで建てる【Java版Minecraft】

# 概要
GCPに構築した統合版Minecraftサーバーの課題対応その３。

今回は、統合版ではなくJava版のMinecraftサーバーに変更する。

# 目的
+ 前回までに構築していた統合版Minecraftサーバーを、Java版に変更する。

# 事前調査
+ **プラグイン対応について**
いろいろ試したが、統合版では現状いろいろ無理…という結論になった。
　PMMP：起動、複数ワールド管理は出来たが、mobが沸かない。PureEntitiesX のPM4対応待ち？
　LiteLoaderBDS：wine併用がめんどくさい、ドキュメントが中国語でわからん……。
　BDSX：起動までは出来たが、プラグインの入れ方がわからんというか、npmで入れるのはわかるんだが、複数ワールドを管理するようなプラグインが見つからなかった。

+ **Java版Minecraftサーバー**
プラグインやmodはJava版の方が充実しており、mobのスポーンや、統合版クライアントからの接続も実用レベルっぽかったので、ダメ元でこちらを構築してみることにする。
これも統合版と同じくというか、それ以上に様々な種類の非公式サーバーが存在するのだが、老舗でプラグイン数も多い「Spigot」をまずは試してみる。
　
公式サイトは下記。
[SpigotMC - High Performance Minecraft](https://www.spigotmc.org/)


# 1. 構築
構築済みの統合版Minecraftサーバー（公式）を、Spigotに置き換える。

## 1.1. コード修正
Minecraftサーバーの構築は、cloud-init（cloud-config）で実施しているので、そこを修正。

<details><summary>service01\templates\cloud-init.yaml</summary><div>

```service01\templates\cloud-init.yaml
#cloud-config
timezone: Asia/Tokyo
locale: ja_JP.utf8
packages:
  - openjdk-17-jre-headless
  - expect
  - apache2
  - unzip
  - zip

users:
  - name: 'minecraft'
    plain_text_passwd: 'minecraft'
    shell: '/bin/bash'
    sudo: 'ALL=(ALL) NOPASSWD:ALL'
    lock_passwd: false

runcmd:
  - mkdir /opt/minecraft
  - mkdir /opt/minecraft/backup_worlds
  - mv /home/minecraft/mcs_backup.sh /opt/minecraft/mcs_backup.sh
  - mv /home/minecraft/spigot_init.sh /opt/minecraft/spigot_init.sh

write_files:
    - path: /home/minecraft/mcs_backup.sh
      content: |
        #!/bin/bash
        screen -r tskserver -X stuff '/save-all\n/save-off\n'

        WORLDS=(hoge)
        BACKUP_BUCKET='gs://hoge/'

        for world in ${WORLDS[@]}; do
          cp -r ${BASH_SOURCE%/*}/${world} ${BASH_SOURCE%/*}/backup_worlds/
        done

        /usr/bin/zip -r ${BASH_SOURCE%/*}/backup_worlds.zip ${BASH_SOURCE%/*}/backup_worlds
        /snap/bin/gsutil cp -R ${BASH_SOURCE%/*}/backup_worlds.zip ${BACKUP_BUCKET}
        /bin/rm ${BASH_SOURCE%/*}/backup_worlds.zip

        screen -r tskserver -X stuff '/save-on\n'
      permissions: '0755'
      owner: root:root
    - path: /home/minecraft/spigot_init.sh
      content: |
        #!/bin/bash
        # おまじない
        cd `dirname $0`

        # 必要ファイルをDL
        wget -P /opt/minecraft https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar
        wget -P /opt/minecraft/plugins https://ci.lucko.me/job/LuckPerms/1429/artifact/bukkit/loader/build/libs/LuckPerms-Bukkit-5.4.21.jar
        wget --trust-server-names -P /opt/minecraft/plugins https://dev.bukkit.org/projects/dynmap/files/latest
        wget --trust-server-names -P /opt/minecraft/plugins https://dev.bukkit.org/projects/worldedit/files/latest
        wget --trust-server-names -P /opt/minecraft/plugins https://dev.bukkit.org/projects/WorldGuard/files/latest
        wget --trust-server-names -P /opt/minecraft/plugins https://dev.bukkit.org/projects/Multiverse-Core/files/latest
        wget --trust-server-names -P /opt/minecraft/plugins https://dev.bukkit.org/projects/Multiverse-Portals/files/latest
        wget --trust-server-names -P /opt/minecraft/plugins https://dev.bukkit.org/projects/multiverse-inventories/files/latest
        wget --trust-server-names -P /opt/minecraft/plugins https://dev.bukkit.org/projects/Multiverse-NetherPortals/files/latest
        wget -P /opt/minecraft/plugins https://ci.opencollab.dev//job/GeyserMC/job/Geyser/job/master/lastSuccessfulBuild/artifact/bootstrap/spigot/target/Geyser-Spigot.jar
        wget -P /opt/minecraft/plugins https://ci.opencollab.dev/job/GeyserMC/job/Floodgate/job/master/lastSuccessfulBuild/artifact/spigot/target/floodgate-spigot.jar
        wget -P /opt/minecraft/plugins https://github.com/Camotoy/GeyserSkinManager/releases/download/1.6/GeyserSkinManager-Spigot.jar
        wget -P /opt/minecraft/plugins https://ci.viaversion.com/job/ViaVersion/lastBuild/artifact/build/libs/ViaVersion-4.2.2-SNAPSHOT.jar
        wget -P /opt/minecraft/plugins https://ci.viaversion.com/view/ViaBackwards/job/ViaBackwards/lastBuild/artifact/build/libs/ViaBackwards-4.2.1.jar
        wget -P /opt/minecraft/plugins https://ci.viaversion.com/view/ViaRewind/job/ViaRewind/lastBuild/artifact/all/target/ViaRewind-2.0.3-SNAPSHOT.jar

        # ビルド
        while [ ! -e BuildTools.jar ] ; do sleep 10; done && java -jar BuildTools.jar --rev latest

        # ビルド終了まで待つ
        while [ ! -e spigot-* ] ; do sleep 10; done

        # metadata_startup_script の起動を簡易化する（バージョンに依存させない）ためにリネーム（コピー）
        ver_num=`ls | grep spigot- | cut -c 8- | rev | cut -c 5- | rev`
        cp spigot-${ver_num}.jar spigot.jar

        # 初回起動
        java -Xms1024M -Xmx8G -jar spigot.jar nogui

        # eula 同意、server.properties 変更
        sed -i".org" -e "s/^online-mode=true$/online-mode=false/g" /opt/minecraft/server.properties
        sed -i -e "s/level-name=world$/level-name=lobby/g" /opt/minecraft/server.properties
        sed -i -e "s/server-port=25565$/server-port=19132/g" /opt/minecraft/server.properties
        sed -i -e '$ a server_name="hoge"' /opt/minecraft/server.properties
        sed -i".org" -e "s/eula=false$/eula=true/g" /opt/minecraft/eula.txt

        # 2回目起動
        java -Xms1024M -Xmx8G -jar spigot.jar nogui

        # プラグインの設定変更
        sed -i".org" -e "s/clone-remote-port: false$/clone-remote-port: true/g" /opt/minecraft/plugins/Geyser-Spigot/config.yml
        sed -i -e "s/auth-type: online$/auth-type: floodgate/g" /opt/minecraft/plugins/Geyser-Spigot/config.yml
        sed -i -e 's/server-name: "Geyser"$/server-name: "hoge"/g' /opt/minecraft/plugins/Geyser-Spigot/config.yml
        sed -i -e "s/passthrough-motd: false$/passthrough-motd: true/g" /opt/minecraft/plugins/Geyser-Spigot/config.yml
        sed -i -e "s/passthrough-player-counts: false$/passthrough-player-counts: true/g" /opt/minecraft/plugins/Geyser-Spigot/config.yml

        # サーバー再起動
        reboot
      permissions: '0755'
      owner: root:root
    - path: /var/www/html/index.html
      content: |
        <!doctype html>
        <html lang="en">
        <head>
          <meta charset="utf-8">
          <title>Jump to Dynmap</title>
          <meta http-equiv="Refresh" content="0;URL=hoge:8123">
        </head>
        <body> </body>
        </html>
      permissions: '0644'
      owner: root:root


# reboot
#power_state:
#  delay: "+15"
#  mode: reboot
```
</div></details>

　
結構大幅に変わっているので、1ブロック毎に解説（というかメモ）、下記は runcmd: 。
```service01\templates\cloud-init.yaml
runcmd:
  - mkdir /opt/minecraft
  - mkdir /opt/minecraft/backup_worlds
  - mv /home/minecraft/mcs_backup.sh /opt/minecraft/mcs_backup.sh
  - mv /home/minecraft/spigot_init.sh /opt/minecraft/spigot_init.sh
```
 runcmd: は数が増えてきたのと、特殊記号のエスケープが面倒すぎるので、スクリプト化した。
「spigot_init.sh」でサーバープログラムやらプラグインやらのDLや、各種設定を行っている。
「mcs_backup.sh」はワールドデータを GCS へバックアップするいつものスクリプトだが、サーバープログラム変更に伴いディレクトリ構成も変わったため、少し修正している（詳細は後述）
スクリプトの内容は write_files: に記載し、rumcmd: では必要最低限のディレクトリ作成やスクリプトファイルの移動のみとした。


バックアップスクリプトは下記。

```service01\templates\cloud-init.yaml
    - path: /home/minecraft/mcs_backup.sh
      content: |
        #!/bin/bash
        screen -r tskserver -X stuff '/save-all\n/save-off\n'

        WORLDS=(hoge)
        BACKUP_BUCKET='gs://hoge/'

        for world in ${WORLDS[@]}; do
          cp -r ${BASH_SOURCE%/*}/${world} ${BASH_SOURCE%/*}/backup_worlds/
        done

        /usr/bin/zip -r ${BASH_SOURCE%/*}/backup_worlds.zip ${BASH_SOURCE%/*}/backup_worlds
        /snap/bin/gsutil cp -R ${BASH_SOURCE%/*}/backup_worlds.zip ${BACKUP_BUCKET}
        /bin/rm ${BASH_SOURCE%/*}/backup_worlds.zip

        screen -r tskserver -X stuff '/save-on\n'
      permissions: '0755'
      owner: root:root
```
統合版のワールドデータはインストールディレクトリ内の「worlds」ディレクトリにまとまっていたのだが、Java版はインストールディレクトリ内に各ワールドのディレクトリが作成される。
そのため、ディレクトリ指定で一括バックアップを取ろうとすると、インストールディレクトリを指定しなければならなくなり、余計なファイルが大量に含まれてしまう。
仕方ないので、各ワールドディレクトリを一旦バックアップ用ディレクトリにコピーするという処理を追加した。
ワールド名はワールド作成後にしかわからないので、手動メンテナンス部分が増えてしまった…。
ワールド名（ディレクトリ名）にプレフィクス（ world_ ）orサフィックス（ _world ）を付けることも考えたがゲーム内でワールド名指定したりすることを考えると、あまり冗長にしたくなかったので非採用。


サーバー初期設定用のスクリプトは下記。
```service01\templates\cloud-init.yaml
    - path: /home/minecraft/spigot_init.sh
      content: |
        #!/bin/bash
        # おまじない
        cd `dirname $0`

        # 必要ファイルをDL
        wget -P /opt/minecraft https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar
        wget -P /opt/minecraft/plugins https://ci.lucko.me/job/LuckPerms/1429/artifact/bukkit/loader/build/libs/LuckPerms-Bukkit-5.4.21.jar
        wget --trust-server-names -P /opt/minecraft/plugins https://dev.bukkit.org/projects/dynmap/files/latest
        wget --trust-server-names -P /opt/minecraft/plugins https://dev.bukkit.org/projects/worldedit/files/latest
        wget --trust-server-names -P /opt/minecraft/plugins https://dev.bukkit.org/projects/WorldGuard/files/latest
        wget --trust-server-names -P /opt/minecraft/plugins https://dev.bukkit.org/projects/Multiverse-Core/files/latest
        wget --trust-server-names -P /opt/minecraft/plugins https://dev.bukkit.org/projects/Multiverse-Portals/files/latest
        wget --trust-server-names -P /opt/minecraft/plugins https://dev.bukkit.org/projects/multiverse-inventories/files/latest
        wget --trust-server-names -P /opt/minecraft/plugins https://dev.bukkit.org/projects/Multiverse-NetherPortals/files/latest
        wget -P /opt/minecraft/plugins https://ci.opencollab.dev//job/GeyserMC/job/Geyser/job/master/lastSuccessfulBuild/artifact/bootstrap/spigot/target/Geyser-Spigot.jar
        wget -P /opt/minecraft/plugins https://ci.opencollab.dev/job/GeyserMC/job/Floodgate/job/master/lastSuccessfulBuild/artifact/spigot/target/floodgate-spigot.jar
        wget -P /opt/minecraft/plugins https://github.com/Camotoy/GeyserSkinManager/releases/download/1.6/GeyserSkinManager-Spigot.jar
        wget -P /opt/minecraft/plugins https://ci.viaversion.com/job/ViaVersion/lastBuild/artifact/build/libs/ViaVersion-4.2.2-SNAPSHOT.jar
        wget -P /opt/minecraft/plugins https://ci.viaversion.com/view/ViaBackwards/job/ViaBackwards/lastBuild/artifact/build/libs/ViaBackwards-4.2.1.jar
        wget -P /opt/minecraft/plugins https://ci.viaversion.com/view/ViaRewind/job/ViaRewind/lastBuild/artifact/all/target/ViaRewind-2.0.3-SNAPSHOT.jar

        # ビルド
        while [ ! -e BuildTools.jar ] ; do sleep 10; done && java -jar BuildTools.jar --rev latest

        # ビルド終了まで待つ
        while [ ! -e spigot-* ] ; do sleep 10; done

        # metadata_startup_script の起動を簡易化する（バージョンに依存させない）ためにリネーム（コピー）
        ver_num=`ls | grep spigot- | cut -c 8- | rev | cut -c 5- | rev`
        cp spigot-${ver_num}.jar spigot.jar

        # 初回起動
        java -Xms1024M -Xmx8G -jar spigot.jar nogui

        # eula 同意、server.properties 変更
        sed -i".org" -e "s/^online-mode=true$/online-mode=false/g" /opt/minecraft/server.properties
        sed -i -e "s/level-name=world$/level-name=lobby/g" /opt/minecraft/server.properties
        sed -i -e "s/server-port=25565$/server-port=19132/g" /opt/minecraft/server.properties
        sed -i -e '$ a server_name="hoge"' /opt/minecraft/server.properties
        sed -i".org" -e "s/eula=false$/eula=true/g" /opt/minecraft/eula.txt

        # 2回目起動
        java -Xms1024M -Xmx8G -jar spigot.jar nogui

        # プラグインの設定変更
        sed -i".org" -e "s/clone-remote-port: false$/clone-remote-port: true/g" /opt/minecraft/plugins/Geyser-Spigot/config.yml
        sed -i -e "s/auth-type: online$/auth-type: floodgate/g" /opt/minecraft/plugins/Geyser-Spigot/config.yml
        sed -i -e 's/server-name: "Geyser"$/server-name: "hoge"/g' /opt/minecraft/plugins/Geyser-Spigot/config.yml
        sed -i -e "s/passthrough-motd: false$/passthrough-motd: true/g" /opt/minecraft/plugins/Geyser-Spigot/config.yml
        sed -i -e "s/passthrough-player-counts: false$/passthrough-player-counts: true/g" /opt/minecraft/plugins/Geyser-Spigot/config.yml

        # サーバー再起動
        reboot
      permissions: '0755'
      owner: root:root
```

結構面倒だったのが、Spigot のビルドを行う「java -jar BuildTools.jar --rev latest」コマンドの部分。
cloud-init中に上記スクリプトを実行すると、どうにもJavaのエラーで失敗してしまう。
JDKのインストールをスクリプト内で行ってみたり、環境変数を明示的に記載してみたり、ビルド結果のファイル作成をループ処理で待ってみたり、いろいろやったがダメだった…。
同じスクリプトを、普通にサーバーにログインして実行すると問題ないので、本スクリプトの実行については手動で行うこととした。

また、ビルドされる実行ファイルにはバージョン番号が含まれており、起動停止を自動化するにあたって邪魔なので、バージョン番号を除いたファイル名でコピーする処理を入れた（リネームでも良かったが、一応オリジナルは残した）
 
下記は、Dynmapプラグイン用。
```service01\templates\cloud-init.yaml
    - path: /var/www/html/index.html
      content: |
        <!doctype html>
        <html lang="en">
        <head>
          <meta charset="utf-8">
          <title>Jump to Dynmap</title>
          <meta http-equiv="Refresh" content="0;URL=hoge:8123">
        </head>
        <body> </body>
        </html>
      permissions: '0644'
      owner: root:root
```
Dynmapというのは、Minecraftゲーム内のマップを、Webブラウザで見れるようにするプラグイン。
便利そう＆面白そうなので入れてみた。
そして、こいつにはWebブラウザからポート8123でアクセスする必要があるため、URL入力を簡易化するために apache を導入し、80 → 8123 へリダイレクトさせている。

ロードバランサーでhttps受けしたりとか、Traffic Director でルーティングしたりとか、もうちょっとスマートな方法もありそうだが、追加料金もかかりそうなので、とりあえず今回はこれで。

また、metadata_shutdown_script も少し手を加えた。
```service01\vars_gce.tf
variable "gce" {
  default = {
    dev = {
      minecraft-game-server = {
        machine_type             = "e2-standard-4"
        image                    = "ubuntu-os-cloud/ubuntu-1804-lts"
        type                     = "pd-standard"
        size                     = "10"
        block-project-ssh-keys   = true
        key_name                 = "mcs_key"
        deletion_protection      = false
        metadata_startup_script  = "cd /opt/minecraft/\nscreen -dmS tskserver java -Xms1024M -Xmx8G -jar spigot.jar nogui"
        metadata_shutdown_script = "cd /opt/minecraft/\nscreen -p 0 -S tskserver -X eval 'stuff 'say 30秒後にサーバーをシャットダウンします、ログアウトしてください\\015'\nsleep 10\nscreen -p 0 -S tskserver -X eval 'stuff 'say 20秒後にサーバーをシャットダウンします、ログアウトしてください\\015'\nsleep 10\nscreen -p 0 -S tskserver -X eval 'stuff 'say 10秒後にサーバーをシャットダウンします、ログアウトしてください\\015'\nsleep 5\nscreen -p 0 -S tskserver -X eval 'stuff 'say 5秒後にサーバーをシャットダウンします、ログアウトしてください\\015'\nsleep 5\nscreen -r tskserver -X stuff 'save-all\\015'\nscreen -r tskserver -X stuff 'stop'\n/opt/minecraft/mcs_backup.sh"
      }
    }
    prd = {
    }
  }
}
```
30秒程度ではあるが、シャットダウン前にゲーム内にカウントダウン通知を行っている。
基本的には、ゲームを最後に抜けた人がDiscordからシャットダウンコマンドを送信するか、CPU使用率を見て自動的にシャットダウンするので、ぶっちゃけ自己満足以外の何者でもないが…。
スクリプトで、Minecraftゲーム内にメッセージを送信出来るというのは、なかなか面白い機能だと思った。


## 1.2. プラグイン
基本的にはDLして置くだけなので前項の cloud-init(cloud-config)にて実施済みだが（初回サーバー起動後に設定が必要な物も一部あるが）、cloud-init(cloud-config)には最終更新ファイルへのリンクしか記載していないので、一応こちらで紹介しておく。

導入したプラグインは下記。

[LuckPerms-Bukkit-5.4.21](https://luckperms.net/)（権限管理）
[Dynmap® v3.4-beta-3](https://www.spigotmc.org/resources/dynmap%C2%AE.274/)（ブラウザからMinecraftのマップを参照）
[WorldEdit for Bukkit 7.2.10](https://dev.bukkit.org/projects/worldedit/)（マップエディター）
[worldguard-bukkit-7.0.7](https://dev.bukkit.org/projects/WorldGuard/)（エリア保護等）
[Multiverse-Core-4.3.1](https://dev.bukkit.org/projects/Multiverse-Core/)（マルチワールド管理）
[Multiverse-Portals-4.2.1](https://dev.bukkit.org/projects/Multiverse-Portals)（マルチワールドポータル）
[Multiverse-NetherPortals-4.2.2](https://dev.bukkit.org/projects/Multiverse-NetherPortals)（ネザーワールド管理）
[Per World Inventory 2.3.2](https://www.spigotmc.org/resources/per-world-inventory.4482/)（マルチワールドインベントリ管理）
[GeyserMC](https://geysermc.org/)（統合版クライアントを接続可能にする）
[Floodgate](https://geysermc.org/)（統合版クライアントを認証無しで接続可能にする）
[GeyserSkinManager 1.6](https://www.spigotmc.org/resources/geyserskinmanager.88607/)（統合版スキンを反映する）
[ViaVersion 4.2.1](https://www.spigotmc.org/resources/viaversion.19254/)（新しいクライアントバージョンを接続可能にする）
[ViaBackwards 4.2.1](https://www.spigotmc.org/resources/viabackwards.27448/)（古いクライアントバージョンを接続可能にする）
[ViaRewind 2.0.2](https://www.spigotmc.org/resources/viarewind.52109/)（古いクライアントバージョンを接続可能にする）

とりあえず必要になりそうなのを片っ端から入れただけなので、正直理解が追いついていない。
遊びながら、いろいろ調整していこう…。
個々のプラグイン解説は、もう少し理解が追いついたら＆気が向いたら別途書くかもしれない、書かないかもしれない。

## 1.3. 動作確認
PC版、Switch版から問題なくログインやワールド移動が出来ることは確認出来た。
mobも問題なく湧いて動いているように見える。
その他、Discordからのサーバー操作や、未使用時の自動停止、ワールドデータのバックアップなどの運用面も一通り正常動作を確認。
ふう、ようやくこれで遊ぶための土台が整ったかなー。

ちなみにワールド構成は、今のところ下記。
1. ロビー用ワールド
2. 家族用ワールド
3. クラン用ワールド 
4. 素材集め用ワールド

最初にサーバーにログインするとロビー用ワールドへ繋がり、その後各ワールドへポータル移動することを想定している。
2・3はとりあえず便宜上分けてはみたが、交流が進めば１つに統合したりするかもしれない。

## 1.4. 補足
Java版に変更したこととワールドを増加したことで、アイドル時（ログインユーザー０時）のCPU使用率が 1% から 7% ほどに上昇した。
そのため、CPU使用率のアラートを、「2%以下」 から 「8%以下」 へ修正した。
```terraform
variable "monitoring" {
  default = {
    dev = {
      cpu_utilization_threshold = 0.08
      cpu_utilization_threshold_percent = 8
      trigger_topic         = true
      available_memory_mb   = 128
      timeout               = 120
      retry                 = true
      entry_point           = "run"
    }
  }
```