LiteFS+SQLiteでフルスタックNext.jsアプリケーションを作る
なぜ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を動かすチュートリアルが先日公開されたのでこれを参考にする。
アプリケーションについて
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コマンドにする
エントリーポイントをlitefsコマンドにするのはLiteFSの設定の中でサーバーの起動やマイグレーションをするため。litefsのコンテキストの中から各コマンドを実行する
-
/data
をsqliteファイル置き場にする -
/mnt/data
はLiteFSが動作中に吐き出すファイルたち
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が生成するジャーナルの方を保存する。
以下のようにファイルの存在を確認できる
$ 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を特定して書き込みようにアプリケーションでサポートする必要があるようだ。
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