こんにちは!バクラク事業部 PlatformEngineering 部の @hira です!
今回は社内オペレーションを自動化するため Connect を利用して Salesforce 連携用の API を作ったので、事例として紹介したいと思います。
そもそも Connect ってなに?
Connect は Protocol Buffers ベースの RPC ライブラリで、 gRPC や gRPC-Web 、独自のプロトコルをサポートしています。 特に独自の Connect プロトコルによって HTTP/1.1 でも動作し、JSON でやり取りできるため、HTTP クライアントがあれば通信できるといったメリットがあります。
バクラク事業部では Connect を利用してサービス間通信の基盤として様々なところで活用しています。
Connect の導入経緯や活用事例については、以下で紹介しているので是非見てください!
tech.layerx.co.jp speakerdeck.com
背景と全体像
そもそも、なぜ Salesforce 連携用の API を作るに至ったのか背景について説明します。
これまでは Salesforce で入力されたお客様の契約内容などは社内の管理画面上で入力してからデータの保存を行ってました。
Salesforce から送信される情報は管理画面のフォーム上で補完はされるものの、最終的には人による確認や修正作業が必要でした。
sequenceDiagram participant SF as Salesforce participant UI as 管理画面 participant Human as 担当者 participant DB as データベース SF->>UI: パラメータ送信 UI->>UI: フォーム自動補完 UI->>Human: 補完済みフォーム表示 Human->>Human: 内容確認 Human->>UI: フォーム送信 UI->>DB: データ送信
人手による作業が発生することで工数の増加やオペレーションミスが発生するといった課題があり、これらの課題を解決するため Salesforce との連携部分を自動化する API を作ることになりました。
API を作るにあたって mitoco X というデータ連携サービスを利用しています。
mitoco X では認証やデータの変換といった作業を行っていて、Salesforce と API 間のデータの橋渡しをしています。
最終的には以下のようなシーケンスになり、Salesforce から送信されたデータは人手を介さずに連携できるようになりました。
sequenceDiagram participant SF as Salesforce participant M as mitoco X participant API as Connect API participant DB as データベース SF->>M: データ送信 M->>M: データ変換 M->>API: JSON リクエスト API->>API: バリデーション API->>DB: データ保存 API->>M: レスポンス M->>SF: 処理結果
なぜ Connect を採用したのか
続いて、技術選択についてですが、当初は Connect と OpenAPI ベースのフレームワークを利用するかで迷っていました。 最終的には Connect を採用したのですが、採用の理由としては以下の 3 点です。
1. 開発工数の削減
社内向けの API ということもあり、限られた時間の中で開発する必要がありました。 すでに大部分が connect で実装されていることもあり、 proto 定義やミドルウェアといった既存の資産を利用できることで工数の削減が見込めました。
2. Swagger → Connect への移行が進められている
社内では一部の古い API で Swagger を利用している API があります。 しかしながら、これらの API も段階的に Connect へ移行する流れになっており、この流れに逆行する OpenAPI フレームワークの導入は負債として残り続ける可能性があったため選択肢から外すことにしました。
3. HTTP/1.1 で通信可能
Connect プロトコルにより HTTP/1.1 で通信ができるため、mitoco X のように gRPC クライアントとしての機能を持たないサービスでも通信できるという点も Connect 採用の大きな決め手となりました。
JSON API として利用する際の注意点
Connect を採用するメリットは大きかったものの、HTTP/1.1 JSON API として利用するには注意すべき点もありました。
1. Connect のエンドポイントは REST ではない
Connect のエンドポイントは REST API とは異なり、以下のような形式になります。
/{proto サービス名}/{RPC 名}
たとえば、以下のような proto があった場合
syntax = "proto3"; package example.v1; message GetExampleRequest { // ID string id = 1; } message GetExampleResponse { Example example = 1; } message Example { // ID string id = 1; // 名前 string name = 2; } service ExampleService { // 取得用エンドポイント rpc GetExample(GetExampleRequest) returns (GetExampleResponse); }
ExampleService.GetExample にリクエストしたい場合は次のようなリクエストになります。
POST /example.v1.ExampleService/GetExample Content-Type: application/json { "id": "12345" }
今回のユースケースとしては社内向けの API だったため特に課題は感じませんでしたが、REST ベースではないので外部に公開する API としては少し採用のハードルが上がるかもしれません。
なお、Connect では参照系の API も POST でリクエストする形になりますが、proto のオプションで idempotency_level
に NO_SIDE_EFFECTS
を設定することで GET リクエストも可能です。
option idempotency_level = NO_SIDE_EFFECTS;
詳細については公式のドキュメントを確認してみてください!
2. Connect には API ドキュメントを生成する仕組みがない
Connect には OpenAPI のように API ドキュメントを生成する機能が用意されていません。
API との繋ぎ込みは BizOps チームのメンバーが担当するため、proto 定義を確認しながら実装してもらうのはハードルが高く、エンジニア以外の方でも仕様を把握しやすいドキュメントが求められました。
そこで解決策として、独自の protoc プラグインを開発し proto 定義から自動的にドキュメントを生成するようにしました。
protoc プラグインの仕組みについて
protoc プラグインの仕組みを利用することで proto ファイルを中心にコードやドキュメントなど様々なものを自動生成できます。
ここで簡単に protoc プラグインの仕組みについて解説します。
protoc プラグインの仕組み自体はシンプルで以下のような流れで動作します。
- protoc が proto ファイルをパースして、AST(抽象構文木)を生成する
- protoc はあらかじめ決められた形式のデータ(
CodeGeneratorRequest
)を標準入力経由でプラグインに送信する - 受け取ったデータをプラグインが処理し、
CodeGeneratorResponse
に変換して標準出力に返す - protoc は受け取ったレスポンスをもとにファイルを生成する
開発したプラグインでは protogen パッケージを利用し、 proto を AST解析した内容をもとにマークダウンにして出力しています。 proto のコメントもドキュメントに反映できるため、開発者は proto ファイルにコメントを書くだけで、自動的にドキュメントに反映させることができます。
参考までに、実装の一部も貼っておきます。
type DocumentGenerator struct { plugin *protogen.Plugin file *protogen.File g *protogen.GeneratedFile } func (d *DocumentGenerator) generateRequestBody(message *protogen.Message) { d.g.P("### リクエストボディ") d.g.P("") d.g.P("| **フィールド** | **フィールド名** | **データ型** | **必須** | **説明** |") d.g.P("| --- | --- | --- | --- | --- |") d.generateMessageFields(message, "") d.g.P("") d.generateJSONSample("リクエスト例", message) } func (d *DocumentGenerator) generateMessageFields(message *protogen.Message, prefix string) { for _, f := range message.Fields { field := d.toCamelCase(string(f.Desc.Name())) // ネストしたメッセージの場合は再帰的に処理する if isMessageType && f.Message != nil { d.generateMessageFields(f.Message, field) continue } // proto のコメントからフィールド名などを抽出 fieldName := d.extractFieldName(field.Comments.Leading) var requiredMark string if d.isRequired(field) { requiredMark = "✅" } description := d.extractDescription(field.Comments.Leading) // マークダウンのテーブル行を生成 d.g.P("| ", field, " | ", fieldName, " | ", d.getDataType(f), " | ", requiredMark, " | ", description, " |") } }
生成されるドキュメントの例:
このドキュメント生成の仕組みにより、常に最新のドキュメントを維持できるため BizOps チームとの連携もスムーズになりました!
まとめ
今回は Salesforce 連携の自動化を目的として Connect を採用した事例を紹介しました。
Connect の採用により、既存資産を活用した効率的な開発をしつつ、オペレーションの自動化という目的を達成することができました。
Connect は gRPC 互換でありながらも HTTP/1.1 での通信も可能という柔軟なプロトコルを兼ね備えているため、今回のような社内システム連携において有効な選択だったと思います。
エンドポイントが REST ベースでなかったり、 API ドキュメント生成機能がないなどの注意点はありますが、対応により十分にカバー可能です。
今後も Connect を積極的に活用し、効率的で堅牢なシステムを開発していきたいと思います!