3
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

Organization

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

Google Cloud + Gaming Advent Calendar 2020の22日目を担当させて頂きます
株式会社インフィニットループのnagodonこと名古屋と申します。

TL;DR

本記事ではGCPのGoogle Cloud Game ServersOpenMatchを使ったマルチプレイを実現する
ゲームサーバ.NET Core3.1でのAgonesOpenMatchの連携方法について記載します。
連携に特化した内容になっていますがご了承ください。
※触っていた段階では.NET5がでてなかったので.NET Core3.1となっております

はじめに

Google Cloud Game Serversに関してはKubernetesでゲームサーバを管理するAgonesのフルマネージドサービスになります。
Google Cloud Japan YuttyさんのGoogle Cloud Game Servers (beta) の紹介の記事を見ていただけるとGoogle Cloud Game Serversの概要がわかるので是非みてください。

Agonesに関しては公式サイトとGoogle Cloud + Gaming Advent Calendar 2020の1日目の
@go_vargoさんのAgonesとKubernetesの拡張機能に詳しく書かれてるので是非みてください。

OpenMatchに関してはGoogle Cloud + Gaming Advent Calendar 2020の13日目の
@castaneaiさんのOpen Matchを使ったローカル開発を考えるに詳しく書かれているので是非みてください。

前提

Google Cloud Game ServersAgonesOpenMatchそれぞれ用語やコンポーネントが多数出てくるため1記事ではとても書ききれるようなものではないため、各前提知識があるという想定で.NET Core3.1での連携の部分に特化して記載します。

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

連携部分はOpenMatchDirectorMatchFunctionを呼び出して、マッチングが成立したタイミングでゲームサーバを割り当てる際に連携します。
基本的にAgonesAllocatorServiceを使用するのが推奨されているため、AllocatorServiceを使用して連携します。

AllocatorServiceに認証について

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

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

証明書の設定

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

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

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

#!/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の準備

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

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

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

  <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サービスを呼び出せるようにするため、AgonesOpenMatchのリポジトリからprotoファイルを取得しProtosディレクトリにコピーします。
※何か他にいい方法知ってる方いましたら教えて下さい

$ 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/

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

  <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データの受け皿となるクラスを用意します。

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を呼び出します。

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を設定してチケットをアサインする
            .........
        }
    }
}

これでAgonesOpenMatchを連携する事ができます。
あとは実際にプロジェクトをコンテナ化して、Kuberetes上のPodに反映することで、連携の確認ができます。

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

最後に

一部抜粋してのご紹介となりましたが、.NET Core3.1を使ったAgonesOpenMatchの連携についてでした。

AgonesOpenMatchも基本go前提と思われますが、他の言語でもMatchFunctionDirectorの実装はできるのでプロジェクトに合わせてこの辺は判断するとよいかなとお思います。
ただやはりgoで書いた方が早く書けると思います。

余談

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
3
Help us understand the problem. What are the problem?