Amazon Web Services ブログ

Amazon EKS Auto Mode 上で Agones を安定稼働させる設計のポイント

はじめに

リアルタイム性の高いマルチプレイヤーゲームは、複数のプレイヤーの入力を集約し、ゲームの状態を一貫させて全員へ配信する仕組みを必要とします。この仕組みの一つに、権威あるサーバーがゲームの状態を管理する専用ゲームサーバー (Dedicated Game Server) 方式があります。対戦の公平性やチート対策、多人数の同時接続が求められるタイトルでは、専用ゲームサーバー方式が広く採られています。この方式では、マッチメイキング、ゲームサーバーの割り当て、セッション終了後のサーバー回収といったライフサイクル管理が必要になり、そのオーケストレーション基盤をどう構築・運用するかが設計上の大きなテーマとなります。

AWS で専用ゲームサーバーをホストする主な選択肢は、マネージドサービスの Amazon GameLift Servers を利用するか、Amazon Elastic Compute Cloud (EC2)Amazon Elastic Container Service (ECS)Amazon Elastic Kubernetes Service (EKS) 上でセルフホストするかの 2 つです。GameLift Servers はインフラ構築・運用の大部分を担いますが、要件によってはより柔軟な制御が必要な場合があり、Agones の利用が選択肢の一つとなります。Agones はオープンソースの専用ゲームサーバーホスティング向け製品で、Amazon EKS 上にデプロイでき、柔軟性を享受しつつ専用ゲームサーバーの構成を簡素化できます。しかし Kubernetes 基盤の運用は避けられず、Agones の運用には一定の負荷が存在しました。

2024 年 12 月に一般提供が始まった Amazon EKS Auto Mode は、Kubernetes の構築・運用負担を低減する機能です。Agones の基盤となる EKS クラスタで EKS Auto Mode を利用できれば、専用ゲームサーバーを柔軟かつ簡素に構築・運用できます。ただし EKS Auto Mode にはいくつかの制約もあります。

本記事では、EKS Auto Mode の制約が Agones の特性を阻害しないようにするための設計方針を紹介します。

Agones リソースの概要

Agones リソースの概要

本記事の内容に関連する Agones カスタムリソースについて説明します。

  • GameServer:プレイヤーが接続するゲームサーバープログラムが動作する Pod にあたるリソースです。状態を持ち、Ready 状態はゲーム開始可能で待機している状態、Allocated 状態はゲームプレイのためにリソースが確保された状態を表します。
  • Fleet:GameServer のまとまりを管理するためのリソースです。常に起動しておくべき GameServer リソースの数を指定でき、Fleet Autoscaler リソースによるスケーリング制御も可能です。
  • GameServer Allocation:ゲームプレイのために GameServer リソースを選択し確保する操作を行うリソースです。この操作により選択された GameServer リソースは Ready 状態から Allocated 状態となります。
  • コントローラ系リソース:Agones の機能を実現・制御するためのリソース群です。

EKS Auto Mode と Agones の共存における課題

EKS Auto Mode は、クラスタのノード管理を AWS の責任範囲で自動化します。例えば Karpenter をベースとしたノードのプロビジョニング・スケーリング・入れ替え、Bottlerocket による最適化された OS、各種アドオンのマネージド管理といった機能により、Kubernetes の運用を大幅に簡素化できます。

一方、ユーザー側で設定できる範囲が狭まるというトレードオフがあります。Agones の基盤として特に考慮が必要なのは、EKS Auto Mode が以下の目的のために自動で行うノードの中断です。

  • Consolidation:稼働中の Pod が少ないノードを終了し、利用率の高いノードへ Pod を集約してコストを最適化する。
  • Drift:NodePool や NodeClass の設定変更、AMI の更新などに追従してノードを入れ替える。
  • Expiration:セキュリティ維持のため、一定期間が経過したノードを強制的に入れ替える。
  • Interruption:Spot Instance の中断通知やハードウェア障害などに応じてノードを終了する。

※EKS Auto Mode の長期実行ワークロードでの扱いについては、builders.flash 記事「Amazon EKS Auto Mode のノード自動更新を Deep Dive する」もあわせて参照ください。

いずれの中断でも、対象ノード上の Pod へ SIGTERM が発行されます。Agones上で動かすような専用ゲームサーバーには「プレイ中のゲームセッションを中断させない」という強い制約があるため、対策をしないとプレイ中のゲームサーバーが強制終了されるリスクがあります。

EKS Auto Mode と Agones を共存させる設定のポイント

