ヤプリ Advent Calender 2024の5日目の記事です。
はじめに
こんにちは、サーバーサイドエンジニアの中川です。
API開発時にリクエストやレスポンス内容を定義して、フロントやネイティブのエンジニアと合意をとってから開発を進めると思います。
最初に仕様定義してるとはいえ、実装途中で「やっぱりこっちの方がいい」となって双方合意の元API仕様を変更することもよくあると思います。
そうなった時、もし社内ドキュメントなどコードとは別にAPIドキュメントをまとめていた場合はそっちも変更する必要がありますよね。変更内容が少なければすぐに変更できますが、変更量が多い場合はドキュメント更新は後回しにされがちですし、リリース後に別の担当者がAPIドキュメントを更新してくれる保証もないと思います。
それが積み重なっていくとAPIドキュメントが信用できないものになって負債になってしまいます。そうならないためにコードとAPIドキュメントを同じ仕組みで更新できると嬉しいですよね。
ヤプリではサーバー↔︎フロント間やサーバー↔︎ネイティブ間の通信の一部にgrpc通信を使用しています。
リクエストやレスポンス内容はprotoファイルで定義されているのでこれをそのままAPIドキュメントにできれば最高ですが、ヤプリではprotoファイルのメンテナンスはサーバーサイドエンジニアが行うことが多く、サーバーサイド以外のメンバーにとっては見慣れないことも多いのでそのまま使うのは難しかったりします。
なので、このprotoファイルをベースにわかりやすいドキュメントを自動生成することを目指します。
実は私がこの記事を書く前からすでにヤプリでは CI での自動生成が行われており、PR上でドキュメント用のページが確認できるようになっています。 また、以前関連した記事を別のメンバーが投稿していたりするので、結論から知りたい方はそちらも参考にされるといいと思います。
本記事では上記過去記事も参考にしながら同じ仕組みを順番に作っていきます。
やること
今回用のディレクトリを作っておきます。
$ mkdir protobuf_document && cd protobuf_document
protoファイルの作成
何はともあれprotoファイルを作っていきます。
grpcやprotobufについてはこちらの記事がわかりやすかったので、その辺りから復習したいという方はそちらを参考にされるといいかと思います。
本記事でも以下の記事を参考にしています。
今回は上記記事を参考に、以下のようなprotoファイルを用意しました。
./protobuf_document ├── api │ └── v1 │ └── hello.proto
hello.proto
syntax = "proto3"; package api.v1; import "google/protobuf/empty.proto"; option go_package = "protobuf/grpc"; service HelloService { rpc GetHello(google.protobuf.Empty) returns (GetHelloResponse); } message GetHelloResponse { string message = 1; }
リクエストはなし、レスポンスにstringのメッセージが1つあるだけのシンプルなファイルです。
このprotoファイルからサーバー側のコードを自動生成していきます。
ヤプリのサーバー側のコードは主に Go が使われているので、ここではGoを使っていきます。
また、2024年現在ヤプリではコードの自動生成は buf を使っているので、同じくbufを使っていきます。
bufへの移行についても以前別のメンバーが記事を書いていたりするので、興味のある方はそちらも参考にしてください。
$ brew install bufbuild/buf/buf $ buf config init
これでbuf.yaml
ファイルが生成されたはずです。
そのまま以下のようなbuf.gen.yaml
ファイルを用意します。
./protobuf_document ├── api │ └── v1 │ └── hello.proto ├── buf.gen.yaml └── buf.yaml
buf.gen.yaml
version: v2 plugins: - remote: buf.build/protocolbuffers/go:v1.35.2 out: . - remote: buf.build/grpc/go:v1.5.1 out: .
ここまでできたらbuf generate
コマンドを実行します。するとコードが自動生成されて以下のようなディレクトリ構成になっているはずです。
$ buf generate ./protobuf_document ├── api │ └── v1 │ └── hello.proto ├── buf.gen.yaml ├── buf.yaml └── protobuf └── grpc ├── hello.pb.go └── hello_grpc.pb.go
これで元となるprotoファイルの用意は終了です。この生成されたコードを使うサーバー側のコードを作っていきます。
サーバー側のコードの作成
以下のようにgrpcサーバーをlocalhost:8080
で動かすためのmain.goを用意します。
./protobuf_document ├── api │ └── v1 │ └── hello.proto ├── buf.gen.yaml ├── buf.yaml ├── cmd │ └── server │ └── main.go ├── go.mod ├── go.sum └── protobuf └── grpc ├── hello.pb.go └── hello_grpc.pb.go
main.go
package main import ( "context" "fmt" "log" "net" "os" "os/signal" hellopb "protobuf_document/protobuf/grpc" "google.golang.org/grpc" "google.golang.org/grpc/reflection" "google.golang.org/protobuf/types/known/emptypb" ) type myServer struct { hellopb.UnimplementedHelloServiceServer } func NewMyServer() *myServer { return &myServer{} } func main() { port := 8080 listner, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) if err != nil { log.Fatalf("error occured err: %v", err) } s := grpc.NewServer() hellopb.RegisterHelloServiceServer(s, NewMyServer()) reflection.Register(s) go func() { log.Printf("start gRPC server port: %v", port) s.Serve(listner) }() quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt) <-quit log.Println("stopping gRPC server...") s.GracefulStop() } func (m *myServer) GetHello(ctx context.Context, _ *emptypb.Empty) (*hellopb.GetHelloResponse, error) { return &hellopb.GetHelloResponse{ Message: "Hello World", }, nil }
go getもお忘れなく。
$ go get -u google.golang.org/grpc
さて、ここまでできたら一度動作確認してみます。 ターミナルを2つ用意し、片方でサーバーを起動します。
$ go run cmd/server/main.go 2024/12/04 23:17:24 start gRPC server port: 8080
もう片方で、起動したlocalhost:8080
にリクエストを送ってみます。
なんでもいいですが、ここではgrpcurlを使います。
$ grpcurl -plaintext localhost:8080 api.v1.HelloService.GetHello { "message": "Hello World" }
上記のようにmessageとしてHello World
が返ってきていれば動作確認完了です。
ドキュメントの追加
さて、リクエストを送るとHello World
を返すだけの簡単なAPIサーバーができたので、ドキュメントの出力までやっていきます。
今回はGitHub Actionsを使って「プルリクエストが作成されると、GitHub Pagesにドキュメント用のページが出力されること」を目指します。
ドキュメントの生成もbufに任せたいので、buf.gen.yamlにドキュメント用の定義を追加します。
./protobuf_document ├── api │ └── v1 │ └── hello.proto ├── buf.gen.yaml ├── buf.yaml ├── cmd │ └── server │ └── main.go ├── go.mod ├── go.sum └── protobuf └── grpc ├── hello.pb.go └── hello_grpc.pb.go
buf.gen.yaml
version: v2 plugins: - remote: buf.build/protocolbuffers/go:v1.35.2 out: . - remote: buf.build/grpc/go:v1.5.1 out: . - remote: buf.build/grpc-ecosystem/openapiv2:v2.24.0 out: docs
以下のようにコマンドを叩くと、docs/api/v1/hello.swaggeer.json
というファイルが生成されるはずです。
$ buf generate
./protobuf_document ├── api │ └── v1 │ └── hello.proto ├── buf.gen.yaml ├── buf.yaml ├── cmd │ └── server │ └── main.go ├── docs │ └── api │ └── v1 │ └── hello.swagger.json ├── go.mod ├── go.sum └── protobuf └── grpc ├── hello.pb.go └── hello_grpc.pb.go
この生成処理と、生成されたJSONからドキュメント用ページを作成する部分をGitHub Actonsに任せます。 事前準備として、GitHub ActionsからGitHub Pagesへの投稿を行うために対象のリポジトリのsettings > pages > Build and deployment の部分の設定を以下のように変えておきます。
次に、GitHub Actions上で動作させるためのyamlファイルを用意します。
./protobuf_document ├── .github │ └── workflows │ └── deploy-to-github-pages.yml ├── api │ └── v1 │ └── hello.proto ├── buf.gen.yaml ├── buf.yaml ├── cmd │ └── server │ └── main.go ├── docs │ └── api │ └── v1 ├── go.mod ├── go.sum └── protobuf └── grpc ├── hello.pb.go └── hello_grpc.pb.go
deploy-to-github-pages.yml
name: Deploy to GitHub Pages on: pull_request: branches: - master types: - opened - synchronize jobs: deploy-to-github-pages: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install buf CLI uses: bufbuild/buf-setup-action@v1 - name: Generate Swagger JSON run: | buf generate - name: Checkout swagger-ui uses: actions/checkout@v3 with: repository: swagger-api/swagger-ui ref: 'v4.15.0' path: swagger-ui - name: Inject Swagger static files run: cp -n swagger-ui/dist/* docs - name: Generate Swagger UI uses: Legion2/swagger-ui-action@v1 with: output: swagger-ui spec-file: docs/api/v1/hello.swagger.json github_token: ${{ secrets.GITHUB_TOKEN }} - name: Push gh-pages branch uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./docs
swaggerの静的ファイルが上書きされないように、GitHubで公開されている index.htmlを参考に、docs配下に以下のindex.htmlも加えておきます。
./protobuf_document ├── .github │ └── workflows │ └── deploy-to-github-pages.yml ├── api │ └── v1 │ └── hello.proto ├── buf.gen.yaml ├── buf.yaml ├── cmd │ └── server │ └── main.go ├── docs │ ├── api │ │ └── v1 │ └── index.html ├── go.mod ├── go.sum └── protobuf └── grpc ├── hello.pb.go └── hello_grpc.pb.go
docs/index.html
<!-- HTML for static distribution bundle build --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Swagger UI</title> <link rel="stylesheet" type="text/css" href="./swagger-ui.css" /> <link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" /> <link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" /> <style> html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; } *, *:before, *:after { box-sizing: inherit; } body { margin:0; background: #fafafa; } </style> </head> <body> <div id="swagger-ui"></div> <script src="./swagger-ui-bundle.js" charset="UTF-8"> </script> <script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script> <script> window.onload = function() { // Begin Swagger UI call region const ui = SwaggerUIBundle({ url: "api/v1/hello.swagger.json", dom_id: '#swagger-ui', deepLinking: true, presets: [ SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset ], plugins: [ SwaggerUIBundle.plugins.DownloadUrl ], layout: "StandaloneLayout" }); // End Swagger UI call region window.ui = ui; }; </script> </body> </html>
ここまでできたら適当なブランチをもう1つ作成し、適当なPRを作成するとワークフローが動作するはずです!
1つめのワークフローでドキュメントの作成が、2つめのワークフローでドキュメントの公開が行われています!
これでドキュメントが自動生成されるようになりました!
今回はわかりやすいようにGitHub Pagesに反映するようにしましたが、静的なHTMLファイルとJSONを読み込んでいるだけなので公開しやすいやり方に変えればOKです!
良いドキュメントライフを!
詰まったこと
本当は生成されるドキュメントのページの見た目をもう少しいい感じにしたく、buf.gen.yaml
で使用しているpluginを別のものに変えようとしていました。
使おうとしたpluginがOpenAPI2.0形式で出力するようなのですが、今回GitHub Pagesにデプロイしようとしているのが問題なのか、OpenAPI2.0で出力しようと以下のようなエラーがでてうまくデプロイできなかったです。
今回は別のpluginを使うことで解消しましたが、この辺りは謎が解けていないので解明できたら別記事にしようと思います。
あとがき
今回は弊社でも行われているドキュメントの生成についてハンズオン形式でまとめました!
ドキュメント生成に限らず他にも色々な工夫があるので、興味を持った方はぜひカジュアル面談にお越しください!