🌏

EC2に建てたマイクラサーバーをdiscordから起動したり自動シャットダウンしたり

2022/05/26に公開

なんとなく備忘録
サーバー費節約のため使うときだけdiscordから起動の指示を出して、誰もいなければ自動で閉じるようにする。作るものは

  1. インスタンスの起動停止 -> マイクラサーバーの起動停止
  2. discordのチャット -> インスタンスの起動停止
  3. 一定時間サーバーが0人 -> インスタンスを停止

のフローで動くもの

instanceのセットアップとサーバー開設

アカウントはもうあってインスタンスの建て方も分かる前提でマイクラサーバー向けの説明多め

インスタンス


インスタンス作成

インスタンスの建て方やサーバー開設の説明はかなり端折るので分からない場合これとか見て
https://dev.classmethod.jp/articles/minecraft-for-aws_ec2-instance/
OSはAmazon Linux2にした、多分何でもいい。
インスタンスタイプは

  • t2.micro 無理 無料枠あるけどJE1.18だとメモリが1Gじゃ多分足りない
  • t4g.small 最低限 メモリ2G
  • t4g.medium こっちのほうがよさそう メモリ4G
    t4gはArmだけど特にハードに依存する部分はなかったと思う。サーバーはCPU能力がシングルスレッド依存らしいからこれ以上増強するならt4g.largeよりr6g.mediumの方がいいのかな。しらんけど。
    てかt4g.mediumを常時起動すると$31/monthかかるけどLightsailなら同じスペックが$20固定で使えるから1日16時間以上起動してるならLightsailの方が安い

インスタンスの設定

セキュリティグループの設定からインバウンドルールを追加し、カスタムTCPの25565番を0.0.0.0/0から受け入れ
ElasticIPを設定しておくとIPが固定されて便利だけどサーバーを閉じてる間も15円/日のペースで課金される

サーバー開設


viの使い方

iでinsertモード escで戻れる
ZZで保存して終了 :q!で保存せず終了

Javaのインストール

ここはEC2上でやるのでインスタンスにアクセス。
Amazonが作ったJDKのCorrettoを入れる。
https://docs.aws.amazon.com/corretto/latest/corretto-18-ug/generic-linux-install.html
これのInstall Amazon Corretto 18 on RPM-Based Linuxから

$ sudo rpm --import https://yum.corretto.aws/corretto.key 
$ sudo curl -L -o /etc/yum.repos.d/corretto.repo https://yum.corretto.aws/corretto.repo
$ sudo yum install -y java-18-amazon-corretto-devel 

を実行するだけ

サーバーのインストール

ホームディレクトリの下にminecraftフォルダを作ってcd minecraftで/home/ec2-user/minecraftに入ったら
wget "https://launcher.mojang.com/v1/objects/125e5adf40c659fd3bce3e66e67a16bb49ecc1b9/server.jar"
で公式サーバーをダウンロード。軽さを求めるならpaperMCやプラグイン入れるならSpigotのダウンロードURLを代わりに入れればいい。その場合ダウンロード後に

$ mv paper-1.18.2-339.jar server.jar

で名前をserver.jarにしておくと以後コピペが楽
でも非公式サーバーだとログの体裁が違ってこれから作るコードが動かないかもしれないから自己責任で

$ vi start.sh

を実行して

#!/bin/sh
java -Xmx1024M -Xms1024M -jar server.jar nogui

を書いて保存して

$ sh start.sh

で起動後勝手に停止。eula.txtのeula=falseをeula=trueにして再度起動。ここで入れるかチェック。

インスタンスの起動とマイクラサーバー起動の連携


https://ayatec.hatenablog.com/entry/2021/01/21/031122
この記事を参考に

$ sudo vim /etc/systemd/system/minecraft.service

で開いて

[Unit]
Description=Minecraft Server
After=network-online.target

[Service]
WorkingDirectory=/home/ec2-user/minecraft
User=ec2-user

ExecStart=/bin/bash -c '/bin/screen -DmSL minecraft /bin/java -server -Xms1024M -Xmx1024M -jar ./server.jar nogui'

ExecReload=/bin/screen -p 0 -S minecraft -X eval 'stuff "reload"\\015'

ExecStop=/bin/screen -p 0 -S minecraft -X eval 'stuff "say Server Shutdown. Saving map..."\\015'
ExecStop=/bin/screen -p 0 -S minecraft -X eval 'stuff "save-all"\\015'
ExecStop=/bin/screen -p 0 -S minecraft -X eval 'stuff "stop"\\015'
ExecStop=/bin/sleep 10

Restart=on-failure
RestartSec=5s

[Install]
WantedBy=network-online.target

を書いて保存。network-online.targetにインストールしてるのをそのまま使ってしまっているが筆者の用途とは違うかもしれない。あと、以降はメモリの変更はここで-Xms1024M -Xmx1024Mを変えることで行う。start.shはもう使わないから消そう。
書けたら

