LayerX エンジニアブログ

LayerX の エンジニアブログです。

Connect を HTTP/1.1 + JSON API として使う!Salesforce 連携 API の実装事例

こんにちは!バクラク事業部 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 間のデータの橋渡しをしています。

www.mitoco.net

最終的には以下のようなシーケンスになり、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_levelNO_SIDE_EFFECTS を設定することで GET リクエストも可能です。

option idempotency_level = NO_SIDE_EFFECTS;

詳細については公式のドキュメントを確認してみてください!

connectrpc.com

2. Connect には API ドキュメントを生成する仕組みがない

Connect には OpenAPI のように API ドキュメントを生成する機能が用意されていません。
API との繋ぎ込みは BizOps チームのメンバーが担当するため、proto 定義を確認しながら実装してもらうのはハードルが高く、エンジニア以外の方でも仕様を把握しやすいドキュメントが求められました。

そこで解決策として、独自の protoc プラグインを開発し proto 定義から自動的にドキュメントを生成するようにしました。

protoc プラグインの仕組みについて

protoc プラグインの仕組みを利用することで proto ファイルを中心にコードやドキュメントなど様々なものを自動生成できます。
ここで簡単に protoc プラグインの仕組みについて解説します。

protoc プラグインの仕組み自体はシンプルで以下のような流れで動作します。

  1. protoc が proto ファイルをパースして、AST(抽象構文木)を生成する
  2. protoc はあらかじめ決められた形式のデータ(CodeGeneratorRequest)を標準入力経由でプラグインに送信する
  3. 受け取ったデータをプラグインが処理し、CodeGeneratorResponse に変換して標準出力に返す
  4. 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 を積極的に活用し、効率的で堅牢なシステムを開発していきたいと思います!