🎮

AgonesをMetalLBを用いて、ローカルのMinikubeクラスタ上で稼働させ疎通確認する

2022/11/20に公開

要約

  • Agonesをローカル環境のMinikubeクラスタで、Hyper-Vドライバを使用して稼働させた
  • 上記の稼働にはMetalLBの力を借りた
  • 構成はTerraformで管理し、下準備が完了していればterraform applyで環境が構築できるようにした

GitHub repo

https://github.com/oniku-2929/tf_agones_minikube_hyperv

READMEはこちら
https://github.com/oniku-2929/tf_agones_minikube_hyperv/blob/main/README-ja.md

動作方法

https://github.com/oniku-2929/tf_agones_minikube_hyperv/blob/main/README-ja.md#通常

経緯

Agonesとは

Agonesとは,GoogleとUbisoftが共同で開発したKubernetes環境における専用ゲームサーバ管理を行う為のOSSです。

What is Agones?
Agones is an open source platform, for deploying, hosting, scaling, and orchestrating dedicated game servers for large scale multiplayer games, built on top of the industry standard, distributed system platform Kubernetes.
Agonesは、業界標準の分散システムプラットフォームであるKubernetes上に構築された、大規模なマルチプレイヤーゲーム用の専用ゲームサーバーのデプロイ、ホスティング、スケーリング、オーケストレーションのためのオープンソースプラットフォームです。

Built with both Cloud and on-premises infrastructure in mind, Agones can adjust its strategies as needed for Fleet management, autoscaling, and more to ensure the resources being used to host dedicated game servers are cost optimal for the environment that they are in.
クラウドとオンプレミスの両方のインフラを考慮して構築されたAgonesは、ゲーム専用サーバーをホストするために使用されているリソースが、その環境に最適なコストであることを保証するために、フリート管理、オートスケールなどの戦略を必要に応じて調整することができます。

上記の記事にも記載されている通り、FPSやMMO,MOBAといったジャンルのゲームサーバを開発する際は、多くの場合ステートフルなアプリケーションが必要とされます

Many of the popular fast-paced online multiplayer games such as competitive FPSs, MMOs and MOBAs require a dedicated game server
Dedicated game servers are stateful applications that retain the full game simulation in memory. But unlike other stateful applications, such as databases, they have a short lifetime. Rather than running for months or years, a dedicated game server runs for a few minutes or hours.
対戦型FPS、MMO、MOBAなど、人気の高い高速オンラインマルチプレイヤーゲームには、専用のゲームサーバーが必要なものが多くあります
ゲーム専用サーバーは、ゲーム・シミュレーションを完全にメモリ上に保持するステートフル・アプリケーションです。しかし、データベースなどの他のステートフル・アプリケーションとは異なり、その寿命は短いものです。数ヶ月、数年単位で稼働するのではなく、数分、数時間単位で稼働するのが専用ゲームサーバーです。
それらのプロセスのオーケストレーションを補助してくれる物となります。

ドキュメントを見てみるとクラスタを各種クラウドベンダーのK8sサービス(GKE,EKS,AKS)で作成する方法も記載されていますが
まずは、ローカルでの動作確認を行いたかったのでMinikubeでの環境構築について確認しました。
この時は、該当ページをよく読みもしないでさらっと流し読みだけして「結構簡単そうだなぁ~ハナホジー😗」と考えていました。
即落とし穴にはまるとも知らずに・・・

Minikubeとは

https://kubernetes.io/ja/docs/setup/learning-environment/minikube/

Minikubeはローカル環境でKubernetesを簡単に実行するためのツールです。

https://minikube.sigs.k8s.io/docs/start/

Installation

の項目にWindowsの場合のインストール方法も記載されています。

Minikubeの上でAgones動かすのなんて余裕ンゴwww → 「ツナガラナイデス・・・」

人生は甘くないし、「自分はドキュメントとか文章をちゃんと読んでいない」事を痛感させられます。
クラスタを作成し、Helmを使用した手順通りインストール作業を実施します。
Agonesのシステム系のコンポーネントの稼働を確認した後に
ゲームサーバを作成し
(この時実は初回のgameserver起動時のみゲームサーバがヘルスチェックに失敗する現象が発生しますが、そちらに関しては後述している「コンテナイメージのプルに時間かかりすぎ問題」をご参照ください)