そこで、Agones のコントローラ系とゲームプレイ中のサーバープログラムをノードの中断から保護する仕組みを、次の 6 つのポイントで構成します。以降、それぞれのポイントについて、実際の設定ファイル (nodepools.yaml / fleet.yaml / agones-values.yaml) を引用しながら、具体的な実装方法を説明します。

Point # 対象 内容
Point 1 共通 NodePool の expireAfterterminationGracePeriod を適切に設定する
Point 2 GameServer GameServer の Pod の terminationGracePeriodSeconds をゲームの最大持続時間より長い値に設定する
Point 3 GameServer GameServer 上で動くサーバーアプリで、適切なシグナルハンドリングを実装する
Point 4 GameServer Eviction を許可してノードの自動最適化を機能させる
(spec.eviction.safe: Always)
Point 5 コントローラ系 Agones コントローラ系と GameServer が配置される NodePool を分ける
Point 6 コントローラ系 agones-allocatoragones-ping の PDB を有効にする

Point 1 (共通) :NodePool の expireAfter / terminationGracePeriod を適切に設定する

EKS Auto Mode では、ノードは必ずいつか中断されます。特に Expiration はノード生存期間最大21日の制約があり、必ず発生します。この expireAfter の値と、ノードが強制的に中断されるまでの猶予期間である terminationGracePeriod を、ワークロード特性に合わせて NodePool に設定することが起点になります。

NodePool とは、Karpenter ベースのノード管理機能で、Pod の要求に応じてインスタンスを自動的にプロビジョニング・スケーリングし、クラスターのコンピュートリソースを柔軟に管理するための仕組みです。

設定にあたっては、次の関係を守るようにしてください。

  • terminationGracePeriod は、後述する Pod の terminationGracePeriodSeconds より長く設定する (ノードが強制削除される前に、Pod が Graceful に終了しきるようにするため) 。
  • expireAfter + terminationGracePeriod の合計は最大 21 日に収める (EKS Auto Mode の制約) 。

experAfter と terminationGracePeriod の関係 また disruption.budgetsnodes: "10%"(NodePool Disruption Budgets: NDB)により、同時に中断処理へ入るノード数を全体の 10 % に制限し、一斉中断の影響を抑えます。consolidationPolicy: WhenEmpty については Point 4 で触れます。

Point 1 を踏まえた NodePool の設定例は次の通りです(nodepools.yaml)。

# GameServer 用 NodePool
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: agones-worker
spec:
  template:
    spec:
      # Point 1: ゲームセッション時間を考慮して設定
      # terminationGracePeriod は Pod の terminationGracePeriodSeconds より長くすること
      expireAfter: 336h          # 14日
      terminationGracePeriod: 4h # expireAfter + terminationGracePeriod が21日(504h)に収まるように
  disruption:
    consolidationPolicy: WhenEmpty
    consolidateAfter: 30s
    budgets:
      - nodes: "10%"

Point 2(GameServer):Pod の terminationGracePeriodSeconds をゲーム最大持続時間より長く設定する

ノードが中断され Pod に SIGTERM が発行されてから、Pod が強制終了(SIGKILL)されるまでの猶予が terminationGracePeriodSeconds です。この値をゲームの最大持続時間より長く設定することで、進行中のゲームが途中で強制終了されることを防ぎます。 experAfter, terminationGracePeriod, terminationGracePeriodSeconds の関係 terminationGracePeriodSeconds は Pod リソースの設定値ですが、Agones では GameServer リソース定義内で設定することになります。また GameServer の制御は Fleet で行うことが一般的なため、ここでは Fleet の設定例を次の通り示します (fleet.yaml)

apiVersion: "agones.dev/v1"
kind: Fleet
metadata:
  name: simple-game-server-fleet
spec:
  replicas: 3
  template:
    spec:
      # ... (省略)
      template:
        spec:
          # Point 2: ゲームセッション最大持続時間 + バッファより長い値を設定
          # 例: ゲーム最大30min + クリーンアップ5min = 35min = 2100sec
          terminationGracePeriodSeconds: 2100

ゲームセッション 1 つの最大持続時間に、クリーンアップ処理のバッファを加えた値を設定します。Point 1 の terminationGracePeriod (ノード側の猶予) を、この terminationGracePeriodSeconds (Pod 側の猶予) より長く設定することで、terminationGracePeriod > terminationGracePeriodSeconds > ゲーム最大持続時間 の関係が成立し、プレイ中のゲームを保護できます。

まとめると、 以下を満たすように各パラメータを設定すればよいことになります。

  • NodePool の terminationGracePeriod > Pod の terminationGracePeriodSeconds > ゲームの最大持続時間
  • NodePool の expireAfter は自由 (terminationGracePeriod との合計は 21 日以下)

