OpenMatch+Agonesを試す(minikube環境) [1/2]

# 概要
k8sのプロダクトである、マッチングシステムの`OpenMatch`とゲームサーバー管理の`Agones`を組み合わせた小規模なアプリケーションをminikube環境で動かします。

https://open-match.dev
https://agones.dev

公式ドキュメント内のGetting StartedとTutorialをベースに変更しながら組み合わせていきます。

※ゲームサーバはAgonesのTutorialを2クライアント接続可能なように改変
※ゲームクライアントは`curl`でOpenMatchから接続情報を取得し、`nc`でUDPパケット送信するだけとします

# 実行環境
- macOS Catalina(10.15.3)
- minikube(--vm-driver=virtualbox)
- OpenMatch(0.9.0)
- Agones(1.3.0)
- Go(1.13.7)

# 全体構成
理解に自信が無いので、間違いがありましたらコメントにてご指摘頂けると嬉しいです。

## 構成図
![OpenMatch+Agones.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/56893/bbfbb378-fe50-e3c7-f97c-f3e720be9104.png)


## シーケンス図
![sequence.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/56893/8d42c5c8-e28d-51f9-1298-6d0d78bd222c.png)


# 構築

## minikube install
デフォルトの設定(cpus 2, memory 2048)ではOpenMatchが動作しないため注意。
https://open-match.dev/site/docs/installation/

```shell:bash

brew install minikube
minikube config set vm-driver virtualbox
minikube config set cpus 4
minikube config set memory 4096
minikube start
eval $(minikube -p minikube docker-env)
```

## GameServer
ソースコード
https://github.com/suecideTech/try-openmatch-agones/blob/master/GameServer/mod_simple-udp/main.go

```golang:main.go
var addrs = map[uint]net.Addr{}
(略)
func readWriteLoop(conn net.PacketConn, stop chan struct{}, s *sdk.SDK) {
	b := make([]byte, 1024)
	for {
		sender, txt := readPacket(conn, b)

		switch addr := sender.(type) {
		case *net.UDPAddr:
			addrs[uint(addr.Port)] = addr
		}
(略)
// respond responds to a given sender.
func respond(conn net.PacketConn, sender net.Addr, txt string) {
	for _, sendaddr := range addrs {
		if _, err := conn.WriteTo([]byte(txt), sendaddr); err != nil {
			log.Fatalf("Could not write to udp stream: %v", err)
		}
	}
}
```

とりあえずローカル環境内で複数クライアントから接続出来るよう、接続元Portのみ記憶するように変更しました。(いろいろと問題はありますが...)

Dockerイメージ名は`localimage/mod_simple-udp:0.1`にします。

```shell:bash
docker build -t localimage/mod_simple-udp:0.1 .
```

## Agones
#### インストール

```shell:bash
kubectl create namespace agones-system
kubectl apply -f https://raw.githubusercontent.com/googleforgames/agones/release-1.3.0/install/yaml/install.yaml
```

#### Fleet

GameServerのイメージ`localimage/mod_simple-udp:0.1`を、2つ確保するFleetを作成します。

Fleet.yaml
https://github.com/suecideTech/try-openmatch-agones/blob/master/Deployment/Fleet.yaml

Deploy Fleet

```shell:bash
kubectl apply -f Fleet.yaml
```
レプリカ数を2で指定しているためGameServerが2つデプロイされていることが確認出来ます。

```shell:bash
$ kubectrl get gameserver
NAME                     STATE   ADDRESS          PORT   NODE       AGE
simple-udp-7pzwj-5hpnz   Ready   192.168.99.100   7052   minikube   5s
simple-udp-7pzwj-tp8dq   Ready   192.168.99.100   7074   minikube   5s
```
試しにncコマンドで接続し、電文「ALLOCATE」を送信します。
電文を受けた際の挙動はGameServerのソースコードを確認してください。

```shell:bash
$ nc -u 192.168.99.100 7052
hello
ACK: hello
ALLOCATE
ACK: ALLOCATE

---

$ kubectl get gameserver
NAME                     STATE       ADDRESS          PORT   NODE       AGE
simple-udp-7pzwj-5hpnz   Allocated   192.168.99.100   7052   minikube   3m27s
simple-udp-7pzwj-tp8dq   Ready       192.168.99.100   7074   minikube   3m27s
```
STATEがAllocatedになりました。
全体構成のシーケンス図ではAllocateServiceがAlocateするよう記載していますが、GameServer内部から自身のStateを変更するSDKも用意されています。

次に電文「EXIT」を送信しGameServerをShutdownします。

```shell:bash
EXIT
ACK: EXIT

---

$ kubectl get gameserver
NAME                     STATE   ADDRESS          PORT   NODE       AGE
simple-udp-7pzwj-l2xtt   Ready   192.168.99.100   7310   minikube   5s
simple-udp-7pzwj-tp8dq   Ready   192.168.99.100   7074   minikube   12m
```
GameServer`simple-udp-7pzwj-5hpnz`が消え、新たに`simple-udp-7pzwj-l2xtt`が生成されています。
GameServerは「EXIT」を受けるとAgones.SDKの`Shutdown()`を実行します。
これによりSTATEがUnhealthyに遷移しますが、FleetはUnhealthyのGameServerを積極的に削除し、新たなGameServerをデプロイするように動作します。

次にFleetAutoscalerをデプロイします。
AgonesのGetting Startedをそのまま使います。

#### FleetAutoscaler