そして、接続する為のサービスを作成してみますが
Pod群はしばらく経つと動き出しますが、サービスの状態を確認するといつまで経ってもEXTERNAL-IPがpending状態で、IPが割り当てられません。
この状態では手元のWindowsからクラスタに対してLoadBalancerを介して、nc等でパケットを投げても内部で動いてるコンテナへパケットが到達できません。

kubectl get svc --all-namespaces
NAMESPACE       NAME                               TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)                  AGE
agones-system   agones-allocator                   LoadBalancer   10.96.110.55     <pending>     443:31503/TCP            48m
agones-system   agones-allocator-metrics-service   ClusterIP      10.103.30.96     <none>        8080/TCP                 48m
agones-system   agones-controller-service          ClusterIP      10.108.162.228   <none>        443/TCP,8080/TCP         48m
agones-system   agones-ping-http-service           LoadBalancer   10.96.220.206    <pending>     80:30494/TCP             48m
agones-system   agones-ping-udp-service            LoadBalancer   10.102.150.109   <pending>     50000:32616/UDP          48m
default         agones-gameserver                  LoadBalancer   10.100.174.130   <pending>     7000:31168/UDP           18s

で、こういう時の為にminikube tunnelというコマンドが用意されています。
https://zenn.dev/gashirar/articles/36252be99e0833#type%3A-loadbalancerなserviceを使いたい

「はいはい、これでおっけーね🤭」とminikube tunnel -p agonesとコマンドを実行します。
すると、とりあえずEXTERNAL-IPにIPが割り当てられるわけですが、

kubectl get svc --all-namespaces
NAMESPACE       NAME                               TYPE           CLUSTER-IP       EXTERNAL-IP      PORT(S)                  AGE
agones-system   agones-allocator                   LoadBalancer   10.96.110.55     10.96.110.55     443:31503/TCP            50m
agones-system   agones-allocator-metrics-service   ClusterIP      10.103.30.96     <none>           8080/TCP                 50m
agones-system   agones-controller-service          ClusterIP      10.108.162.228   <none>           443/TCP,8080/TCP         50m
agones-system   agones-ping-http-service           LoadBalancer   10.96.220.206    10.96.220.206    80:30494/TCP             50m
agones-system   agones-ping-udp-service            LoadBalancer   10.102.150.109   10.102.150.109   50000:32616/UDP          50m
default         agones-gameserver                  LoadBalancer   10.100.174.130   10.100.174.130   7000:31168/UDP           96s

この状態で、nc -u 10.100.174.130 7000とコマンド打って、手元からagones-gameserver:LoadBalancer経由で接続しようとしても、接続ができません。
で、調べてみた所例えば

  • nc 10.96.220.206 80としてagones-ping-http-serviceにTCPで接続にいくと接続できる」
  • 「接続先をagones-ping-udp-serviceにするとこれも接続できない」
    という症状が発生している事がわかるので、なんかUDP通信の場合のみ疎通ができていない事がわかります。

「え、なんで?」となり、よくよくドキュメントを読み返してみます。

Known working drivers

Windows (amd64)
hyper-v (might need this blog post and/or this comment for WSL support)

「うん、自分の環境はWindowsでHyper-Vドライバで稼働させてるな・・・」

Depending on your operating system and virtualization platform that you are using with Minikube, it may not be possible to connect directly to a GameServer hosted on Agones as you would on a cloud hosted Kubernetes cluster.
Minikubeで使用しているOSや仮想化プラットフォームによっては、クラウドでホストされているKubernetesクラスタのように、Agones上でホストされているGameServerに直接接続できない場合があります。

「むむ?🤨」

Warning
minikube tunnel (docs) does not support UDP (Github Issue) on some combination of operating system, platforms and drivers, but is required when using the Service workaround.
警告
minikube tunnel (docs) は、OS、プラットフォーム、ドライバの組み合わせによってはUDP (Github Issue) をサポートしていませんが、Service workaroundを使用する場合は必要です。

Use a different driver
If you cannot connect through the Serviceor use other workarounds, you may want to try a different minikube driver, and if that doesn’t work, connection via UDP may not be possible with minikube, and you may want to try either a different local Kubernetes tool or use a cloud hosted Kubernetes cluster.
別のドライバを使用する
Service経由で接続できない場合は、別のminikubeドライバを試してみて、それでもうまくいかない場合は、UDP経由の接続はminikubeではできない可能性があり、別のローカルKubernetesツールを試すか、クラウドホストのKubernetesクラスタを使用した方がよいかもしれません。

