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

# はじめに
本記事は以下記事の続きです。

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

# 構築
とりあえず以下のAllocateServiceに対してhttpRequestを出せばゲームサーバーが確保され、IPとPortが返ってくる状況になってます。
`fleet-allocator-backend.svc.cluster.local:80/address`

## OpenMatch

Tutorial(matchmaker101)をベースに進めます。
https://open-match.dev/site/docs/tutorials/matchmaker101/

#### Director

サーバ確保を前途のendpointに対して問い合わせた結果を用いるように変更します。

ソースコード
https://github.com/suecideTech/try-openmatch-agones/blob/master/OpenMatch/mod_matchmaker101/director/main.go

```golang:main.go
const (
	// The Host and Port for the AllocateService endpoint.
	allocateHostName = "http://fleet-allocator-endpoint.default.svc.cluster.local/address"
	allocateKey      = "v1GameClientKey"
	allocatePass     = "EAEC945C371B2EC361DE399C2F11E"
)
：(省略)
func assign(be pb.BackendServiceClient, matches []*pb.Match) error {
：
		for _, t := range match.GetTickets() {
			ticketIDs = append(ticketIDs, t.Id)
		}

		// Request Connection to AllocateService.
		aloReq, err := http.NewRequest("GET", allocateHostName, nil)
		aloReq.SetBasicAuth(allocateKey, allocatePass)
		client := new(http.Client)
		resp, err := client.Do(aloReq)
：
		conn = fmt.Sprintf("%s:%d", alo.Status.Address, alo.Status.Ports[0].Port)

		req := &pb.AssignTicketsRequest{
			TicketIds: ticketIDs,
			Assignment: &pb.Assignment{
				Connection: conn,
			},
		}

```

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

```sh:bash
docker build -t localimage/mod_director:0.1 .
```

尚、このTutorial(matchmaker101)では以下3種類のゲームモードが用意されており、各ゲームモードで4人揃ったタイミングでMatch成立になります。

```golang:profile.go
// generateProfiles generates test profiles for the matchmaker101 tutorial.
func generateProfiles() []*pb.MatchProfile {
	var profiles []*pb.MatchProfile
	modes := []string{"mode.demo", "mode.ctf", "mode.battleroyale"}
```

#### Matchfunction

4人揃わないとマッチングしないのはデバッグがつらいので2人に変更します。

ソースコード
https://github.com/suecideTech/try-openmatch-agones/blob/master/OpenMatch/mod_matchmaker101/matchfunction/mmf/matchfunction.go

```go:matchfunction.go
const (
	matchName              = "basic-matchfunction"
	ticketsPerPoolPerMatch = 2 // ← 4
)
```

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

```sh:bash
docker build -t localimage/mod_matchfunction:0.1 .
```

#### MatchFrontend

Tutorial(matchmaker101)のFrontendは以下1〜3を繰り返しキックするものになっています。

1. ランダムにゲームモードを選択
2. ゲームモードを元にTicketをCreate
3. 別スレッドを立ててTicketをポーリング、Assignmentがあればログを出しTicketをDelete

Clientに要求を受けTicketを作成し、AssignmentをClientへ応答したいのでRESTの口を用意します。

ソースコード
https://github.com/suecideTech/try-openmatch-agones/blob/master/OpenMatch/mod_matchmaker101/frontend/main.go

labstack/echoを使わせて頂きRESTサーバーにしました。

以下のようなNodePortで公開したURLへアクセスするとゲームモードに対応するTicketを作成し、マッチングが完了次第IPとPortをClientへ応答します。
`http://xxx.xxx.xxx.xxx:PORT/frontend/ゲームモード(mode.demo/mode.ctf/mode.battleroyale)`

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

```sh:bash
docker build -t localimage/mod_frontend:0.1 .
```

#### Deploy

上記Director, Matchfunction, MatchFrontendをまとめて以下yamlでデプロイします。

https://github.com/suecideTech/try-openmatch-agones/blob/master/OpenMatch/mod_matchmaker101/matchmaker.yaml

## 動作確認

`curl`と`nc`コマンドで動作確認をします。
`minikube service`でIP/Portを確認し、minikube外の2端末からcurlでアクセスします。

```shell:bash
$ minikube service list
|---------------|---------------------------|-----------------------------|-----|
|   NAMESPACE   |           NAME            |         TARGET PORT         | URL |
|---------------|---------------------------|-----------------------------|-----
    :
| openmatch     | frontend-endpoint         | http://192.168.99.100:30940 |
    :
|---------------|---------------------------|-----------------------------|-----

--- ★端末1★ ---

$ curl http://192.168.99.100:30940/frontend/mode.demo
{"ip":"192.168.99.100","port":"7558"}

--- ★端末2★ ---

$ curl http://192.168.99.100:30940/frontend/mode.demo
{"ip":"192.168.99.100","port":"7558"}

---

$ kubectl get gs
NAME                     STATE       ADDRESS          PORT   NODE       AGE
simple-udp-7pzwj-frzfr   Ready       192.168.99.100   7718   minikube   63s
simple-udp-7pzwj-hn6br   Allocated   192.168.99.100   7558   minikube   5m47s
simple-udp-7pzwj-sfdvs   Ready       192.168.99.100   7327   minikube   5m43s
```

取得したIPとPortに対してncコマンドで繋ぎます。

