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 Goでprotocプラグイン 作った話 DMM.go #3 DMM.com プラットフォーム事業本部 メンバーシップ開発部 基盤開発グループ

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

    基盤開発グループ • Go歴は1年くらい • Go製プロダクトの保守 • プロキシやツールの実装
  3. © DMM.com 今日のお話 • 運用自動化のためのツールをGoで自作した話 • Protocol Buffersのスキーマ定義からGoのコード生成を行った • コード生成に関連した話題

    • GoでGoのコード生成をする時の話
  4. © DMM.com 背景 Goでprotocプラグインを自作して運用自動化

  5. © DMM.com 基盤開発グループ • プラットフォーム事業本部(PF)では各事業共通で使われる 種々のWebAPIを提供している(会員基盤・決済基盤 etc.) • 基盤開発グループはそれらのAPIの「横断処理」の部分を担当 •

    API Gateway(Go製) • 認可API • この他PF全体のAPI改善に向けた取り組みも行っている • PF内のDDD推進など
  6. © DMM.com API定義の管理 • API-Gatewayが提供するAPI定義の管理基盤が欲しい • APIの追加時にレビューして品質を高めたい • 管理したデータを活用して各種の自動化を行いたい Protocol

    Buffers + Git で基盤構築
  7. © 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; }
  8. © DMM.com Protocol Buffersの採用 • スキーマ記述言語としてのIDLが使いやすい • 部分構造を分けて記述できたり、簡素で読みやすい • コンパイラ(protoc)によって幅広くスキーマ定義を活用できる

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

    • スキーマ定義からgRPC実装やドキュメントを生成できる • プラグインで拡張すれば他にも様々なコード生成が可能 ➡ API管理のためのフォーマットとして優秀 この部分の例の話
  10. © 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
  11. © 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実装を生成
  12. © 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 }
  13. © 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 } ホストを指定してルーターに 埋め込む部分は手作業
  14. © DMM.com 運用自動化 • ルーティング設定を自動化したい • ホストの設定は別で管理して注入できるようにしたい • Gatewayに新規サービスが追加されるたびに作業するのは面倒 protocプラグインで自動生成

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

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

  17. © DMM.com 使うライブラリ google.golang.org/protobufモジュールを利用する • proto • Goからprotobufの各種処理を行うためのライブラリ • types/pluginpb

    • protoc・プラグイン間の通信のためのメッセージ型 • types/descriptorpb • protoファイル情報を記述したメッセージ型
  18. © DMM.com コード生成の流れ • リクエストを標準入力から読み取る • リクエストから情報抽出 • 抽出した情報からコード生成 •

    レスポンスを標準出力へ書き込む
  19. © 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 }
  20. © 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ファイル名
  21. © 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” ) テンプレート 生成テキスト
  22. © 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) } }
  23. © 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ファイル
  24. © DMM.com コード生成で気をつけたこと • コレクションに対するコード生成 • Code generatedコメント • フォーマッタの適用

  25. © DMM.com コレクションに対するコード生成 • 生成のたびに要素の順序が大きく変わると差分の管理が面倒 • 生成時に利用するコレクションはスライスで順序を管理する • ソートしてツールの側で順序が一定になるように保証する •

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

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

    細かなインデントの調整などをしなくて済む • 今回だとGoコードにおけるimportの重複排除もコレでサボっている var buf bytes.Buffer // コード生成先buffer ... // コード生成 ... out, err := format.Source(buf.Bytes()) // 生成コードをフォーマット
  28. © 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) ... }
  29. © DMM.com 展望・まとめ Goでprotocプラグインを自作して運用自動化

  30. © DMM.com Goとコード生成 • GoでGoのコードを生成するツールは揃っている • テンプレートエンジン・フォーマッタが標準ライブラリにある • 単独の実行形式をビルドしやすい点でもGoはツール向き •

    protocプラグインを作る上でもこの点はありがたかった • コード生成をうまく活用すればGoの表現力を補強できる • goa:DSLからWebAPIのIO部に関するコード生成 • sqlboiler:DBスキーマからORMの実装を生成
  31. © DMM.com 現状 • まだ実運用には乗っていない • gRPCのバックエンドがまだ出てきていない • 良い感じの運用プロトタイプはできた •

    Github API + CircleCIで自動化 • protobufの更新にフックしてコード生成 + rest-frontend更新
  32. © DMM.com さらなる展望 • protobufにはカスタムオプションという仕組みがある • スキーマ定義にドメイン固有の独自情報を追加できる • APIスキーマ以外にも様々なデータを管理できるかも •

    API-Gatewayのタイムアウト / 認可で使うURN etc. • 管理できる情報が増えれば自動化の幅を増せる • Gatewayの設定ファイルを自動生成 etc.
  33. © DMM.com まとめ • protocプラグインを自作して運用を自動化しました • Goを使ったGoのコード生成の例を紹介しました • protobufの活用の幅は広いので色々運用改善に役立てたい

  34. © 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