Minecraft (java) のMODサーバーをGCEインスタンスで構築・Discord Botで操作の備忘録

# はじめに
　初めてQiitaを使うのでおかしなところがあったらごめんなさい。
## できること
・MinecraftのModサーバーをローコストで運用
・Discord BotによるMinecraftサーバーの起動、停止、IPアドレスの確認、ステータスの
　確認、Modファイルの操作（アップロード、リストアップ、削除）
## きっかけと概要
　本当はマインクラフトサーバーを自宅PC上で構築してみんなでマルチしたかったのですが、家のネットが共用サーバーのためポート開放ができず。
 
　VPNを使う方法はクライアントで毎回接続するのが面倒なので、いろいろ方法を探した結果、GoogleCloudのCompute Engine（以下GCE）が**無料**で**90日間**、あるいは**300ドル分**のクレジットを使用できるとのことでGCEサーバーの構築を決意。（これをする前に一度AWSのEC2でサーバー運用していたのですが、**なんせ料金が高い...**）
  
　しかし、24時間常に起動するのは料金的に現実的ではないし、24時間サーバーを開く必要もないので、Discordでサーバーを起動、停止、IPアドレスの確認、Modファイルの操作をできるようにする必要もある。エンドポイントやらなんやらを利用してサーバーレスでDiscord Botを運用できるような記事を見たが、以前試したときなぜかうまくできなかったため、今回はEC2の無料利用枠をつかってDiscord Botを24時間運用することに。Discord BotはPythonスクリプト1つを動かすだけなので**スペックは一切気にする必要なし**。

　EC2とGCEはgcloud CLIをつかって、EC2上でGCEをコマンドライン操作できるように設定することで起動、IPアドレスの確認、停止が可能。さらにEC2とGCEをSSH接続することでModファイルの操作も可能に。



[GoogleCloud]:https://www.googleadservices.com/pagead/aclk?sa=L&ai=DChcSEwjtr7-q3bOMAxUzH0QIHb8BI5kYABAAGgJkeg&co=1&ase=2&gclid=Cj0KCQjw16O_BhDNARIsAC3i2GDag7E6rj9ITtItOCoWxfnuPHXOGz9791Ld0zeJjv4C4YVdheQDL-AaAnkhEALw_wcB&ei=KjvqZ6mZH_CVvr0P5vWaCA&ohost=www.google.com&cid=CAESV-D2-cJh3R4PSYXIJ5dUa3iyQkI4EL8czRPL4eTE1TFpYk_RXtD-i-Zlznq-NFKpqlnH53xspALNVlkP130jyp15OJMjXwbUf3rAUuv-g7ShPa6WeGIZag&sig=AOD64_213xN8ahhODITjIKmZNPZei-4tFw&q&sqi=2&nis=4&adurl&ved=2ahUKEwip1rWq3bOMAxXwiq8BHea6BgEQ0Qx6BAgKEAE
[AWS]:https://aws.amazon.com/jp/?nc2=h_lg
[Discord]:https://discord.com/
[Discord Dev Portal]:https://discord.com/developers/docs/intro
[ChatGPT]:https://openai.com/ja-JP/chatgpt/overview/



# 本文
### 用意するもの
1. **やる気と根気**
1. [GoogleCloud]アカウント
1. [AWS]アカウント
1. [Discord]アカウント
1. [ChatGPT]アカウント（あったらデバッグに超便利）


### VM（仮想マシン）の構成
・GCEサーバー → Minecraft Modサーバー
・EC2サーバー → Discord Botサーバー


### やることリスト
#### Minecraftサーバーの構築（VMで行う操作は（**GCE**）または（**EC2**）と語尾に記載）
1. [GoogleCloud]の登録（90日間無料、要クレカ）
1. インスタンスの作成
1. ポート開放
1. Javaのインストール（**GCE**）
1. NeoForgeのインストール（Mod要らないなら通常サーバーのインストール）（**GCE**）
1. Pythonのインストール（**GCE**)
1. サーバー自動停止スクリプトの作成 （**GCE**）
1. ワールドデータバックアップスクリプトの作成 （**GCE**）
1. 上記７, ８のcrontabへの登録（**GCE**）
1. サーバー自動起動のスクリプトの作成（**GCE**）
1. 作成したファイルへの権限付与（**GCE**）

#### Discord Botサーバーの構築とBotの作成
~~0. [AWS]の登録（無料利用枠はアカウント登録から12か月間、毎月750時間まで無料）~~
~~0. [Discord]のアカウント登録（さすがに無料。ていうかみんな持ってるでしょ。）~~
1. [Discord Dev Portal]でBotの作成（無料）
1. AWS EC2インスタンスの作成
1. Python3のインストール（**EC2**）
1. gcloud CLIのインストールと初期設定（**EC2**）
1. BotのPythonスクリプトの作成（**EC2**）
1. サーバー自動停止時のメッセージ送信スクリプトの設定（**EC2**）
1. SSHの設定（**EC2**）

___

### Minecraftサーバーの構築
#### 1. GoogleCloudの登録
[ここ][GoogleCloud]からGoogleCloudに登録します。登録方法に関しては省略します。

#### 2. インスタンスの作成
　まずMinecraftのワールドを動かすサーバーを作っていきます。
 　左上のGoogleCloudのロゴがある左側の3本線をクリックして、**Compute Engine** → **VMインスタンス**をクリック。
  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942774/00f18e50-7428-4f54-bca2-b9a436c57b95.png)
