おうちk8sでmisskeyインスタンスを建てて、分散型SNSを体感した。


はじめに

分散型SNSといえば、ActivityPubのMastodonMisskeyが有名だ。最近だとBlueskyがTwitterの後継として注目を集めている。

ということで今回、Misskeyをおうちk8sクラスタに導入して、分散型SNSの仕組みについて学んでみることにした。

Misskey HubにはKubernetes/TrueNASを使ったMisskey構築というk8sでのmisskey構築方法を解説する記事が公開されているが、

  • バージョンがちょっと古い
  • 積極的にメンテされている様子ではない
  • そもそも素の状態で動かなかった

ので、misskeyのリポジトリのdocker-compose.ymlの内容と、こちらのQiitaの記事を参考に、マニフェストから手動でmisskeyを構築した。

因みに、上記記事ではPi3Bでギリギリ動作すると書いてあったけれど、手元のPi4B(8GB)x3なら余裕をもって動作している様子だった。

k8sからのNFSサーバ利用については以前の記事で扱っているので、ここでは詳しく解説しない。

構築手順

misskeyコンテナは大きく分けてweb・DB(Postgre)・NoSQL(Redis)からなる。このうち、PostgreとRedisは永続化の必要があるのでPVを用意する。

また、アップロードしたデータを格納するためのPVも用意する必要がある。

NAS側での前準備

  • /k8s/misskey/db
  • /k8s/misskey/config
  • /k8s/misskey/files
  • /k8s/misskey/redis

上記ディレクトリを作成しておく。

/k8s/misskey/configディレクトリにはこのyamlファイルdefault.ymlという名前でコピーして、URLやPostgre、Redisの設定をした後保存しておく。urlの末尾は/じゃないとエラーになる(1敗)。DBおよびRedisのhost名はmy-svc.my-namespace.svc.cluster.local形式1で指定する。

また、この後の作業でRedisのコンテナ起動時、CrashLoopbackOffになってしまったので、 NFS側のsquashオプションでno_root_squashを設定しておいた。

参考:https://github.com/kubernetes/kubernetes/issues/54601

Redis

以下をapply

redis.yaml
apiVersion: v1
kind: Service
metadata:
  name: redis
  namespace: misskey
spec:
  selector:
    app: redis
  ports:
  - name: http
    port: 6379
---
kind: PersistentVolume
apiVersion: v1
metadata:
  name: pv-misskey-redis
  namespace: misskey
spec:
  storageClassName: misskey-redis
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  nfs:
    server: {NFSのIP}
    path: /k8s/misskey/redis
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: pvc-misskey-redis
  namespace: misskey
spec:
  storageClassName: misskey-redis
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
  name: redis
  namespace: misskey
  labels:
    app: redis
spec:
  restartPolicy: Always
  containers:
  - name: redis
    image: redis:7
    volumeMounts:
    - mountPath: /data
      name: redis-volume
    resources:
      limits:
        memory: "100Mi"
        cpu: "250m"
    ports:
    - containerPort: 6379
  volumes:
  - name: redis-volume
    persistentVolumeClaim:
      claimName: pvc-misskey-redis

Postgre

postgreのユーザ名・パスワード・DB名は環境変数から流し込めるようだったので、 別途secretリソースを作成して反映させる。

db.yaml
apiVersion: v1
kind: Secret
metadata:
  name: postgre-secret
  namespace: misskey
data:
  POSTGRES_PASSWORD: {base64でエンコードしたパスワード}
type: Opaque
stringData:
  POSTGRES_USER: {ユーザ名}
  POSTGRES_DB: misskey
---
apiVersion: v1
kind: Service
metadata:
  name: db
  namespace: misskey
spec:
  selector:
    app: db
  ports:
  - name: http
    port: 5432
---
kind: PersistentVolume
apiVersion: v1
metadata:
  name: pv-misskey-db
  namespace: misskey
spec:
  storageClassName: misskey-db
  capacity:
    storage: 4Gi
  accessModes:
  - ReadWriteOnce
  nfs:
    server: {NFSのIP}
    path: /k8s/misskey/db
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: pvc-misskey-db
  namespace: misskey
spec:
  storageClassName: misskey-db
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 4Gi
---
apiVersion: v1
kind: Pod
metadata:
  name: db
  namespace: misskey
  labels:
    app: db
spec:
  restartPolicy: Always
  containers:
  - name: psql
    image: postgres:15
    volumeMounts:
    - mountPath: /var/lib/postgresql/data
      name: db-volume
    envFrom:
      - secretRef:
          name: postgre-secret
    resources:
      limits:
        memory: "800Mi"
        cpu: "1"
    ports:
    - containerPort: 5432
  volumes:
  - name: db-volume
    persistentVolumeClaim:
      claimName: pvc-misskey-db

