💭

LiteFS+SQLiteでフルスタックNext.jsアプリケーションを作る

2022/10/23に公開

なぜLiteFS+SQLiteか

個人開発のコストはDB次第」でサーバーレス環境でコンピューティングリソースを節約できたけどマネージドDBはまだ高いよ(要約)ということを言っていたら「本番環境でSQLiteを使うといいよ」と何人かの人に教えてもらってLitestreamのことを知った。

LitestreamはDBを読み書きするプロセスを1つにして利用するので、サーバーレス環境でsqliteファイルをパスで参照できて、複数箇所から掴まないように構築すれば条件は整えることができる(Cloud Runのように並行に呼び出してもインスタンスが共有されるサービス+最大サイズを1にしておく、とか)。

Litestreamのみでも便利に使えていたんだけど、プロジェクトをウォッチしていたらその後サーバーを複数台にしてそれぞれのインスタンスで同じ結果を得られたり、書き込み先を適切にハンドリングするデザインを最初から取り入れたLiteFSが発表された。

Litestreamで使っていたS3へのバックアップの用途もこちらでサポートされるらしく(Streaming S3 Backups #18)上位互換として使えそうだと思ったのでLiteFSを使うことにした
(LiteFS入門を参照)。

LiteFSチュートリアル

goのアプリケーションサーバーといっしょにfly.ioでLiteFSを動かすチュートリアルが先日公開されたのでこれを参考にする。

https://fly.io/docs/litefs/getting-started/

アプリケーションについて

LiteFSチュートリアルはgoだけど自分の用途ではNode.jsとNext.jsでアプリケーションを書きたいのでNode.js版を作成する。

PrismaのexamplesにあるNext.jsのAPI Routesを使ったブログ風サイトをカスタマイズ利用する。

デプロイ先

LiteFSチュートリアルと同じくfly.ioにデプロイする。LiteFSは書き込み先を特定するためにConsulを使っていて(LiteFS Architecture)、別途Consulサーバーを立てる必要があるがfly.ioで側がそれをホストしてくれるため。これは一時的なものかもしれないけど、開発元なので動作させるのが簡単になっているだろうと期待した。

fly.ioでPrisma+SQLite3なアプリケーションを作るということでRemix Indie Stackの設定を参考にした(Remixの成果物をNext.jsで使っているのでなんか不道徳な感じがする)。

Docker環境を作成

fly.ioはコンテナをデプロイして動作させるいわゆるCaaSなので、まずプロジェクトのDockerfileを書く。以下を行った

  • litefsコマンドを使えるようにする
    • 依存しているfuseパッケージを追加
  • エントリーポイントをlitefsコマンドにする

https://github.com/laiso/nextjs-prisma-litefs-sqlite/blob/main/Dockerfile

エントリーポイントをlitefsコマンドにするのはLiteFSの設定の中でサーバーの起動やマイグレーションをするため。litefsのコンテキストの中から各コマンドを実行する

  • /data をsqliteファイル置き場にする
  • /mnt/data はLiteFSが動作中に吐き出すファイルたち

https://github.com/laiso/nextjs-prisma-litefs-sqlite/blob/master/etc/litefs.yml

start.sh はアプリケーションの起動

https://github.com/laiso/nextjs-prisma-litefs-sqlite/blob/master/start.sh

問題(1): Consulサーバー接続エラー

go版は問題が起きないがNode.js版だけfly.ioのConsulサーバーに証明書エラー("x509: certificate signed by unknown authority")で接続できなかった。

go版にあわせてベースイメージをalpineにすることで回避した。

問題(2): Out of memory: Killed process

SSRしてみたらOOMで殺されてた。デフォルトのVMは256MBなので倍に増やした

fly scale vm shared-cpu-1x --memory 512

dataディレクトリの永続化

この時点ではsqliteファイルはインスタンスの内部に作成されて保存されないので、再起動やデプロイをするとDBが消し飛んでしまう。Litestreamのようなバックアップ/リストアはLiteFSにまだないのでコンテナの外に保持されるパスをマウントする必要がある。

fly.ioではVolumesという機能があって任意のパスにディスクを簡単にアタッチできるのでこれを利用する。

fly volumes create myapp_data --region sin --size 1

このリソースは任意のリージョンに所属する。注意点としてはsqliteファイルがあるディレクトリを保持するのではなくて、LiteFSが生成するジャーナルの方を保存する。

https://github.com/laiso/nextjs-prisma-litefs-sqlite/blob/main/fly.toml#L8-L10

以下のようにファイルの存在を確認できる

$ flyctl ssh sftp shell

» ls /data
dev.db
» ls /mnt/data/dbs/dev.db
database
ltx/
journal

Region分散

LiteFS入門 で行ったテストと同じ内容を行いたい。

東京regionを追加して、インスタンスを1から2に増やしてみる。

❯ fly volumes create data --region nrt --size 1
❯ fly scale count 2
❯ fly status       
App
  Name     = rest-nextjs-api-routes          
  Owner    = personal                        
  Version  = 17                              
  Status   = running                         
  Hostname = rest-nextjs-api-routes.fly.dev  
  Platform = nomad                           

Instances
ID              PROCESS VERSION REGION  DESIRED STATUS  HEALTH CHECKS   RESTARTS        CREATED    
39e6a7d6        app     17      nrt     run     running                 0               2m15s ago       
e554f731        app     17      sin     run     running                 0               13m48s ago 

nrtインスタンスのSQLiteがリードレプリカになりデータを書き込むとsinのインスタンスで書き込まれる——はずだが現状エラーが発生してうまくいなかった。

追記

この問題について解決策を見つけた。FUSEの特別なディレクトリ名でプライマリーなsqliteを特定して書き込みようにアプリケーションでサポートする必要があるようだ。

https://kentcdodds.com/blog/i-migrated-from-a-postgres-cluster-to-distributed-sqlite-with-litefs

Tips(1): デプロイ時間短縮

fly deployのデフォルトはremote(fly.io内に立てたビルドインスタンスを使う)でdocker buildしてて結構遅い…… デプロイ自体のテストをした時はローカル環境でdocker buildするように--local-onlyオプションを入れる。

fly deploy --local-only

fly.ioやLiteFSの設定ファイルをいじった時でもnext build含めフルセットでビルドが走って遅い…… 単独で確認できれば充分な箇所はマルチステージの特定のターゲットのみを指定する。

fly deploy --local-only --build-target base

Tips(2) SSHログインしてデバッグ

VMにsshログインできる機能があって、そこでsqliteファイルを直接見ることができる。

$ flyctl ssh console

>> sqlite3 /data/dev.db

Discussion