Point 3 (GameServer) :適切なシグナルハンドリングを実装する

Point 1・2 で猶予期間を確保しても、SIGTERM を受け取ったサーバープログラムが適切に振る舞わなければ、ゲームサーバーは安全に終了できません。GameServer 上で動くサーバープログラムでは、SIGTERM を受信したときに次の動作を実装します。

  • Allocated 状態 (ゲーム進行中) :進行中のゲームの終了を待ち、クリーンアップ処理を行った後に Agones SDK の sdk.Shutdown() を呼び出す。
  • Ready 状態 (待機中) :新規 Allocation を防ぐため、即時 sdk.Shutdown() を呼び出す。

この実装により、ノード中断のシグナルを受けても、進行中のゲームを守りつつ、待機中のサーバーは速やかに退去できます。

Point 4 (GameServer) :Eviction を許可してノードの自動最適化を機能させる (spec.eviction.safe: Always)

Point 1〜3 でプレイ中のゲームを保護できたら、次に Agones の spec.eviction.safeAlways に設定し、EKS Auto Mode によるノードの自動最適化を機能させます (fleet.yaml) 。

spec:
  template:
    spec:
      # Point 4: eviction.safe を Always に設定
      # Karpenter の Drift/Consolidation/Expiration 時に Graceful な Eviction を許可する
      eviction:
        safe: Always

spec.eviction.safe は、各 GameServer の Pod に対して Agones が内部的に作成する PDB (maxUnavailable: 0%) を有効化するかどうかを制御します。デフォルトの Never の場合、常に eviction をブロックする動きとなります。

