DiscordからGCP上のマイクラサーバーを制御してインスタンス料金をケチる話

# 概要
GCP、Python初心者が勉強ついでマイクラサーバーの自動制御システムを作成するお話です。

# 導入
知り合いから「マイクラやりたいからサーバー立てて！」と言われたのですが、自宅のサーバーが空いていませんでした。
そんなとき、GCPの入門者向け勉強会に出たときに「3万円分無料で使えるよ」という話を聞き、勉強ついでに適当に触ってみることにしました。

# 目標
お金を少しでもケチケチするために、DiscordからGCP上に立てたGCEインスタンスの起動/停止を制御する

# 構成
すごく雑ですがシステム構成図
![Untitled Diagram (1).png](https://qiita-image-store.s3.amazonaws.com/0/161926/19920209-5535-617f-5d75-f8ce680da581.png)

# 手順
## Google Cloud Platformのプロジェクトを作成
何はともあれ、GCPを使うための準備から始めます。
とりあえずGoogleのアカウントさえあれば特に何も考えずに作成できます。
詳しくは、下記のリンクを参照してください。

- [第0回　Google Cloud Platformをはじめよう！ アカウント登録～画像認識APIを試してみよう](https://book.mynavi.jp/manatee/detail/id=65673)

## マイクラサーバーインスタンスを作成
さて、本命のマイクラ鯖のインスタンスを作成します。
Google Cloud Consoleから
Compute Engine　→　VMインスタンス　→　インスタンスを作成
を選択します。
自分は以下の設定を行いました

|項目|内容|
|:-:|:-:|
|名前|minecraft-server|
|リージョン|asia-northeast1(東京)|
|ゾーン|asia-northeast1-b|
|マシンタイプ|vCPUx2|
|ブートディスク|Ubunt 18.04 LTS|
|管理<br>可用性ポリシー<br>プリエンプティブ|オン|
|ネットワーキング<br>ネットワークインターフェース<br>外部IP|IPアドレスを作成|

ここでケチケチポイント
プリエンプティブの設定をオンにするとインスタンス費がかなり安くなります。
そのかわり、24時間で自動で停止されたり、システム需要に応じて突然停止させられることもあります。
しかし、マイクラ鯖なんてそんな常時ログインしているものでもないですし、個人的にはこれって好都合じゃない？って思いました。
停止し忘れて余計なインスタンス費がかさむこともありません！
また、検証されているサイトを見たところ、ほぼほぼ停止するような自体にはならないとのこと。
私のように、土日くらいしかプレイしないという方は設定してみてはいかがでしょうか？

- [GCEのプリエンプティブインスタンスがどのくらい落とされるのか調べてみた](https://qiita.com/EastResident/items/442616d6ad1fe8108ea3)

## マイクラサーバーインスタンスの設定
なんとこちらはGCP公式の設定方法ガイドラインがあります。

- [Google Compute Engine での Minecraft サーバーのセットアップ](https://cloud.google.com/solutions/gaming/minecraft-server?hl=ja#automate_start_up_and_shutdown_procedures)

自分はこちらの手順のバックアップ部分を省略した内容で設定を行いました。

## Discord Botインスタンスを作成
こちらは一番小さいインスタンスを作成して超ケチケチします。
Google Cloud Consoleから
Compute Engine　→　VMインスタンス　→　インスタンスを作成
を選択します。
自分は以下の設定を行いました

|項目|内容|
|:-:|:-:|
|名前|discord-bot|
|リージョン|asia-northeast1(東京)|
|ゾーン|asia-northeast1-b|
|マシンタイプ|micro|
|ブートディスク|Ubunt 18.04 LTS|

お好みで静的なIPアドレスを振っても良いかもしれませんが、自分はどうせBot用鯖なので設定しませんでした。
また、リージョンについては、初めはアメリカの無料で使用できるリージョンを選択していましたが、レイテンシが高かったりと面倒だったので、どうせ無料分でまかなえるだろうと思い東京リージョンで作成し直しました。（後に西海岸のリージョンだと快適と聞きました。）
無料リージョンについてはこちらを参考にどうぞ。

- [GCE の無料枠のサーバを立るときに、初見でハマりそうなところ](https://qiita.com/ndxbn/items/7ef0a96e409a5b5837bd)

## サービスアカウントの作成
次にサービスアカウントと呼ばれる、GCPで一定の権限を持ったアカウントを作成します。
今回はこのアカウントに起動や停止などの権限を付与します。
Google Cloud Consoleから
IAMと管理　→　役割　→　役割を作成
を選択します。
自分は以下の設定を行いました

|項目|内容|
|:-:|:-:|
|タイトル|MineAdmin|
|権限|compute.instances.start<br>compute.instances.stop<br>compute.instances.get<br>compute.zoneOperations.get|

こちらが作成できたら、サービスアカウントを作成します
Google Cloud Consoleから
IAMと管理　→　サービスアカウント　→　サービスアカウントを作成
を選択します。

|項目|内容|
|:-:|:-:|
|サービスアカウント名|MineAdmin|
|サービスアカウントID|mineadmin@{ProjectID}.iam.gserviceaccount.com|
|サービスアカウントの権限|MineAdmin（上記で作成したもの）|

上記の設定が終了したあとにキーを作成というボタンがあると思います。
こちらでキーのタイプにてJsonを選択して作成をクリックし、Jsonファイルをダウンロードしてください。

- [GCE インスタンスの起動/停止を gcloud + cron でスケジューリングする](https://qiita.com/mazzzzam/items/7fd6b60e15caef6786cd)

## Discord Botコード作成
インスタンスも立ち上がったのでBotを作成します。
とりあえずDiscordでBotアカウントを作成します。
手順はこちらを参照

- [Pythonで実用Discord bot(discord.py解説)](https://qiita.com/1ntegrale9/items/9d570ef8175cf178468f)

さて、Botアカウントが作成できたら上記のリンクにあるようにPythonのdiscord.pyライブラリを使用してBotを作成すればよいのですが、公式リファレンスを見ているとPython3.7に対応するために破壊的変更が入るとの記述を見ました。
まだPython3.7に完全対応したver1.0.0もリリースしていなかったので、ここはDoscordの公式APIを叩いて実装することにしました。（ライブラリはこのあたりを簡単に出来るようにしているだけなので、やっている事自体は変わりません）

```Python:MineAdmin.py
import os
import os.path
import dotenv
import requests
import json
import datetime
import subprocess

dotenvPath = os.path.join(os.path.dirname(__file__), '.env')
dotenv.load_dotenv(dotenvPath)

DISCORD_TOKEN = os.environ['DISCORD_TOKEN']
CHECK_CHANNEL_ID = os.environ['CHECK_CHANNEL_ID']

url = f'https://discordapp.com/api/channels/{CHECK_CHANNEL_ID}/messages'

queryString = {'limit':'1'}

payload = ''
headers = {
    'Content-Type': 'application/json',
    'Authorization': f'Bot {DISCORD_TOKEN}',
    'cache-control': 'no-cache',
}

response = requests.request('GET', url, data='', headers=headers, params=queryString)
lastMessageJson = json.loads(response.text).pop()

userName = lastMessageJson['author']['username'] + '#' + lastMessageJson['author']['discriminator']
content = lastMessageJson['content']
timestamp = datetime.datetime.strptime(lastMessageJson['timestamp'][:-13], "%Y-%m-%dT%H:%M:%S")
validationTime = datetime.datetime.now() - datetime.timedelta(minutes=2)

# 古い投稿は無視
if validationTime > timestamp:
    exit()

SERVICE_ACCOUNT_ID = os.environ['SERVICE_ACCOUNT_ID']
GCP_PROJECT_NAME = os.environ['GCP_PROJECT_NAME']
MINECRAFT_INSTANCE_NAME = os.environ['MINECRAFT_INSTANCE_NAME']
MINECRAFT_INSTANCE_ZONE = os.environ['MINECRAFT_INSTANCE_ZONE']

payload = ''
option = ''
if content == '/start':
    payload = '{"content":"Starting up server...","tts":false,"embed":{}}'
    option = 'start'
    
elif content == '/stop':
    payload = '{"content":"Stopping server...","tts":false,"embed":{}}'
    option = 'stop'

if payload != '':
    requests.request('POST', url, data=payload, headers=headers, params='')
    command = f'/snap/bin/gcloud --account={SERVICE_ACCOUNT_ID} compute instances {option} {MINECRAFT_INSTANCE_NAME} --project {GCP_PROJECT_NAME} --zone {MINECRAFT_INSTANCE_ZONE}'
    subprocess.call(command.split())
```

```bash:.env
# Discordのトークンを記述
DISCORD_TOKEN={DiscordのBotの作り方のリンクから取得方法を確認してコピペ}
# コマンドを入力するチャンネルのID
CHECK_CHANNEL_ID={取得方法は下記にリンクを記載}

# サービスアカウントID
SERVICE_ACCOUNT_ID=mineadmin@{ProjectID}.iam.gserviceaccount.com
# プロジェクト名
GCP_PROJECT_NAME={ProjectID}
# マイクラサーバーインスタンスの名前
MINECRAFT_INSTANCE_NAME=minecraft-server
# マイクラサーバーインスタンスのゾーン
MINECRAFT_INSTANCE_ZONE=asia-northeast1-b
```

こんな感じの雑な方法。
本来であれば、gcloudのライブラリを使用すればよかったのかもしれませんが、これを作成したときはライブラリがPython2系にしか存在していないと勘違いしておりました…
気が向いたなら更新します。
余談ですが、初めはこのコードをGoogle Cloud Shell上でコーディングしておりPython3.5を使用していたのですが、フォーマット文字列がPython3.6以上から出ないと知らず少し悩みました。

チャンネルのIDの取得方法は以下のリンクを参照

- [ユーザー/サーバー/メッセージIDはどこで見つけられる？](https://support.discordapp.com/hc/ja/articles/206346498-%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC-%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC-%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8ID%E3%81%AF%E3%81%A9%E3%81%93%E3%81%A7%E8%A6%8B%E3%81%A4%E3%81%91%E3%82%89%E3%82%8C%E3%82%8B-)


最後の作成したスクリプトをcrontabに設定します。

```bash:crontab
* * * * * for i in `seq 0 5 59`;do (sleep ${i} ; /usr/bin/python3 /home/{user name}/MineAdmin.py) & done;  >>  /home/{user name}/error.log 2>&1
```

設定は以上になります。

# まとめ
今回は、勉強ついでにGCP上にマイクラサーバーを立てました。
GCPは便利な反面、各サービスがマイクロサービス化されているため、どんなサービスが有り何が出来るかを把握するのが大変ですね…
こればかりは色々と探したり、知見のある人に聞きながらやるのが良さそうです。
自分はGCPに詳しいX氏にご助力いただきました。
この場を借りてお礼申し上げます。
現状まとめたコードなどは最低限のものなので、今後はログ周りの修正や、Mod管理用のスクリプトとかを増やしていきたいですね。
この記事が、少しでも皆様のお役に立てばと思います。
良いマイクラライフを～