Unity+MagicOnionでメタバース空間を作ってみる（第二回）

はじめに
------------------------------------------------------
本記事は、複数回に分けてUnityとMagicOnionを用いてメタバース空間を構築する内容（備忘録）となっています。
前回は、UnityとMagicOnionで簡単なチャット機能とアバターの位置同期を実装しました。

第二回目は、前回実装したサーバー側のプログラムをAWS上にデプロイする方法を紹介します。
具体的には、サーバー側のプログラムをDockerを用いてコンテナ化し、AWSのEC2上にデプロイします。
なお、筆者はUnityやサーバーサイドの経験は浅く現在も学習中であるため、間違っている部分がある場合は教えて下さると幸いです。

------------------------------------------------------

動作環境や使用したアセットなど
--------------
* Windows 10
* Unity 2021.3.5f1
* Visual Studio 2019 16.11
* MagicOnion 4.5.1
* MessagePack 2.3.85
* gRPC 2.47.0
* Unity-Chan! Model 1.2.2
* Docker 20.10.17


# 1. サーバー側のプログラムをコンテナ化
## 1.1. プログラムの修正
前回作成したプログラムは、ローカル環境下でのみ通信ができますがコンテナ化してしまうと通信ができません。
そのため、Program.csを修正します。具体的には、プログラムが指定するIPアドレスをlocalhostではなく0.0.0.0:80に変更します。
```cs:Program.cs
using System;
using Grpc.Core;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Core;

namespace MagicOnionTest_Server
{
    class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder
                        .UseKestrel(options =>
                        {
                            // WORKAROUND: Accept HTTP/2 only to allow insecure HTTP/2 connections during development.
                            options.ConfigureEndpointDefaults(endpointOptions =>
                            {
                                endpointOptions.Protocols = HttpProtocols.Http2;
                            });
                        })
                        .UseUrls("http://0.0.0.0:80") //ここで待ち受けるIPアドレスを修正する
                        //.UseUrls("http://localhost:80")
                        .UseStartup<Startup>();
                });
    }
}
```
今回は待ち受けポートを80番に設定しました。

## 1.2. アプリの発行
サーバー側のプログラムは、.Net Coreを使用しており、今回は用意されているCLIを用いて発行します。
```
dotnet publish -c Release
```
無事に発行されたかを確認してみると、確かにアプリの実行に必要なファイル群ができていました。
![スクリーンショット 2022-08-27 022317.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/290975/2293298b-3625-9474-bdd5-f8cbf34f2312.png)

# 1.3. Dockerfileの作成
次に、このアプリをコンテナ上で実行できるようDockerfileをこのディレクトリ内に作成します。
```dockerfile:Dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:5.0
WORKDIR /
COPY . .
ENTRYPOINT ["dotnet", "MagicOnionTest-Server.dll"]
EXPOSE 80
```

続いて、カレントディレクトリをDockerfileを設置した場所へ移動します。
移動先は開発環境によって異なりますが、今回作成しているプロジェクトの設定では以下のように叩きます。
```
cd "プロジェクトディレクトリ"/bin/Release/net5.0
```
ひとまずこれでコンテナイメージをAWSに上げる前段階が終わりました。

# 2. AWS側の設定
## 2.1. ECR（Elastic Container Registry）
今回は、ECR内にコンテナイメージを保存し、ECS（Elastic Container Service）を用いてEC2インスタンスでコンテナを起動するようにします。なお、今回の設定は必要最低限のものとなりますので、実際に運用する場合ではセキュリティ面やロードバランサーの設定等を行った方が良いかと思われます。
また、AWSのアカウント設定やAWS CLIのインストール等は既に完了している前提で進めますのでご了承ください。

まずは、ECR内にリポジトリを作成します。今回はプライベートリポジトリとして設定します。
![スクリーンショット 2022-08-27 023606.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/290975/2e5d90fb-8825-ae96-10f8-a3c25eb7841c.png)
作成したリポジトリを選択すると、イメージのリストがでてきます。
![スクリーンショット 2022-08-27 023742.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/290975/acb25933-7be4-9b1f-c4b7-e2a56a0be2b4.png)
右上にある「プッシュコマンドの表示」を押すと、ありがたいことに作成したリポジトリに対応したプッシュコマンドをコピペすることができるようになっています。
先程カレントディレクトリをDockerfileを設定した場所に移動していたので、この状態で手順に沿ってコマンドを叩いていきます。
![スクリーンショット 2022-08-27 024014.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/290975/774e3f5b-760c-affb-af82-752c4cb13ca2.png)
最後の手順まで終わると、今回作成したアプリを実行できるコンテナイメージがリポジトリ内に出ているかと思います。
最後に、プッシュしたイメージのURIをコピーしておきます（後ほど使います）。
![スクリーンショット 2022-08-27 024505.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/290975/461465d5-0ee8-c16d-3404-122fb58d0606.png)

