disocrdBOTから、EC2上に建てたマインクラフトサーバーの「起動・停止」をできるようにした話

皆さんこんにちは、takazonessです。
今回は、discordのBOTからマインクラフトサーバーの「起動・停止」をできるようにしました。
正直、構築したのが結構前で、写真等もないので今回の記事はかなりわかりづらいと思います。申し訳ないです。

https://qiita.com/nahiro_tus/items/6b90699ab5aba8dbbede
↑こちらの記事を参考にしました。

上から順番に実行してみてください！

# 用意するもの
・AWSのアカウント
・2つのインスタンス(Ubuntu)
→マインクラフトサーバーの起動用・マインクラフトサーバー本体(Java)

・インスタンス(鯖)間での、SSHのノンパス設定を完了させる！
→https://dev.classmethod.jp/articles/jw-attempt-to-connect-ssh-without-a-password-between-ec2-instances/
↑この記事見ればできると思います！

・discordBOTの作成！
→https://discordpy.readthedocs.io/ja/latest/discord.html
↑この記事見ればできると思います！

・マインクラフトサーバー本体に、jarファイルのインストール、構築
(今回は触れません、おそらく記事はたくさんあるのでそちらを参照していただければと思います。)

***


# スペック

マイクラ鯖起動用
インスタンスタイプ : t2.micro(動けばなんでもヨシ！)
OS : Ubuntu 22.04

マイクラ鯖本体
インスタンスタイプ : t3a.medium(動けばなんでもヨシ！)
OS : Ubuntu 22.04
*********************************

# Python3のインストール
```sh
sudo apt update
```
```shell
sudo apt install -y python3-pip
```
これでPython3をインストール！
# paramikoのインストール
```sh
pip install paramiko
```
# AWS CLIのインストール
```:Linux x86 (64-bit)の場合
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
```
```sh:Linux ARMの場合
curl "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o "awscliv2.zip"
```
```sh
unzip awscliv2.zip
```
```sh
sudo ./aws/install
```
ここまでできて、大丈夫そうでしたら、`version`を確認しておきましょう！
（しっかりインストールできてるかの確認）
```sh
aws --version
```