Agones の eviction.safe 設定の意味と独自 PDB の動作 (表は https://agones.dev/site/docs/advanced/controlling-disruption/ より)

Never のままでは、この PDB がすべての Eviction をブロックし、EKS Auto Mode 環境で次の問題が発生します。

  • Karpenter が Consolidation / Drift を試みても Pod に SIGTERM が発行されず退去しないため、Cordon 状態のノードが既存 Pod ごと残り続け、コスト効率が悪化する。
  • Drift (AMI 更新) が PDB にブロックされ、セキュリティパッチの適用が遅延する。
  • 上記の動作の結果、近い時間帯に起動した全ノード上の Pod が Expiration まで生存し続け、それらの Pod が近い時間帯に一斉に SIGKILL されるリスクが生じる。

Always を設定すると Agones のネイティブ PDB が無効化され、Eviction が許可されます。Pod が稼働中のノードでも Drift による AMI 自動更新と、Expiration 時の Graceful なノード入れ替えが機能するようになります (Never ではこれらが PDB にブロックされます) 。プレイ中のゲームの保護は Point 1〜3 で確保した猶予期間とシグナルハンドリングが担保するため、Always にしても進行中のゲームが即座に切断されることはありません。

Always の設定でも安全に運用する目的で、Point 1 で設定した consolidationPolicy: WhenEmpty (Pod が残っているノードでは Consolidation を発動させない) と NDB (nodes: "10%"、同時中断ノード数の制限) を補完策として組み合わせます。Always は Drift / Expiration を有効に機能させる一方、Consolidation まで WhenEmptyOrUnderutilized にすると稼働中 Pod のあるノードも集約対象となりゲームサーバーが頻繁に Evict され得るため、Consolidation は WhenEmpty に絞り、空ノードの回収のみに留めます。Agones の Fleet 設定 spec.scheduling の値はデフォルトで Packed となっており、できるだけ既に GameServer が動作しているノードに新規 GameServer を起動するようにスケジュールするため、WhenEmpty の設定でも十分なコスト最適化効果が見込めます。

Point 5 (コントローラ系) :Agones コントローラ系と GameServer の NodePool を分ける

Point 1 で設定した expireAfter / terminationGracePeriod は NodePool 単位の設定です。Agones のコントローラ系 (controller / extensions / allocator / ping) と GameServer では、必要な猶予期間が異なります。

  • コントローラ系:短時間の再起動が許容されるため、長期安定稼働を優先しつつ猶予は比較的短くてよい。
  • GameServer:ゲーム最大持続時間に対応した長い猶予 (terminationGracePeriod) が必要。

そこで、コントローラ系用と GameServer 用で NodePool を分離します (nodepools.yaml) 。

# コントローラ用 NodePool
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: agones-controller
spec:
  template:
    metadata:
      labels:
        agones-role: controller   # ラベルで役割を区別
    spec:
      # コントローラは長期安定稼働を優先
      expireAfter: 336h           # 14日
      terminationGracePeriod: 72h # 3日
---
# GameServer 用 NodePool (Point 1 で前掲) 
# labels: agones-role: gameserver / terminationGracePeriod: 4h

そのうえで、GameServer 側は Fleet の nodeSelector で GameServer 用 NodePool を指定します (fleet.yaml) 。

      template:
        spec:
          # Point 5: GameServer 用 NodePool にスケジュール
          nodeSelector:
            agones-role: gameserver

コントローラ系は、Agones の Helm Values で nodeSelector を指定し、コントローラ用 NodePool に配置します (agones-values.yaml) 。

agones:
  controller:
    # Point 5: コントローラ用 NodePool にスケジュール
    nodeSelector:
      agones-role: controller
  extensions:
    nodeSelector:
      agones-role: controller
  allocator:
    nodeSelector:
      agones-role: controller
  ping:
    nodeSelector:
      agones-role: controller

これにより、それぞれのワークロード特性に合った猶予期間を両立できます。 コントローラ系と GameServer で NodePool を分ける

Point 6 (コントローラ系) :agones-allocator と agones-ping の PDB を有効にする

前述の通り EKS Auto Mode ではいつか必ずノードの中断が発生するため、Agones のコントローラ系サービスでも可用性維持のための設定が重要です。agones-allocator (ゲームサーバーの割り当てリクエストを受け付けるサービス) と agones-ping (レイテンシー計測用サービス) はデフォルトで PDB が無効のため、ノードが中断されてもこれらが全停止しないよう、PDB を有効化します (agones-values.yaml) 。

agones:
  allocator:
    replicas: 3
    # Point 6: allocator の PDB を有効化
    pdb:
      enabled: true
      minAvailable: 1
  ping:
    replicas: 2
    # Point 6: ping の PDB を有効化
    pdb:
      enabled: true
      minAvailable: 1

minAvailable: 1 により、ノード中断時にも最低 1 レプリカが稼働し続けることを保証し、コントロールプレーンの可用性を維持します。

なお、agones-controller, agones-extensions はデフォルトで PDB が有効となっています。

さらに安定した運用のための補足 (レアケースへの対処)

Point 1〜6 の設計により、通常のノード中断からのゲームサーバー保護を実現できます。さらに安定した運用のためには、次のレアケースへの対処も検討ポイントになります。いずれも各実装に依存するため、必要に応じて検討してください。

GameServerAllocation のレースコンディション

Ready 状態の GameServer が SIGTERM を受信した直後から Shutdown 完了までの間に、GameServerAllocation によって Allocate される可能性がゼロではありません。これが起こると、プレイヤーが接続するころには Allocate された GameServer が既に停止しておりエラーとなるリスクがあります。以下はこれを防ぐ方法の例です。

  • GameServer にあらかじめラベル (allocationblock=false 等) を付与し、GameServerAllocation 側でそのラベルを持つ Pod のみを対象とする設定を行う。SIGTERM 受信時には即座にラベルを外す。ラベル削除後も一定時間待機し、その間に Allocate された場合はゲームを継続し、終了後に Shutdown する。

起動中 Pod への SIGTERM 発行時のハンドリング不可

コンテナ起動中 (Starting / ContainerCreating) に SIGTERM が発行されると、アプリケーション側でハンドリングできず、GameServer は正常に起動し Ready 状態となる一方、 Pod は terminationGracePeriodSeconds 経過後に停止してしまう動作となり、ゲームを保護できません。これを防ぐには以下の方法が例として挙げられます。

  • 対策例 1:GameServer にあらかじめラベル (allocationblock=false 等) を付与し、GameServerAllocation 側でそのラベルを持つ Pod のみを対象とする設定を行う。外部コントローラで Pod の deletionTimestamp を監視し、非 null、つまり削除予定がある状態なら、前述の Allocation 用ラベルを外し、新規 Allocation を防ぐ。
  • 対策例 2:イメージサイズの削減・DaemonSet 等による事前 pullといった方法で起動時間短縮を図り、発生確率を最小化する。

まとめ

本記事では、Agones の基盤に EKS Auto Mode を採用する際、ノードの自動中断からゲームサーバーを保護する設計を 6 つのポイントで紹介しました。専用ゲームサーバーのセルフホストで、Agones の柔軟性を活かしつつ EKS Auto Mode で運用管理をシンプルにするには、EKS Auto Mode のノード中断と Agones のゲームサーバーライフサイクルの競合を解消する設計が鍵になります。

本記事で紹介した Point 1〜6 の設計パターンと実装例 (nodepools.yaml / fleet.yaml / agones-values.yaml) が、EKS Auto Mode 上での Agones 導入を検討する方の参考になれば幸いです。