Slide 1

Slide 1 text

© DMM.com Goでprotocプラグイン 作った話 DMM.go #3 DMM.com プラットフォーム事業本部 メンバーシップ開発部 基盤開発グループ 松村 有倫

Slide 2

Slide 2 text

© DMM.com 自己紹介 松村 有倫(まつむらありみち) • 20新卒入社 • プラットフォーム事業本部 • 基盤開発グループ • Go歴は1年くらい • Go製プロダクトの保守 • プロキシやツールの実装

Slide 3

Slide 3 text

© DMM.com 今日のお話 • 運用自動化のためのツールをGoで自作した話 • Protocol Buffersのスキーマ定義からGoのコード生成を行った • コード生成に関連した話題 • GoでGoのコード生成をする時の話

Slide 4

Slide 4 text

© DMM.com 背景 Goでprotocプラグインを自作して運用自動化

Slide 5

Slide 5 text

© DMM.com 基盤開発グループ • プラットフォーム事業本部(PF)では各事業共通で使われる 種々のWebAPIを提供している(会員基盤・決済基盤 etc.) • 基盤開発グループはそれらのAPIの「横断処理」の部分を担当 • API Gateway(Go製) • 認可API • この他PF全体のAPI改善に向けた取り組みも行っている • PF内のDDD推進など

Slide 6

Slide 6 text

© DMM.com API定義の管理 • API-Gatewayが提供するAPI定義の管理基盤が欲しい • APIの追加時にレビューして品質を高めたい • 管理したデータを活用して各種の自動化を行いたい Protocol Buffers + Git で基盤構築

Slide 7

Slide 7 text

© DMM.com Protocol Buffers • Google製の通信データのシリアライズフォーマット • プログラミング言語非依存 • 専用のInterface Definition Language(IDL)で通信スキーマを定義 service Ping { rpc Ping (PingReq) returns (PingResp); } message PingReq { string path_id = 1; string query_id = 2; } message PingResp { string id = 1; }

Slide 8

Slide 8 text

© DMM.com Protocol Buffersの採用 • スキーマ記述言語としてのIDLが使いやすい • 部分構造を分けて記述できたり、簡素で読みやすい • コンパイラ(protoc)によって幅広くスキーマ定義を活用できる • スキーマ定義からgRPC実装やドキュメントを生成できる • プラグインで拡張すれば他にも様々なコード生成が可能 ➡ API管理のためのフォーマットとして優秀

Slide 9

Slide 9 text

© DMM.com Protocol Buffersの採用 • スキーマ記述言語としてのIDLが使いやすい • 部分構造を分けて記述できたり、簡素で読みやすい • コンパイラ(protoc)によって幅広くスキーマ定義を活用できる • スキーマ定義からgRPC実装やドキュメントを生成できる • プラグインで拡張すれば他にも様々なコード生成が可能 ➡ API管理のためのフォーマットとして優秀 この部分の例の話

Slide 10

Slide 10 text

© DMM.com rest-frontend • rest-frontend:API-Gateway用のREST/gRPC変換プロキシ • バックエンドはREST / gRPC両系統をサポートしたい • API-Gatewayからは共通してRESTで提供したい API Gateway Client Backend Service (gRPC) Backend Service (REST) rest-frontend REST REST REST gRPC

Slide 11

Slide 11 text

© DMM.com GitHub.「grpc-ecosystem/grpc-gateway: gRPC to JSON proxy generator following the gRPC HTTP spec」. https://github.com/grpc-ecosystem/grpc-gateway,(参照 2021-07-06). rest-frontendのコード生成 • rest-frontend用のコードは grpc-gatewayで生成 • protobufからサービス定義ごとに 変換プロキシのGo実装を生成

Slide 12

Slide 12 text