Web

コンフィグファイル用のpv/pvcとアップロードしたファイル用のpv/pvcを作成する。

misskey-pvs.yaml
kind: PersistentVolume
apiVersion: v1
metadata:
  name: pv-misskey-config
  namespace: misskey
spec:
  storageClassName: misskey-config
  capacity:
    storage: 5Mi
  accessModes:
  - ReadWriteOnce
  nfs:
    server: {NFSのIP}
    path: /k8s/misskey/config
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: pvc-misskey-config
  namespace: misskey
spec:
  storageClassName: misskey-config
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 5Mi
---
kind: PersistentVolume
apiVersion: v1
metadata:
  name: pv-misskey-files
  namespace: misskey
spec:
  storageClassName: misskey-files
  capacity:
    storage: 10Gi
  accessModes:
  - ReadWriteOnce
  nfs:
    server: {NFSのIP}
    path: /k8s/misskey/files
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: pvc-misskey-files
  namespace: misskey
spec:
  storageClassName: misskey-files
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi

続いて、サービスのデプロイ

misskey.yaml
apiVersion: v1
kind: Service
metadata:
  name: web
  namespace: misskey
spec:
  type: NodePort
  selector:
    app: web
  ports:
  - name: http
    protocol: TCP
    port: 3000
    targetPort: 3000
    nodePort: 30100
  externalTrafficPolicy: Local
  selector:
    app: web
---
kind: Deployment
apiVersion: apps/v1
metadata:
  name: web-deployment
  namespace: misskey
spec:
  replicas: 2
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: "50%"
      maxSurge: "50%"
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: web
        image: misskey/misskey:latest
        volumeMounts:
        - mountPath: /misskey/files
          name: misskey-files
        - mountPath: /misskey/.config
          name: misskey-config
        ports:
        - containerPort: 3000
        resources:
          limits:
            cpu: "2"
        command: ["pnpm", "run", "migrateandstart"]
      volumes:
      - name: misskey-files
        persistentVolumeClaim:
          claimName: pvc-misskey-files
      - name: misskey-config
        persistentVolumeClaim:
          claimName: pvc-misskey-config

DBのマイグレーションも含めてそれなりに時間がかるので、 kubectl logs -n misskey web-deployment... -fとかで確認しておくと良い。

あとは適当なサービスでpodを公開する。

Ingressを利用して公開する場合は以下のように設定する。

...
  - host: {misskey host name}
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: web
            port:
              number: 3000
...

使ってみる

デプロイ完了後、default.ymlで設定したurlにアクセスすれば、以下のような初回ログイン画面が出てくる。

ユーザ名とパスワードを入力した後、その他プロフィール情報を入力するよう求められる(後で設定することも可能)。

プロフィール画像がエラーでアップロードできなかったので、以下のエラー内容で検索したところ、NASにsshログインしてchown -hR 991.991 ./filesしてやる必要があることがわかった。