「ええ、UDPでLoadbalancerを介して接続したいのに・・・😑」
なんか打開策はないかと、人類の叡智が集まるGithubのissueの海を泳いでいると、

minikube tunnel doesn't appear to support UDP LoadBalancers #12362
https://github.com/kubernetes/minikube/issues/12362

「ぐぬぬ・・・やっぱりそういうissue上がってる・・・、どないしよ・・・」と落胆している所

以下の書き込みを見つけます。
https://github.com/kubernetes/minikube/issues/12362#issuecomment-1034678334

My eventual solution was to use metallb to satisfy the load balancer requirement, and then the hyperv driver with an external vswitch to get a routable/accessible IP. Published a public version of the config here for reference:
https://github.com/comerford/minikube-agones-cluster
私の最終的な解決策は、ロードバランサーの要件を満たすためにmetallbを使用し、その後、ルーティング可能/アクセス可能なIPを取得するために外部のvswitchとhypervドライバを使用することでした。参考のため、コンフィグのパブリックバージョンをここに公開します。

「MetalLBとはなんぞや?」となり、調べることでどうやら今回のようなケースでもロードバランサーに固有のIPを割り振れるかも、という事で次の項目に続きます。

MetalLBとは

既に偉大なる先人の方々が残した記録の力を借ります。
https://zenn.dev/vampire_yuta/articles/ccbc57be8e092a
https://blog.framinal.life/entry/2020/04/16/022042

MetalLBはオンプレミスでもtype: LoadBalancerを扱えるようにする、kubernetes上で動作するOSSのアプリケーションです。
クラウドでtype: LoadBalancerを使った場合は勝手にIPアドレスが払い出されますが、MetalLBの場合はあらかじめユーザが指定したIPアドレスプール(例:192.168.0.0/24など)からの払い出しになります。

つまりは自分でLoadbalancerに割り当てるipを決めて、振り分けすることができるという事になります。
minikube ipで手元からminikubeに到達するためのIP(今回の場合だとagonesという名前のVMに到達する為のIP)が取得できます。
と、いうことは今回の場合はこのIPをmetalLBの設定として渡しておけば、起動したLoadbalancerに自動で指定したIPを払い出す事ができます。

何とかできそうな見立てがついたので、Terraformの構成に落とし込みました。

構成について

構成は大きく

  • helm_metallb.tf MetalLB関連の構成
  • helm_agones.tf Agones関連の構成
  • helm_gameserver gameserver関連の構成

に3分割されており,それぞれ上から順番に実行されていくような形です。

もっと詳細なフローで言うと

  • data.external.minikube_ip"minikube ipコマンドの結果を受け取り、保持
  • クラスタ初回起動時の対策としてあらかじめnull_resource.preload_imagesで指定されているコンテナイメージを全てminikube image loadコマンド経由でPullする
    • なぜ明示的にPullしているのかは、後述している「コンテナイメージのプルに時間かかりすぎ問題」をご参照ください。
  • helm_release.metallb(helm_metallb.tf)を通じて、MetalLBのシステムコンポーネント(speakerとcontrollerやCRD定義など)がインストールされる
  • helm_release.metallb_minikube(helm_metallb.tf)を通じて、MetalLBの利用者側で設定するCRD(IPAddressPoolとL2Advertisement)がインストールされる
  • helm_release.agones(helm_agones.tf)を通じて、Agonesのシステムコンポーネント(controller,allocator etc)がインストールされる
  • helm_release.gameserver(helm_gameserver.tf)を通じて、gameserverがインストールされる

というのが流れとなります。

Input variables等については以下をご参照ください。
https://github.com/oniku-2929/tf_agones_minikube_hyperv#terraform-information

構成の作成中に発生した問題

CRDを含むHelmチャートを作成し、その依存するサブチャートとしてCRD定義が行われるチャートを設定しても上手く動作しない

https://github.com/oniku-2929/tf_agones_minikube_hyperv/blob/main/README-ja.md#なぜmetallbのチャートを分割しているのかhelm_releasemetallb-と-helm_releasemetallb_minikubeについて
つまり、MetalLBのケースで言うと

  • 「helm_release.metallb」のインストールによってCRD関連の定義が先にHelmリリースとしてインストールされる、この場合は「IPAddressPool」「L2Advertisement」のCRD定義が作成される
  • その後、「IPAddressPool」「L2Advertisement」自体の定義が作成される

