NestJS + TypeORM + Serverless Framework で API Gateway + Lambda + RDS Proxy + RDS for MySQL で動く ToDo REST API 作ってみた。CodePipeline による CD もあるよ。
この詰め込み過ぎ感大好き🤗
所要時間は実装以外の調査やデプロイ待ち時間など諸々込みで35時間45分でした。月曜日に「今週中によろしく」って言われてもできますね🙄
と言ってもまだまだ粗削りで不完全なので引き続きブラッシュアップしていきます💪
成果物
GitHub で公開しています。
活動履歴も残しているので興味のある方はどうぞ。
https://github.com/dafujii/todo-rest-api-nest-sls/blob/master/docs/ActivityLog.md
Q. なんでこの構成?
A. 作ってみたかった。
個人開発で何か Web アプリを作ろうと思っていましたが、既存のスキルセットではモダンな開発を進めることが難しいと感じていたため、新たなスキルを習得しようと思っていました。その一環として TypeScript をやりたいと思っていた経緯があります。
4月に業務で TypeORM にチョット触れる機会があり TypeORM を知ったり NestJS の存在を知りました。5月には NestJS が Serverless Framework と組み合わせることができてLambda 上で動かせるのを知りました。実際に試して note も書きました。
あとは RDS Proxy リベンジしたかった思いもあります。以前セキュリティグループが原因って気づかずに全然繋がらんと悩んでましたから。
じゃあ知ってるもの全部組み合わせて NestJS + TypeORM + Serverless Framework でAPI Gateway + Lambda + RDS Proxy + RDS for MySQL 構成で個人開発アプリ作ろう。まずは手始めにベタに ToDo アプリでも作ることにしました。
そのため NestJS は今まで触ったことが無く、TypeORM は導入済みプロジェクトでエンティティ作ってリレーション張って find() した程度。Serverless Framework は基本的な部分は使ったことがある。RDS Proxy はセキュリティグループの設定漏れに気付かず悪戦苦闘していた、RDS for MySQLは業務でも作って使っている。Serverless Framework の CD は経験済み。そんな人です😙 ちなみに Lambda 以外でのサーバサイド Node.js も初めて。
やったこと&振り返り
リソース類作成
今までずっと(自分で作ったとは言え)既存の VPC やセキュリティグループやRDSやらがあるところに、Serverless Framework で Lambda 関数をデプロイするということをしていたので、今回も何も考えずに初めにコンソールでポチポチ作ってしまっていた。この辺も serverless.yml に書いたり、AWS CDK で書いておけばカッコよかったのかな。
ローカルでプロジェクト作成
ちょっと前に自分で書いた記事が役立った。
とはいえデプロイして RDS Proxy を介して RDS との疎通確認試みるも無事失敗。ここで詰まっていても開発は進まないので AWS 側は置いといてローカルで開発を進めることにしました。良い判断だった。
リソース類はそのまま置いておいたので RDS Proxy で課金発生してた😔
認証周り
NestJS の公式ドキュメント見ながら実装(写経)しました。
Controller にデコレータ付けときゃ JWT で認証してユーザ専用ページが作れるってすごい。JWT 認証の仕組みなど、まだ理解が追い付いていないので深掘りしたい。
ドキュメントはそんなに RESTful API とか考えて作られていなさそうなので、URL 設計どうにかしたい。
GitHubリポジトリ準備
GitHub に公開する以上、シークレットキーなどは隠蔽しておかなければならないので Systems Manager パラメータストアに設定し、serverless.yml で SSM から取得し Lambda の環境変数に入れ、それを参照するようにしました。serverless-offlineを使えばローカルでも動作するのが素晴らしい。
プライベートリポジトリならソースコード内に直書きしてましたね😛
テストやドキュメント周り
ここで少し時間が空いてしまったのでリハビリも兼ねて軽い物からやっていくようにしました。
NestJS はプロジェクト作成時に単体テストと E2E テストも生成されるありがたいやつなので、まずは単体テストを動くように改修しました。
NestJS はクライアントサーバ間で DTO 使ってデータをやり取りするらしく、久々に DTO って聞いて懐かしくなりましたね。
ER 図でテーブル構成やリソースの構成を書きました。Draw.io 使いましたがブラウザ版です。
TypeORM 導入
ローカルでサクッとやるために SQLite で。DB に保存されているユーザ情報を使ってログインできるようにした。それに合わせてテストも修正。
いくら TypeORM で DB 間の差異を埋めてくれると言っても DB に依存しちゃう部分がでるので、ローカルでも Docker で MySQL 動かしときゃよかったなと後から思うこともありました。
DB モックの良い書き方分からん。
ToDo実装
✔ ユーザ新規登録実装
✔ ToDo 一覧取得
✔ ToDo 登録
✔ ToDo 削除
✔ ToDo 取得(ここから TDD やってみた)
✔ ToDo 更新
✔ ToDo 検索
この辺りは作っていて気持ちよかったですね。コードとしては数行でできるので、ちょっと実装して、テスト書いて、動作確認してっていうサイクルを素早く回せたので。途中から TDD も取り入れましたし。
初めて触るフレームワークでテストも書き方も知らない状態で TDD やるなら、最初は E2E テストでテスト書くことになるんだろうか?
「一覧を取得」にとらわれ過ぎて、 GET /todos/list と RESTful な設計から反することやってしまいがちということに気付きました。あと検索クエリをこれから改修していきたいけど、しっくりこない。
OpenAPI
公式ドキュメント通りに導入しました。
デコレータ地獄😨 多分もう使わない……。
ただサーバ動かしてブラウザで確認するやつなので、どうやって静的ファイルとして出力などして アプリケーション 本体とは別に公開するのかが分かりませんでした。CI 環境でサーバ立てて JSON 出力してどっかに配置?
このタイミングで他のユーザの ToDo を更新ができるという致命的なバグに気付き修正しました😆
リファクタリング
TypeORM が生成するテーブルをきれいに。自動生成されるやつはどうして思い通りの命名規則になってくれないんだろう。リレーション周りは業務でもやってたのでスムーズに実現できました。
タイトルと内容の2つ作るつもりが、内容しか持たせていなかった😅 この辺ちゃんと設計せずに頭の中にあるやつをそのまま実装しているから差異が出てくるんでしょうね。
AWS デプロイ
今回 ToDo 作るにあたって一番ハマったところ。Lambda でエラー出る、RDS に繋がらない、繋がってもエラー出ると地道に一個ずつ解決しました。
使用するパッケージがネイティブモジュール使ってるとローカルから気軽に Lambda デプロイできないですね💦 そのため急遽 CodePipeline を使って CD できるようにしました。以前経験しといてよかった。
RDS 繋がらん問題に関しては、色々と原因があったのですが、特にハマったものとしては、AWS コンソールで Secret Manager 設定時に Chrome 拡張機能の LastPass が勝手に動いて入力したパスワードを、保存しているパスワードに上書きしていました。通りで全然繋がらないわけだ。
TypeORM の同期機能を使えば勝手にテーブル作成できると期待していたけど、うまくいかないことが分かり、CodeBuild の中に組み込むことで同期を実現しました。ただ開発環境ならこれでいいんでしょうが、本番環境となるとマイグレーションをどうやるかが今のところ分かってなかったりします。
さらにデータベースの charset が latin1 になっているせいで起きる文字化けを修正もしました。まぁちゃんとユーザやデータベース作れと。今まで PHPMyAdmin に頼りすぎてましたね。
これで一通りは動くものができ、今に至ります。
これから
次は Nuxt.js の勉強のためにこの ToDo のフロントエンドを作ってみようと思っています。
そのためには API をちゃんと RESTful な設計にしたり、検索機能を強化したり、バリデーションや例外処理実装したりとまだまだ不完全な部分を作り込んでいこうと思います。
最後に
TypeScript はいいぞ!
NestJS はいいぞ!
Serverless はいいぞ!
プログラミング全然わからん!
😉