Discordへの/コマンドでGCEのマイクラサーバーを起動・停止するBotを作成した

# はじめに
マイクラサーバーを立てるにあたって、一緒にプレイする他の人もサーバーの立ち上げ・停止ができるようにしたかったので、Discordのテキストチャンネルにコマンドを打つことでサーバーのON/OFFの切り替えができるようなBotを作成しました。

## 前提
- マイクラサーバー
    - GCEのインスタンスに立てています
- デプロイ
    - EC2のインスタンス上に設置
    - 難しいことをしないでgit cloneでデプロイしています
    - インスタンス起動と同時にscreenでスクリプト走らせてBotを起動するようにしました

## アイデア元
[MinecraftマルチサーバーをGCPに上でケチケチ運用する](http://mpiyok.hatenablog.com/entry/2017/12/10/094638)

# 事前準備
- [discord.py](https://github.com/Rapptz/discord.py)
- [Compute Engine - Python クライアント ライブラリの使用](https://cloud.google.com/compute/docs/tutorials/python-guide?hl=ja)

Discord Botの作成・トークンの取得に関しては省略します。
下記の記事を参考にしました。
[Pythonで簡単なDiscord Botの作り方](https://qiita.com/PinappleHunter/items/af4ccdbb04727437477f)


## Python
以下の記事を参考にpyenvを導入してPython3系を使えるようにします。
[Amazon LinuxでPython3系を使う](https://qiita.com/michimani/items/fc64dcbe721d91579ccb)

Pythonのバージョンは以下の通りです。

```bash
$ python3 -V
Python 3.6.5
```

### 必要となるライブラリをインストール

```bash
$ pip install discord
```

## Google Cloud SDK関係
マイクラサーバーをGCEのインスタンス上に立ててあるので、これを操作するためのSDKをインストールします。

### インストール
- google cloud sdk

下記を参照にインスタンスにSDKを導入から初期化まで行います。
[Linux 用のクイックスタート](https://cloud.google.com/sdk/docs/quickstart-linux?hl=ja)

```bash
# google-cloud-sdkの取得
$ wget https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-200.0.0-linux-x86.tar.gz

# 上記ファイルの解凍
$ tar zxvf google-cloud-sdk-200.0.0-linux-x86.tar.gz

# インストールスクリプト実行
$ ./google-cloud-sdk/install.sh

# .bashrc再読込
$ source ~/.bashrc

$ gcloud -v
Google Cloud SDK 200.0.0
bq 2.0.33
core 2018.04.30
gsutil 4.31


Updates are available for some Cloud SDK components.  To install them,
please run:
  $ gcloud components update
```
表示通りSDKをアップデートします。

```bash
$ gcloud components update
```
### 初期化
`$ gcloud init`で初期化を行います。

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

Your current configuration has been set to: [default]

You can skip diagnostics next time by using the following flag:
  gcloud init --skip-diagnostics

<...中略...>

Go to the following link in your browser:

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

Enter verification code:
```
Go to 以下に表示されるURLにアクセスし、ブラウザ上でログインのための承認を行います。
承認が完了すると認証用のコードが表示されるので、これをコピーして上記のEnter...以下に貼り付けてEnterを押します。

続けて、デフォルトのプロジェクトとリージョンを選択して決定すると初期化が完了します。
以上でSDKへのログインは完了です。

そのまま、API利用の認証作業も行います。

```bash
$ gcloud auth application-default login --no-launch-browser
Go to the following link in your browser:

    https://accounts.google.com/o/oauth2.....
Enter verification code:
```
ログイン時と同様の作業を行うと、インスタンス上からGCEへのAPI操作が可能になります。

## Google API Clientのインストール
Python上でGoogle APIの認証およびAPI操作を行うためのライブラリをインストールします。

```bash
$ pip install --upgrade google-api-python-client
```
# 実装
以下Botのスクリプト全体です。

```python:bot.py
import asyncio
import os
from time import sleep

from googleapiclient import discovery
import discord

client = discord.Client()

BOT_TOKEN = os.getenv('BOT_TOKEN')

# Instance information
PROJECT = 'YOUR_PROJECT_NAME'
ZONE = 'asia-east1-a'
INSTANCE = 'YOUR_INSTANCE_NAME'

# Build and nitialize google api
compute = discovery.build('compute', 'v1')

@client.event
async def on_ready():
    print('Logged in as')
    print(client.user.name)
    print(client.user.id)
    print('------')

@client.event
async def on_message(message):
    if message.content.startswith('/minecraft'):
        # ex.) message.content: '/minecraft start' => command: 'start'
        command = message.content.split(' ')[1]

        if command == 'start':
            m = await client.send_message(message.channel, 'Server starting up...')
            start_server(PROJECT, ZONE, INSTANCE)
            await client.edit_message(m, 'Success! Server started up.')
            sleep(10)
            await client.delete_messages([message, m])
        elif command == 'stop':
            m = await client.send_message(message.channel, 'Server stopping...')
            stop_server(PROJECT, ZONE, INSTANCE)
            await client.edit_message(m, 'Success! Server stopped.')
            sleep(10)
            await client.delete_messages([message, m])
        elif command == 'status':
            status = get_server_status(PROJECT, ZONE, INSTANCE)

            if status == 'RUNNING':
                m = await client.send_message(message.channel, 'Server is running! Please enjoy Minecraft!')
                sleep(10)
                await client.delete_messages([message, m])
            elif status in {'STOPPING', 'STOPPED'}:
                m = await client.send_message(message.channel,
                                'Server is stopped. If you play Minecraft, please chat in this channel, `/minecraft start`.')
                sleep(10)
                await client.delete_messages([message, m])
            else:
                m = await client.send_message(message.channel,
                                'Server is not running. Please wait for a while, and chat in this channel, `/minecraft start`.')
                sleep(10)
                await client.delete_messages([message, m])
        elif command == 'help':
            m = '''
            ```Usage: /minecraft [start][stop][status]
    start   Start up minecraft server
    stop    Stop minecraft server
    status  Show minecraft server status(running or stopped)```
            '''.strip()
            await client.send_message(message.channel, m)

def start_server(project, zone, instance):
    compute.instances().start(project=project, zone=zone, instance=instance).execute()

def stop_server(project, zone, instance):
    compute.instances().stop(project=project, zone=zone, instance=instance).execute()

def get_server_status(project, zone, instance):
    res = compute.instances().get(project=project, zone=zone, instance=instance).execute()
    return res['status']

client.run(BOT_TOKEN)
```
## discord.py
難しいことはしないので、公式の[Quick Example](https://github.com/Rapptz/discord.py#quick-example)をほぼそのまま利用しています。
### bot tokenの設定
Botを弄るためにトークンが必要なので環境変数に設定しておきます。

```bash:~/.bash_profile
.
.
.
<省略>
export BOT_TOKEN='****************'
```

## Compute Engine API
以下のリファレンスが詳しいので参照。
[Compute Engine API](https://cloud.google.com/compute/docs/reference/rest/v1/)
今回は

- instances().start
- instances().stop
- instances().get

を使っています。

それぞれ引数にプロジェクト名、ゾーン、インスタンス名が必要なので最初に定数として設定しました。
API利用の認証が完了していれば、`compute = googleapiclient.discovery.build('compute', 'v1')`で上記のAPIが利用可能になります。

# How to use
今回はBotが発言できるチャンネルを限定してはいないので、Bot作成時に選択したDiscord Serverのいずれかのテキストチャンネルでコマンドを入力することでマイクラサーバーの起動や停止、ステータスの確認ができます。

- `/minecraft start`
- `/minecraft stop`
- `/minecraft status`

# 終わりに
とりあえず使えるものを作ろうと思っていたので実装甘い部分が多いです。
需要なさそうな話題でしたが、もし何らかの参考になれば幸いです。