共同編集できるWiki「Hedgedoc」のデータをLiteStreamでS3にレプリケーションし永続化する

# はじめに

どうも、共同編集できるWikiが大好きなinajobです。

今回はHackMDのオープンソース版の[Hedgedoc](https://hedgedoc.org/)のデータをLiteStreamを使いS3にレプリケーションして永続化する方法について紹介します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100375/dd8cc698-ddf9-4dde-b597-335cdbf6a694.png)


（[LiteStream](https://litestream.io/)を使いたかっただけです。）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100375/8295981f-f698-3b61-f25c-5741e01121b0.png)


# 材料

共同編集ができるWikiを動かす時に必要なのは、アプリケーションをホストするサーバと、データを蓄えるストレージです。

愚直にやるならサーバにアプリケーションと、データベースサーバをインストールして、設定ファイルをいい感じに書く、というのをやればこれを用意できます。

しかし、昨今はXaaS(なんとか あず あ サービス)の時代。愚直にサーバにセットアップするより、それぞれを得意とするサービスを組み合わせてサービスを構築することで、より柔軟なシステムを作ることができます。

アプリケーションを動かすプラットフォーム、 データを保存するプラットフォームは様々あり、それぞれが特徴を持っていますが、今回はアプリケーションを動かすプラットフォームとしては、Kubernetes（これは社内にあるものを使います）、 S3互換のサービスとしては[Tebi.io](https://tebi.io/)を選択しました（これも、特にこだわりはなくS3互換であれば何でも良いです）。

Hedgedocはコンテンツの保存にはMySQLやPostgreSQL、SQLiteなどを想定しており、S3互換のサービスは普通は利用できません。しかし今回紹介する[LiteStream](https://litestream.io/)というソフトを使うことで、SQLiteのデータをS3互換のサービスにレプリケーションできます。

# 構成

```mermaid
graph TD
subgraph Pod
Hedgedoc
LiteStream
Volume
end
S3

LiteStream -- レプリケーション --> S3
Hedgedoc --> Volume
LiteStream --> Volume
```

# 詳細

淡々とKubernetesのManifestファイルを紹介します

## deploymentのManifest

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: hedgedoc-sqlite
  name: hedgedoc-sqlite
spec:
  replicas: 1
  selector:
    matchLabels:
      app: hedgedoc-sqlite
  template:
    metadata:
      labels:
        app: hedgedoc-sqlite
    spec:
      initContainers:
      - image: litestream/litestream # すでにあるバックアップから復元する処理
        name: litestream-init
        command: ["/bin/sh", '-c']
        args:
        - |
          /usr/local/bin/litestream restore -config /opt/litestream/litestream.yaml /data/hedgedoc.sqlite && chmod 777 /data/hedgedoc.sqlite || true
        volumeMounts:
        - name: data
          mountPath: /data
        - name: litestream-secret
          mountPath: /opt/litestream/litestream.yaml
          subPath: litestream.yaml
      containers:
      - image: quay.io/hedgedoc/hedgedoc #　Hedgedoc本体
        name: hedgedoc
        env:
        - name: CMD_DOMAIN
          value: ＜ingressなどで公開するドメインに合わせる＞
        - name: CMD_DB_DIALECT
          value: sqlite
        volumeMounts:
        - name: data
          mountPath: /data
        - name: hedgedoc-config
          mountPath: /hedgedoc/config.json
          subPath: config.json
      - image: litestream/litestream # hedgedoc動作中にレプリケーションするサイドカー
        name: litestream
        command: ["/bin/sh", '-c']
        args:
        - |
          /usr/local/bin/litestream replicate -config /opt/litestream/litestream.yaml
        volumeMounts:
        - name: data
          mountPath: /data
        - name: litestream-secret
          mountPath: /opt/litestream/litestream.yaml
          subPath: litestream.yaml
      volumes:
      - name: hedgedoc-config
        configMap:
          name: hedgedoc-sqlite
          items:
          - key: config.json
            path: config.json
      - name: litestream-secret
        secret:
          secretName: litestream
          items:
          - key: litestream.yaml
            path: litestream.yaml
      - name: data #　コンテナ間でデータを共用するためのボリューム
        emptyDir: {}
```

## 設定ファイルのManifest

```litestream.yaml
dbs:
  - path: /data/hedgedoc.sqlite
    replicas:
      - type: s3
        endpoint: https://s3.tebi.io
        name: hedgedoc.sqlite
        bucket: hedgedoc-backup
        path: hedgedoc.sqlite
        forcePathStyle: true
        sync-interval: 1s
        access-key-id: ＜アクセスキー＞
        secret-access-key: ＜シークレットアクセスキー＞
```

以下のコマンドで上記をSecretリソースとしてデプロイします

```bash
$ kubectl create secret generic litestream --from-file=litestream.yaml
```

```config.json
{
  "production": {
    "db": {
      "dialect": "sqlite",
      "storage": "/data/hedgedoc.sqlite"
    }
  }
}
```

以下のコマンドで上記をConfigMapリソースとしてデプロイします

```
$ kubectl create configmap hedgedoc-sqlite --from-file=config.json
```

## 外部からのアクセスのためのマニフェスト

メインのmanifestはここまでで、以下はクラスタ外からアクセスできるようにするためのServiceとIngressリソースです。
クラスタの構成によってはServiceのtype=Loadbalanerを使ったり、別途DNSの設定が必要なこともあります。

```yaml
apiVersion: v1
kind: Service
metadata:
  labels:
    app: hedgedoc-sqlite
  name: hedgedoc-sqlite
spec:
  ports:
  - port: 3000
    protocol: TCP
    targetPort: 3000
  selector:
    app: hedgedoc-sqlite
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: hedgedoc-sqlite
spec:
  rules:
  - host: ＜外部に公開するドメイン名＞
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: hedgedoc-sqlite
            port:
              number: 3000
```


# 感想

ここまでのマニフェストをデプロイすると、普通にHedgedocがデプロイでき、1s間隔でデータベースの変更がS3にレプリケーションされるようになります。

Kubernetes上のPodが再生成されても、データは復元されます。

もちろんこの構成にはパッと思いつくだけでも以下のようなたくさんの制限があります

- 複数のPodを作ろうとするとデータが不整合になる
  - スケールアウトできない
  - 無停止でのバージョンアップができない
- 同期タイミングより早くアプリケーションが落ちるとその間のデータは失われる
- データが多くなると再デプロイ時の復元でネットワーク帯域を消費する
- データの変更が多い使い方をするとネットワークの帯域を消費する
- 書き込みのオーバーヘッドが生じる

しかし、この制限を補ってあまりあるほどの簡潔さがこの仕組みにはあるように感じました。

ちょっと個人でデータを永続化するサービスを設計する際はもちろん、大規模なサービスを構成するパターンの1つとして、エッジで動くSQLiteと永続化のためのS3という構成は役立つ場面が多いのではと思います。

# 参考

正直ほとんどこの記事をなぞっただけです。
https://zenn.dev/mattn/articles/fef682a8b204ac