AlexaからGCEのマイクラサーバ(統合版)を起動する

この記事は
[KLab Engineer Advent Calendar 2020](https://qiita.com/advent-calendar/2020/klab)と[Google Cloud + Gaming Advent Calendar 2020](https://qiita.com/advent-calendar/2020/gcp-gaming)の10日目の記事です。

[本記事のセッション動画はこちら](https://youtu.be/chMpYjLHHwI)

こんにちはhamasan05です。
今年は一家でマイクラにハマった年だったので、マイクラ絡みでなにか書けないかと思い、
勉強がてらやっていたGCPとAlexaの話を記事にしました。

前提としてGCPのサインアップとプロジェクトの作成、
alexa developer consoleのサインアップまでは済ませているものとします。

# レシピ
1. GCEでマイクラサーバを作る
1. GCFで起動の処理を作る
1. Alexaからつなぎこむ

# GCEでマイクラサーバを作る

今回はGCEのインスタンス作成の細かい手順は割愛して
- rcスクリプトへの登録
- サーバの自動停止

までを実施することにします。
※細かい手順は「[Google Cloud + Gaming Advent Calendar 2020](https://qiita.com/advent-calendar/2020/gcp-gaming)」３日目の記事が参考になると思います。

## VMの作成
GCPのダッシュボードの画面からVMインスタンスを選択して作成をします。
マイクラ統合版のサーバは実用はともかくとして動かすだけなら完全無料枠のusリージョンのf1-microで十分です。
必要に応じてスケールすればよいと思います。
OSは無難にUbuntuにしておきましょう。
ほかのLinuxのOSでもマイクラ統合版のサーバプログラムは動作しますが[公式サイト](https://www.minecraft.net/en-us/download/server/bedrock)によるとUbuntu用とのことです。
ネットワークについては`VPCネットワーク`から`静的アドレスを予約`して
一つIPv4のアドレスを確保しておくとよいと思います。
準備が済んだらVMを起動してSSH接続をします。
私はブラウザからのsshは不便なので、ユーザーの追加、wheelグループなどの追加をしました。

## マイクラ統合版のサーバプログラムのインストール
サーバプログラムは[日本語のサイト](https://www.minecraft.net/ja-jp/download/server/bedrock)だとダウンロードできない場合もあるので[英語のサイト](https://www.minecraft.net/en-us/download/server/bedrock)からダウンロードするとよいです。
規約に同意するチェックボックスをクリックして、ダウンロードボタンを右クリックしてURLをコピーしておきます。

コマンド例：

```
$ mkdir be
$ cd be
$ curl -O  https://minecraft.azureedge.net/bin-linux/bedrock-server-1.16.101.01.zip
$ unzip bedrock-server-1.16.101.01.zip
```
マイクラのサーバプログラムは標準入力を使うのでscreenを使ってバックグラウンドで動作できるようにします。

```
$ sudo apt-get install screen
```

下記のような内容のstartスクリプトを作成しておきます。
ほかにも起動時に何かしたいことがあったら記載しておくとよいと思います。

```
$ cd
$ vi start.sh
```

```bash:start.sh
#!/bin/bash
cd /home/xxx/be
screen -dmS be ./bedrock_server
```
正直これだけだったら直接後述の`/etc/rc.local`に記載してもいいかな感はありますが
homeディレクトリにあるもので制御できるようにしておくと、後でいろいろいじるときに楽です。

## OS起動時にマイクラ統合版サーバプログラムを自動起動する
今回はサービス化せずにシンプルに起動するだけの処理を書くことにします。
OS起動時に呼ばれる`/etc/rc.local`に起動スクリプトを登録しておきます。

```
$ sudo vi /etc/rc.local
```

```bash:/etc/rc.local
#!/bin/sh
sudo -u xxx /home/xxx/start.sh
```

これでOSが起動したらマイクラ統合版のサーバプログラムも自動で起動するようになりました。
最後に停止忘れの防止のために深夜0時になったらインスタンスを停止するようにcronを仕込んでおきます

```
$ crontab -e
```

```
0 0 * * * sudo shutdown -h now > /dev/null
```
いったんこの時点でVMを再起動してマイクラから接続確認をしておきましょう
確認できたらダッシュボードからVMを停止しておきます。


# GCFで起動の処理を作る

## 関数の作成
GCPのダッシュボードからCloud Functionsの画面に行き関数を作成します。
関数名は任意のもの、リージョンはGCEと同じ場所にします。
認証は`未認証の呼び出しを許可`にします。
公開APIとして作成して、別途Alexaから送られてくるJSONで認証するからです。
ほかはデフォルトでよいです。

ランタイムは`Python 3.8`を選択してエントリポイントを`alexa`とします。
`main.py`を開いて以下の通り入力します。
設定類は各自調べて入力してください。
`AlexaのID`の部分はあとから入力するので変更しないでOKです。

```python:main.py
import threading
from typing import Optional

import googleapiclient.discovery
from flask import json
from oauth2client.client import GoogleCredentials

PROJECT = "{GCPのプロジェクトID}"
ZONE = "{GCEのリージョン}"
INSTANCE_NAME = "{GCEのインスタンス名}"
USER_ID = [
    "amzn1.ask.....形式のAlexaのID(コンソール用)",
    "amzn1.ask.....形式のAlexaのID(デバイス用)",
]


dictionary: dict = {
    "started": "起動しました",
    "stopped": "停止しました",
    "alreadyStarted": "すでに起動しています",
    "alreadyStopped": "すでに停止しています",
    "NotSupportedFunction": "その操作はサポートしていません",
    "NotAuthorized": "権限がありません",
    "LeaveItToMe": "私にお任せください",
}


def alexa(request):
    ret = _auth_alexa(request)
    if ret:
        return _build_response(ret)
    thread = threading.Thread(target=_execute, args=(["start"]))
    thread.start()
    return _build_response("LeaveItToMe")


def _execute(function) -> str:
    if function == "start":
        compute = _init()
        ret = _start_instance(compute, PROJECT, ZONE, INSTANCE_NAME)
        if ret:
            return "started"
        return "alreadyStarted"
    elif function == "stop":
        compute = _init()
        ret = _stop_instance(compute, PROJECT, ZONE, INSTANCE_NAME)
        if ret:
            return "stopped"
        return "alreadyStopped!"
    return "NotSupportedFunction"


def _init():
    credentials = GoogleCredentials.get_application_default()
    return googleapiclient.discovery.build("compute", "v1", credentials=credentials)


def _auth_alexa(request) -> Optional[str]:
    request_json = request.get_json(silent=True)
    if request_json["context"]["System"]["user"]["userId"] not in USER_ID:
        print(f'alexa: {request_json["context"]["System"]["user"]["userId"]}')
        return "NotAuthorized"
    return None


def _start_instance(compute, project: str, zone: str, name: str) -> bool:
    instance = (
        compute.instances().get(project=project, zone=zone, instance=name).execute()
    )
    if instance["status"] == "TERMINATED":
        compute.instances().start(project=project, zone=zone, instance=name).execute()
        return True
    else:
        return False


def __start_instance(compute, project: str, zone: str, name: str):
    compute.instances().start(project=project, zone=zone, instance=name).execute()


def _stop_instance(compute, project: str, zone: str, name: str) -> bool:
    instance = (
        compute.instances().get(project=project, zone=zone, instance=name).execute()
    )
    if instance["status"] == "RUNNING":
        compute.instances().stop(project=project, zone=zone, instance=name).execute()
        return True
    else:
        return False


def _build_response(message: str) -> str:
    response = {
        "version": "1.0",
        "response": {
            "outputSpeech": {
                "type": "PlainText",
                "text": _translate(message),
            }
        },
    }
    return json.dumps(response)


def _translate(message: str) -> str:
    return dictionary[message]

```

GCEインスタンスの起動時にバックグラウンドでの実行をしているのですが、これは[GCFのガイド](https://cloud.google.com/functions/docs/bestpractices/tips?hl=ja#do_not_start_background_activities)に反するような内容になっています。

バックグラウンドにしている理由としては、処理に時間がかかりすぎてAlexaがなかなか返事をしてくれなかったりタイムアウトしてしまうからです。
業務等に使う場合にはGCF(フロント) -> Pub/Sub -> GCF(バックエンド)のようにして
フロントで一旦レスポンスを返して、バックエンド側でGCEインスタンスの起動処理を行うのが良いと思います。

続いて`requirements.txt`です。
テンプレートからのコピー＋αなので余計なものが混ざっているかもしれませんがとりあえず下記で動くようです。
作業した時期が割と前なので実行すると若干ログに警告的なものが出てしまってます・・・ので気が向いたら訂正しておきます。

```
cachetools==4.0.0
certifi==2019.11.28
chardet==3.0.4
Click==7.0
Flask==1.1.1
google-api-core==1.16.0
google-api-python-client==1.7.11
google-auth==1.11.2
google-auth-httplib2==0.0.3
google-cloud-core==1.3.0
google-cloud-error-reporting==0.33.0
google-cloud-logging==1.15.0
google-cloud-storage==1.26.0
google-resumable-media==0.5.0
googleapis-common-protos==1.51.0
grpcio==1.27.2
httplib2==0.17.0
idna==2.9
itsdangerous==1.1.0
Jinja2==2.11.1
MarkupSafe==1.1.1
oauth2client==4.1.3
protobuf==3.11.3
pyasn1==0.4.8
pyasn1-modules==0.2.8
pytz==2019.3
requests==2.23.0
rsa==4.0
six==1.14.0
uritemplate==3.0.1
urllib3==1.25.8
Werkzeug==1.0.0
```

入力が終わったらデプロイボタンを押します。
このままだとAlexaからアクセスしても認証エラーでこけてしまうので
Alexa側のセットアップが終わったら再度関数を編集します。

# Alexaとつなぎこむ

## スキルの作成
まずはalexa developer consoleからスキルを作成します。
`1. スキルに追加するモデルを選択`を`カスタム`
`2. スキルのバックエンドリソースをホスティングする方法を選択`を`ユーザー定義のプロビジョニング`
で作成します。

`スキルに追加するテンプレートを選択`は`スクラッチで作成`を選択します。

## スキルのカスタマイズ
### 呼び出し名
呼び出し名はAlexaに話しかけるときのキーワードなので何か好きなものを入力してください。
スキル名がデフォルトで入っているのでそのままでもよいです。
私は`マイクラのサーバを起動して`にしました。

### 対話モデル
インテントに`HelloWorldIntent`があるので削除します。
今回に関してはあっても実害はないのですが使わないものがおいてあると気持ち悪い程度の感じです。

### エンドポイント
デフォルトでは`AWS LambdaのARN`が選択されているので`HTTPS`にしてからデフォルトの地域に
GCPのダッシュボードからGCFの`トリガー`からURLをコピーして貼り付けます。
おそらく下記のようなURLになっていると思います
`https://{リージョン名}-{プロジェクトID}.cloudfunctions.net/{関数名}`

`SSL証明書の種類を選択`は
`開発用のエンドポイントは証明機関が発行したワイルドカード証明書を持つドメインのサブドメインです`を選択します。
これ間違えると全く動きません。よく考えれば当たり前のことでしたが。

`エンドポイントを保存`します。

## テスト
alexa developer consoleの`テスト`タブから操作します。
スキルテストが有効になっているステージを`開発中`に変更します。
`入力またはマイクを長押しして発話`に呼び出し名(`マイクラのサーバを起動して`)をテキストで入力してEnterを押します。
`権限がありません`と返事があったら成功です。

### ユーザーIDの追加
再びGCPのダッシュボードに戻ってGCFの関数名(`alexa`)をクリックしてログを見ます。
alexa: amzn1.ask.xxxxxというログがあるのでamzn1以降をコピーしてログ画面の上部にある`編集`を押して`次へ`を押して
main.pyの`USER_ID`の`amzn1.ask.....形式のAlexaのID(コンソール用)`を置き換えて`デプロイ`を押します。

この状態でalexa developer consoleからの動作するはずです。
※起動したらGCPのダッシュボードからGCEのVMインスタンスを停止しておきましょう

## Amazon Echoなどの実機で使う
alexa developer consoleの`公開`タブから操作します。
`スキルのプレビュー`と`プライバシーとコンプライアンス`を適切に入力します。
`公開範囲`の`ベータテスト`を選んで`ベータテスト管理者用Eメールアドレス`と`スキルのベータテスト`を
どちらもAlexaが動いているAmazonのアカウントに紐づいたメールアドレスを入力します。
`ベータテストを有効化`を押すと
`You're invited to beta test a new Alexa skill`というタイトルのメールが届くので下記をクリックして有効化します。

```
JP customers: To get started, follow this link:
Enable Alexa skill "スキル名"
```

Echoなどの実機でスキル名でコールするとスキルが動くのですが、権限がありませんと返答されるはずです。
ユーザーIDの追加で記載した通りのことを再び実施するとEchoなどの実機から動作するようになったと思います。

# 最後に
AmazonのスマートスピーカーとGoogleのクラウドサービスを連携させるという珍しいことをやってみましたが
Alexaから何らかのサーバを起動するということだけに関していうと
おそらくAWSのEC2インスタンス上に作ったサーバを起動するほうがはるかに楽だと思います。
alexa developer console側に標準でLambdaに連携する機能があるので
通信のところで悩むところが減るはずです。

収穫としてはAlexaのスキルがエンドポイントに送ってくるJSONの中身を詳しく見ることができたというところだと思います。
この辺りはマニュアルを読んでいても一番よくわからないところでした。
今回はインスタンスの停止まではやっていませんが、同じ要領で停止は可能です。
`main.py`にその機能はありますが、Alexaからのつなぎこみをしていないのでその部分だけ作りこめば良いはずです。

GCPを触ってみてよかったなと思ったところは、
- GCEのインスタンス/静的アドレスの無料枠
- サインアップ時にGooglePlayのクレジットカードを連携できる
という点で、学習するまでのハードルが低いと感じました。
試行錯誤する範囲については無料というのは初めて触るときに安心ですよね。
※比べられるほど他社のクラウドを触り込んでないので割と表層的なところですが・・・

単にマイクラをオンラインで遊びたいだけならRealms Plusや[ConoHa VPS](https://www.conoha.jp/vps/function/minecraft/)をお勧めします。
が・・・私は本格的にマイクラサーバを使うとなったらこのままGCEを使うんだろうなぁという気持ちです。
途中にも書きましたが、まだまだ色々整備できるところがあるので、それを楽しんでいきたいと思っているからです。
GCPやAlexaをスキル勉強したいけど事務的に触ってみるのはツラいという方の参考になればと思います。

記事にはしませんでしたが試行錯誤するにあたってgcloudコマンドラインツールやPyCharmにお世話になりました。
コマンドラインツールやIDEは開発の強い味方ですね。感謝感謝。