という挙動を期待して、初めはシステム系のサブチャートとそれを含む親のチャートして管理しようとしました。
このまとめた親となるチャートを使用してリリースを作成しようとしても

│ Error: unable to build kubernetes objects from release manifest: [resource mapping not found for name: "local-address-pool" namespace: "metallb-system" from "": no matches for kind "IPAddressPool" in version "metallb.io/v1beta1"
│ ensure CRDs are installed first, resource mapping not found for name: "L2-advertisement-default" namespace: "metallb-system" from "": no matches for kind "L2Advertisement" in version "metallb.io/v1beta1"
│ ensure CRDs are installed first]
│
│   with helm_release.metallb_minikube,
│   on helm_metallb.tf line 89, in resource "helm_release" "metallb_minikube":
│   89: resource "helm_release" "metallb_minikube" {

という形で「先にCRDを定義しろ!」と怒られます。
当然、自分で定義するCRD定義側のtemplateのannotationにはHelm hookpost installを付与していました。
それでもやっぱり、同じエラーが出力され動作しませんでした。
「え?先にCRDが定義されて、その後CRD自体が作成される事を期待してるんだけど・・・依存関係はChart.yamlで定義してるし・・・」と思って調べてみると

https://github.com/helm/helm/issues/11422#issuecomment-1281158642

It is not like it is not supported. It is like it can't be supported as of now, because k8s gives no way to understand when a custom resource can be marked ready. And hence the current implementation of helm can only identify limited set of k8s-resources for which Helm-Hooks are applicable.
サポートされていないわけではありません。なぜなら、k8sはカスタムリソースがいつreadyとマークされるかを理解する方法を提供しないからです。そのため、現在のhelmの実装では、Helm-Hooksが適用できるk8sリソースのセットを限定的にしか特定できません。

なるほど、つまりHelm単体ではCRDを含むチャートのHookが上手く動作しないようでした。(v3.8.2 2022/11/19現在)

という事で、「だったら依存関係はTerraform側に逃してしまえ!」という事で
2つのresourceにhelm_releaseを分割し、depends_onで依存関係を定義することでお茶を濁しました。

コンテナイメージのプルに時間かかりすぎ問題

ある程度Terraform構成での環境構築が安定してきた & testフォルダ以下にtf構成のテストを書いた事で分かったことですが

  • 「初回だけterraform applyが完了するまで5分強くらい時間がかかる、クラスタを削除せず2回目以降の適用では数十秒程度で完了する
  • 「クラスタ作成後の初回のインストールだけこける、gameserverのヘルスチェックがこける」
  • tfの構成自体はMacでも動作できるようにしていたのでそちらで試してみると、初回から数十秒程度で適用が完了し、gameserverも動作している
    という現象が発生していました。
    Windowsでクラスタ作成直後にterraform applyした後に、kubectl describe pod ~で様子を見てみると・・・
Events:
  Type     Reason     Age                From               Message      
  ----     ------     ----               ----               -------      
  Normal   Scheduled  110s               default-scheduler  Successfully 
assigned metallb-system/metallb-speaker-9p8l5 to agones
  Normal   Pulling    109s               kubelet            Pulling image "quay.io/metallb/speaker:v0.13.7"
  Normal   Pulled     43s                kubelet            Successfully 
pulled image "quay.io/metallb/speaker:v0.13.7" in 1m5.997699979s
  Warning  Failed     14s (x4 over 43s)  kubelet            Error: secret "metallb-memberlist" not found
  Normal   Pulled     14s (x3 over 43s)  kubelet   

コンテナのPullに非常に時間がかかっている事がわかります。
どうやらコンテナのPullに1分以上かかっていることが原因で、ヘルスチェックに失敗していることが原因だったようです。
この現象、例えばコマンドminikube ssh -p agones docker pull quay.io/metallb/speaker:v0.13.7と言う形で
クラスタのノード上でdokcer pull経由でコンテナイメージをプルした時間を測ってみても、以下のように時間がかかっている事が分かります。

minikube ssh -p agones time docker pull quay.io/metallb/speaker:v0.13.7
v0.13.7: Pulling from metallb/speaker
213ec9aee27d: Pull complete
dc9e1a886587: Pull complete
f17e814c34e8: Pull complete
5ba64e3f5d64: Pull complete
dce6a2f1bc62: Pull complete
Digest: sha256:aeab5aeeb25c7ca31a1c468a35b1db4513cd6199bf376bdf19ff5689d2ebad28
Status: Downloaded newer image for quay.io/metallb/speaker:v0.13.7
quay.io/metallb/speaker:v0.13.7

real    1m16.251s
user    0m0.013s
sys     0m0.007s

MinikubeのGithub issueに関連するものを見つけたのですが
https://github.com/kubernetes/minikube/issues/14789

ここをみた上で自分の環境についての情報をまとめると

  • driverはDockerを使用、バージョン情報は以下
dockerのバージョン情報

$ docker version
Client:
Version: 20.10.20
API version: 1.41
Go version: go1.18.7
Git commit: 9fdeb9c
Built: Tue Oct 18 18:14:26 2022
OS/Arch: linux/amd64
Context: default
Experimental: true

Server: Docker Engine - Community
Engine:
Version: 20.10.20
API version: 1.41 (minimum version 1.12)
Go version: go1.18.7
Git commit: 03df974
Built: Tue Oct 18 18:20:17 2022
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: v1.6.8
GitCommit: 9cd3357b7fd7218e4aec3eae239db1f68a5a6ec6
runc:
Version: 1.1.4
GitCommit: 5fd4c4d144137e991c4acebb2146ab1483a97925
docker-init:
Version: 0.19.0
GitCommit: de40ad0

cri-dockerd --version
cri-dockerd 0.2.1 (HEAD)

  • Kubernetesのバージョンはv1.23.9
  • minikubeのバージョンはv1.28.0
  • minikube image loadコマンド経由でイメージをクラスタにあらかじめ登録した場合は10秒弱程度でコンテナイメージをクラスタが稼働しているNodeに登録できる
    image loadの場合

    約9秒
    Measure-Command {minikube image load quay.io/metallb/speaker:v0.13.7 -p agones}

    Seconds           : 9
    Milliseconds      : 28
    Ticks             : 90281477
    TotalDays         : 0.000104492450231481
    TotalHours        : 0.00250781880555556
    TotalMinutes      : 0.150469128333333
    TotalSeconds      : 9.0281477
    TotalMilliseconds : 9028.1477
    
    • 当初はminikube cache addを使用していましたが、deprecatedのようなので最終的にimage loadにした

という状態だったので、最終的にminikube image loadコマンドを発行するprovisinerを,あらかじめloadが必要なコンテナ数分のnull_resource.preload_imagesとして定義する事でお茶を濁してます。
ワークアラウンドとして異なるドライバ(containerdなど)を使用する事についても記載されていますが、そちらは確認していません。

Linuxだとdata.externalの動作時に固まって構成が適用できない問題

Macでも動作確認できたので、一応Linuxでも動作確認するかと思ってWSL2上のUbuntu 20.04で動作確認しようと試みた所
data.external.minikube_ip_unixでminikube ipの出力をとってくる際に固まってしまう現象が発生しました。
詳細は一応issueとして書いてますが
時間があるときに調べようかなと思ってます。
https://github.com/oniku-2929/tf_agones_minikube_hyperv/issues/1

所感

  • test以下のtf構成のテストにはTerratestを初めて使ってみた所、便利だった事
  • GithubのReadme書くのに当たってterraform-docsの力を借りた所、それも便利だった事

これらについても、初めはこちらの記事に記載しようかなと思ったのですが
同じ記事にまとめてしまうとあまりに記事が長くなりそうなので
別途記事書こうと思ってます。

構成の量自体は少ないですが、今回上記の右往左往する過程が(たどったGithubのIssueとか調査ログとか)
自分の作業バックログとして使用しているTrelloのボードにつらつら縦方向に書き並べられているのを見て
「ああ、こういうときにスクラップを利用したらいいのかぁ」と思った次第でした。

😁

「"AgonesのTerraform構成盛りプレート、MetalLBを添えて~"と言うエレガントな名前は如何でしょうか?」

😶🧑

「濁すためのお茶っ葉もないですね。」

😇

参照

Agonesドキュメント

https://agones.dev/site/

Agone GitHub

https://github.com/googleforgames/agones

MetalLBドキュメント

https://metallb.universe.tf/

MetalLB GitHub

https://github.com/metallb/metallb

Minikubeドキュメント

https://minikube.sigs.k8s.io/docs/

Minikube GitHub

https://github.com/kubernetes/minikube

Helmドキュメント

https://helm.sh/docs/

Helm GitHub

https://github.com/helm/helm

Discussion