$ sudo systemctl daemon-reload
$ sudo systemctl enable minecraft

で自動起動を有効に

discordbot


ここではdiscordにチャットでコマンドを打ち込むとEC2のインスタンスを起動停止してくれるdiscordbotを作成し、無料で常時使えるHeroku上にデプロイする。(2022/12/13追記)Heroku有料になっちゃった…

discordbotの作成と招待

https://discordpy.readthedocs.io/ja/latest/discord.html
2.Applecationページに移動してサーバーに招待するまでが困ったので書いておく

OAuth2のScopesのbotにチェックを入れ、権限を設定すると招待URLができるので動かしたいサーバーに入れておく

Bot -> TokenからTokenを生成してメモしておく。AWSのアクセスキーも取得してメモしておく。

discordbot本体のコード

ここは自PCで行う
root権限を与えたアクセスキーが流出するとやばいのでbot用に権限を絞ったIAMユーザーを作って使う。

詳しくはこちらの記事参照↓
https://qiita.com/kimihiro_n/items/f3ce86472152b2676004

discordbot.py
# インストールした discord.py を読み込む
import discord
import boto3
import sys
# 自分のBotのアクセストークンに置き換えてください
TOKEN_DIR="token.txt"
with open(TOKEN_DIR) as f:
    lines = f.read().splitlines()
    TOKEN = lines[0]
    SERVER_CHANNEL = int(lines[1])
    AWSAccessKeyId = lines[2]
    AWSSecretKey = lines[3]
    AWSInstanceID = lines[4]

ec2 = boto3.resource('ec2',
        aws_access_key_id = AWSAccessKeyId,
        aws_secret_access_key = AWSSecretKey ,
        region_name ='ap-northeast-1'
)
instance = ec2.Instance(AWSInstanceID)
# 接続に必要なオブジェクトを生成
client = discord.Client()

# 起動時に動作する処理
@client.event
async def on_ready():
    # 起動したらターミナルにログイン通知が表示される
    await client.get_channel(SERVER_CHANNEL).send('bot online.')
# メッセージ受信時に動作する処理
@client.event
async def on_message(message):
    # メッセージ送信者がBotだった場合は無視する
    if message.author.bot or message.channel.id != SERVER_CHANNEL:
        #print(message.content)
        return
    if message.content=="!exit":
        exit()
        return
    if message.content=='!start':
        try:
            instance.start()
            instance.wait_until_running()
            await message.channel.send('server online.')
        except:
            await message.channel.send('failed to start instance.')
        return
    if message.content=='!stop':
        instance.stop()
        instance.wait_until_stopped()
        await message.channel.send('server stopped.')
        return
    if message.content.startswith('!'):
        await message.channel.send('WARNING: not a command.')
# Botの起動とDiscordサーバーへの接続
client.run(TOKEN)

region_name ='ap-northeast-1'って東京リージョンだと決め打ちしちゃってるけど違う場合は適宜変えて
次に、やめた方がいいんだけどdiscordbot.pyと同じフォルダにtoken.txtファイルを作って

NKASDFi3niGEPi39fJDiscordBotToken390ng/
2379279239789617
AS9-AWSACCESTOKENfMI)Aji
weAWSACCESSSecreTKeyj911
i-0instanceIdt4

こんな感じに

  1. DiscordBotのトークン
  2. コマンドの対象にしたいDiscordチャンネルのID
  3. AWSアクセストークン
  4. AWSシークレットキー
  5. インスタンスID

のデータを作成した(上のはダミーだよ)。これを読んでいる人がいたら序盤のトークンのところは適当に書き換えて環境変数を読むように設定してほしい。流出したらほんとに大変なので。

https://qiita.com/mochizukikotaro/items/a0e98ff0063a77e7b694

Herokuへのデプロイ

Heroku
https://jp.heroku.com/
アカウントを作ってHeroku CLIを自PCにインストールしておく。Gitも。以前はGithubにあげると自動でデプロイされる設定ができたけど流出事件だか何だかで今(2022/5/26)は使えない。
Herokuが読み込めるようにdiscordbot.pyと同じフォルダに追加ファイルを用意。名前も大事
runtime.txt

python-3.8.12

requirements.txt

discord.py
boto3

Procfileにはさっき作ったpyファイルの名前を入れる。名前の大文字小文字を間違えないように(後述)
Procfile

discordbot: python discordbot.py

デプロイのやり方は全部HerokuのDeployのページのDeploy using Heroku Gitのところに書いてあるのでそれを参考にここでは簡単に書く。

$ heroku login

でログインし

$ cd <discordbot.pyとかがあるフォルダ>
$ git init
$ git add .
$ git commit -am "mayusuki"
$ git push heroku master

でgitを使ってherokuにアップする。

Herokuは1機だけ無料で以降は金がかかるらしい。
動いていればdiscordサーバーにbot online.と送信されてくるはず。
discordのチャットに!startでインスタンスの起動、!stopで停止