```shell:bash
$ kubectl apply -f https://raw.githubusercontent.com/googleforgames/agones/release-1.3.0/examples/simple-udp/fleetautoscaler.yaml
```
FleetAutoscalerデプロイ後に再度GameServerをAllocateしてみます。

```shell:bash
$ nc -u 192.168.99.100 7310
ALLOCATE
ACK: ALLOCATE

---

kubectl get gameserver
NAME                     STATE       ADDRESS          PORT   NODE       AGE
simple-udp-7pzwj-l2xtt   Allocated   192.168.99.100   7310   minikube   11m
simple-udp-7pzwj-rm4p7   Ready       192.168.99.100   7157   minikube   3s
simple-udp-7pzwj-tp8dq   Ready       192.168.99.100   7074   minikube   23m
```

ReadyのGameServerが2つ保たれているのがわかります。
先程デプロイしたyamlファイルには`bufferSize: 2`と定義しているため、StateがReadyのGameServerが２つになるようにFleetのreplica数を調整しています。
> bufferSize is the size of a buffer of “ready” and “reserved” game server instances.

Fleetの詳細を確認しておきます。

```shell:bash
$ kubectl describe fleet simple-udp
    :
Spec:
  Replicas:    3
    :
Status:
  Allocated Replicas:  1
  Ready Replicas:      2
  Replicas:            3
  Reserved Replicas:   0
    :
Events:
  Type    Reason                 Age    From              Message
  ----    ------                 ----   ----              -------
  Normal  CreatingGameServerSet  30m    fleet-controller  Created GameServerSet simple-udp-7pzwj
  Normal  ScalingGameServerSet   6m50s  fleet-controller  Scaling active GameServerSet simple-udp-7pzwj from 2 to 3
```

#### AllocateService

以下のTutorialをベースに、GameServerNameとIP,Portを応答するように改造します。
https://agones.dev/site/docs/tutorials/allocator-service-go/

オレオレ証明書を作成するなどの手順がありますが、minikubeのローカルな環境にデプロイするためhttp(80)で通信するように変更もしておきます。

ソースコード
https://github.com/suecideTech/try-openmatch-agones/blob/master/AllocateService/mod_allocator-service/main.go

Dockerイメージ名は`localimage/mod_allocator-service:0.1`にします。

```shell:bash
docker build -t localimage/mod_allocator-service:0.1 .
```
デプロイ

```shell:bash
kubectl create -f service-account.yaml
kubectl apply -f allocator-service.yaml
```
これで`fleet-allocator-backend.svc.cluster.local:80/address`にhttpRequestを出せばサーバを確保し、接続用のIP/Portが返ってきます。

デバッグ用にNodePortで公開しているので`minikube service`でIP/Portを確認し、minikube外からアクセスしてAllocateを実施してみます。

```shell:bash
$ minikube service list
|---------------|---------------------------|-----------------------------|-----|
|   NAMESPACE   |           NAME            |         TARGET PORT         | URL |
|---------------|---------------------------|-----------------------------|-----|
    :
| default       | fleet-allocator-backend   | http://192.168.99.100:32288 |
    :
|---------------|---------------------------|-----------------------------|-----|

$ kubectl get gs
NAME                     STATE   ADDRESS          PORT   NODE       AGE
simple-udp-7pzwj-rm4p7   Ready   192.168.99.100   7157   minikube   107m
simple-udp-7pzwj-wnt28   Ready   192.168.99.100   7150   minikube   113s

$ curl -k -u v1GameClientKey:EAEC945C371B2EC361DE399C2F11E http://192.168.99.100:32288/address
{"status":{"state":"Allocated","gameServerName":"simple-udp-7pzwj-rm4p7","ports":[{"name":"default","port":7157}],"address":"192.168.99.100","nodeName":"minikube"}}

$ kubectl get gs
NAME                     STATE       ADDRESS          PORT   NODE       AGE
simple-udp-7pzwj-rm4p7   Allocated   192.168.99.100   7157   minikube   107m
simple-udp-7pzwj-wnt28   Ready       192.168.99.100   7150   minikube   2m32s
simple-udp-7pzwj-xfzx9   Ready       192.168.99.100   7795   minikube   9s
```

curlの応答として以下が取得出来ました。

- GameServerName: "simple-udp-7pzwj-rm4p7"
- Ports: 7157
- Address: 192.168.99.100

このIPとPortはクラスタ外からアクセス可能なものです。

また、GameServerの1つがSTATE: Allocatedに変化していることがわかります。

##### Tipes
Allocate直後に情報の受け渡しを実施する際は以下の方法が考えられます

- GameServerのLabelに情報をセット
    - GameServer内からはAgonesのSDKにmetadataを取得/監視するAPIが用意されている
- RESTやgRPCで通知
    - GameServerNameとPod名は同一のためPodの詳細からクラスタ内のIPアドレスがわかる
- GameServerから別のサーバーリソースへ問い合わせ
    - 前途のmetadata監視APIでAllocatedになったタイミングでコールバック実行

# 今日はここまで
ここまででゲームサーバーを確保、再起動、スケーリングする方法がわかりました。

長くなりましたので記事を分けます。
後半ではOpenMatchの変更箇所と動作確認をした後にAgones、OpenMatchの自分の理解を書き連ねます。

【後半】
OpenMatch+Agonesを試す(minikube環境) [1/2]
https://qiita.com/suecideTech/items/4a04febaaf4f21e7e1a6