自分のブログのバックエンドを Kotlin/ktor で作成した。
備忘と誰かの参考になればということで概要をまとめる。
リポジトリはこちら。
これは REST API サーバーであり、フロントエンドは別にある。
利用技術
- アプリケーション
- Kotlin
- ktor/server
- Logback
- Koin
- Exposed
- Flyway
- Kotlin Poet
- OpenAPI Generator
- テスト、CI/CD
- JUnit5
- Kotest
- Mockk
- karate
- GitHub Actions
- インフラ
- AWS
- ALB
- ECS
- terraform
Kotlin/ktor と AWS を利用するのは自分の学習のためで確定、それ以外はこの前提に合わせて適当なものをピックアップした。
アプリケーションアーキテクチャ
Application, Routing, Service, Repository の4レイヤーの構造になっている。
それぞれのレイヤー間は Koin で DI している。
- ktor に依存するレイヤー
- Application: routing を参照してエンドポイントを利用できるようにする。その他 ktor の各種 plugin を利用する
- Routing: 実際のエンドポイントを記述するレイヤー。1パス(例:/users)に対して1ファイルで、その中にHTTPメソッドごとの処理を記述する
- ktor に依存しないレイヤー
- Service: ドメインロジックを記述するレイヤー。普通は1つ以上の repository を利用して読み書きする
- Repository: アプリケーション外との疎通を抽象化するレイヤー。現状は DB アクセスを抽象化している
実装のポイント
ktor は薄めのサーバーアプリケーションであり、実際に便利に開発するにあたっては、いくつかのポイントで判断が必要になる。
いくつか特徴的なポイントについて紹介する。
OpenApi Generator によるパス・モデルのコード自動生成
OpenApi で定義したパスとモデルを ktor で利用できるようにコードを自動生成している。
詳細は別の記事に記載した。
この方法にはいくつかの利点がある。
- クライアント側も OpenApi に基づいて自動生成したコードを利用できるので実装が簡便になる
- リクエストのバリデーションも自動生成に任せて型安全に扱うことができる。結果、 Location を使う必要がなくなる
当初はパスやモデルを扱う機能として Location を利用していた。
ただ、 Location が Experimental である点や、リクエストボディ は Location ではバリデーションできない点から使いづらさを感じていた。
OpenApi で自動生成したコードでパス、モデル、パスパラメータやリクエストボディのバリデーションを行うことで、Location は不要になった。
自動生成したコードを信頼すれば、かなり楽になったといえる。
ただ、 OpenApi Generator による自動生成は .mustache をベースにしていて可読性・保守性に欠ける。
将来的には KSP や Kotlin Poet 等を利用して Kotlin にフォーカスした自動生成に挑戦したい。
DBまわり: ORM とマイグレーション
ktor には DB に関連する機能は一切ないので、自分で選定する必要がある。
ORM としては Exposed、DB マイグレーションツールとして Flyway を利用することにした。
Exposed は Kotlin で書かれた ORM で、 現状の利用範囲では特に不都合を感じていない。
ただ、メジャーリリースはまだされていないので、大規模サービスで SQL をチューニングしたいといった需要に応えられるかどうかはわからない。
Exposed の課題としては、DB マイグレーションに関する機能がないことだ。
話題には上がっている が、未解決のままだ。
現時点では他のマイグレーションツールを利用するのが妥当と思われる。
JVM 系のマイグレーションツールをざっと調べて、 Flyway を選択した。
バージョニングと Kotlin Poet を利用したコード自動生成
バージョニングにはセマンティックバージョニングを用いず、{APIバージョン}-{日時}
とした。
また、ビルド時にはコミットハッシュを含めてサーバーが動作しているコミットを確認できるようにした。
バージョニングにセマンティックバージョニングを用いなかったのは以下の理由からである。
- GitHub フローで高頻度で main ブランチへのマージと Docker image の push を行っており、機械的にバージョンを上げやすくするため
- API バージョンは v1 という粒度で切られており、このバージョンと対応させるため
- クライアント視点で問題になるのも API バージョン(正確にはAPIエンドポイントのパス)であり、その他のマイナー・パッチのバージョンは利用しないため
- 個人開発でメジャー、マイナー、パッチのどのバージョンを上げるか悩まなくてもよいようにするため
また、バージョニングとは別に、特に開発中に動作しているサーバーがいつの時点のコードをもとにビルドしたものかを確認したかった。
そのために毎回のビルド時に Kotlin Poet を利用してコミットハッシュをコードに埋め込み、参照できるようにした。
ビルド時点とコミットハッシュが対応するようになることで動作確認をしやすくなった。
karate による e2e テスト
JVM 系の技術で e2e テストをするために、 karate を選択した。
書くにあたっては Java や JS が入り混じったかなり発達した独自構文に慣れる必要はある。
しかし、読むのは容易で意図をシンプルに反映したテストを書くことができる。
karate はアプリケーションには依存しない完全な別モジュールとして扱っている。
e2e テストにはかなり助けられた。
例えば、当初 Location など ktor の機能を使ってルーティングしていたところを、 OpenApi Generator で自動生成したコードに書き換えたことがあった。
このときにすべてを手動テストで担保しなければならないとしたら、個人開発では大きなコストになっていた。
e2e テストでクライアントに対して担保すべき動作が規定されることで、大きな構造の変更も妥当なコストでできる。
感想
バックエンド開発ははじめてかつ ktor が薄いフレームワークということもあり、いろいろ勉強になった。
一方で、早くかつ信頼性のあるアプリケーションを開発するのには ktor は不向きかと思う。
例えば、 CSRF トークンに関する実装やライブラリがなく、現在自分で実装中だ。
それはそれで勉強にはなるものの、セキュリティに関する部分を独自開発というのはリスクが伴う。
基本的な部分は枯れた信頼性のあるフレームワークを利用するのが望ましい。
小さめの、特定の機能に特化した外部インターフェースとしてのサーバーを開発する場合には ktor は適した道具といえそうだ。
ktor の v2 が先日 beta になった。
楽しみにしつつ、少しずつ機能追加していきたい。