gitを使うのが初めての場合

$ git config --global user.name "Namae Anata"
$ git config --global user.email namae.anata@example.com

といった具合に名前とメアドの設定が必要だと思う

Procfileをprocfileとしてしまい動かない問題が起きたがこれだった。

https://qiita.com/go_new_innov/items/0c6f064e84f07958a113

自動シャットダウン

cronを使って5分おきにサーバー人数を確認して0人が続いていたらシャットダウンするコードを作成し、インスタンス上に設定する。なお、厳密に最後のログアウトから5分後にシャットダウンするわけではないのでご注意を。

インスタンス停止用コード

インスタンスに接続すると/home/ec2-userにいるはずなので

$ pip3 install boto3

でboto3をインストール。

$ vi count_active.py

を実行し、下記のきったねぇコードを打ち込む

count_active.py
import os
import boto3
import re

TOKEN_DIR="token.txt"
LOG_DIR='minecraft/screenlog.0'
with open(TOKEN_DIR) as f:
    lines = f.read().splitlines()
    TOKEN = lines[0]
    SERVER_CHANNEL = int(lines[1])
    AWSAccessKeyId = lines[2]
    AWSSecretKey = lines[3]
    AWSInstanceID = lines[4]

ec2 = boto3.resource('ec2',
        aws_access_key_id = AWSAccessKeyId,
        aws_secret_access_key = AWSSecretKey ,
        region_name ='ap-northeast-1'
)
pattern = r'^\[\d\d:\d\d:\d\d\]\s\[Server thread/INFO\]:\sThere\sare\s(\d+)\sof\sa\smax\sof\s\d+\splayers\sonline:.*$'
repat = re.compile(pattern)

line_latest=None
line_before=None
def isListLog(line):
    return repat.fullmatch(line) is not None

def main():
    with open(LOG_DIR) as f:
        lines = f.read().splitlines()
        lines.reverse()
        i=0
        for s in lines:
            if isListLog(s) and i==0:
                line_latest=s
                i+=1
                continue
            if isListLog(s) and i==1:
                line_before=s
                i+=1
                break
        if i<2:
           exit()
    num_latest=int(repat.fullmatch(line_latest).groups()[0])
    num_before=int(repat.fullmatch(line_before).groups()[0])
    if num_latest == 0 and num_before == 0:
        instance = ec2.Instance(AWSInstanceID)
        instance.stop()
if __name__ == '__main__':
    main()

さっき作ったtoken.txtをec2に送り付けておく。

cronの設定

$ crontab -e

でcronの設定を開き、

* * * * * /bin/screen -p 0 -S minecraft -X eval 'stuff "list\015"'
*/5 * * * * /usr/bin/python3 /home/ec2-user/count_active.py

とする。これで1分に1度サーバーにlistコマンドを打ち、count_active.pyで人数の確認とサーバーのシャットダウンをする。*/5*/10にするとタイムリミットが10分になる
cronについて詳しくはこちら

https://www.kagoya.jp/howto/it-glossary/server/cron/

起動時設定

screenlog.0が残っているのでcronの時間周期がサーバー起動と重なって起動してすぐcount_active.pyが実行されると即終了してしまう。
ということで終了時にscreenlog.0を消す設定をさっき作ったminecraft.serviceに追加する。

$ sudo vim /etc/systemd/system/minecraft.service

から

ExecStop=/bin/rm /home/ec2-user/minecraft/screenlog.0

を追加する、すなわち

[Unit]
Description=Minecraft Server
After=network-online.target

[Service]
WorkingDirectory=/home/ec2-user/minecraft
User=ec2-user

ExecStart=/bin/bash -c '/bin/screen -DmSL minecraft /bin/java -server -Xms1024M -Xmx1024M -jar ./server.jar nogui'

ExecReload=/bin/screen -p 0 -S minecraft -X eval 'stuff "reload"\\015'

ExecStop=/bin/screen -p 0 -S minecraft -X eval 'stuff "say Server Shutdown. Saving map..."\\015'
ExecStop=/bin/screen -p 0 -S minecraft -X eval 'stuff "save-all"\\015'
ExecStop=/bin/screen -p 0 -S minecraft -X eval 'stuff "stop"\\015'
+ ExecStop=/bin/rm /home/ec2-user/minecraft/screenlog.0
ExecStop=/bin/sleep 10

Restart=on-failure
RestartSec=5s

[Install]
WantedBy=network-online.target

となるので保存

$ sudo systemctl daemon-reload
$ sudo systemctl enable minecraft

で設定すればOK(これ打たなくても設定されてるかも)
自動で閉じるようになったのでさっき作ったdiscordbotで停止コマンドを打つ部分

    if message.content=='!stop':
        instance.stop()
        instance.wait_until_stopped()
        await message.channel.send('server stopped.')
        return

は削除してもいい

Discussion