___
　次に、青色の**インスタンスを作成**ボタンをクリック。マシンの構成を以下のように設定。
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942774/b6150b1f-f9ac-487b-8c8d-5b26ec9344ad.png)

 ・名前：minecraft-server
　　マシン名のタイポには気を付けてください（後になぜかうまくいかなくて結構悩んだ）
 ・リージョン：asia-northeast1(東京）またはasia-northeast2(大阪)
 ・ゾーン：asia-northeast1-b またはa, c
 これらは後で設定時に指定する必要があるので覚えておく。（後で確認可能）
___
 [こちらの記事](https://zenn.dev/cloud_ace/articles/minecraft-performance-2024)を参考に、インスタンスタイプを決定します。
 今回はModを導入するので、あるコスト料金がかさまずに（90日間で300ドルいかないような）高性能なインスタンスを作成するため、以下のように設定しました。
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942774/c5aa77e0-10d2-4f64-9344-724186f1bd07.png)
 
　Modの導入が少ない場合や人数が少ない場合はプリセットのn2d-highcpu-4でいいかも。
 ___
　次に、OSとストレージタブに移動し、**変更**をクリック。OSをUbuntuにします。
 （ここはお好みで。OSによってはコマンドが違ったり追加料金がかかるのでわからなければUbuntuにしてね。）
 サイズはそこまで大きくなくてもいいけど20GBが無難。
 （**画像では10GBのままですが気にしないでください。変えるの忘れてました。**）


 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942774/4c63cbcb-bd9c-400e-a5de-1abbebf0a50c.png)

 ___
 
 左側のネットワーキングタブに移動し、ネットワークタグをminecraft-serverにする。
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942774/6c081f84-ce7e-409f-a2a7-9351fad0e819.png)

　少し下に移動して、ネットワークインターフェースの確認を行う。画像のようになっていればOK。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942774/5b625e52-b288-41bb-ba2d-45c27cd1cfcf.png)

　ここまで出来たら、下の作成ボタンをクリックしてインスタンスを作成する。
___

 #### 3. ポート開放
 　次に、ナビゲーションバー（3本線）から**VPC ネットワーク** → **ファイアウォール**を選択

  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942774/a74caa02-5fe5-42ca-b1f5-232ab9016337.png)

  上部の**ファイアウォールルールの作成**をクリックし、名前を**ネットワークタグと同じ名前**に設定し、送信元IPv4範囲を**0.0.0.0/0**で指定する。その下のプロトコルとポートをTCPにチェックし、ポート番号25565を入力する。ここまで出来たら下の作成をクリック。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942774/5265ce9b-723f-4135-9f49-3b89dbd18c54.png)

#### 4. Java（OpenJDK）のインストール
###### *ここからGCEで使用するコマンドは、Ubuntuのコマンドのため、ほかのOSとのコマンドとは異なります。*
　ようやくサーバーを起動することができます。**ナビゲーションバー** → **VMインスタンス**から、先程作成したインスタンスのチェックを入れて**開始/再開**をクリックします。インスタンスが起動まで少し待ちます。
　起動したら**SSH**をクリックし、出てきたウィンドウで**Authorize**をクリック。

 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942774/22d3cde6-a0fb-4e60-9ebc-40c919f015e5.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942774/96f3a1cf-6a64-4484-b19f-4d263757e41d.png)

　するとコンソールが立ち上がります。ここから様々な操作を行います。まずはJava（OpenJDK）のインストールから行きます。
 あ、その前にaptのアップデートから行います。
~~~terminal
$ sudo apt update
~~~
　ここで、Minecraftをプレイするバージョンの確認を行います。Minecraftはゲームバージョンによって使用するJavaのバージョンが違うので、以下の表を参考にバージョンを指定してください。

|Minecraft バージョン|Java バージョン|インストールコマンド例|
|:-:|:-:|:-:|
|1.7.10～1.16.5   |  Java8またはJava11 | sudo apt install -y openjdk-8-jdk  |
| 1.17～1.17.1  |  Java16 | sudo apt install -y openjdk-16-jdk  |
| 1.18～1.20.4  | 	Java17  | sudo apt install -y openjdk-17-jdk  |
| 1.20.5～  |  	Java21 | sudo apt install -y openjdk-21-jdk  |


今回は1.21.1で遊ぶので以下のコマンドでインストールしました。
```terminal
$ sudo apt install -y openjdk-21-jdk
```

インストールが完了したら、以下のコマンドでできているか確認します。

```terminal
$ java -version
openjdk version "21.0.6" 2025-01-21
OpenJDK Runtime Environment (build 21.0.6+7-Ubuntu-120.04.1)
OpenJDK 64-Bit Server VM (build 21.0.6+7-Ubuntu-120.04.1, mixed mode, sharing)
```

#### 5. NeoForgeのインストール
　Modサーバーを作りたいので、NeoForgeのインストールを行います。他のModローダーの場合は読み替えてください。
 　まず、minecraftディレクトリを作成します。
 ```terminal
 $ mkdir minecraft
```

