はじめに
こんにちは トモニテ でバックエンド周りの開発を行っている rymiyamoto です。
今回はトモニテの新規事業として、2023 年 11 月 30 日にローンチした家族・家庭や恋愛に対する悩みをプロのカウンセラーと相談出来る新サービス トモニテ相談室 の開発に関わる中で API サーバーの開発環境での工夫について紹介していきます。
トモニテ相談室の通話周りの基盤の話は別の記事で紹介されていますので、こちらも合わせてご覧いただけると幸いです。
技術選定
新規でサービスを立ち上げるに当たっての技術選定は 2 点を意識して選びました
- できるだけ既存の技術をを使う
- 面倒なところは楽できるようにする
できるだけ既存の技術をを使う
こちらに関しては基本的にはトモニテの技術スタックを踏襲することにしました。
- 使用言語: Go
- フレームワーク: echo
- インフラ: AWS ECS
どちらも現在のトモニテだけに関わらず社内での技術スタックとして定着しているものを選びました。
面倒なところは楽できるようにする
トモニテ本体だとリリースしてから大きく内部のツールの変更はしていないままで、以下のようなアジリティが悪いところがありました。
- テーブル定義後のモデル定義が手動で、dbr だと eager loading が面倒
- エンドポイントのドキュメント定義がソースからの定義で乖離しやすく扱いづらい(echo-swagger)
そのため、今回はトモニテ本体にも後々展開もしやすくある程度ある活発なパッケージを選んでいます。
- ORM として スキーマファーストな sqlboiler
- oapi-codegen でドキュメント駆動開発
開発(ローカル)環境
ローカル環境は docker に固めて開発を行っており、go のイメージに対して、以下のようなツールを入れています。
ツール | 用途 |
---|---|
air | ホットリロード |
sql-migrate | マイグレーション |
sqlboiler | ORM |
oapi-codegen | ドキュメント駆動開発 |
mockgen | モック生成 |
gofumpt | フォーマット |
golangci-lint | 静的解析 |
ARG GO_VERSION=1.21.4 FROM golang:${GO_VERSION} AS dev RUN go install github.com/cosmtrek/air@latest RUN go install github.com/rubenv/sql-migrate/...@latest RUN go install github.com/volatiletech/sqlboiler/v4@v4.15.0 RUN go install github.com/volatiletech/sqlboiler/v4/drivers/sqlboiler-mysql@v4.15.0 RUN go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@v1.13.4 RUN go install go.uber.org/mock/mockgen@v0.2.0 RUN go install mvdan.cc/gofumpt@latest RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.55.2 COPY .air.toml / CMD ["air", "-c", ".air.toml"]
モデル生成(sqlboiler)の活用
sqlboiler はスキーマファーストなので、一般的な ORM とは異なりソースからモデルを生成するのではなく、テーブルからモデルを生成を行うことができます。
そのため前もって DB にマイグレーションをかけておくことでモデルを生成することができます。
(今回は トモニテでも運用している sql-migrate でマイグレーションを管理することにしました)
sqlboiler を使用するにあたって DB 情報や出力先を config.toml
に記載しておきます
# 生成コマンド実行時に既存のディレクトリを削除するかどうか wipe = true output = "internal/models" no-tests = true templates = [ "/go/pkg/mod/github.com/volatiletech/sqlboiler/v4@v4.15.0/templates/main" ] [mysql] dbname = "example" host = "db" port = 3306 user = "root" pass = "password" sslmode = "false" blacklist = ["gorp_migrations"]
ここまで準備できてコマンドを実行するとモデルが生成されます。
(そのままコマンドを実行すると毎回 docker compose exec app XXX
と打たないといけないので make コマンドを用意しています。)
# model生成 .PHONY: model model: model docker compose exec app sqlboiler mysql --config ./asset/sqlboiler/config.toml --add-soft-deletes
$ make model
こうすることで、internal/models
配下に DB の制約に応じたリレーションを考慮したモデルが生成されて利用ができるようになります。
ORM としての使い方は公式ドキュメントを参照してください。
ドキュメント駆動開発(oapi-codegen)の活用
oapi-codegen は OpenAPI からコードを生成するツールです。
OpenAPI の定義を api/app.yaml
に記載しておきます。
(記述は一般的な OpenAPI の構文が使えます)
openapi: 3.1.0 info: title: Example API version: 1.0.0 servers: - url: http://localhost:1323/1.0/app paths: /notifications: get: tags: - notifications summary: お知らせ一覧を取得 operationId: getNotifications responses: "200": description: OK content: application/json: schema: type: array items: $ref: "#/components/schemas/Notification" components: schemas: Notification: type: object required: - id - title - body properties: id: type: integer title: type: string body: type: string
ここまで準備できてコマンドを実行するとコード (エンドポイントの定義) が生成されます。
# openapiからのコード生成 .PHONY: openapi openapi: openapi docker compose exec app oapi-codegen -generate server -package api -o ./internal/app/api/openapi_api.gen.go api/app.yml docker compose exec app oapi-codegen -generate types -package api -o ./internal/app/api/openapi_types.gen.go api/app.yml
$ make openapi
こうすることで、internal/app/api
配下に OpenAPI に従ったエンドポイントの定義が生成されます。
その後 RegisterHandlersWithBaseURL
(自動作成される関数)を main.go で呼び出してエンドポイントの登録を行うことで、エンドポイントの定義が完了するので、実際の処理を handler に書くだけで実装が完了します。
エンドポイントへの登録
internal/app/server.go
package app import ( "github.com/rymiyamoto/server/internal/app/api" "github.com/rymiyamoto/server/internal/app/handler" "github.com/labstack/echo/v4" ) type server struct { *handler.Handler } func newServer() *server { return &server{ handler.NewHandler(), // 内部での依存解決は handler に任せる } } func RegisterHandlers(e *echo.Echo, basePath string) { server := newServer() api.RegisterHandlersWithBaseURL(e, server, basePath) }
main.go
package main import ( "github.com/rymiyamoto/server/internal/app" "github.com/labstack/echo/v4" ) func main() { e := echo.New() // ... app.RegisterHandlers(e, "/1.0/app") e.Start(":1323") }
まとめ
基本的な大枠(言語やフレームワーク)はトモニテの技術スタックを踏襲しつつ、面倒なところは楽できるようにすることで開発効率を上げることができました。
導入当初はいくらツールの歴史があるといえどキャッチアップに時間がかかることもありますが、今後の開発効率向上には必要な投資だと考えています。
また、今回はローカル環境での開発を docker に固めて行っていますが、これにより開発環境の差異をなくすことができ、開発者の環境構築にかかる時間を削減することができました。
今後もトモニテ相談室の開発においては、技術選定や開発環境の構築についても工夫を重ねていきたいと考えています。