```sh:bash
--- ★端末1★ ---
$ nc -u 192.168.99.100 7558
hi
ACK: hi
ACK: hello
yes
ACK: yes
ACK: no
EXIT
ACK: EXIT

$

--- ★端末2★ ---
$ nc -u 192.168.99.100 7558
hello
ACK: hello
ACK: yes
no
ACK: no
ACK: EXIT

$

---
$ kubectl get gs
NAME                     STATE   ADDRESS          PORT   NODE       AGE
simple-udp-7pzwj-9ffss   Ready   192.168.99.100   7245   minikube   5m28s
simple-udp-7pzwj-frzfr   Ready   192.168.99.100   7718   minikube   9m35s
```

少しわかりにくいですが...以下で動作を確認しました。

- 端末1で送った「yes」を端末1, 2共に受信
- 端末2で送った「no」を端末1, 2共に受信
- EXITでサーバを落とした後GameServer一覧からPort: 7558なGameServerが無い

# 所感
OpenMatchとAgonesを触れてみた感想を書き連ねます。

### OpenMatchについて

公式ドキュメントの他に以下の記事がとても参考になりました。
https://medium.com/google-cloud-jp/open-match-super101-aaf3e54bd4e7
https://storage.googleapis.com/gweb-cloudnext19.appspot.com/event-assets/tokyo/materials/D1-5-I08.pdf

*この記事を書いている段階ではまだRelease1.0に到達してないのですが、*
*各コンポーネントの名前が何度か変わっているようなので混乱しないよう注意*
*筆者は何度か勘違いしました...*

＊追記予定＊

### Agonesについて

公式ドキュメントの他に以下の記事がとても参考になりました。
https://medium.com/google-cloud-jp/agones-beginner-jp-5a6553e7e9a4

以下、上記記事の引用
> Agones を k8s 上で実行することにより、以下のようなメリットがあります。
> 1. 実行中のゲームサーバーの保護
> 2. デファクトスタンダードであるk8s上で実行できるため、オペレーションの簡素化ができる
> 3. SDK がいろいろな言語で提供されている

個人的には使う時だけゲームサーバーを保護するので、使っていないゲームサーバーはどんどんスケールインすることができ、サーバリソースの最適化にk8sの恩恵をフルで受けれるのが嬉しいポイントかなと思います。

また、ゲームだけでなくステートフルな回線繋ぎっぱなしのWebアプリケーションを運用する際にもAgones使えるんじゃないかなって思いました。

#### GameServer

- 詳しく確認していませんが基本的にPodを拡張したCRDのはず
  - `kubectl get pods`で一覧にGameServerも表示される
  - Podの機能は使えるかと思います
- 基本的な状態遷移はゲームサーバーを使い捨てる前提になっています
  - ゲームセッション前はState: Ready
  - ゲームセッション中はState: Allocated
  - ゲームセッション後はState: Unhealthy
- 現在サポートされているSDKの機能は以下
  - `Ready()`
     - State: Readyに遷移
  - `Shutdown()`
     - State: Unhealthyに遷移
  - `Allocate()`
     - State: Allocatedに遷移
  - `Reserve(sec)`
     - State: Reservedに遷移、指定時間後Readyに遷移
     - State: ReservedはFleetの削除対象にならない
     - GameServerAllocationでのAllocateも出来ない
     - マッチメイキングシステムにGameServerを登録するような使い方をする時に使用する？
  - `GameServer()`
     - GameServerの詳細を取得
     - `kuberctl describe gameserver`と同レベルの情報が得られるはず？
  - `WatchGameServer(func(...))`
     - GameServerの構成が変更時に登録したコールバックを呼び出す
     - GameServer内で、State, metadata(Labelやannotation)の変化を検知出来る
     - 後述のSetLabelやSetAnnotationで外部のプロセスと情報の伝達が出来る
  - `SetLabel(key, value)`
     - metadetaのLabelを変更する
  - `SetAnnotation(key, value)`
     - metadetaのAnnotationを変更する

状態遷移について
https://agones.dev/site/docs/reference/gameserver/
SDK
https://agones.dev/site/docs/guides/client-sdks/


#### Fleet

- 詳しく確認していませんが基本的にReplicaSetを拡張したCRDのはず
  - `kubectl get replicasets`で一覧にFleetも表示される
- Kubernetesでステートフルなゲームサーバをシンプルに運用するなら以下を担保する必要がある
    - ゲームセッション中はPodが勝手に消えないこと
    - ゲームセッション中にPodが別Nodeに移動しないこと
- FleetはReplicaSetに以下の要素が加わり上記の条件を満たしている
    - StateがAllocatedのGameServerは削除しない
        - GameServerのスケールインはゲームセッション前のものが対象となる
    - StateがAllocatedのGameServerは別Nodeに移動しない
    - GameServerはなるべく同一Nodeに固めて配置する
        - NodeのスケールインはPodの無いNodeが対象となる

https://agones.dev/site/docs/reference/fleet/
https://agones.dev/site/docs/advanced/scheduling-and-autoscaling/

#### FleetAutoscaler

- ReadyのGameServer数を調整出来る
    - https://agones.dev/site/docs/getting-started/create-fleetautoscaler/
- 30秒毎に設定(json)を同期するwebhookも用意されている
    - https://agones.dev/site/docs/getting-started/create-webhook-fleetautoscaler/

https://agones.dev/site/docs/reference/fleetautoscaler/