【discord bot】マイクラサーバーの起動・停止をdiscordからできるようにしてみた

# はじめに
マイクラのサーバーをdiscordから誰でも起動できるようにしたくてdiscord botからEC2・マイクラサーバーの起動・停止を行えるようにしてみました。
EC2は従量課金制なので、起動していればその分だけお金がかかってしまいます。誰も使っていないのに起動しているのは無駄なので、だれでもdiscordから自由に起動と停止ができるようにしてみました。

# この記事の対象者
- Pythonの基本的な構文が理解できている方
- EC2の制御をdiscord botから行ってみたい方

# 構成図
![Minecraft Bot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1586001/431cf935-1403-810f-c3ea-64e581eb4220.png)

- __t2.micro:discord bot用__
- __t2.medium:minecraft server用__

discord bot用サーバーは常時動いており、minecraft用サーバーを必要なときに起動するような構成となっています。

# 使用ライブラリなど
- Python 3.7.10
- paramiko 2.8.1
- discord.py 1.7.3

# 構築手順

1.discord botのトークン発行
2.discord bot用サーバーの構築
3.minecraft用サーバーの構築
4.discord bot用サーバー→minecraft用サーバーのノンパス設定
5.discord bot起動
の流れになっています。

## discordのbotトークン発行
[こちら](https://note.com/bami55/n/ncc3a68652697)を参考にdiscordのbot用トークンを発行します。
発行したトークンは後で使うので控えておきます。

## discord bot用サーバーの構築
discord bot用サーバーの構築を行います。

### サーバーをたてる

EC2でdiscord bot用サーバーを作成します。
以下構成内容
__インスタンス：t2.micro__
__OS:Amazon Linux 2__
※そのほかはデフォルト設定

- 必要なライブラリのインストール
(AmazonLinux2ではPython3があらかじめインストールされているため、インストール不要)

```bash
python3 -m pip install -U discord.py
pip3 install paramiko 
```

### discord bot用のコードを書く

処理内容としては
1.minecraft用サーバーを起動する
2.minecraft用サーバーにSSH接続する
3.minecraftサーバーを起動する
となっています。

```python:discordbot.py
import discord
import subprocess
import paramiko
import time

# 自分のBotのアクセストークンに置き換えてください
TOKEN = '発行したトークンをここに貼り付けてください'
# game-serverのインスタンスidを指定してください
INSTANCEID = '後で作成するminecraft用サーバーのインスタンスidをここに入力してください'
# regionを指定
REGION = "minecraft用サーバーを作ったリージョンを書いてください"

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

# ***************************
# ***    処理関数
# ***************************


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("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="ec2-user", timeout=1.0)
            time.sleep(2)

            # SSHでminecraftサーバー起動
            stdin, stdout, stderr = self.SSHClient.exec_command(
                "cd /home/ec2-user/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="ec2-user", timeout=1.0)
            time.sleep(2)

            stdin, stdout, stderr = self.SSHClient.exec_command(
                "cd /home/ec2-user/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)
```

## minecraft用サーバーの構築

minecraftサーバーを建てた後に、起動と停止を制御するシェルスクリプトを書きます。

### minecraftサーバーを建てる

基本的な構築方法は[こちら](https://qiita.com/nahiro_tus/items/021d095f49cb65404a2c)

### シェルスクリプトを作成する

minecraftサーバーの起動、停止用のスクリプトを作成します。`~/minecraft/`に配置しています。

- 開始時の処理ではssh接続が切れても処理を継続させたい＆追加でコマンドを入力したかったので、`screen`コマンドで

```bash:startminecraft.sh
#! /bin/bash

# screenの名前
readonly SCREEN_NAME='minecraft'

screen -AmdS $SCREEN_NAME java -Xmx1024M -Xms1024M -Dlog4j2.formatMsgNoLookups=true -jar server.jar nogui pause
```

- 終了時は念のため以下のプロセスを踏んでいます。

1.マイクラ内にアナウンス
2.マイクラサーバーを停止
3.停止コマンドを実行したら30秒待機(マイクラサーバーが停止するまで待機)

```bash: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 bot用サーバー→minecraft用サーバーのノンパス設定を行う

開始シェルや終了シェルを実行したいので、ノンパス設定を行います。
書くと記事が長くなってしまうので、[こちら](https://chat-rate.com/it/2087/)を参考にしてください。

## IAMロールの設定
discord bot用サーバーからminecraft用サーバーの起動・停止を制御するために行います。
1.IAMロールを作成する
2.IAMロールには以下の権限を付ける

- 全部のリソースに対して
  - DescribeInstances
  - DescribeInstanceStatus
- ARN指定
  - StartInstances
  - StopInstances

3.IAMロールをdiscord bot用サーバーに付与する

## discord botを起動
ssh接続が切れても処理が継続されるように、以下のコマンドで実行する

```bash
nohup python3 main.py &
```

# 動作確認
minecraftのサーバーが立ち上がることを確認できました。
![スクリーンショット 2021-12-19 164018.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1586001/30a7be0d-739e-1917-5ccd-5a0f1d0ea095.png)

# まとめ
今回はdiscordからminecraftサーバーの起動・停止を制御できるようにしてみました。
今後はこちらのツールをterraformを用いて管理したり、discordのスラッシュコマンド機能を利用したものに移行することなどを行っていきたいと思っています。

# 参考文献
- [DiscordからMinecraftサーバーを立ち上げた話](https://qiita.com/sobassy/items/0e1aedf8b1b9dbf5c262)
- [簡単なDiscord Botの作り方（初心者向け）](https://note.com/bami55/n/ncc3a68652697)
- [パスワードなしでssh接続できるようにする方法](https://qiita.com/shuntaro_tamura/items/048a1d9521bf7579892b)