© DMM.com 生成されたコードの利用 func run() error { ... opts := []grpc.DialOption{grpc.WithInsecure()} // オプション mux := runtime.NewServeMux() // grpc-gateway用ルーター生成 // ホストを指定してサービスごとに生成されたプロキシをマウント err := gw.RegisterFooHandlerFromEndpoint(ctx, mux, "foo.example.com", opts) if err != nil { return err } err := gw.RegisterBarHandlerFromEndpoint(ctx, mux, "bar.example.com", opts) ... return http.ListenAndServe(":8081", mux) // http.Handlerとしてlisten }

Slide 13

Slide 13 text

© DMM.com 生成されたコードの利用 func run() error { ... opts := []grpc.DialOption{grpc.WithInsecure()} // オプション mux := runtime.NewServeMux() // grpc-gateway用ルーター生成 // ホストを指定してサービスごとに生成されたプロキシをマウント err := gw.RegisterFooHandlerFromEndpoint(ctx, mux, "foo.example.com", opts) if err != nil { return err } err := gw.RegisterBarHandlerFromEndpoint(ctx, mux, "bar.example.com", opts) ... return http.ListenAndServe(":8081", mux) // http.Handlerとしてlisten } ホストを指定してルーターに 埋め込む部分は手作業

Slide 14

Slide 14 text

© DMM.com 運用自動化 • ルーティング設定を自動化したい • ホストの設定は別で管理して注入できるようにしたい • Gatewayに新規サービスが追加されるたびに作業するのは面倒 protocプラグインで自動生成

Slide 15

Slide 15 text

© DMM.com protocプラグイン Goでprotocプラグインを自作して運用自動化

Slide 16

Slide 16 text

© DMM.com プラグインの作り • プラグインの実体はprotocと標準入出力で通信する実行形式 • 通信フォーマットはなんとprotobuf • protobufを解釈できれば実装に使う言語は自由(今回はGo)

Slide 17

Slide 17 text

© DMM.com 使うライブラリ google.golang.org/protobufモジュールを利用する • proto • Goからprotobufの各種処理を行うためのライブラリ • types/pluginpb • protoc・プラグイン間の通信のためのメッセージ型 • types/descriptorpb • protoファイル情報を記述したメッセージ型

Slide 18

Slide 18 text

© DMM.com コード生成の流れ • リクエストを標準入力から読み取る • リクエストから情報抽出 • 抽出した情報からコード生成 • レスポンスを標準出力へ書き込む

Slide 19

Slide 19 text

© DMM.com リクエスト読み取り // protocからのファイル生成リクエストを読み取る func parseReq() (*pluginpb.CodeGeneratorRequest, error) { // 標準入力から読み込み buf, err := io.ReadAll(os.Stdin) if err != nil { return nil, err } // proto.UnmarshalでGoの型にデコード var req pluginpb.CodeGeneratorRequest if err = proto.Unmarshal(buf, &req); err != nil { return nil, err } return &req, nil }

Slide 20

Slide 20 text

© DMM.com 情報抽出 // pluginpb.CodeGeneratorRequestの定義 // https://pkg.go.dev/google.golang.org/protobuf/types/pluginpb より // スライドの都合で一部編集 type CodeGeneratorRequest struct { // コード生成の対象となっているprotoファイル FileToGenerate []string // プラグインに渡される引数 Parameter *string // リクエストに含まれる全てのprotoファイル情報 ProtoFile []*descriptorpb.FileDescriptorProto // protocのバージョン CompilerVersion *Version } FileToGenerate + Protofile → 全サービス定義 Parameter →ホスト情報のyamlファイル名

Slide 21

Slide 21 text

© DMM.com コード生成 • コード生成はGo標準のtemplateパッケージを利用 • 渡した値を処理してテンプレートからテキスト生成 import ( {{range .Imports}}{{printf "\t%s %q\n" .Alias .ImportPath}}{{end}} ) import ( foo “git.dmm.com/yourorg/yourrepo/gen/go/foo” bar “git.dmm.com/yourorg/yourrepo/gen/go/bar” ) テンプレート 生成テキスト

Slide 22

Slide 22 text

© DMM.com レスポンス書き込み // protocへ生成ファイル情報を返す func emitResp(resp *pluginpb.CodeGeneratorResponse) { // proto.Marshalでprotobufにエンコード buf, err := proto.Marshal(resp) if err != nil { log.Fatal(err) } // 標準出力に書き込み if _, err = os.Stdout.Write(buf); err != nil { log.Fatal(err) } }

Slide 23

Slide 23 text

© DMM.com プラグインの利用方法 • 作ったプラグインを実行形式にビルド • ”protoc-gen-”から始まる名前にしてパスを通す • protocから”--xxx_out”オプションをつけてプラグイン呼び出し • “xxx” はプラグイン名の ”protoc-gen-” 以降の部分 • ”protoc-gen-” + “xxx” で呼び出されるプラグインが決まる # protoc-gen-grpc-gateway-muxの呼び出し protoc -I proto/ \ # protoファイルのimportパス --grpc-gateway-mux_out=. \ # 生成先ディレクトリ --grpc-gateway-mux_opt=config_file=routing.yml \ # プラグイン引数 *.proto # コード生成対象のprotoファイル

Slide 24

Slide 24 text

© DMM.com コード生成で気をつけたこと • コレクションに対するコード生成 • Code generatedコメント • フォーマッタの適用

Slide 25

Slide 25 text

© DMM.com コレクションに対するコード生成 • 生成のたびに要素の順序が大きく変わると差分の管理が面倒 • 生成時に利用するコレクションはスライスで順序を管理する • ソートしてツールの側で順序が一定になるように保証する • Goはmapのイテレーション順序がランダムなので注意 // コード生成用のデータ type registry struct { MuxPackage string // 生成するGoコードのpackage名 ProtoFiles []string // ソースとなったprotoファイル一覧 Imports []goImport // Goコードのimport Services []service // serviceに関する情報 }

Slide 26

Slide 26 text

© DMM.com Code generatedコメント • 生成されたコードであることを明示するためのコメント • 公式に仕様がある • 以下の正規表現にマッチするものをコードの先頭につける • 今回のコード生成でつけたコメント: • ツールなどが生成ファイルを識別する手段なのでつけておく ^// Code generated .* DO NOT EDIT\.$ // Code generated by protoc-gen-grpc-gateway-mux. DO NOT EDIT.

Slide 27

Slide 27 text

© DMM.com フォーマッタの適用 • 生成したコードに対してフォーマッタをかけておく • GoコードのフォーマッタはGoから呼べる ⇨ format.Source() • 細かなインデントの調整などをしなくて済む • 今回だとGoコードにおけるimportの重複排除もコレでサボっている var buf bytes.Buffer // コード生成先buffer ... // コード生成 ... out, err := format.Source(buf.Bytes()) // 生成コードをフォーマット

Slide 28

Slide 28 text

© DMM.com Before / After ルーター作成部分の処理を生成コードに切り出せた! func run() error { ... - mux := runtime.NewServeMux() - err := gw.RegisterFooHandlerFromEndpoint(ctx, mux, "foo.example.com", opts) - if err != nil { - return err - } - err := gw.RegisterBarHandlerFromEndpoint(ctx, mux, "bar.example.com", opts) - ... + mux, err := mux.New(ctx, opts) ... }

Slide 29

Slide 29 text

© DMM.com 展望・まとめ Goでprotocプラグインを自作して運用自動化

Slide 30

Slide 30 text

© DMM.com Goとコード生成 • GoでGoのコードを生成するツールは揃っている • テンプレートエンジン・フォーマッタが標準ライブラリにある • 単独の実行形式をビルドしやすい点でもGoはツール向き • protocプラグインを作る上でもこの点はありがたかった • コード生成をうまく活用すればGoの表現力を補強できる • goa:DSLからWebAPIのIO部に関するコード生成 • sqlboiler:DBスキーマからORMの実装を生成

Slide 31

Slide 31 text

© DMM.com 現状 • まだ実運用には乗っていない • gRPCのバックエンドがまだ出てきていない • 良い感じの運用プロトタイプはできた • Github API + CircleCIで自動化 • protobufの更新にフックしてコード生成 + rest-frontend更新

Slide 32

Slide 32 text

© DMM.com さらなる展望 • protobufにはカスタムオプションという仕組みがある • スキーマ定義にドメイン固有の独自情報を追加できる • APIスキーマ以外にも様々なデータを管理できるかも • API-Gatewayのタイムアウト / 認可で使うURN etc. • 管理できる情報が増えれば自動化の幅を増せる • Gatewayの設定ファイルを自動生成 etc.

Slide 33

Slide 33 text

© DMM.com まとめ • protocプラグインを自作して運用を自動化しました • Goを使ったGoのコード生成の例を紹介しました • protobufの活用の幅は広いので色々運用改善に役立てたい

Slide 34

Slide 34 text

© DMM.com 参考資料 今さらProtocol Buffersと、手に馴染む道具の話 https://qiita.com/yugui/items/160737021d25d761b353 protocプラグインの書き方 https://qiita.com/yugui/items/87d00d77dee159e74886 go generate のベストプラクティス https://qiita.com/yaegashi/items/d1fd9f7d0c75b2bb7446