　次に、自分のPCでNeoForgeのダウンロードページまで行きます。今回は1.21.1なので対応するバージョンを探してダウンロードボタンを右クリック → リンクのアドレスをコピーでダウンロード先のリンクをコピーします。

 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942774/6af3dd02-5dfa-4cec-bf9f-c06a57c7dc1e.png)


 次に、作成したディレクトリに移動してから、以下の通りにGCEのコンソールで入力します。(URLの部分はコピーしたURLに置き換えてください。）
 ```terminal
 $ cd minecraft
 $ sudo wget "<URL>"
 
 // インストール例
 $ sudo wget "https://maven.neoforged.net/releases/net/neoforged/neoforge/21.1.141/neoforge-21.1.141-installer.jar" 
```
これでmkdirディレクトリにダウンロードできたと思います。
lsコマンドで確認してください。
 ```terminal
 $ ls
 neoforge-21.1.141-installer.jar
```
このファイルを解凍します。以下のコマンドで解凍します。
```terminal
// NeoForge
$ java -jar neoforge-21.1.141-installer.jar
```

最後にこのように出力されたら成功です。
```
The server installed successfully
You can delete this installer file now if you wish
```
___
 
 ※Forgeの場合は以下のようにしてください。
 ```terminal
 // Foge
 $ java -jar minecraftforge-installer.jar --installServer
```

※Fabricの場合は以下の通りにしてください。
```
// Fabric
java -jar fabric-installer.jar server
```
___

 ここまでできれば、サーバーを起動できます。lsコマンドで確認すると、run.shが生成されているはずです。
 ```terminal
$ ls
libraries                        neoforge-21.1.141-installer.jar.log  run.sh      user_jvm_args.txt
neoforge-21.1.141-installer.jar  run.bat                              server.jar
```
___

生成されていることが確認出来たら、run.shを編集していきます。
以下のコマンドで編集を行い、完了したらCtl + Sで保存し、Ctl + Xでnanoを抜けます。

```terminal
$ nano user_jvm_args.txt
```

___
使用するメモリ量を指定する以下の二行を一番下に追加します。

```
-Xmx5G -Xms512M
```
※メモリの数値に関してはマシンのスペックに合わせて変えてください。マシンのRAMギリギリまで使用すると逆にサーバーの動作が重くなってしまいます。少なすぎず、多すぎずのリソースを確保してください。
___
これができたら、Ctl + Xでnanoを抜けて、run.shを起動します。最初は起動に失敗します。
```terminal
$ ./run.sh
```
ここで、run.shとuser_jvm_args.txtの二つに権限を付与します。
```terminal
chmod +x run.sh
chmod +x user_jvm_args.txt
```
___
次に、以下のコマンドでeula.txtを編集します。

```terminal
$ nano eula.txt
```
falseになっている部分をtrueに変更します。
```
eula=true
```

保存したらnanoを抜けて、run.shを起動します。
```terminal
$ ./run.sh
```
最後の行に以下が出力されれば起動成功です。
```
[Server thread/INFO] [ne.ne.ne.se.pe.PermissionAPI/]: Successfully initialized permission handler neoforge:default_handler
```
___


　ここでサーバーに入れるかどうかの確認を行います。ブラウザのGCEのインスタンス一覧の画面で、〇の部分にIPアドレスが表示されているはずです。
　ここのIPアドレスをコピーし、MinecraftのIPアドレス欄に打ち込んでください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942774/d7d96490-fc97-4815-8b22-8b2bd8ae2345.png)

例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942774/3f6e03e3-52fe-48f1-b018-641fd03cba01.png)

　これでワールドに入れたら成功です。確認出来たらGCEコンソールでstopと打ち込んで終了します。
___

　最後に、このMinecraftサーバーのコンソールをスクリーンを利用してデタッチします。（裏画面で起動）
　まずはScreenをインストールします。
 
```terminal
 $ sudo apt install screen
```
次に、screenを利用してサーバーを建てるシェルスクリプトを作成します。

```terminal
$ nano start_with_screen.sh
```

中身は以下のとおりです。
**YOURUSERNAMEは変更してください。**
```
#!/bin/bash
screen -dmS minecraft /home/<YOURUSERNAME>/minecraft/run.sh
```

ここまで出来たらMinecraftサーバーの起動部分はおおむね完成です。

___

#### 6. Pythonのインストール
※Minecraftサーバーはstopと打ち込むと停止します。

　次に、インスタンス自動停止のためのPythonのインストールを行います。最新バージョンがインストールされます。

 ```terminal
 $ sudo apt-get install -y python3
```

インストールが完了したら、確認を行います。このように出力されれば成功です。
```terminal
$ python3 --version
Python 3.9.20
```

___