kubectl logsの出力
INFO *  [drive register]        {"size":5915,"md5":"74c5e9a818e9be5ceb0c85b6899b915c","type":{"mime":"image/png","ext":"png"},"width":128,"height":128,"blurhash":"eEK|[3|c6h]92E|c,Y$5$5A=2EFIWpJl$56hWp$5,YFI2E$5FIfQJl","sensitive":false,"porn":false,"warnings":[]}
INFO *  [drive register]        web image not created (original satisfies webpublic)
Error: EACCES: permission denied, copyfile '/tmp/tmp-117-dsjY37tT1s3S' -> '/misskey/files/aaf99761-2544-4eb8-a589-33f31a71e4db'
    at Module.copyFileSync (node:fs:2894:3)
    at InternalStorageService.saveFromPath (file:///misskey/packages/backend/built/core/InternalStorageService.js:49:12)
    at DriveService.save (file:///misskey/packages/backend/built/core/DriveService.js:137:53)
    at async DriveService.addFile (file:///misskey/packages/backend/built/core/DriveService.js:457:20)
    at async file:///misskey/packages/backend/built/server/api/endpoints/drive/files/create.js:124:35
    at async ApiCallService.call (file:///misskey/packages/backend/built/server/api/ApiCallService.js:285:16) {
  errno: -13,
  syscall: 'copyfile',
  code: 'EACCES',
  path: '/tmp/tmp-117-dsjY37tT1s3S',
  dest: '/misskey/files/aaf99761-2544-4eb8-a589-33f31a71e4db'

[v13] Drive upload failed (permission denied) · Issue #9564 · misskey-dev/misskey

💡 Summary When attempting to upload files to Drive while object storage is not in use, a permission error occurs. 🥰 Expected Behavior Drive uploads should be successful. 🤬 Actual Behavior An error ...

github.com favicon image github.com

↑上記issueを参考にした

プロフィール情報を入力し終えると、すぐに呟ける状態になった(misskey的にはノートを作成出来るようになった。だろうか)

初期状態だと、どのmisskeyインスタンスとも通信していないので、 たとえグローバルタイムラインを開こうと、閑古鳥が鳴いているばかりである。

試しに誰かフォローしようにも、検索欄からユーザ名を入れても何も出てこない。 どうやらリモートのユーザをフォローするには、照会からユーザ名を直接入力してフォローする必要があることがようだ。

指定の仕方は@ユーザ名@サーバ名。たとえばmisskey.io4nm1tsuをフォローするなら@4nm1tsu@misskey.ioと言った具合。

UIがシンプルすぎて、一般ユーザには敷居が高そうだなというか、あくまで現状はGeek向けのSNSなんだという印象。

因みに、互いに通信状態にあるインスタンスをmisskeyでは連合というらしい。

試しにmisskey.ioのユーザをフォローしたらmisskey.ioが連合になった。

ここらへんの仕様は、はじめからユーザ数の多いインスタンスなら気にならないだろうけど、マイナーなインスタンスで初めてmisskeyを使い始める人は混乱しそうだなと感じた。

それと、リモートのユーザに関する情報は正確ではない。フォロー・フォロワーは同じインスタンス内で当該リモートユーザとフォロー・フォロワー関係にある人の分しか反映されないし、ノートはリモートユーザを観測した時点での直近のノートおよび観測以降のノートしか見れない。

基本的にリモートユーザの正確な情報を見るには、リモートユーザが所属しているインスタンスを直接見に行く必要があるが、ローカルの検索機能でリモートユーザのノートの詳細URLを入力すると、ローカル上にもそのノートが反映される。リモートユーザの過去のノートをローカル上でRenoteしたい際にはこの手順を踏む必要がある。

1つMisskeyを使ってみて嬉しい誤算だったのが、ローカルに追加したカスタム絵文字は、フォローしているリモートのユーザに対してもリアクションとして使えるということだ。misskey.io等大手インスタンスでは、カスタム絵文字の追加をするためにはPatreonで月額課金が必要なので、これだけでも自鯖でmisskeyを運用するメリットがあると感じる。

他には、

  • 新規登録時に招待コードが必要になる設定
  • プッシュ通知が出来るように、ServiceWorkerの設定
    • npx web-push generate-vapid-keysを実行して得られるPublik keyとPrivate keyをを貼り付けるだけ
  • botプロテクション設定から、Cloudflare Turnstileの有効化
    • ここからサインアップしてサイトを追加後、キーをmisskey側で入力する
  • メールサーバの設定
  • 検索機能を使えるようにベースロールの修正
  • 見た目関連の設定
    • デフォルトテーマ
    • テーマカラー
    • favicon設定

等のサーバ設定をしておいた。

PWA対応なので、iOSネイティブアプリのようにプッシュ通知が来て便利

リレー設定

一通り快適に使えるようにはなったが、いかんせんGTL(グローバルタイムライン)が静かすぎて悲しい。

解決方法を調べてみると、どうやらActivity Pubを実装するSNSではpub-relayによる連合をすることで、リレーサーバに登録している他のサーバのアクティビティを受信出来るようになるらしいということがわかった。 早速下記のページから適当に3つ程リレーサービスを選んで、自鯖に登録した。

Fediverseリレーサーバー一覧2023(for Mastodon / Misskey / Pleroma)

hisubway.online favicon image hisubway.online

1時間程待つとステータスが承認済になり、またたく間にTL上に全てを目で追うのが難しい位のノートが流れ込んできた。

リレーサーバに登録した途端に一気にSNSらしくなった感じがする。これ、相当ストレージ容量食いそうだな。。

↑気休め程度に「コントロールパネル」→「全般」から「リモートのファイルをキャッシュする」を無効化しておいた。

おわりに

最後に、misskeyでインターネット上の他のインスタンスとやりとりすれば、そのインスタンス上に自分のデータが残ることになる。

いくらローカルで管理しているとはいえ、アカウントを削除すれば綺麗さっぱりインターネット上からデータが消えるというわけではないということを肝に銘じておく必要がある。

基本的な注意事項 | Misskey Hub

Misskeyサーバーにアカウントを作成する前の基本的な注意事項です。

misskey-hub.net favicon image misskey-hub.net

↑このページにあるように、

  • ノートの公開範囲をローカルに限定しようと、それを他のインスタンスが同じように非公開として扱ってくれるか
  • ノートを削除しても、他のインスタンスでそのノートを削除してくれるかどうか

は保証してくれないようなので、情報の取り扱いには気をつけようと思った。