だいぶ前にこういうポストをしていて、諸々環境が整ってきたので実際にやってみました。
パルワールドやってみたいというか、パルワールド鯖を手持ちの k8s クラスタで立てて運用してみたい
— chanyou (@chanyou0311) January 23, 2024
目新しい情報は特になくて、巨人の肩に乗っかると簡単に Kubernetes で起動できるという話です。
Kubernetes で立てるモチベーション #
- CronJob を定義すれば、バックアップなどの任意の処理を定期的に実行できる
- 設定値を宣言的に定義できる
- やはり浪漫…浪漫がある…
というわけで、カジュアルにおうち k8s クラスタに立ててみます。
まずはネイティブで試す #
Minecraft などのゲームサーバーを立てたことがなかったので、どう立てるかわかってない状態でした。
まずは Kubernetes を使わずにサーバーを立ててみます。公式ドキュメントの通りに進めるとサクッと起動できました。
専用サーバーの構築 | Palworld tech guide
SteamCMD という CLI ツールをインストールして、適切なパラメータを渡して起動するだけです。デフォルトでは UDP 8211 番で待ち受ける形となっています。
外部ネットワークからアクセスするにはルーターのポートフォワードなどの設定を行うことで可能です。Palworld だと 8211 がデフォルトポートで、このポートが使用されることが多いのだと思います。
Kubernetes 起動への道のりを考える #
Docker Image を用意する #
SteamCMD は Windows と Linux に対応しているので、 SteamCMD が使える Docker Image を作ってあげれば Kubernetes で起動できそうです。
で、調べると既に有志により palworld-server-docker
というリポジトリが公開されており、活発にメンテナンスされているのがわかります。
A Docker Container to easily run a Palworld dedicated server.
このリポジトリで管理されている Docker Image に乗っかるのが簡単そうです。
Kubernetes リソースを用意する #
いい感じの Docker Image が公開されているので、いい感じに Deployment やら PVC やらのリソースを定義して kubectl apply -f palworld.yml
すれば起動できそう感があります。
palworld-server-docker
のリポジトリの README を読み進めると、世界は広いのか狭いのか、既に有志により Helm Chart が別リポジトリで作られていることがわかります。
A helm chart for the popular palworld-server-docker Docker image
Fork 数や Star 数をみると、 Kubernetes で Palworld サーバーを立てようとする人口の少なさを感じますが、 Official Helm Chart ということで使ってみます。
定義されているリソースもトリッキーなものはなさそうで、 Kubernetes 初心者かつゲームサーバー初心者の自分には勉強になります。
手持ちの Kubernetes クラスタで起動する #
Argo CD アプリケーションを定義する #
自宅の Kubernetes クラスタでは Argo CD が動いていて、 GitOps できるようになっています。
Helm Chart を導入するときは Argo CD の Application リソースとして定義して、宣言的に管理するようにしています。
今回の場合は以下のようになりました。
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: palworld
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
destination:
namespace: palworld
server: {{.Values.spec.destination.server}}
project: default
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
source:
chart: palworld
repoURL: https://twinki14.github.io/palworld-server-chart
targetRevision: 0.30.1
helm:
releaseName: palworld
values: |-
# 一部省略
server:
config:
env:
# All environment variables
# https://github.com/thijsvanloef/palworld-server-docker/blob/main/.env.example
# Server Settings
# https://palworld-server-docker.loef.dev/getting-started/configuration/server-settings
BACKUP_ENABLED: "true"
DELETE_OLD_BACKUPS: "false"
OLD_BACKUP_DAYS: "30"
BACKUP_CRON_EXPRESSION: "0 4 * * *" # 日本時間の毎日 04:00
AUTO_UPDATE_ENABLED: "false"
AUTO_UPDATE_CRON_EXPRESSION: "0 * * * *"
AUTO_UPDATE_WARN_MINUTES: "30"
AUTO_REBOOT_ENABLED: "false"
AUTO_REBOOT_EVEN_IF_PLAYERS_ONLINE: "false"
AUTO_REBOOT_WARN_MINUTES: "5"
AUTO_REBOOT_CRON_EXPRESSION: "0 0 * * *"
ENABLE_PLAYER_LOGGING: "true"
PLAYER_LOGGING_POLL_PERIOD: "10"
# Game Settings
# https://palworld-server-docker.loef.dev/getting-started/configuration/game-settings
DIFFICULTY: "None"
DAYTIME_SPEEDRATE: "1.000000"
NIGHTTIME_SPEEDRATE: "1.000000"
EXP_RATE: "5.000000"
PAL_CAPTURE_RATE: "0.500000"
PAL_SPAWN_NUM_RATE: "1.000000"
PAL_DAMAGE_RATE_ATTACK: "1.000000"
PAL_DAMAGE_RATE_DEFENSE: "1.000000"
PLAYER_DAMAGE_RATE_ATTACK: "2.000000"
PLAYER_DAMAGE_RATE_DEFENSE: "1.00000"
PLAYER_STOMACH_DECREASE_RATE: "0.100000"
PLAYER_STAMINA_DECREASE_RATE: "0.0100000"
PLAYER_AUTO_HP_REGEN_RATE: "1.000000"
PLAYER_AUTO_HP_REGEN_RATE_IN_SLEEP: "1.000000"
PAL_STOMACH_DECREASE_RATE: "0.100000"
PAL_STAMINA_DECREASE_RATE: "0.100000"
PAL_AUTO_HP_REGEN_RATE: "1.000000"
PAL_AUTO_HP_REGEN_RATE_IN_SLEEP: "1.000000"
BUILD_OBJECT_DAMAGE_RATE: "0.1000000"
BUILD_OBJECT_DETERIORATION_DAMAGE_RATE: "1.000000"
COLLECTION_DROP_RATE: "5.000000"
COLLECTION_OBJECT_HP_RATE: "1.000000"
COLLECTION_OBJECT_RESPAWN_SPEED_RATE: "10.000000"
ENEMY_DROP_ITEM_RATE: "5.000000"
DEATH_PENALTY: "Item"
ENABLE_PLAYER_TO_PLAYER_DAMAGE: "False"
ENABLE_FRIENDLY_FIRE: "False"
ENABLE_INVADER_ENEMY: "True"
ACTIVE_UNKO: "False"
ENABLE_AIM_ASSIST_PAD: "True"
ENABLE_AIM_ASSIST_KEYBOARD: "False"
DROP_ITEM_MAX_NUM: "3000"
DROP_ITEM_MAX_NUM_UNKO: "100"
BASE_CAMP_MAX_NUM: "128"
BASE_CAMP_WORKER_MAX_NUM: "20"
DROP_ITEM_ALIVE_MAX_HOURS: "1.000000"
AUTO_RESET_GUILD_NO_ONLINE_PLAYERS: "False"
AUTO_RESET_GUILD_TIME_NO_ONLINE_PLAYERS: "72.00000"
GUILD_PLAYER_MAX_NUM: "20"
PAL_EGG_DEFAULT_HATCHING_TIME: "0.00000"
WORK_SPEED_RATE: "2.000000"
IS_MULTIPLAY: "False"
IS_PVP: "False"
CAN_PICKUP_OTHER_GUILD_DEATH_PENALTY_DROP: "False"
ENABLE_NON_LOGIN_PENALTY: "True"
ENABLE_FAST_TRAVEL: "True"
IS_START_LOCATION_SELECT_BY_MAP: "True"
EXIST_PLAYER_AFTER_LOGOUT: "False"
ENABLE_DEFENSE_OTHER_GUILD_PLAYER: "False"
COOP_PLAYER_MAX_NUM: "4"
REGION: ""
USEAUTH: "True"
BAN_LIST_URL: "https://api.palworldgame.com/api/banlist.txt"
SHOW_PLAYER_LIST: "True"
# Engine Settings
# https://palworld-server-docker.loef.dev/getting-started/configuration/engine-settings
DISABLE_GENERATE_SETTINGS: "false"
values.yaml
に相当する内容は長いので一部省略していますが
Helm Chart のデフォルトの values.yaml を移して、適宜修正して記載しています。
ゲーム内容の設定値は環境変数で指定するようになっていて、 palworld-server-docker のドキュメント と 公式ドキュメント を見ながら、必要に応じて設定していきます。
ちなみに秘匿な情報は Sealed Secrets で管理するようにしていて、以下のように定義して、 values で指定しています。
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
creationTimestamp: null
name: palworld
namespace: palworld
spec:
encryptedData:
admin-password: AgB2s6SZtB672+qMdez2aJiNTvJBQIr96FBblwQYRLcBW+7ktNh1WY+/W9l4AFcD2p2meSjEP6LDX9/hZMfv3rUPs/qaY9eLQ38uJ+9DwZJvSZ/+R+Qyy52Svd9B0IzqNAFp14CusUmnh5Je2mo29k6wkBz5ywgGJYNJRfJ7Iyhtbyuvv+f1DIaRyITCp2+xABA9V4/DUFUfeQSWBOglboA24KAJEjcfaHi4OpHtjTvdevInyJ3QqJCTqJPnm1JBYc29ww3hnIo4k98VChxLzcsR+s0MVlnNUg3ivbs1B7VREW5KvoQktfR1A/LrAYa8Vid+x+KgfVboUkQQnmWODy3AIWnhtLFv3FDI+skVPuoJutF5jlhfm9xkFoIijnUVYKq03lNnTJtGAq9Kq4v0CaYAIJaJy2Vv/6mZcE/+VrkE6fAU+A97zyIN2dZNOIb6ATaBK+O7J/2+PWqI2b0x1Ol+Bsj90qRahEQ4bH+BAMoJbVvatlSRirC6LswKD230QoZnbU/J/VtZc3tdNcv2xYXm3gnGZ9ZoU+ASDEYiZCtEw0cB0i3jDAd5pY6ETXhz22s92E7roz/f06ixa5BFdUzBwyQzHErFcWUjvy739EUV/+OH1bJo4jv1Z2OYCryoTP2t1M9q88KW/5hNz1cd24V8WDulFLxTG2bwn7y3i5Pywk8RHXJP4GPISyYj83HZ9a5y2CADZK6mV7uVCKWWApk5HIN3RqAJf+H0uP9hdy5P1Q==
server-password: AgAmQRBosh6kyxITFFspicL5EaRedIigZF7bgkIcv8Cq7ZeJLORZ4BR4sr7LiL5cw58ffimxSkp9U2ehKNvzmiPe99I56QPJZMVJXy9l4UAqqqIE5tmRkQavGxZih4ppbDdIOyikpzeSlD52KH32novHf4jVg5G8tZoRQpfsEElBW4jDm0+SuKr7xkkjLQ33CeYfIVB7ljSqDkTqmVZWvKXAMS+USWrCHOxNl/ChjGg7UDsdWjAymtGrO+MohjGnHanXhsdumm+f24Chon3ZO2nNukHhy4xJ0qKRqaUNBXDChsYRrHVp1aijrqbBplbQbLgzTYiKag6vPNoVjAXWZyYp3B7nRW0iP6BgUc6h9JK2EzbOzIayurY4tZOZNYW431SLx0IngVCJMjkUBqULWcpXCQZcg3mtZ3YmZYhhSure9GNiOKrTLwbGieAnJheDBaqeQiUQIV3x4WFobpSSgtOmhqGigwJypJGL8k+WNeEfhxvO3Q4oVmyqqHUJfogn3ZUw3ceZNQXoRpxlJ93bX1MAQIrYMqORym+dueLcDvsQ4s7xHgro89UtNd3CoN/rxK5Clt6JV8kiUTmdoydNFtBtRoihdnZ9Y7oidwP11vgshdcIrwU8P6mPYtq4tFIQ+wCUGiKuP4OEKws4tmz+0HQP0BezqwpvypQfpZXLYf/GdfgG/qN1yrXMbPy3YnP8hPRezvdWcqih
template:
metadata:
creationTimestamp: null
name: palworld
namespace: palworld
Argo CD の Application リソースを管理するリポジトリ自体は非公開にしているんですが、上記のように public リポジトリにおいても運用できるので安心感が高いですね。
ネットワーク周りをどうにかする #
Argo CD の Application を作成したことで Palworld サーバーの起動はできたものの、このままだと外から接続することができません。
Helm Chart の values.yaml の service 周りの設定値を確認します。
service:
enabled: true
annotations: {}
labels: {}
# For minikube, set this to NodePort, elsewhere use LoadBalancer
# Use ClusterIP if your setup includes ingress controller
#
type: ClusterIP
いつもだと ClusterIP
を指定して、 Ingress でいい感じにサブドメインを切って外部公開しているので、今回もその方法を取ろうとしました。が、調べてみると Kubernetes の Ingress って HTTP トラフィックのルーティングのみ対応しているんですね。
ピンチと思いきや、自分のクラスタでは ingress-nginx
を使っているのですが、 HTTP 以外の
TCP/UDP の外部公開に対応していました。ありがたい…
HTTP 以外のトラフィックを扱おうとして初めて直面して知ったので、ゲームでもなんでもやってみるもんですね。
自分の場合は ingress-nginx
も Argo CD の Application として定義しているので、以下のように parameter を追加しました。
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ingress-nginx
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
destination:
namespace: ingress-nginx
server: {{.Values.spec.destination.server}}
project: default
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
source:
chart: ingress-nginx
repoURL: https://kubernetes.github.io/ingress-nginx
targetRevision: 4.9.1
helm:
releaseName: ingress-nginx
valueFiles:
- values.yaml
parameters:
# Palworld Settings
- name: tcp.25575
value: "palworld/palworld-svc:25575"
- name: udp.8211
value: "palworld/palworld-svc:8211"
- name: udp.27015
value: "palworld/palworld-svc:27015"
Palworld の values を見直し、今回は例外的に service.type に LoadBalancer を指定して、 UDP でルーティングできるようにします。
External IP で IP が払い出されているのが確認できました。
$ kubectl get svc -n palworld
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
palworld-svc LoadBalancer 10.233.38.238 10.3.0.2 8211:31950/UDP,27015:31387/UDP,25575:31614/TCP 13d
ルーターでこの IP を公開するように設定して、完了です。
接続テストしてみる #
palworld.chanyou.app:8211
でアクセスできました。
パスワードは知人の方にはお伝えしますので、気軽に @chanyou0311 宛に DM で聞いてもらえたらと思います。
さいごに #
巨人の肩に乗っかって、 Palworld サーバーを Kubernetes で動かすことができました。UDP を扱ったのが初めてだったので、ルーティング周りが勉強になりました。
設定値を宣言的に設定できるので、カジュアルに設定変えられて体験いい感じです。
ちなみに values で LoadBalancerIP を指定できるとより宣言的で良いんですが、非対応のようでした。コントリビュートチャンスかも。
遊ぶ時間が取れてないので、代わりに上述のサーバーで遊んでほしいです。
おわり。