#### 7. サーバー自動停止スクリプトの作成
　Pythonのインストールができたら、次はワールド内のプレイヤー数を数えて0人の場合は自動でインスタンスを停止させるPythonスクリプトを作成します。@ekkoさんの[記事](https://qiita.com/ekko/items/851fb0cc9eddb9680071)を参考にさせていただきました。
 


```terminal
$ nano ~/minecraft/shutdown.py
```

　ワールド内にいる人数をログファイルから認識し、0人の場合は自動でサーバーをシャットダウンするスクリプトです。
**7行目の<YOURUSERNAME>はGCEアカウントのユーザー名（ユーザーID）に変更してください。**
その他ディレクトリも変更している場合は修正してください。

```python
import os
import sys
import datetime
import subprocess

# GCEアカウントのユーザー名
username = "<YOURUSERNAME>"

# 今日の日付のログファイルのパスを指定
today = datetime.date.today()
file_path = "/home/{username}/minecraft/logs/latest.log"

# ログファイルが存在しているかを確認
# ファイルが存在しない場合にはプログラムを終了
d = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
is_file = os.path.isfile(file_path)
if is_file:
    pass
else:
    text = "{} No log found".format(d)
    print(text)
    sys.exit()

# 「joined」と「left」のカウント用変数（初期化）
join_cnt = 0
left_cnt = 0

# ログファイルを開き「joined」と「left」の数をそれぞれカウント
with open(file_path) as log_file:
    for line in log_file:
        if "joined" in line:
            join_cnt += 1
        elif "left" in line:
            left_cnt += 1

# 「joined」と「left」が同数の場合には停止スクリプトを実行し1分後にインスタンスを停止
# 同数でない場合には「誰かがログイン中」と表示
if join_cnt == left_cnt:
    subprocess.call(["python3", "/home/{username}/minecraft/notify_shutdown.py"])
    subprocess.call(["bash", "-c", "/home/{username}/minecraft/stop.sh"])
    subprocess.call(["sudo", "shutdown", "-h", "+1"])
else:
    print("someone is online")
```

___

#### 8. ワールドデータバックアップスクリプトの作成
　サーバー運用するにあたり、**バックアップは必須**と言っても過言ではありません。
 （過去にバックアップなしで運用して、なぜかワールドが上書き生成され、数十時間を水の泡にした経験あり）
　
　ということで、ディレクトリとシェルスクリプトを作成します。バックアップは1時間に１回、24時間分のバックアップを作成します。
　このシェルスクリプトは、最終更新日が**24時間前のファイルを全て削除**します。サーバーの起動に日が空く場合は**バックアップの方法を変更**してください。（例：バックアップファイルは最新の2つを保持する等）


 まずはディレクトリの作成を行います。
 ```terminal
 $ mkdir ~/minecraft/backups
 $ nano ~/minecraft/backup.sh
```

シェルスクリプトの作成
**YOURUSERNAMEは適宜変更してください。**
```sh
#!/bin/bash

# === 設定 ===
WORLD_DIR="/home//minecraft/world"
BACKUP_DIR="/home/<YOURUSERNAME>/minecraft/backups"
TIMESTAMP=$(date +"%Y%m%d_%H%M")
BACKUP_FILE="$BACKUP_DIR/world_$TIMESTAMP.tar.gz"

# === バックアップ先がなければ作成 ===
mkdir -p "$BACKUP_DIR"

# === バックアップ作成 ===
tar -czf "$BACKUP_FILE" -C "$WORLD_DIR" .

# === 24時間以上前のバックアップを削除 ===
find "$BACKUP_DIR" -type f -name "world_*.tar.gz" -mmin +1440 -delete
```

権限付与も忘れずに。
```terminal
$ chmod +x backup.sh
```

___

#### 9. ７, ８のcrontabへの登録
　crontabを使うことで定期的にスクリプトを動かすことができます。バックアップとシャットダウンのスクリプトをcrontabを使ってそれぞれ1時間毎と10分毎に行うようにします。

crontabの編集を行います。crontabの初回起動時は、エディタをどれに選択するかを問われます。なんでもいいですがnanoが一番使いやすいので<1>を入力してnanoを使います。

crontabを開きます。
 ```terminal
 $ crontab -e
```

crontabが開いたら、以下の二行を追加します。時間の設定方法については省略します。
```
*/10 * * * *    python3 /home/<YOURUSERNAME>/minecraft/shutdown.py
0 * * * * /home/<YOURUSERNAME>/minecraft/backup.sh >> /home/<YOURUSERNAME>/minecraft/backup.log   
```

 これでcrontabへの登録は完了です。

 ___

#### 10. サーバー停止時のワールド保存スクリプトの作成
　サーバーを閉じる際に、セーブせずにインスタンスをシャットダウンするとワールドデータが壊れる可能性があるので、ワールドデータをセーブするシェルスクリプトを作成します。
　まずはファイルを作成します。
 
```terminal
$ nano ~/minecraft/stop.sh
```

　次に、以下の内容で書き込みます。
 
```sh
#!/bin/bash

# Minecraftサーバーの screen セッション名
SESSION="minecraft"

echo "セーブ＆停止処理を開始します..."

# save-all コマンド送信
screen -S "$SESSION" -p 0 -X stuff "save-all$(printf \\r)"
echo "ワールドを保存中..."

# セーブ完了待ち（1〜2秒）
sleep 2

# stop コマンド送信
screen -S "$SESSION" -p 0 -X stuff "stop$(printf \\r)"
echo "サーバーを安全に停止しました。"
```

これで自動シャットダウン時にMinecraftサーバーのワールドデータが自動で保存されるようになります。

___

#### 11. サーバー自動起動のスクリプトの作成
　これでGCEサーバー側は最後です。（執筆がんばれ俺）
　特定のタイミングでスクリプトを実行できるsystemdを使用します。
 
　まずはsystemdのディレクトリまで移動します。
 
```terminal
 $ cd /etc/systemd/system
```

　次にサービスファイルを作成します。
 
```terminal
 $ nano auto_shell.service
```

　以下の内容で書き込みます。必ずパスはフルパスを利用してください。でないとシステムに怒られます。毎度のとおり**YOURUSERNAMEは適宜変更**してください。

```
[Unit]
Description=minecraft service
After=network.target

[Service]
User=<YOURUSERNAME>
ExecStart=/home/<YOURUSERNAME>/minecraft/start_with_screen.sh
ExecStop=/usr/bin/screen -S minecraft -X quit
WorkingDirectory=/home/<YOURUSERNAME>/minecraft
Restart=always
Type=forking
[Install]
WantedBy=multi-user.target
```

___

これができたら、登録と起動を行います。
**Minecraftサーバーが起動していない状態で行ってください。** systemctl startの際に25565ポートが使用済みでサーバーが起動できず、エラー状態になります。
```terminal
$ sudo systemctl daemon-reload
$ sudo systemctl enable auto_shell.service
$ sudo systemctl start auto_shell.service
```

完了したら、auto_shell.serviceの確認を行います。
このような出力になれば正常に動作しています。
```
sudo systemctl status auto_shell
● auto_shell.service - minecraft service
     Loaded: loaded (/etc/systemd/system/auto_shell.service; enabled; vendor preset: enabled)
     Active: active (running) since Mon 2025-03-31 10:26:21 UTC; 1h 18min ago
    Process: 522 ExecStart=/home/<YOURUSERNAME>/minecraft/start_with_screen.sh (code=exited, status=0/SUCCESS)
   Main PID: 616 (screen)
      Tasks: 46 (limit: 9510)
     Memory: 1.1G
     CGroup: /system.slice/auto_shell.service
             ├─616 SCREEN -dmS minecraft /home/<YOURUSERNAME>/minecraft/run.sh
             ├─624 bash /home/<YOURUSERNAME>/minecraft/run.sh
             └─625 java @user_jvm_args.txt @libraries/net/neoforged/neoforge/21.1.140/unix_args.txt

Mar 31 10:26:20 minecraft-server systemd[1]: Starting minecraft service...
Mar 31 10:26:21 minecraft-server systemd[1]: Started minecraft service.
```

___

#### 12. 作成したファイルへの権限付与

最後に権限を付与します。

```terminal
$ chmod +x /etc/systemd/system/auto_shell.service
$ chmod +x ~/minecraft/stop.sh
$ chmod +x ~/minecraft/backup.sh
$ chmod +x ~/minecraft/shutdown.py
$ chmod +x  ~/minecraft/start_with_screen.sh
$ chmod +x ~/minecraft/run.sh
$ chmod +x ~/minecraft/user_jvm_args.txt
```


これでGCEサーバー側での作業は(※)**1つを除いて**完了（notify_shutdown.pyが未作成）です。次にDiscord Botを運用するEC2側の作業に進みます。（涙目）

※Discord Botの作成によるトークンと通知先のチャンネルIDが必要なのでGCEサーバーは未完成ですが一度先に進みます。

___

### Discord Botサーバーの構築

Discord のアカウント登録、EC2のアカウント登録については省略します。させてください。

#### 1. Discord Botの作成
　[Discord Dev Portal]にアクセスし、左側の**Applications** → 右側の**New Application**を選択。
 名前を入力して、チェックボックスにチェックを入れてCreate
 
 
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942774/4426ba68-7710-4454-8a00-ac9bab6e6ead.png)
 