# マイクラ鯖起動用に配置するファイル、BOTの中身！(Pythonファイル)
適当に
```sh
mkdir minecraft-bot
```
とかで、ディレクトリを作ってもらってその中に下記のPythonファイルを配置してください！
ここで、`discordBOTのトークン`、`マイクラサーバー本体のインスタンスID`、`作成したリージョン`を指定してください。
```py:discordbot.py
import discord
import subprocess
import paramiko
import time

# 自分のBotのアクセストークンに置き換えてください
TOKEN = ''
# game-serverのインスタンスidを指定してください
INSTANCEID = ''
# regionを指定
REGION = ""

# 接続に必要なオブジェクトを生成
#client = discord.Client()
intents = discord.Intents.default()
intents.message_content = True

client = discord.Client(intents=intents)
# ***************************
# ***    処理関数
# ***************************


class DiscordBOT:
    def __init__(self, client):
        self.SSHClient = None

    async def main(self, discord_event):
        get_text = discord_event.content
        send_text = ""

        if "$start minecraft" in get_text:
            await discord_event.channel.send("サーバーを起動します。(起動には1分ほど時間がかかります。)")
            # インスタンスの起動
            subprocess.call(
                "aws ec2 start-instances --instance-ids {} --region {}".format(INSTANCEID, REGION), shell=True)
            time.sleep(1)

            # インスタンスが起動するまで待機
            subprocess.call(
                "aws ec2 wait instance-running --instance-ids {}  --region {}".format(INSTANCEID, REGION), shell=True)
            time.sleep(1)

            # インスタンスのIPアドレスを取得
            proc = subprocess.run(["aws ec2 describe-instances --instance-ids {} --region {} --query 'Reservations[*].Instances[*].PublicIpAddress' --output text".format(
                INSTANCEID, REGION)], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
            ip_add = proc.stdout.decode("utf-8")
            ip_add = ip_add.replace("\n", "")
            self.server_ip = ip_add
            print(f"server_ip=", self.server_ip)

            time.sleep(15)

            # SSH接続クライアント作成
            self.SSHClient = paramiko.SSHClient()
            self.SSHClient.set_missing_host_key_policy(
                paramiko.WarningPolicy())
            self.SSHClient.connect(
                self.server_ip, username="ubuntu", timeout=1.0)
            time.sleep(2)

            # SSHでminecraftサーバー起動
            stdin, stdout, stderr = self.SSHClient.exec_command(
                "cd /home/ubuntu/minecraft ; source startminecraft.sh")
            for x in stdout:
                print(x)
            for x in stderr:
                print("error:", x)
            time.sleep(10)

            # 接続用のIPアドレスをdiscordに送信
            send_text = "インスタンスの起動とminecraftサーバーへの接続に成功しました。\n サーバー起動後は {} で接続できます".format(
                ip_add.replace("-", "."))

        elif "$stop minecraft" in get_text:
            await discord_event.channel.send("サーバーを停止します。（この処理は1分ほど時間がかかります。）")
            # サーバーの停止
            self.SSHClient.connect(
                self.server_ip, username="ubuntu", timeout=1.0)
            time.sleep(2)

            stdin, stdout, stderr = self.SSHClient.exec_command(
                "cd /home/ubuntu/minecraft ; source stopminecraft.sh")
            self.SSHClient.close()
            time.sleep(60)

            # インスタンスの停止
            subprocess.call(
                "aws ec2 stop-instances --instance-ids {} --region {}".format(INSTANCEID, REGION), shell=True)
            send_text = "サーバーの停止が完了しました。"

        if send_text:
            await discord_event.channel.send(send_text)


discordbot = DiscordBOT(client)


@client.event
async def on_ready():
    print('ログインしました')

# on get message


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

    await discordbot.main(message)

# Botの起動とDiscordサーバーへの接続
client.run(TOKEN)
```

↑こちらを起動用のインスタンスに配置してください。

`screen`コマンドなどを使用して、常時起動させておきましょう！
https://qiita.com/hnishi/items/3190f2901f88e2594a5f
↑こちらの記事が参考になると思います。

# マイクラ鯖本体に配置する、「起動・停止」のシェルスクリプトファイル
おそらく、本体のインスタンスには、`minecraft`専用のディレクトリがあると思いますので、
そちらに配置してください！
```sh:startminecraft.sh
#! /bin/bash

# screenの名前
readonly SCREEN_NAME='minecraft'

screen -AmdS $SCREEN_NAME java -Xmx1024M -Xms1024M -Dlog4j2.formatMsgNoLookups=true -jar server.jar nogui pause
```
```sh:stopminecraft.sh
#! /bin/bash

# screen name
readonly SCREEN_NAME='minecraft'

if screen -ls | grep -o ${SCREEN_NAME}; then

    # 停止開始
    echo [date '+%F %T'] 'server stop script start'

    # サーバー内にアナウンス
    screen -S $SCREEN_NAME -X stuff 'say 30秒後にサーバーを停止します\n'
    sleep 30s

    # saveコマンド発行
    screen -S $SCREEN_NAME -X stuff 'save-all\n'
    sleep 5s

    # stopコマンド発行
    screen -S $SCREEN_NAME -X stuff 'stop\n'

    # 停止実行待機
    sleep 30s

else
    echo [date '+%F %T'] 'server is not runnning'
fi
```
# discord上から起動！
discordサーバー上から`$start minecraft`で起動・`$stop minecraft`で停止できるようになります！

# 終わり
多分これでできると思います。権限等`chmod`を使って、適切に変更してください！
わかりづらいところ等多いと思いますが、何卒ご容赦ください。
コメント等、お気軽にお待ちしております！