はじめに
この記事は、フラー株式会社 Advent Calendar 2021 の13日目の記事です。 12日目は@seto_inugami で「技術選定を疎かにしたツケを払ったお話し
」でした。
さて、フラーではデジタルパートナー事業 の一環としてこれまで何本かのスマートフォン アプリケーションをリリース、運用しています。当然ながらアプリが利用するデータはサーバーで管理されており、ほとんどのアプリでそのバックエンド(Web API やインフラ)もフラーが開発、運用しています。
今回はフラーがGoのWebアプリケーションフレームワーク として採用しているGoaの紹介と、フラー流の使い方について紹介します。
Goaとは
GoaはAPI デザインをDSL で記述すると、Web API をホスティング するためのベースとなる部分(ルーティングやリクエス トボディの構築など)や、API クライアントを開発するためのベースとなる部分を自動生成してくれるフレームワーク です。
goa.design
Goaの特徴
Goaは独自定義のDSL をベースにAPI サーバー、API クライアント、Swagger、果てはテストコードまで生成することができます。とても機能が多く、GoのWebフレームワークとしてメジャーなGin やEcho と比べたらとても重いフレームワーク だと思います。
なぜGoaなのか?
Goaの特徴のひとつであるSwaggerを生成できる という点に魅力を感じているからです。
フラーではサーバーとクライアントの間のWeb API 仕様はSwaggerによって共有しています。
開発をスムーズに進めるためには、Web API の仕様と実装の同期を保てるか(=Swaggerを最新の情報に保ち続けられるか)がとても重要なポイントとなります。
GoaはひとつのDSL からアプリケーションコードとSwaggerファイルが同時に生成されるので、仕様と実装が一致した状態を簡単に保つことができます。また、Swaggerも自動生成なので、ニンゲンがあのYAML やJSON を記述するより遥かに正確・確実に更新することができます。
よくGoのWebアプリケーション界隈では「ドキュメントと実装の整合性をどう保つか」みたいな問題が発生して、go-swagger をはじめとするSwaggerと連携できるライブラリと、GinやEchoなどのWeb API フレームワーク をどう組み合わせるか、みたいな苦労をしている印象があります。
GoaはWeb API 開発とドキュメンテーション をセットで扱えるため、そういったライブラリ/フレームワーク の相性問題を考える必要がありません。
Goaの開発事情
現在Goaはv3とv1の2ラインがあり(なぜかv2は無かったことにされている)、メインの開発はv3で行われています。フラーでは昔からv1を使っていたのですが、v3リリース後も(2021年12月現在)移行に踏み切れていません。完全に下火になってしまった本家にコントリビュートし続けることに疲れた我々は、ついに弊社テックリード が独自カスタマイズを加えたfork版を使うことにしました。
github.com
完全に本家と袂を分かったことで、完全にv3にマイグレーション するきっかけを見失ってしまいました。。どうしましょうかね。要検討です。
実際にGoaで開発してみる
文章でつらつら書いててもアレなので、実際にGoaでWeb API を開発するときのサンプルコードを用意してみました。
github.com
GET /v1/users/{user_id}
という単一のエンドポイントを持つシンプルなコードです。アプリケーションを起動して次のようにリクエス トをすると、予め登録された情報が返ってきます。
$ curl http://127.0.0.1:8080/v1/users/1
{
"created_at": "2021-09-25T11:22:33Z",
"family_name": "田中",
"given_name": "太郎",
"user_id": 1
}
自動生成されたSwaggerファイルをSwagger UIで表示すると、このエンドポイントの仕様を確認することができます。
Goaから生成されたSwagger
Goaに関係する部分だけ抜き出すと大体こんな感じになってます。
webapi/
├── app # APIを実装するための自動生成コード
├── design # DSL
├── gen # DSLから各種コードを生成するコマンド
├── swagger # Swaggerファイル
├── user.go
├── user_test.go
└── webapi.go
注目すべきは gen
ディレクト リです。
gen
にはDSL からコードを生成するためのプログラムが入っています。次のようなコードです。
package main
import (
_ "github.com/furusax0621/goa-sample/webapi/design"
"github.com/shogo82148/goa-v1/design"
"github.com/shogo82148/goa-v1/goagen/codegen"
genapp "github.com/shogo82148/goa-v1/goagen/gen_app"
genswagger "github.com/shogo82148/goa-v1/goagen/gen_swagger"
)
func main() {
codegen.ParseDSL()
codegen.Run(
genswagger.NewGenerator(
genswagger.API(design.Design),
),
genapp.NewGenerator(
genapp.API(design.Design),
genapp.OutDir("app" ),
genapp.Target("app" ),
),
)
}
このコードを go generate
で実行されるよう設定しておき、DSL を更新したら go generate ./...
と実行することで自動生成コードを更新する、といった手法で開発しています。
GoaはDSL から各種コードを生成するときに通常 goagen
というコマンドをインストールして使います。 goagen
を使わないようにしているのは、弊社が複数のアプリを開発・運用しているという事情が関係しています。
通常、1台の開発環境にインストールできる goagen
コマンドはひとつだけです。 goagen
をビルドしたGoaのバージョンとDSL が依存しているGoaのバーションは一致している必要があるので、複数のプロジェクトが同時稼働しているとバージョン管理がとても困難になってしまいます。
プロジェクト間やエンジニア間での混乱を防ぐため、Goの標準コマンドである generate
で管理できるような構成にしているというわけです。
API を追加してみる
では、新しいエンドポイントとしてユーザーを登録するための POST /v1/users
というエンドポイントを追加してみましょう。
まずはDSL を記述します。実装したいアクション、ルーティングパス、パラメータ、レスポンスの種類などを記述していきます。
Action("post" , func () {
Routing(POST("" ))
Payload(func () {
Member("given_name" , String, func () {
Description("名前" )
Example("太郎" )
})
Member("family_name" , String, func () {
Description("姓" )
Example("田中" )
})
Required("given_name" , "family_name" )
})
Response(Created, UserMedia)
})
DSL を記述したら go generate ./...
を実行して、自動生成コードやSwaggerを更新します。
$ go generate ./...
swagger
swagger/swagger.json
swagger/swagger.yaml
app
app/contexts.go
app/controllers.go
app/hrefs.go
app/media_types.go
app/user_types.go
app/test
app/test/user_testing.go
この時点でSwaggerファイルも自動的に更新されます。便利ですね。
更新後のSwagger
自動生成したコードを利用してControllerにメソッドを追加します。 app
パッケージには、実装すべきControllerのインターフェースが宣言されています。
type UserController interface {
goa.Muxer
Get(*GetUserContext) error
Post(*PostUserContext) error
}
リクエス トのパラメータやペイロード で宣言した変数や、宣言したレスポンスを返すためのメソッドは app
パッケージの (Action名)(Resource名)Context
に内蔵されています。
type PostUserContext struct {
context.Context
*goa.ResponseData
*goa.RequestData
Payload *PostUserPayload
}
Controllerはこれを引数として受けつつ、ここから情報を受け取りながらビジネスロジック を書いていきます。
実際に実装した例がこちらになります。
github.com
Goaのここがつらい
初期導入コストが高い
これはこのぐらい大きなフレームワーク ならあるあるだと思うのですが、そもそもGoaの学習コストがとても高いです。アプリケーションの記述方法が決まってるだけでなく、独自定義のDSL も覚える必要があります。
フラーのサーバーサイドエンジニアとして入社すると、まずGoaとお友達にならなくてはなりません。最近入社した方は、オンボーディング期間中に一回Goaを使えるようにするためのチュートリアル をやってもらうようにしています。
また、前準備として記述しなくてはいけないコード量が多いので、最低限のAPI サーバーを立ち上げるだけでもそれなりの時間を要します。「ちょっと試しにAPI 作ってみよ」という軽いノリで導入できるものではありません。
コードの記述量も多くなりがち
今回紹介したシンプルなコードですらそれなりのコーディングを要求されますし、何より自動生成されるコード量がとても多いです。
ほとんどはレビュー対象から外れるとは言え、うっかり変更が10,000行超えるPull Requestなんかが簡単に発生してしまうので、
レビュワーの精神力をごっそり持っていきがちです。
swagger.json が確定コンフリクトする
Goaで生成されるSwaggerファイルにはYAML とJSON フォーマットがあるのですが、この内JSON はインデントはおろか改行すらされず、一行にすべての内容がガッと記述されてしまいます。
これの何が困るって、複数のAPI を同時並行で開発していると、確定でコンフリクトを起こしてしまうんですね。複数人で開発を進めていると毎度一回mergeするなりrebaseするなり対応しなくてはいけないので、非常に面倒です。YAML ファイルさえ生きていればJSON ファイルは不要なので、フラーでは大抵 swagegr.json をGit管理から外すことで対応しています。
github.com
おわりに
というわけで、フラーで採用しているGoaフレームワーク に簡単に触れてみました。
Goaにはまだまだたくさんの機能があり、慣れれば色々なユースケース に対応したAPI を開発することができます。
Goでバリバリ活躍したい方、Goaフレームワーク に対する理解をもっと深めたい方、ぜひ私達と一緒に働きましょう!!フラーは通年でサーバーサイドエンジニアを募集しています!!!
明日は@SaturnR7 さんで「何かを」です。お楽しみに!