　作成できたら、左側のナビゲーションバーから**Bot**をクリック。**Add Bot**をクリックします。続けて**Yes, do it!**をクリックし、BOTを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942774/a8ee965d-24b8-492b-a514-1d412d449032.png)

　次に、その下の**Reset Token**ボタンをクリックし、トークンを取得します。
**このページを閉じるとトークンは二度と表示されないので注意！！**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942774/99fc6577-b921-42ac-88dd-463fb9d8ccff.png)

:::note alert
警告
このトークンは第三者に流出しないように気を付けてください。このトークンがあるとBOTが乗っ取られます。
:::

次に、少し下の**Message Content Intent**のチェックをONにしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942774/bcb9b74f-51d8-427a-97b9-197b9535cd1f.png)

___

そして、左のタブからOAuth2を選択し、botにチェックマークを入れ、下に出てきたSend Messagesにチェックを入れ、その他必要な権限があれば付与させてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942774/a6b42969-9f80-4dc7-8966-185079e2883f.png)

画面下部のURLをコピーしてブラウザで開き、管理者権限のあるサーバーに招待してください。
ここまで出来たらBot本体の作成は完了です。

___
 
#### 2. EC2インスタンスの作成
　AmazonWebServiceのEC2インスタンスを利用して、DiscordBotを運用します。
 [アカウント登録](https://signin.aws.amazon.com/signup?request_type=register)は自力で頑張ってください。

 https://signin.aws.amazon.com/signup?request_type=register
 

 　アカウント登録ができたら、EC2インスタンスを作っていきます。[ここ](https://ap-northeast-1.console.aws.amazon.com/ec2/home?region=ap-northeast-1)からインスタンスの作成を行います。

  https://ap-northeast-1.console.aws.amazon.com/ec2/home?region=ap-northeast-1

右上のパシフィックが「アジアパシフィック(東京)」になっていることを確認し、**左側のメニュー** → **インスタンス**をクリック。
右の黄色いボタンの**インスタンスを起動**をクリック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942774/07ef6625-cb59-4bc0-bc31-53ec66255b91.png)

名前をdiscord_botとし、キーペアを同様の名前で作成・ダウンロードする。
他はデフォルトでOK。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942774/023c5d56-c500-4484-874a-f5e1a4b8bda7.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942774/44328326-ce18-4bb9-93fd-2e701b8623be.png)

そしたら右の**インスタンスを起動**のボタンを押して、起動が完了したら**インスタンスに接続**　→ **接続**で接続する。

___

#### 3. Pythonのインストール
　**ここからEC2のターミナルでの作業に移行します**

　こちらも同様にPythonのインストールをしていきます。まずはパッケージのアップデートから。
 
```terminal
$ sudo yum update -y
```

まぁ多分この出力になると思うけど、、、