## 2.2. ECS（Elastic Container Service）
次に、ECS上で設定していきます。まずはクラスターを作成します。
今回は、「EC2 Linux + ネットワーキング」のテンプレートを選択します。
![スクリーンショット 2022-08-27 025208.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/290975/bf9feb55-0503-0695-3e36-30e18cebcd1e.png)
次のステップに進むと、クラスター名を始めインスタンスタイプの設定がありますが、これらの設定は各自の予算や状況によって適宜設定してください。なお、今回は予算を最低限に抑えるためにインスタンスタイプを「t2.micro」、インスタンス数は1に設定しました。

続いて、タスク定義を設定します。タスク定義は、保存したコンテナイメージを使用するために必要な物になります。
今回はEC2インスタンス上でコンテナを起動したいので、EC2インスタンスを選択します。
![スクリーンショット 2022-08-27 030039.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/290975/bb825d0d-27d7-7fbf-e635-9c09e84b8154.png)
次のステップでは、タスク定義名やタスクサイズの設定がありますが、こちらは先程と同様に各自の環境や状況に合わせて設定してください。
そして設定項目の中に「コンテナの追加」があるので、こちらを押すと起動するコンテナの詳細な設定ができます。
ここで、先程コピーしていたイメージURIをイメージ欄にペーストします。
また、ポートマッピングは、「80:80 tcp」で設定しておきます。
![スクリーンショット 2022-08-27 025942.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/290975/50aa540e-e773-f2d7-0d20-3ae005e91a3a.png)
その他設定を済ませると、無事にタスク定義が作成できました。

しかし、タスク定義はあくまで「定義」なので、これだけではコンテナを起動させることができません。
そのため、次はタスク定義に基づいてコンテナを起動させるためのサービスを作成していきます。
先程作成したクラスターを選択し、サービスタブから作成をします。
ここでの設定は、
・起動タイプ：EC2
・タスク定義：先程作成したタスク定義
・タスクの数：必要に応じて数値を設定
としておくこと以外は、ほとんど設定をいじる必要はなさそうです。
タスクの数を「必要に応じて数値を設定」としているのは、例えば「今はコンテナを起動させておかなくてもいいかな」という場合に「0」を設定することでコンテナの停止することができるためです。今回はタスクの数を「1」にしてみます。
![スクリーンショット 2022-08-27 031554.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/290975/6d753eec-e3c3-84b0-b9eb-a0628be5c33d.png)
無事にサービスの作成が完了すると、少し時間が経ってからタスクが実行し始めます。

## 2.3. Elastic IP
起動しているEC2インスタンスにElastic IPを設定することで、固定されたIPアドレスを割り当てることができます。
割り当て方等は今回は省略させていただきます。
割り当てたElastic IPに接続できるように、クライアント側のプログラムの修正を行います。具体的には、前回作成したChatApp.cs内で設定しているIPアドレスの値をElastic IPの値に変更します。
```cs:ChatApp.cs
        #region ボタン処理
        ///<summary>
        ///入室
        /// </summary>
        private async void OnClick_JoinButton()
        {
            //if (instance == null) return;

            if (!string.IsNullOrEmpty(nameInput.text))
            {
                userName = nameInput.text;
            }

            channel = GrpcChannelx.ForAddress("IPアドレス"); //ここをElastic IPの値に修正する
            streamingClient = await StreamingHubClient.ConnectAsync<IChatAppHub, IChatAppHubReceiver>(channel, this, cancellationToken: shutdownCancellation.Token);

            //既に参加しているユーザ情報を取得
            JoinerInfo joinerInfo = await streamingClient.JoinAsync(roomName, userName, Vector3.zero, Quaternion.identity);
            
            //自分のアバターの設定
            MyAvatarSetting(joinerInfo);

            //既に参加しているユーザをスポーン
            AddClientPlayers(joinerInfo.players);

            //スポーン
            chatPage.SetActive(true);
            isSpawn = true;
        }
```
修正し、クライアント側をビルドできたら複数立ち上げて接続ができるか、同期ができているかを確認します。
無事に確認ができたら、これでデプロイは完了です。

# 3. まとめ
所々端折っている部分がありますが、最低限の設定でAWS ECS上でサーバー側のアプリを動かすことができました。
AWSの設定は記事が多く残っているため、足りない部分は補っていただけると幸いです。
ひとまずこれでデスクトップアプリとして通信プレイをすることができましたが、せっかくのメタバース空間なので今度はVRに対応していきたいと思います。
ということで、次回はOculus Integrationを用いてアプリのVR化をしていきたいと思います。