k8sでNetHack鯖をたてて、discordにリザルトを通知する
Table of Contents
k8sでNethack3.6.7のPublic Serverをたてて、discordにリザルトを通知するアプリをGoで作る
できたもの
構成
Nethack Public Serverコンテナと、Goで作った通知用Appコンテナをサイドカーとして、それぞれ同一のvolumeを参照するようにk8s上でデプロイする。
nethack-server
下記サイトの手順を参照しながらDockerfileを書くだけ
Installing NH 3.6.6 - Nethack Server
/home/nethack
がchrootディレクトリとなるので、通知用App側とはこのパスを共有する。
nethack-notifier
このアプリがやっているのは、inotifyのGolangラッパーであるfsnotifyを利用し、以下のディレクトリ・ファイルを監視して
/home/nethack/nh367/record
レコードファイル/home/nethack/dgldir/inprogress-nh367
進行中のゲームのttyrecファイル
変更を検知したらファイルの中身やファイル名を読んで、適当にフォーマットしてdiscordのwebhookを叩くということ。
問題その1: “no such file or directory"と言われる
ローカルでビルドと動作確認に成功していたのに、なぜかdockerだとアプリのバイナリがないと言われる。
以下はそのDockerfile。マルチステージビルドをする際は、build時にCGO_ENABLED=0
を指定してやらないといけないらしい。
# --------------------------------------------------------------------------------
# base image
# --------------------------------------------------------------------------------
FROM golang:1.21 AS build
WORKDIR /app
COPY . .
RUN go mod download
RUN CGO_ENABLED=0 go build -o nethack-notifier
# --------------------------------------------------------------------------------
# run image
# --------------------------------------------------------------------------------
FROM gcr.io/distroless/static
WORKDIR /app
COPY --from=build /app/nethack-notifier .
ENV RECORD_FILE_NAME="/home/nethack/nh367/record"
ENV IN_PROGRESS_DIR="/home/nethack/dgldir/inprogress-nh367"
ENV WEBHOOK_URL="https://sample.com"
ENV AVATAR_URL=""
ENV USER_NAME="Nethack Notifer"
CMD [ "/app/nethack-notifier" ]
Docker with GoのMulti Stage Buildで"no such file or directory"と出てハマった話 - @teitei_tk Blog
問題その2: NFS上の一時ファイルのせいでプレイ中のユーザ名が取れない
{ユーザ名}:{タイムスタンプ}.ttyrec
のようなファイルから、ゲームをプレイ中のユーザ名を取得する実装にしていたが、実際k8sにデプロイしてみると、以下のようなログが確認できた。
2023/10/09 09:39:34 EVENT: CREATE /home/nethack/dgldir/inprogress-nh367/4nm1tsu:2023-10-09.00:39:34.ttyrec
2023/10/09 09:39:34 EVENT: WRITE /home/nethack/dgldir/inprogress-nh367/4nm1tsu:2023-10-09.00:39:34.ttyrec
2023/10/09 09:39:41 EVENT: REMOVE /home/nethack/dgldir/inprogress-nh367/.nfs000000000098009200000020
どうやらファイルの削除時に.nfs{数字24桁}
という一時ファイルとして認識されているようで、これはv4.1以前のNFSでは回避できない挙動らしい。
仕方がないので、アプリ側でユーザ名のリストを持ち、ファイルが削除されたときにそのリストと突合することで削除されたファイルを識別する愚直な実装に変更した。
.nfsXXXX files appearing, what are those?
k8s deploy
nethack-server
はコンテナがUpになる前に/home/nethack
に必要なファイルを作成する。
これをnethack-notifier
側に共有するために永続ストレージ(emptyDirでも可)をマウントするとなると、当然マウントはこれらの処理が終わった後にされるので、事前に永続ストレージ側に必要なファイル群が入っている必要がある。卵が先か鶏が先か、ではないが、このような場合どうするのがベストプラクティスなんだろうか。
結局initコンテナでNFSにファイルが生成されていなければ生成し、/home/nethack
としてマウントし直すという泥臭いやり方になってしまったが、動いているのでまあいいか。
kind: Namespace
apiVersion: v1
metadata:
name: nethack-server
labels:
name: nethack-server
---
...
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nethack-server-deployment
namespace: nethack-server
spec:
selector:
matchLabels:
app: nethack-server-pod
replicas: 1
template:
metadata:
name: nethack-server-pod
namespace: nethack-server
labels:
app: nethack-server-pod
spec:
initContainers:
- name: nethack-init-container
image: 4nm1tsu/nethack-server:{SOME_HASH}
ports:
- containerPort: 23
env:
- name: TZ
value: "Asia/Tokyo"
volumeMounts:
- name: nethack-server
mountPath: /home/nas
command: ["sh", "-c"]
args: [
"if [ -z \"$(ls -A /home/nas)\" ]; then cp -r -p /home/nethack/* /home/nas && chmod 644 /home/nas/nh367/record; fi"
]
containers:
- name: nethack-server-container
image: 4nm1tsu/nethack-server:{SOME_HASH}
ports:
- containerPort: 23
env:
- name: TZ
value: "Asia/Tokyo"
volumeMounts:
- name: nethack-server
mountPath: /home/nethack
- name: nethack-notifier-container
image: 4nm1tsu/nethack-notifier:{SOME_HASH}
env:
- name: TZ
value: "Asia/Tokyo"
- name: RECORD_FILE_NAME
value: "/home/nethack/nh367/record"
- name: IN_PROGRESS_DIR
value: "/home/nethack/dgldir/inprogress-nh367"
- name: WEBHOOK_URL
value: "{WEBHOOK_URL}"
- name: AVATAR_URL
value: "{AVATAR_URL}"
- name: USER_NAME
value: "Nethack Notifier"
volumeMounts:
- name: nethack-server
mountPath: /home/nethack
volumes:
- name: nethack-server
persistentVolumeClaim:
claimName: pvc-nethack-server
おわりに
今回はdistroless+goシングルバイナリ+サイドカーパターンをお試ししてみたかったので、思いつきでやったみたいなところはある。