Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Goでprotocプラグイン作った話 DMM.go #3

Goでprotocプラグイン作った話 DMM.go #3

Arimichi Matsumura

July 26, 2021
Tweet

Other Decks in Programming

Transcript

  1. © DMM.com 自己紹介 松村 有倫(まつむらありみち) • 20新卒入社 • プラットフォーム事業本部 •

    基盤開発グループ • Go歴は1年くらい • Go製プロダクトの保守 • プロキシやツールの実装
  2. © 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; }
  3. © DMM.com Protocol Buffersの採用 • スキーマ記述言語としてのIDLが使いやすい • 部分構造を分けて記述できたり、簡素で読みやすい • コンパイラ(protoc)によって幅広くスキーマ定義を活用できる

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

    • スキーマ定義からgRPC実装やドキュメントを生成できる • プラグインで拡張すれば他にも様々なコード生成が可能 ➡ API管理のためのフォーマットとして優秀 この部分の例の話
  5. © 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
  6. © 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実装を生成
  7. © 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 }
  8. © 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 } ホストを指定してルーターに 埋め込む部分は手作業
  9. © DMM.com 使うライブラリ google.golang.org/protobufモジュールを利用する • proto • Goからprotobufの各種処理を行うためのライブラリ • types/pluginpb

    • protoc・プラグイン間の通信のためのメッセージ型 • types/descriptorpb • protoファイル情報を記述したメッセージ型
  10. © 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 }
  11. © 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ファイル名
  12. © 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” ) テンプレート 生成テキスト
  13. © 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) } }
  14. © 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ファイル
  15. © DMM.com コレクションに対するコード生成 • 生成のたびに要素の順序が大きく変わると差分の管理が面倒 • 生成時に利用するコレクションはスライスで順序を管理する • ソートしてツールの側で順序が一定になるように保証する •

    Goはmapのイテレーション順序がランダムなので注意 // コード生成用のデータ type registry struct { MuxPackage string // 生成するGoコードのpackage名 ProtoFiles []string // ソースとなったprotoファイル一覧 Imports []goImport // Goコードのimport Services []service // serviceに関する情報 }
  16. © DMM.com Code generatedコメント • 生成されたコードであることを明示するためのコメント • 公式に仕様がある • 以下の正規表現にマッチするものをコードの先頭につける

    • 今回のコード生成でつけたコメント: • ツールなどが生成ファイルを識別する手段なのでつけておく ^// Code generated .* DO NOT EDIT\.$ // Code generated by protoc-gen-grpc-gateway-mux. DO NOT EDIT.
  17. © DMM.com フォーマッタの適用 • 生成したコードに対してフォーマッタをかけておく • GoコードのフォーマッタはGoから呼べる ⇨ format.Source() •

    細かなインデントの調整などをしなくて済む • 今回だとGoコードにおけるimportの重複排除もコレでサボっている var buf bytes.Buffer // コード生成先buffer ... // コード生成 ... out, err := format.Source(buf.Bytes()) // 生成コードをフォーマット
  18. © 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) ... }
  19. © DMM.com Goとコード生成 • GoでGoのコードを生成するツールは揃っている • テンプレートエンジン・フォーマッタが標準ライブラリにある • 単独の実行形式をビルドしやすい点でもGoはツール向き •

    protocプラグインを作る上でもこの点はありがたかった • コード生成をうまく活用すればGoの表現力を補強できる • goa:DSLからWebAPIのIO部に関するコード生成 • sqlboiler:DBスキーマからORMの実装を生成
  20. © DMM.com さらなる展望 • protobufにはカスタムオプションという仕組みがある • スキーマ定義にドメイン固有の独自情報を追加できる • APIスキーマ以外にも様々なデータを管理できるかも •

    API-Gatewayのタイムアウト / 認可で使うURN etc. • 管理できる情報が増えれば自動化の幅を増せる • Gatewayの設定ファイルを自動生成 etc.