.NETCoreでGoogle Cloud Game ServersとOpenMatchの連携してみる

[Google Cloud + Gaming Advent Calendar 2020](https://qiita.com/advent-calendar/2020/gcp-gaming)の22日目を担当させて頂きます
[株式会社インフィニットループ](https://www.infiniteloop.co.jp/)のnagodonこと名古屋と申します。

# TL;DR

本記事ではGCPの[Google Cloud Game Servers](https://cloud.google.com/game-servers/?hl=ja)と[OpenMatch](https://open-match.dev/site/)を使った`マルチプレイ`を実現する
`ゲームサーバ`の`.NET Core3.1`での[Agones](https://agones.dev/site/)と[OpenMatch](https://open-match.dev/site/)の連携方法について記載します。
連携に特化した内容になっていますがご了承ください。
※触っていた段階では`.NET5`がでてなかったので`.NET Core3.1`となっております

# はじめに

[Google Cloud Game Servers](https://cloud.google.com/game-servers/?hl=ja)に関しては[Kubernetes](https://kubernetes.io/ja/)でゲームサーバを管理する[Agones](https://agones.dev/site/)のフルマネージドサービスになります。
Google Cloud Japan [Yutty](https://medium.com/@ykawahara)さんの[Google Cloud Game Servers (beta) の紹介](https://medium.com/google-cloud-jp/google-cloud-game-servers-beta-dfd4da6f3e7b)の記事を見ていただけると[Google Cloud Game Servers](https://cloud.google.com/game-servers/?hl=ja)の概要がわかるので是非みてください。

[Agones](https://agones.dev/site/)に関しては公式サイトと[Google Cloud + Gaming Advent Calendar 2020](https://qiita.com/advent-calendar/2020/gcp-gaming)の1日目の
@go_vargoさんの[AgonesとKubernetesの拡張機能](https://qiita.com/go_vargo/items/f2ef5c799c7c73a2d0ff)に詳しく書かれてるので是非みてください。

[OpenMatch](https://open-match.dev/site/)に関しては[Google Cloud + Gaming Advent Calendar 2020](https://qiita.com/advent-calendar/2020/gcp-gaming)の13日目の
@castaneaiさんの[Open Matchを使ったローカル開発を考える](https://castaneai.hatenablog.com/entry/open-match-local-development)に詳しく書かれているので是非みてください。

# 前提

[Google Cloud Game Servers](https://cloud.google.com/game-servers/?hl=ja)、[Agones](https://agones.dev/site/)、[OpenMatch](https://open-match.dev/site/)それぞれ用語やコンポーネントが多数出てくるため１記事ではとても書ききれるようなものではないため、各前提知識があるという想定で`.NET Core3.1`での連携の部分に特化して記載します。

# AgonesとOpenMatchの連携方法について

連携部分は[OpenMatch](https://open-match.dev/site/)の`Director`で`MatchFunction`を呼び出して、マッチングが成立したタイミングで`ゲームサーバ`を割り当てる際に連携します。
基本的に[Agones](https://agones.dev/site/)の`AllocatorService`を使用するのが推奨されているため、`AllocatorService`を使用して連携します。

# AllocatorServiceに認証について

AllocatorServiceはmTLS認証を用いて認証を行うため事前準備として証明書の設定が必要になります。
https://agones.dev/site/docs/advanced/allocator-service/
※予約済みIPを使う場合はサーバの証明書は設定する必要はありません

今回は予約済みIPを使わない想定でサーバ証明証明書を設定するやり方を記載します。
基本的には公式のドキュメント通りになるのですが、環境構築ごとに毎回ビルドをしなくてもいいように
`GCP`の[SecretManager](https://cloud.google.com/secret-manager)に自身で作成した証明書を保存してソースコードで使えるようにします。

# 証明書の設定

証明書を設定するには`cert-manager`が必要ため、`Kubernetes`に事前にインストールします。

```shell
$ kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.1.0/cert-manager.yaml
```

次に以下のシェルスクリプトを作成して、`agones-allocator`の`LoadBalancer`に`エフェメラルグローバルIP`が付与された後にスクリプトを実行します。
やっていることは自己証明書を作成して、[Agones](https://agones.dev/site/)への設定と`.NET Core3.1`のアプリから証明書を取得できるように[SecretManager](https://cloud.google.com/secret-manager)にデータを保存する形になります。

```shell
#!/bin/bash -u

KEY_FILE=client.key
CERT_FILE=client.crt
TLS_CA_FILE=ca.crt
EXTERNAL_IP=$(kubectl get services agones-allocator -n agones-system -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
REGION=asia-northeast2

# 初回のみ実行されるので適宜質問に答えてファイルを生成します
if [ ! -f "${KEY_FILE}" ]; then
  openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ${KEY_FILE} -out ${CERT_FILE}
fi

# Create a self-signed ClusterIssuer
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: selfsigned
spec:
  selfSigned: {}
EOF

# Create a Certificate with IP for the allocator-tls secret
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: allocator-tls
  namespace: agones-system
spec:
  commonName: ${EXTERNAL_IP}
  ipAddresses:
    - ${EXTERNAL_IP}
  secretName: allocator-tls
  issuerRef:
    name: selfsigned
    kind: ClusterIssuer
EOF

CERT_FILE_VALUE=$(cat ${CERT_FILE} | base64 -w 0)
TLS_CA_VALUE=$(kubectl get secret allocator-tls -n agones-system -ojsonpath='{.data.ca\.crt}')
KEY_FILE_VALUE=$(cat ${KEY_FILE} | base64 -w 0)

kubectl get secret allocator-tls-ca -o json -n agones-system | jq '.data["tls-ca.crt"]="'${TLS_CA_VALUE}'"' | kubectl apply -f -
kubectl get secret allocator-client-ca -o json -n agones-system | jq '.data["client_trial.crt"]="'${CERT_FILE_VALUE}'"' | kubectl apply -f -

SECRET_JSON="{\"Ip\":\"${EXTERNAL_IP}\",\"ClientKey\":\"${KEY_FILE_VALUE}\",\"ClientCert\":\"${CERT_FILE_VALUE}\",\"TlsCert\":\"${TLS_CA_VALUE}\"}"
echo ${SECRET_JSON} | gcloud secrets create agones-allocator-info --data-file=- --replication-policy user-managed --locations ${REGION}
```

これで準備が整ったので`.NET Core3.1`で作成した`Director`のコードから呼び出せるようにします。

# Directorの準備

`AllocatorService`は`gRPC`または`HTTP`で呼び出すことができますが、`gRPC`で実装するのがお勧めなので、今回は`gRPC`で進めます。
事前に`nuget`で以下のパッケージを`プロジェクト`にインストールします。

- Google.Api.CommonProtos
- Google.Cloud.SecretManager.V1
- Google.Protobuf
- Grpc
- Grpc.Core
- Grpc.Net.Client
- Grpc.Tools
- Newtonsoft.Json

`.csproj`ファイル的には以下の形になります(実装時点)

```xml
  <ItemGroup>
    <PackageReference Include="Google.Api.CommonProtos" Version="2.2.0" />
    <PackageReference Include="Google.Cloud.SecretManager.V1" Version="1.2.0" />
    <PackageReference Include="Google.Protobuf" Version="3.14.0" />
    <PackageReference Include="Grpc" Version="2.33.1" />
    <PackageReference Include="Grpc.Core" Version="2.33.1" />
    <PackageReference Include="Grpc.Net.Client" Version="2.33.1" />
    <PackageReference Include="Grpc.Tools" Version="2.33.1">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
  </ItemGroup>
```

また`Director`のコード内から`Grpcサービス`を呼び出せるようにするため、[Agones](https://agones.dev/site/)と[OpenMatch](https://open-match.dev/site/)のリポジトリから`proto`ファイルを取得し`Protos`ディレクトリにコピーします。
※何か他にいい方法知ってる方いましたら教えて下さい

```shell-session
$ go get -u github.com/GoogleCloudPlatform/open-match
$ go get -u github.com/GoogleCloudPlatform/agones
$ mkdir -p Protos/{api,proto,google/protobuf}
$ cp $GOPATH/src/github.com/GoogleCloudPlatform/open-match/api/backend.proto Protos/api/
$ cp $GOPATH/src/github.com/GoogleCloudPlatform/open-match/api/messages.proto Protos/api/
$ cp -r $GOPATH/src/github.com/GoogleCloudPlatform/agones/proto/allocation Protos/proto/
$ cp -r $GOPATH/src/github.com/GoogleCloudPlatform/open-match/third_party/google Protos/
$ cp -r $GOPATH/src/github.com/GoogleCloudPlatform/open-match/third_party/protoc-gen-swagger Protos/
$ cp -r $GOPATH/src/github.com/protocolbuffers/protobuf/src/google/protobuf Protos/google/
$ cp $GOPATH/src/github.com/GoogleCloudPlatform/agones/vendor/github.com/golang/protobuf/ptypes/any/any.proto Protos/google/protobuf/
$ cp $GOPATH/src/github.com/GoogleCloudPlatform/agones/vendor/github.com/golang/protobuf/ptypes/timestamp/timestamp.proto Protos/google/protobuf/
```

コピーしたら自動生成できるように`.csproj`に`ItemGroup`タグを追加して、その中に`Protobuf`タグで必要な`proto`ファイルを`インポート`するように設定します。

```xml
  <ItemGroup>
    <Protobuf Include="Protos/protoc-gen-swagger/options/annotations.proto" GrpcServices="None" AdditionalImportDirs="./Protos/" />
    <Protobuf Include="Protos/protoc-gen-swagger/options/openapiv2.proto" GrpcServices="None" AdditionalImportDirs="./Protos/" />
    <Protobuf Include="Protos/api/messages.proto" GrpcServices="None" AdditionalImportDirs="./Protos/" />
    <Protobuf Include="Protos/api/backend.proto" GrpcServices="Client" AdditionalImportDirs="./Protos/" />
    <Protobuf Include="Protos/proto/allocation/allocation.proto" GrpcServices="Client" AdditionalImportDirs="./Protos/" />
  </ItemGroup>
```

編集が終わったらあとは`dotnet build`して`Grpc`のコードを生成して使用できるようにします。

# DirectorからAllocatorServiceを呼び出す

まず`SecretManager`から受け取るJsonデータの受け皿となるクラスを用意します。

```csharp
using System;
using System.Text;

namespace MatchDirector.Domain.Models
{
    public class AgonesAllocatorInfo
    {
        public string Ip { get; set; }
        public string ClientKey { get; set; }
        public string ClientCert { get; set; }
        public string TlsCert { get; set; }

        public string RawClientKey()
        {
            return FromBase64String(ClientKey);
        }

        public string RawClientCert()
        {
            return FromBase64String(ClientCert);
        }

        public string RawTlsCert()
        {
            return FromBase64String(TlsCert);
        }

        private string FromBase64String(string base64String)
        {
            return new UTF8Encoding().GetString(Convert.FromBase64String(base64String));
        }
    }
}

```

次に`Program.cs`で実際に`AllocatorService`を呼び出します。

```csharp
using static Allocation.AllocationService;
using static OpenMatch.BackendService;
using Allocation;
using Google.Api.Gax.ResourceNames;
using Google.Cloud.SecretManager.V1;
using Google.Protobuf;
using Grpc.Core;
using Grpc.Net.Client;
using MatchDirector.Domain.Models;
using Newtonsoft.Json;
using OpenMatch;

namespace MatchDirector
{
    class Program
    {
        public static async Task Main(string[] args)
        {
　　　　　　　// SecretManagerから証明書情報取得してDeserializeしておく
            SecretManagerServiceClient client = SecretManagerServiceClient.Create();
            var json = client.AccessSecretVersion(new SecretVersionName(ProjectId, "agones-allocator-info", "1")).Payload.Data.ToStringUtf8();
            var agonesAllocatorInfo = JsonConvert.DeserializeObject<AgonesAllocatorInfo>(json);

            // MatchFunctionを実行してマッチング情報を取得する
            .........

            // 取得した証明書を使用してAllocatorServiceを呼び出す
            var creds = new SslCredentials(agonesAllocatorInfo.RawTlsCert(), new KeyCertificatePair(agonesAllocatorInfo.RawClientCert(), agonesAllocatorInfo.RawClientKey()));
            var channel = new Channel($"{agonesAllocatorInfo.Ip}:443", creds);
            var client = new AllocationService.AllocationServiceClient(channel);
            var allocationResponse = await client.AllocateAsync(new AllocationRequest {
                Namespace = "default",
                MultiClusterSetting = new Allocation.MultiClusterSetting { Enabled = false }
            });

            // あとはAssignGroupにallocationResponseにあるIPとPortを設定してチケットをアサインする
            .........
        }
    }
}
```

これで[Agones](https://agones.dev/site/)と[OpenMatch](https://open-match.dev/site/)を連携する事ができます。
あとは実際にプロジェクトをコンテナ化して、`Kuberetes`上の`Pod`に反映することで、連携の確認ができます。

実際に試せるコードをリポジトリにあげておきましたのでご参考までに見て頂ければと思います。
[nagodon/agones-open-match-demo](https://github.com/nagodon/agones-open-match-demo)
あくまでサンプルなので運用で使うことはしないようにお願い致します。
また今回の記事は結構抜粋してるためリポジトリの内容と異なっていますのでご注意ください。

# 最後に

一部抜粋してのご紹介となりましたが、`.NET Core3.1`を使った[Agones](https://agones.dev/site/)と[OpenMatch](https://open-match.dev/site/)の連携についてでした。

[Agones](https://agones.dev/site/)も[OpenMatch](https://open-match.dev/site/)も基本`go`前提と思われますが、他の言語でも`MatchFunction`、`Director`の実装はできるのでプロジェクトに合わせてこの辺は判断するとよいかなとお思います。
ただやはりgoで書いた方が早く書けると思います。

# 余談

僕自身は`go`は書いてなかったので`go`のサンプルを`C#`に置き換えるのにはちょっと苦労しました