```terminal
$ sudo yum update -y
Amazon Linux 2023 Kernel Livepatch repository                                                                                                                                       99 kB/s |  15 kB     00:00    
Dependencies resolved.
Nothing to do.
Complete!
```
___
python3をインストール
```terminal
$ sudo yum install python3 -y
```

どうやら標準でインストールされてたらしいです。

```terminal
$ sudo yum install python3 -y
Last metadata expiration check: 0:02:33 ago on Mon Mar 31 12:00:18 2025.
Package python3-3.9.21-1.amzn2023.0.2.x86_64 is already installed.
Dependencies resolved.
Nothing to do.
Complete!
```

ちゃんと入ってた。
```terminal
$ python3 --version
Python 3.9.21
```

___

#### 4. gcloud CLIのインストール
gcloudは、GoogleCloudをCLIで操作できるツールです。ファイル操作に必要です。
まず、依存関係のあるファイルをインストールします。
```terminal
$ sudo yum install -y curl python3 unzip
```


ホームディレクトリにダウンロードして、展開します。
```terminal
$ curl -O https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-linux-x86_64.tar.gz
$ tar -xf google-cloud-cli-linux-x86_64.tar.gz
```

gcloud CLIをインストールします。気にせずEnter連打。
```terminal
$ ./google-cloud-sdk/install.sh
```

___

次に、パスを通します。
```terminal
$ source ~/google-cloud-sdk/path.bash.inc
```

バージョンの確認
以下のように出力されたら成功です。
```terminal
$ gcloud version

Google Cloud SDK 516.0.0
bq 2.1.14
bundled-python3-unix 3.12.8
core 2025.03.24
gcloud-crc32c 1.0.0
gsutil 5.33
```

___

初期設定を行います。


```terminal
$ gcloud init
Welcome! This command will take you through the configuration of gcloud.

                                    (中略）

You must sign in to continue. Would you like to sign in (Y/n)?  y

Go to the following link in your browser, and complete the sign-in prompts:

    https://accounts.google.com/o/oauth2/auth?...

Once finished, enter the verification code provided in your browser: 
```

　出力されるURLに飛んで、許可しまくったら表示されるコードをコピーして、ターミナルに貼り付ける。（この時のアカウントはGCEサーバーを作成したアカウントでログインしてください）
 
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942774/3d7618f3-9ad5-45c4-8b46-24bfab876b19.png)
　コードをターミナルに入力し、正しく認証されると次のような出力が得られます。
 ```
 You are signed in as: [mailaddress].

Pick cloud project to use: 
 [1] active-dahlia-455113-n9
 [2] Enter a project ID
 [3] Create a new project
Please enter numeric choice or text value (must exactly match list item):  1
```

どのプロジェクトを選択するか聞かれるので、「1」を入力してEnter。すると1行目の出力が出るので、yを押して続行します。
GCEサーバーを作成するときに、選択した場所を選んでください。asia-northeast1-bで作成したので、「32」を入力します。
```
Do you want to configure a default Compute Region and Zone? (Y/n)?  y

Which Google Compute Engine zone would you like to use as project default?
If you do not specify a zone via a command line flag while working with Compute Engine resources, the default is assumed.
 [1] us-east1-b
 [2] us-east1-c
 [3] us-east1-d
 [4] us-east4-c
 [5] us-east4-b
    　（中略）
 [26] asia-east1-b
 [27] asia-east1-a
 [28] asia-east1-c
 [29] asia-southeast1-b
 [30] asia-southeast1-a
 [31] asia-southeast1-c
 [32] asia-northeast1-b
 [33] asia-northeast1-c
 [34] asia-northeast1-a
 [35] asia-south1-c
      （中略）
 [48] asia-east2-b
 [49] asia-east2-c
 [50] asia-northeast2-a
Did not print [78] options.
Too many options [128]. Enter "list" at prompt to print choices fully.
Please enter numeric choice or text value (must exactly match list item):  32
```
これでgcloud CLIの初期設定は完了です。

___

#### 5. Pythonスクリプトの作成
　EC2でもPythonスクリプトを作成していきます。こちらのスクリプトは、Discordのメッセージを受信してメッセージの内容に応じてGCEインスタンスを操作するスクリプトです。

まず、必要なパッケージをインストールしていきます。

pip3をインストール（多分しなくてもよい）
```terminal
$ python3 -m ensurepip --upgrade
$ pip3 --version
```
___
discord.pyのインストール
```terminal
$ pip3 install -U discord.py
```

___

準備が整ったので、Pythonスクリプトを作成します。
```terminal
$ nano  discord_bot.py
```

