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

nethack.zacharymwyatt.com favicon image nethack.zacharymwyatt.com

/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を指定してやらないといけないらしい。

Dockerfile
# --------------------------------------------------------------------------------
# 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

対象のDockerfile。特に特殊なことは行っていない素朴なDockerfile。 FROM golang:1.14.4 as builder WORKDIR /app COPY . . RUN go mod download RUN go build path/to/main.go FROM alpine RUN apk add --no-cache ca-certificates COPY --from=builder /app/path/to/main /app/main EXPOSE 5000 ENTRYPOINT [ "/app/main" ] build log $ docker…

teitei-tk.hatenablog.com favicon image teitei-tk.hatenablog.com

問題その2: NFS上の一時ファイルのせいでプレイ中のユーザ名が取れない

{ユーザ名}:{タイムスタンプ}.ttyrecのようなファイルから、ゲームをプレイ中のユーザ名を取得する実装にしていたが、実際k8sにデプロイしてみると、以下のようなログが確認できた。

kubectl logs
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?

I have an application running (on RHEL5) that streams data onto an NFS share. Recently, I saw a lot of .nfsXXXX... (xxx being a hexadecimal number) appearing in its working directory, where the

serverfault.com favicon image serverfault.com

k8s deploy

nethack-serverはコンテナがUpになる前に/home/nethackに必要なファイルを作成する。 これをnethack-notifier側に共有するために永続ストレージ(emptyDirでも可)をマウントするとなると、当然マウントはこれらの処理が終わった後にされるので、事前に永続ストレージ側に必要なファイル群が入っている必要がある。卵が先か鶏が先か、ではないが、このような場合どうするのがベストプラクティスなんだろうか。 結局initコンテナでNFSにファイルが生成されていなければ生成し、/home/nethackとしてマウントし直すという泥臭いやり方になってしまったが、動いているのでまあいいか。

nethack-server.yaml
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シングルバイナリ+サイドカーパターンをお試ししてみたかったので、思いつきでやったみたいなところはある。