Unity:MagicOnionの.NET CoreコンテナサーバーをAWS Fargateで実行する方法

# 本記事を書くにあたって
オンラインゲームを作りたいと思ってる人は多いけど、作って動かしたことある人はとても少ないイメージです。
その理由として聞こえてきそうなものに以下があると思います。

- 作り方に詳しくない
- サーバーマシンとかクラウドサービスとか、利用するまでに時間かかりそう
- ちょっと勉強すると、やっぱり学習コストが高いことがわかるので、途中であきらめた

今回はそんな人たちに、イマドキ？の Unity オンラインゲームのサンプルの作り方を示します。
読者のみなさんには**「一から勉強するよりは、この記事をなぞって動かしてみようかな？」**くらいの気持が沸くことを目指して記事を書くことにします。
…筆者も今回初めて動かしたレベルです。プログラムは動きますが、説明が怪しい点はご容赦ください。

# 本記事の内容

Windows 10 Pro. 環境で作業しました。
ゲーム開発エンジンとして [Unity 2019.1.0f2](https://blogs.unity3d.com/jp/2019/04/16/introducing-unity-2019-1/) を利用します。
Unity 向けのリアルタイム通信エンジンとして [株式会社Cysharp](https://cysharp.co.jp/) からリリースされている [MagicOnion](https://github.com/Cysharp/MagicOnion) を使います。
その MagicOnion の samples フォルダに格納されている ChatAppCodeLink のクライアントとサーバーを利用します。
そのサーバーは .NET Core ランタイムで動きますので、これをコンテナとして使用できるように準備します。
オンライン化のため [Amazon ECR](https://aws.amazon.com/jp/ecr/) というコンテナイメージを簡単に保存できるサービスを利用し
その保存したコンテナをサーバー管理せずに使用できる [AWS Fargate](https://aws.amazon.com/jp/fargate/) を利用します。
Fargate の管理画面で示される公開 IP アドレスに自宅の PC から接続してオンラインでチャットができるまでの具体的な手順が示されています。

# Magic Onion のサンプルをローカルで実行

## Magic Onion について
Magic Onion は C# をあるルールで記述すると、あたかもクライアントとサーバーのコードがつながっているかのように機能する
いわゆる遠隔で関数を呼び出す RPC を提供してくれる Unity ゲーム開発者にフレンドリーなリアルタイム通信エンジンです。
今回はアプリを動かすだけなので、気になる記述ルールはこちらの[公式技術ブログ](http://tech.cygames.co.jp/archives/3181/)を読むと、プログラムの書き方がはっきり見えてくると思います。

## Magic Onion のサンプルプログラムを取得
さて、サンプルをローカルで実行するには [README](https://github.com/Cysharp/MagicOnion/tree/master/samples) の手順に従うのが一番です。
が、簡単に書かれていることを要約すると Unity のフォルダの内容を共有する方法が二つあるよと CodeLink のタイプと、もう一つのタイプがあって、どちらも同じサンプルだよ。と書いてあります。
私は CodeLink の方のみを試しました。手順を以下に記します。

git コマンドを Windows 環境で打てるように [git for windows](https://gitforwindows.org/) をインストールして
git bash のコマンドプロンプトで git clone コマンドで適当なフォルダに Magic Onion のリポジトリを引いてきます。
ブランチは master ではなく sample_hosting を選びました。確認したところ最終コミットの日付は 2019/04/18 16:34 の頃のやつです。
master だとプログラムを実行するとすぐに正常終了するのですが、こちらの sample_hosting ブランチは、サーバーとして起動し続けてくれるようになりました。

## Magic Onion のサンプルサーバーの起動
プログラムをビルドして .dll ファイルを作り、その .dll ファイルを実行することで起動完了です。
環境準備と起動方法を記録します。

### サーバーコードの修正

MagicOnion\samples\ChatAppCodeLink\ChatApp.Server フォルダに Program.cs ファイルがありますが、ここの16行目あたりを次のように変更しておきました。具体的には"localhost"→"0.0.0.0"

```cs
new ServerPort("0.0.0.0", 12345, ServerCredentials.Insecure) }
```

この後の作業で不都合があることがわかったので、手を入れておきます。
localhost との違いは何ですか？と調べましたが、私が理解できる説明は見つけていません。。。

### dotnet コマンドで Release ビルド
dotnet コマンドが使えないといけないので [Download .NET Core SDK](https://dotnet.microsoft.com/download) の最新版を取得し、インストールしました。
以下の help コマンドでバージョン情報と使い方が示されればインストールは完了です。

```bash
$ dotnet --help
.NET Core SDK (3.0.100-preview3-010431)
Usage: dotnet [runtime-options] [path-to-application] [arguments]
```
MagicOnion\samples\ChatAppCodeLink\ChatApp.Server フォルダに移動して次の dotnet コマンドを叩きました。

```bash
$ dotnet publish -c Release -o out
```

out フォルダが作られ以下のような dll ファイル群が出力されていれば Release ビルド完了です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106364/eba22050-0d66-664f-0393-a84f0aabf151.png)

### Dockerfile で image を作成
dll ファイル群がある out フォルダの中に Dockerfile という名前のテキストファイルを配置して以下のコマンドを記入します。

```dockerfile:Dockerfile
FROM mcr.microsoft.com/dotnet/core/runtime:2.2
WORKDIR /app
COPY . .
ENTRYPOINT ["dotnet", "ChatApp.Server.dll"]
```

docker コマンドを Windows 10 Pro. 環境で実行できるようにするため [Docker for Windows](https://docs.docker.com/docker-for-windows/) を取得してインストールします。

git bash 上で次の version コマンドが機能すればインストール完了です。

```bash
$ docker --version
Docker version 18.09.2, build 6247962
```

out フォルダに Dockerfile を作ったので、out フォルダをカレントフォルダに以下の docker コマンドを実行します。

```bash
$ docker build . -t chatappserver:1.0
```

このコマンドは docker コンテナのベースとなる image を作成する(buildする)コマンドで、期待では chatappserver:1.0 という名前の image が作られていてほしいものです。

image 一覧を確認するコマンドで作業完了かチェックしましょう。

```bash
$ docker images
REPOSITORY         TAG                 IMAGE ID            CREATED             SIZE
chatappserver      1.0                 35XXXXXXXXXX        7 hours ago         232MB
```
期待通りなので、docker image の作成は成功です。

### Docker image を起動

docker image があるので、これを run コマンドで起動するだけです。
具体的には次の docker コマンドを一行書いて実行し、サンプルの Server プログラムが port 番号 12345 でリッスン中になってくれたら成功です。

```bash
$ docker run --rm -p 12345:12345 chatappserver:1.0
D0420 12:05:17.671366 Grpc.Core.Internal.UnmanagedLibrary Attempting to load native library "/app/runtimes/linux/native/libgrpc_csharp_ext.x64.so"
D0420 12:05:17.710327 Grpc.Core.Internal.NativeExtension gRPC native library loaded successfully.
Application started. Press Ctrl+C to shut down.
Hosting environment: Production
Content root path: /app/

```

クライアントから接続確認するまで、このままにしておきましょう。

## Magic Onion サンプルクライアントの起動

MagicOnion\samples\ChatAppCodeLink\ChatApp.Unity フォルダを Unity 2019.1.0f2 で開いてみました。
ChatScene を開いて、Play モードで実行するとチャットとして機能しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106364/4c9444ff-4261-a387-3f97-70ab0d3f0fb2.png)

Client は単なる Unity シーンなので解説は以上です。
Build して、アプリと Unity Editor 画面でやりとりしてみると、リアルタイム通信が確認できます。

# AWS のクラウド上に image を配置してオンライン化する

Docker の image を作り、これを実行してローカルPC内でリアルタイム通信できたところまで来ました。
オンライン化したければこの image を AWS などのクラウドサービス会社のコンテナ実行サービス上で実行すれば良いです。
コンテナ技術は AWS だけのものではないので、Google や Microsoft も、同じようなサービスがあるか、もし無くても、じきに出てくると思います。[Google Cloud Run](https://cloud.google.com/run/docs/?hl=ja) とか？（この記事では触れません）

では AWS 上に image を置いてrun する具体的な手順を示します。

## Amazon ECR に docker image を配置する
[Amazon Elastic Container Registry (ECR)](https://aws.amazon.com/jp/ecr/) というサービスの一つを使います。
管理コンソールを使う手順は[このリンク先のおっちゃんの解説](https://aws.amazon.com/jp/ecr/getting-started/)を2分聞けばマスターできました。

要するにサービス一覧から ECR を選択し「リポジトリを作成」ボタンをおして image の名前を入力します。
今回は chatappserver という名前にしました。**ローカルで作った image の名前と合わせるのが基本です**
作ったら、その chatappserver の空っぽのリポジトリ内に「プッシュコマンドの表示」ボタンがあるのでこれを押すと「この手順でコマンドを打て」と案内が出ます。
それに従います。具体的には

```bash
$(aws ecr get-login --no-include-email --region ap-northeast-1)
```

で、あなたの ECR にログインして、いつでも image を配置（プッシュ）できる権限を持った状態になります。
次に、docker コマンドを使って先ほど作った chatapserver:1.0 イメージに別名としてあなたの id 入りの URL imageタグを作成します。

```bash
docker tag chatappserver:1.0 30XXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/chatappserver:1.0
```

あとは、このimageタグを利用して push するだけです。具体的には以下の docker push コマンドを実行しました。

```bash
docker push 30XXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/chatappserver:1.0
```

しばらくすると ECR のコンソール上で image がある状態に変わります。
その image には対となる URL が発行されるので、これをメモ帳に記録しておきます。（このあとすぐに使います）

## AWS Fargate で ECR の image を利用
[AWS Fargate](https://aws.amazon.com/jp/fargate/) は、 [Amazon ECS](https://aws.amazon.com/jp/ecs/) というコンテナ実行サービスのうち、サーバーマシンを意識せずに、Docker image を渡せば Docker コンテナを動かしてくれるという起動タイプです。
Amazon ECS を使いつつ、各所で Fagate タイプを選択して、細かいサーバー設定をスキップしつつ、docker image を利用する設定を書いて実行すると思えば、初学者は容易に理解できると思います。

具体的な手順を以下に示します。

### タスクが所属するクラスターの作成
Docker image ＝コンテナです。そのコンテナを登録できるタスクを定義します。タスクを実行するには、 一つのクラスターに所属させる必要があるため、最初にクラスターを作っておきます。
Amazon ECS のサービスの管理コンソールへ移動し、クラスターのサイドメニューを選択して「クラスターの作成」ボタンを押します。
クラスターテンプレートを選ぶように言われるので、AWS Fargaate を使用できるテンプレートを選択します。（「ネットワーキングのみ」を選びました）
必須項目はクラスター名だけですので、unity-chatapp-cluster という名前を付けてクラスターの作成は完了です。

### タスクの定義
タスク定義のサイドメニューを選択して「新しいタスク定義の作成」ボタンを押します。
Fargate or EC2 の2択を迫られるので Fargate を選びます。
タスク定義名は必須項目なので unity-chatapp-task と名前をつけました。
タスクロールは空欄（今回のコンテナは AWS とやりとりしないので、権限は与えない）
ネットワークモードは Fargate の場合 awsvpc 一択でした。（考えることをやめておきます）
タスクの実行 IAM ロールの欄には、ECR から image を取得する権限と CloudWatch にログを書き出す権限を与えたいので、そのための IAM ロールを作って設定します。

■タスク実行 IAM ロールの作成
IAM マネジメントコンソールを別窓で開いて、ロールのサイドメニューを選択して「ロールの作成」ボタンを押します。
ロールを使用するサービスを聞かれるので「Elastic Container Service」と伝えます。
ユースケースは？とさらに細かく聞かれるので「Elastic Container Service Task」と伝えます。
すると、どのポリシーにしますかと、無数のポリシー選択肢を見せてくるので「task」とポリシーのフィルタに書き込みます。
絞り込みで AmazonECSTaskExecutionRolePolicy が現れるので、左のチェックボックスにチェックを入れて、次へ進みます。
タグは検索や情報整理に使うものなので、今回はスキップします。（ゲームタイトルごとにタグを作るとかですかね？）
最後に必須項目としてロール名を記入します。名前から権限範囲がわかるように ecsTaskExecutionRole という名前を付けました。

先ほどのタスク定義のタスクの実行 IAM ロールの欄に戻り、作った ecsTaskExecutionRole を設定します。
これで ECR に push した iamge を利用する権限がタスクに付与されました。

タスクサイズは最小の 0.5G メモリ、0.25 vCPU を設定しました。
最後に ECR の image を使うコンテナを追加します。

■ECRのimageを利用するコンテナの追加
コンテナの定義の項目に「コンテナの追加」ボタンがあるので押します。
コンテナ名は今回は誰にも参照されないので適当で良いです。chatserver としました。
イメージは必須項目です。ECR に push したときに URL を確認してメモ帳に記録しましたので、それをここに記入します。
プライベートレジストリの認証は、ECR を使う場合のみチェックを外します。（認証機構が違うので、チェック入れるとエラーありタスク定義になりました）
メモリ制限はソフト制限 128 MB としました。（512以上はそもそも、タスク定義で制約を与えたので設定できない）
ポートマッピングでは 12345 の tcp を設定しました。（今回のサンプルでは 12345 ポートで通信を行うため）
その後の詳細コンテナ設定は、今回は無視して「追加」ボタンを押してコンテナの追加を終えます。

これで ECR に push 済みの chatappserver:1.0 の image を run するコンテナが一つ追加されたタスク定義を記入したことになるので「作成」ボタンを押して完了です。（エラーが出たら、上記項目で何か見落としがあるはず）

### タスクの実行
ここまでの手順で用意したクラスターとタスク定義を使って、タスクを実行する操作を示します。

コンソールからAmazon ECS のサービスを選んで、タスク定義をサイドメニューから選択、unity-chatapp-task と先ほど名付けたタスク定義があるので、これの左側のチェックボックスにチェックを入れて「アクション」のドロップダウンメニューを表示します。
「タスクの実行」を選んで、起動タイプのラジオボタンを FARGATE に設定します。
所属するクラスターは、先ほど作った unity-chatapp-cluster を選びました。
タスク数は 1 を記入。
クラスター VPC は、選べるの一つしかなかったのでそれを選びます。（コンテナインスタンスの所属するプライベートネットワーク領域のこと）
起動する可能性のあるサブネットも必須選択なので設定します。（どのアドレス範囲にコンテナが起動しても、今回のアプリは AWS を利用しないから、気にする必要はありません）
セキュリティーグループは新しく作りました。

■セキュリティグループの作成
インバウンドという、外部からコンテナ内部へ向けた通信の許可ルールを作ります。
タイプ customtcp でプロトコル TCP として、ポート範囲は 12345 のみを設定し、接続元としてのソース項目には自宅の IP アドレスの CIDR 表記を記入します。
自宅の IP アドレスは git bash 上で curl コマンドが実行できるはずなので、次のコマンドを実行して取得します。

```bash
$ curl httpbin.org/ip
```
CIDR 表記は、IPアドレスの範囲指定の書式のことです。ある IP からのみ接続を許可するようにしたい場合は
XXX.XXX.XXX.XXX/32
と 32 の数字を添えれば OK です。これを記入します。
32 から数字を小さくすると、どんどんIPアドレスの範囲が広がっていくので、その分セキュリティのリスクが上がります。

パブリック IP の割り当ては ENABLED にして、その他の詳細オプションの設定はスキップします。
「タスクの実行」ボタンを押します。

クラスターのタスク一覧画面に飛ばされるので、タスクの項目を選択して内容を細かくチェックします。
例えば、ステータスが RUNNING になっていること
Public IP アドレスが記されているので、これを記録します。
またログドライバーの項目に CloudWatch のログを表示とあるので、現在のコンテナの状態を細かくチェックしましょう。
ローカル環境と同じ出力が確認できれば一安心ですね

ここまでのステップの確認で、全世界からアクセスできる（セキュリティグループとしては自宅のIPからのみ 12345 ポートだけの） Chat サーバーが起動中です。

## オンラインサーバーにクライアントからアクセス

さっそく、Fargate を使ってオンラインに立てた ChatApp.Server コンテナにアクセスしてみましょう。
先ほど記録した Public IP を Magic Onion の
MagicOnion\samples\ChatAppCodeLink\ChatApp.Unity\Assets\Scripts にある

```cs:ChatComponent.cs
        private void InitializeClient()
        {
            // Initialize the Hub
            this.channel = new Channel("XXX.XXX.XXX.XXX", 12345, ChannelCredentials.Insecure);

```
に記入します。

あとはローカルで試したようにUnityシーンを起動して、ルームに入れるか試してみてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106364/e81403eb-6e0b-6766-6ebc-81c38246e6fb.png)

期待通り、オンラインサーバーへ接続して動きましたね。
安全のためセキュリティグループのIP制限を自宅からしか接続できないように設定しましたので
その接続元の IP アドレスの制限を取り除けば、地球の裏側にいるユーザーともリアルタイム通信ができることが期待できますね。

# まとめ
[MagicOnion](https://github.com/Cysharp/MagicOnion) の sample のチャットアプリのサーバーを docker image にしてローカルで動くことを確認して
オンライン化のため [Amazon ECR](https://aws.amazon.com/jp/ecr/) に image を push し、[AWS Fargate](https://aws.amazon.com/jp/fargate/) のタスクとして image を実行しました。
タスクの公開 IP アドレスに接続してオンラインでチャットが動くことを確認しました。
以上の内容について具体的な手順を記録・説明しました。

オンラインゲームのサーバー側の設定は疎通確認さえできてしまえば、残るは単なるゲームプログラミングです。
ここを踏み台にどんなオンラインゲームにすると面白くなるか、ゲーム内容に専念して構想していただけたら良いなと思います。

## 参考文献

[Amazon Elastic Container Service とは](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/Welcome.html)
この公式ドキュメントを最初からタスク定義の章まで、示されるリンク先も確認してこの記事を書きました。 
私の説明が信用ならない方は、このドキュメントを全て読み切ることをオススメします。