```python
import discord
import subprocess
import os

GCLOUD = "/home/ec2-user/google-cloud-sdk/bin/gcloud"
TOKEN = "<BOTACCESSTOKEN>" 
INSTANCE = "minecraft-server"
ZONE = "asia-northeast1-b"
GCE_USER = "<YOURUSERNAME>"  # GCEのユーザー名（例：ec2-user）
MOD_REMOTE_DIR = "/home/{GCE_USER}/minecraft/mods"
LOCAL_TMP_DIR = "/tmp/mc_mods"

# 初期化
os.makedirs(LOCAL_TMP_DIR, exist_ok=True)
intents = discord.Intents.default()
intents.message_content = True
client = discord.Client(intents=intents)

# ===== 共通関数 =====

def run_gcloud_command(command_args):
    try:
        result = subprocess.run(
            command_args,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True
        )
        if result.returncode != 0:
            return None, result.stderr.strip()
        return result.stdout.strip(), None
    except Exception as e:
        return None, str(e)

def get_ip():
    return run_gcloud_command([
        GCLOUD, "compute", "instances", "describe", INSTANCE,
        "--zone", ZONE,
        "--format=get(networkInterfaces[0].accessConfigs[0].natIP)"
    ])

def get_status():
    return run_gcloud_command([
        GCLOUD, "compute", "instances", "describe", INSTANCE,
        "--zone", ZONE,
        "--format=get(status)"
    ])

def start_instance():
    return run_gcloud_command([
        GCLOUD, "compute", "instances", "start", INSTANCE,
        "--zone", ZONE
    ])

def stop_instance():
    return run_gcloud_command([
        GCLOUD, "compute", "instances", "stop", INSTANCE,
        "--zone", ZONE
    ])

def scp_to_gce(local_path, remote_name):
    remote_path = f"{GCE_USER}@{INSTANCE}:{MOD_REMOTE_DIR}/{remote_name}"
    return run_gcloud_command([
        GCLOUD, "compute", "scp", local_path, remote_path,
        "--zone", ZONE
    ])

def ssh_to_gce(command):
    return run_gcloud_command([
        GCLOUD, "compute", "ssh", f"{GCE_USER}@{INSTANCE}",
        "--zone", ZONE,
        "--command", command
    ])

# ===== Discord Bot イベント =====

@client.event
async def on_ready():
    print(f"✅ Bot 起動完了: {client.user}")

@client.event
async def on_message(message):
    if message.author.bot:
        return

    content = message.content.lower()

    if content == "!ip":
        await message.channel.send("📡 IPアドレスを取得中...")
        ip, err = get_ip()
        if err:
            await message.channel.send(f"❌ 取得失敗:\n```\n{err}\n```")
        elif ip:
            await message.channel.send(f"🌐 IPアドレス: `{ip}`")
        else:
            await message.channel.send("⚠️ IPが取得できません（インスタンスが停止中かも）")

    elif content == "!status":
        status, err = get_status()
        if err:
            await message.channel.send(f"❌ ステータス取得失敗:\n```\n{err}\n```")
        else:
            await message.channel.send(f"🖥️ サーバーの状態: **{status}**")

    elif content == "!start":
        await message.channel.send("🚀 サーバーを起動中です...")
        _, err = start_instance()
        if err:
            await message.channel.send(f"❌ 起動失敗:\n```\n{err}\n```")
        else:
            await message.channel.send("✅ 起動完了！IPを取得します...")
            ip, err = get_ip()
            if ip:
                await message.channel.send(f"🌐 IPアドレス: `{ip}`\n🕒 ゲームサーバーの起動まで約20秒かかります。")
            else:
                await message.channel.send("⚠️ IPがまだ割り当てられていません。少し待ってから再度 `!ip` を試してください。")

    elif content == "!stop":
        await message.channel.send("🛑 サーバーを停止中です...")
        _, err = stop_instance()
        if err:
            await message.channel.send(f"❌ 停止失敗:\n```\n{err}\n```")
        else:
            await message.channel.send("✅ サーバーを停止しました。")

    elif content.startswith("!addmod"):
        if not message.attachments:
            await message.channel.send("❗ 添付ファイルが見つかりません。modファイルを添付してください。")
            return

        for attachment in message.attachments:
            filename = attachment.filename
            local_path = os.path.join(LOCAL_TMP_DIR, filename)
            try:
                await attachment.save(local_path)
                await message.channel.send(f"📥 `{filename}` を一時保存しました。GCEに転送中...")

                _, err = scp_to_gce(local_path, filename)
                if err:
                    await message.channel.send(f"❌ 転送失敗:\n```\n{err}\n```")
                else:
                    await message.channel.send(f"✅ `{filename}` を GCE に転送しました！")
            except Exception as e:
                await message.channel.send(f"❌ 保存・転送エラー:\n```\n{str(e)}\n```")

    elif content.startswith("!lsmod"):
        cmd = f"ls {MOD_REMOTE_DIR}"
        output, err = ssh_to_gce(cmd)
        if err:
            await message.channel.send(f"❌ mod一覧取得失敗:\n```\n{err}\n```")
        elif output.strip() == "":
            await message.channel.send("📁 modディレクトリは空です。")
        else:
            await message.channel.send(f"📄 mod一覧:\n```\n{output}\n```")

    elif content.startswith("!rmmod"):
        parts = message.content.split(maxsplit=1)
        if len(parts) != 2:
            await message.channel.send("⚠️ 使用法: `!rmmod <ファイル名>`")
            return

        filename = parts[1]
        cmd = f"rm {MOD_REMOTE_DIR}/{filename}"
        _, err = ssh_to_gce(cmd)
        if err:
            await message.channel.send(f"❌ 削除失敗:\n```\n{err}\n```")
        else:
            await message.channel.send(f"🗑️ `{filename}` を削除しました。")

client.run(TOKEN)

```

このスクリプトは以下のコマンドに対応します。
 ・!start：GCEサーバーの起動, IPアドレスの出力
 ・!stop：GCEサーバーの停止
 ・!ip：IPアドレスの出力
 ・!addmod（ファイル添付と共に送信）：modの追加
 ・!lsmod：modsファイルの中身を出力
 ・!rmmod [filename]：指定したファイル名のファイルをmodsディレクトリ内から削除

 ___

 systemdにサービスファイルを追加し、上記スクリプトをインスタンスの起動時に自動実行するように設定していきます。
 スクリプトファイルの作成
 ```terminal
 $ nano /etc/systemd/system/discord-bot.service
```

下記の内容を追加

```
[Unit]
Description=Discord Minecraft Control Bot
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/python3 /home/ec2-user/discord_bot.py
WorkingDirectory=/home/ec2-user
User=ec2-user
Restart=always
RestartSec=10
Environment=PYTHONUNBUFFERED=1

[Install]
WantedBy=multi-user.target
```

次に、追加したサービスファイルを登録します。
```terminal
$ sudo systemctl daemon-reload
$ sudo systemctl enable discord-bot.service
$ sudo systemctl start discord-bot.service
```

正常に作動していることを確認します。

```terminal
$ sudo systemctl status discord-bot.service
● discord-bot.service - Discord Minecraft Control Bot
     Loaded: loaded (/etc/systemd/system/discord-bot.service; enabled; preset: disabled)
     Active: active (running) since Mon 2025-03-31 05:57:47 UTC; 10h ago
   Main PID: 127050 (python3)
      Tasks: 3 (limit: 1111)
     Memory: 27.6M
        CPU: 27.978s
     CGroup: /system.slice/discord-bot.service
             └─127050 /usr/bin/python3 /home/ec2-user/discord_bot.py
```

___
#### 6. サーバー自動停止時のメッセージ送信スクリプトの設定
　**GCEサーバーに戻って**作業を行います。
　ワールドに誰もいなかった場合に自動停止したときのメッセージ送信をするスクリプトを作成します。

　先ほどと同様にpip3をインストール
```terminal
$ python3 -m ensurepip --upgrade
$ pip3 --version
```

discord.pyのインストール
```terminal
$ pip3 install -U discord.py
```
スクリプトファイルの作成

```terminal
 $ nano ~/minecraft/discord-notify.py
```

チャンネルIDは、botがメッセージを送るテキストチャンネルです。
テキストチャンネルを右クリックすると一番下にチャンネルIDをコピーとあるのでそれをクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942774/a7d09dbd-79ff-46c2-8a7d-1d791ead8a1a.png)

コピーできたら、以下の内容を記述します。
**アクセストークンとチャンネルIDを変更してください**
```python
import discord
import asyncio

# Botトークン & チャンネルID
TOKEN = "<BOTACCESSTOKEN>"
CHANNEL_ID = <TEXTCHANNELID>

async def notify_shutdown():
    intents = discord.Intents.default()
    client = discord.Client(intents=intents)

    @client.event
    async def on_ready():
        channel = client.get_channel(CHANNEL_ID)
        if channel:
            await channel.send("🛑 サーバーに誰もいなかったため、自動シャットダウンします。")
        await client.close()

    await client.start(TOKEN)

if __name__ == "__main__":
    asyncio.run(notify_shutdown())
```

___

#### 7. SSH接続の設定
　**EC2に戻ります**
 
　まず、EC2でSSH鍵ペアを作成します。YOURUSERNAMEには、GCEサーバーのユーザーIDを指定してください。

```terminal
$ ssh-keygen -t rsa -f ~/.ssh/gce_key -C <YOURUSERNAME>
```
___
次に、以下によって得られる出力を間違わないようにコピーします。
```terminal
$ cat ~/.ssh/gce_key.pub
```
___
　gcloud CLIのssh機能を用いて接続します。 例によって、YOURUSERNAMEは適宜変更してください。尚、 YOURGCEINSTANCENAMEはGCEサーバーのインスタンス名を入力してください。
　さらに、<ZONE>には東京リージョンならasia-northeast1-a, asia-northeast1-b, asia-northeast1-cのうち適切なものを、大阪リージョンならasia-northeast2-a, asia-northeast2-b, asia-northeast2-c, のうち適切なものを選択してください。
 
```terminal
// テンプレート
$ gcloud compute ssh <YOURUSERNAME>@<YOURGCEINSTANCENAME> --zone <ZONE>

// 実際に使用したコマンド
$ gcloud compute ssh <MYUSERNAME>@minecraft-server --zone asia-northeast1-b
```

___

最後に、権限を付与しておきます。

```terminal
$ chmod +x discord-notify.py
$ chmod +x discord-bot.service
```

これで、EC2インスタンスを再起動させるとdiscord_bot.pyとnotify_discord.pyが自動で起動し、DiscordサーバーでBOTがオンラインになるはずです。


## 動作確認
1. EC2サーバーを再起動して少し待つとDiscord Botがサーバー上でオンラインになります
1. !startコマンドを実行し、少し待ってからメッセージで出たIPアドレスにMinecraftで接続します
1. !ipコマンドでも同様のIPアドレスが出力されることを確認します
1. !addmodコマンドでファイルを送信することができることを確認します
1. !lsmodコマンドでmodsディレクトリの中身のリストが出力されることを確認します
1. !rmmodコマンドでmodsディレクトリ内の指定したファイルが削除されていることを確認します
1. ワールドにプレイヤーがいる場合といない場合で10分毎の動作の確認を行います
    1. プレイヤーがいない場合
    　コンソールにプレイヤーがいない旨の出力と自動でシャットダウンがされることを確認します
    1. プレイヤーがいる場合
    　勝手にサーバーが停止しないことを確認します
1. 1時間毎にバックアップが取られていることを確認します
　ディレクトリはGCEサーバー内で ~/minecraft/backups 内に生成されます
1. !stopコマンドでサーバーが停止することを確認します

これらすべての動作が確認できれば成功です！！**お疲れさまでした！！！！**

# 感想
　もっと簡潔に書けたと思うなぁ。あと絶対どこか間違えてる。w