1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

HerokuAdvent Calendar 2024

Day 15

Heroku Integration をほんのちょっぴりだが体験した

Last updated at Posted at 2024-12-14

おれは"Salesforce Functions"を試そうと思ったら、"Heroku Integration"を試していた
な、何を言っているのかわからねーと思うが(ry

ようこそ Heroku Integration !!
Dreamforce2024で発表されたHeroku Integrationを触ってみたので紹介したいと思います。
※2024.12月時点ではまだパイロット版で、今後変わる可能性もあります。

本記事では、Getting Started Heroku Integrationに沿って試してみて、つまずいたポイントなども併せて紹介します。

0.前提

  • Heroku Integration Pilot Programに申込済みであること
  • Salesforce Sandbox環境(or ScratchOrg)があること
  • Heroku CLIをインストールしていること
  • 環境
    • MacOS 14.5 (Apple M2)
    • Node.js: v20.15.0
    • npm: 10.7.0
    • heroku-cli: heroku/9.5.0 darwin-arm64 node-v16.20.2

1.Heroku Integration Pluginのインストール

以下のコマンドでインストールします。

$ heroku plugins:install @heroku-cli/plugin-integration

正常にインストールされれば、以下のコマンドで integration 0.0.9 が返ってきます。

$ heroku plugins
integration 0.0.9

2.Integration Projectの作成

$ heroku integration:project welcome-integration

Generating a Salesforce integration project in JavaScript
   create bin/invoke.sh
   create src/plugins/heroku-salesforce.js
   create src/plugins/README.md
   create src/routes/index.js
   create src/app.js
   create test/plugins/heroku-salesforce.test.js
   create test/routes/example.test.js
   create test/routes/root.test.js
   create test/helper.js
   create api-spec.yaml
   create app.json
   create Procfile
   create README.md
   create package.json

こんな感じにプロジェクトが作成されます。

screenshot_001.png

ちょっと一息(1)

(12月時点では)さりげない落とし穴があるので、ちょっと一息つきましょう。
上記コマンドで作成された api-spec.yaml を確認してみます。

api-spec.yaml
openapi: 3.0.0
info:
  version: 1.0.0
  title: Heroku-Salesforce API
  description: Example Heroku app as an API using Salesforce SDK.
servers:
  - url: http://127.0.0.1:300
paths:
  /accounts:
    get:
      operationId: GetAccounts,
      description: Returns list of array of Accounts
      responses:
        '200':
          description: Successfully returned a list of Accounts
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: string
                    name:
                      type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/DeveloperError'
        '503':
          $ref: '#/components/responses/UnknownError'
  /unitofwork:
    post:
      operationId: UnitOfWork
      description:
        Receives a payload containing Account, Contact, and Case details and uses the
        Unit of Work pattern to assign the corresponding values to to its Record
        while maintaining the relationships. It then commits the unit of work and
        returns the Record Id's for each object.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                accountName:
                  type: string
                lastName:
                  type: string
                subject:
                  type: string
                callbackUrl:
                  type: string
      responses:
        '201':
          description: Received UnitOfWork API request
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/DeveloperError'
        '503':
          $ref: '#/components/responses/UnknownError'
      callbacks:
        unitOfWorkResponse:
          '{$request.body#/callbackUrl}':
            post:
              description: Response from /unitofwork API
              operationId: unitOfWorkResponseCallback
              requestBody:
                content:
                  application/json:
                    schema:
                      type: object
                      properties:
                        accountId:
                          type: string
                        contactId:
                          type: string
                        cases:
                          type: object
                          properties:
                            serviceCaseId:
                              type: string
                            followupCaseId:
                              type: string
              responses:   # Expected responses to the callback message
                '200':
                  description: Your server returns this code if it accepts the callback
components:
  responses:
    NotFound:
      description: The specified resource was not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Unauthorized
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    DeveloperError:
      description: DeveloperError
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    UnknownError:
      description: UnknownError
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

  schemas:
    # Schema for error response body
    Error:
      type: object
      properties:
        code:
          type: string
        message:
          type: string
      required:
        - code
        - message

なんと、operationId: GetAccounts, と謎のカンマが。
これがあると、Salesforceにインポートした後の動作検証で期待通りに動かなくなります。
この時点で余計なカンマは外しておきましょう。

screenshot_002.png

--- (ちょっと一息(1)終わり) ---

そのまま、モジュールをインストールして確認してみましょう

$ npm install
...

$ npm run dev

> welcone-integration@0.0.0 dev
> fastify start -w -l debug -d -P src/app.js

Debugger listening on ws://127.0.0.1:9320/ac4c56e5-10db-4e4b-8ba6-a4530a6c81b7
For help, see: https://nodejs.org/en/docs/inspector
[17:13:23.614] INFO (71180): Server listening at http://[::1]:3000
[17:13:23.615] INFO (71180): Server listening at http://127.0.0.1:3000

ひとまず、ローカルでの動作を確認できました。

さらにProcfileを以下のように変更します。

web: APP_PORT=3000 heroku-integration-service-mesh npm start

3.Heroku Appの用意とデプロイ

Heroku Appを作成して、addonを追加します。

$ heroku create welcome-integration
...

$ heroku addons:create heroku-integration -a welcome-integration

Creating heroku-integration on ⬢ welcome-integration... free
Your add-on is being provisioned.
integration-round-64060 is being created in the background. The app will restart when complete...
Use heroku addons:info integration-round-64060 to check creation progress
Use heroku addons:docs example-app to view documentation

heroku-integration を addon として追加すると、管理画面にも追加されたことが確認できます。(まだ Test というラベルが付いてますね)

screenshot_003.png

次に、buildpackを追加します。heroku-integration-service-meshと、nodejsの2つになります。

$ heroku buildpacks:add https://github.com/heroku/heroku-buildpack-heroku-integration-service-mesh -a welcome-integration
$ heroku buildpacks:add heroku/nodejs -a welcome-integration

heroku-integration-service-meshのREADMEにもありますが、service meshがHeroku Routerとデプロイしたアプリの間に入る仕組みになっているようです。

ちょっと一息(2)

Heroku Appの環境変数にFASTIFY_PORTという名前で3000という値を追加します。
fastifyがheroku-integration-service-meshからのリクエストを受け付けられるよう、ポート番号として3000を指定しておきます。
Procfile内に記載しているAPP_PORTと合わせておく、という具合です
fastify起動時のオプションについて

ここがずれていると、heroku-integration-service-meshからのリクエストで

Failed to forward request: Get \"http://127.0.0.1:3000/accounts\

というエラーが発生します。

--- (ちょっと一息(2)終わり) ---

最後に、herokuにソースコードをデプロイします

$ heroku git:remote -a welcome-integration
$ git push heroku main

4.Salesforceへの接続

※sandbox環境で接続してみますが、Scratch Orgでも可能です。

$ heroku salesforce:connect sandbox-org --login-url "https://test.salesforce.com" -a welcome-integration --store-as-run-as-user
Press any key to open up the browser to connect ⬢ welcome-integration to sandbox-org or q to exit:
Opening browser to https://test.salesforce.com/services/oauth2/authorize?client_id=…
Connecting ⬢ welcome-integration to sandbox-org... done
Connected org 00Dxxxxxxxxxxxxx to ⬢ welcome-integration.

ブラウザが起動してログインを要求されるので、そのままログインしてください。
(APIインテグレーション用ユーザでログインすることが推奨されてます)

正常に接続完了すると、以下のコマンドで接続状態を確認できます。

$ heroku integration:connections -a welcome-integration
=== Heroku Integration connections for app ⬢ welcome-integration

 Type           Org Name    State     Run As User
 ────────────── ─────────── ───────── ──────────────────────────────────────
 Salesforce Org sandbox-org Connected (接続したユーザID)

さらに、Salesforce側には以下2つの権限セットが作成されます。

  • Heroku Integration
  • HerokuIntegrationAuthorization

5.Heroku AppをSalesforce環境にインポート

Salesforce側でApexやFlowで使用できるように、外部サービスとして登録します。

$ heroku salesforce:import api-spec.yaml -a welcome-integration --org-name sandbox-org --client-name HerokuAPI --generate-auth-permission-set

Importing app ⬢ welcome-integration as 'HerokuAPI' to org sandbox-org... done

管理画面 > Heroku > アプリケーションメニューで、Herokuアプリケーションが表示されますので、その中身を見ると以下のようになります。
先ほどのコマンドで client-name に指定した値が名前として登録されるようです。

screenshot_004.png

さらに、上記コマンドで以下の権限セットが作成されるので、この外部サービスを利用したいユーザに割り当てておきます。

  • HerokuAPI
  • HerokuAPIAuthorization
    • 2つ目はセッションベースの権限セットになります。ユーザ画面から紐つける際には「権限セットの割り当て: 有効化が必要」のセクションから追加します。

6.Salesforce環境での動作検証

では、Salesforce側から呼び出してみましょう。
開発者コンソールで匿名実行させてみます。

ExternalService.HerokuAPI herokuAPI = new ExternalService.HerokuAPI();
ExternalService.HerokuAPI.GetAccounts_Response response = herokuAPI.GetAccounts();
System.debug(JSON.serializePretty(response));
18:25:41.0 (59086675)|METHOD_ENTRY|[3]||ExternalService.HerokuAPI.GetAccounts()
18:25:43.750 (2750098585)|NAMED_CREDENTIAL_REQUEST|NamedCallout[Named Credential Id=0XAA20000000AH7, Named Credential Name=HerokuAPI, Endpoint=https://o360-integration-working-43d0c9ef2b77.herokuapp.com/accounts, Method=GET, External Credential Type=EXTERNAL, HTTP Header Authorization=Method: Not set - Credential: Not set, Content-Type=null, Request Size bytes=-1, Retry on 401=True]
18:25:43.751 (2751218621)|NAMED_CREDENTIAL_RESPONSE|NamedCallout[Named Credential Id=0XAA20000000AH7, Named Credential Name=HerokuAPI, Status Code=200, Content-Type=application/json; charset=utf-8, Response Size bytes=6695, Overall Callout Time ms=1375, Connect Time ms=3
18:25:43.754 (2754400793)|EXTERNAL_SERVICE_REQUEST|ExternalServiceSchemaType:OpenApi3|ExternalServiceName:HerokuAPI|SystemVersion:5|Action:GET|InvocationType:Apex|InvocationContext:Synchronous|SchemaProvider:Heroku|SystemNamespace:ExternalService|InputParameters:{}
18:25:43.754 (2754862708)|EXTERNAL_SERVICE_RESPONSE|ExternalServiceSchemaType:OpenApi3|ExternalServiceName:HerokuAPI|SystemVersion:5|Action:GET|InvocationType:Apex|InvocationContext:Synchronous|SchemaProvider:Heroku|SystemNamespace:ExternalService|OutputParameters:{"200":"[{\"id\":\"001F700001j0eAqIAI\",\"name\":\"テストデータ01株式会社\"},{\"id\":\"001F700001j0eArIAI\",\"name\":\"テストデータ03株式会社\"},{\"id\":\"001F700001j0eAsIAI\",\"name\":\"株式会社テストデータ05\"},{\"id\":\"001F700   <... truncated: 4804 characters ...>   |HttpResponseStatus:200
18:25:43.935 (2935429904)|HEAP_ALLOCATE|[10]|Bytes:116
18:25:43.935 (2935523731)|HEAP_ALLOCATE|[10]|Bytes:3
18:25:43.935 (2936069853)|HEAP_ALLOCATE|[10]|Bytes:4
18:25:43.935 (2936089505)|HEAP_ALLOCATE|[10]|Bytes:12
18:25:43.935 (2936141298)|HEAP_ALLOCATE|[EXTERNAL]|Bytes:4
18:25:43.935 (2936187532)|VARIABLE_ASSIGNMENT|[10]|this.Code200|[]|0x12463033
18:25:43.935 (2936458603)|HEAP_ALLOCATE|[10]|Bytes:156
18:25:43.935 (2936478975)|HEAP_ALLOCATE|[10]|Bytes:7
18:25:43.935 (2950907353)|METHOD_EXIT|[3]||ExternalService.HerokuAPI.GetAccounts()
18:25:43.935 (2950928263)|SYSTEM_MODE_EXIT|false
18:25:43.935 (2951066885)|VARIABLE_ASSIGNMENT|[3]|response|{"Code200":"0x47fd04c3","responseCode":200}|0x12463033
18:25:43.935 (2951075774)|STATEMENT_EXECUTE|[4]
18:25:43.935 (2951122098)|SYSTEM_MODE_ENTER|false
18:25:43.935 (2951138650)|HEAP_ALLOCATE|[4]|Bytes:5
18:25:43.935 (2951213515)|HEAP_ALLOCATE|[4]|Bytes:24
18:25:43.935 (2951237797)|SYSTEM_METHOD_ENTRY|[1]|JSON.JSON()
18:25:43.935 (2951244037)|STATEMENT_EXECUTE|[1]
18:25:43.935 (2951284410)|SYSTEM_METHOD_EXIT|[1]|JSON
18:25:43.935 (2951299291)|METHOD_ENTRY|[4]||System.JSON.serializePretty(Object)
18:25:43.935 (2954243835)|METHOD_EXIT|[4]||System.JSON.serializePretty(Object)
18:25:43.935 (2954261007)|SYSTEM_MODE_EXIT|false
18:25:43.935 (2954290719)|USER_DEBUG|[4]|DEBUG|{
  "responseCode" : 200,
  "Code200" : [ {
    "name_set" : true,
    "name" : "テストデータ01株式会社",
    "id_set" : true,
    "id" : "001F700001j0eAqIAI"
  }, {
    "name_set" : true,
    "name" : "テストデータ03株式会社",
    "id_set" : true,
    "id" : "001F700001j0eArIAI"
  }, {
   ...
  } ]
}

SalesforceからHeroku Appに、Salesforceの取引先を取得するようリクエストした結果が返ってきました。

Heroku App側のログを見ると、heroku-integration-service-meshがリクエストを受け取り、認証後にデプロイしたアプリにリクエストを転送しているようです。

app/web.1 time=2024-12-14T09:25:43.638Z level=INFO msg="Processing request to /accounts..." app=local source=heroku-integration-service-mesh request-id=00Dxxxxxxxxxxxxxxx-d78d4596-87a2-4164-8682-bd23a77ea566
app/web.1 time=2024-12-14T09:25:43.639Z level=INFO msg="Validating request..." app=local source=heroku-integration-service-mesh request-id=00Dxxxxxxxxxxxxxxx-d78d4596-87a2-4164-8682-bd23a77ea566
app/web.1 time=2024-12-14T09:25:43.641Z level=INFO msg="Valid request!" app=local source=heroku-integration-service-mesh request-id=00Dxxxxxxxxxxxxxxx-d78d4596-87a2-4164-8682-bd23a77ea566
app/web.1 time=2024-12-14T09:25:43.641Z level=INFO msg="Found Salesforce request" app=local source=heroku-integration-service-mesh request-id=00Dxxxxxxxxxxxxxxx-d78d4596-87a2-4164-8682-bd23a77ea566
app/web.1 time=2024-12-14T09:25:43.641Z level=INFO msg="Authenticating Salesforce request for org 00Dxxxxxxxxxxxxxxx, domain https://xxxxx.sandbox.my.salesforce.com..." app=local source=heroku-integration-service-mesh request-id=00Dxxxxxxxxxxxxxxx-d78d4596-87a2-4164-8682-bd23a77ea566
app/web.1 time=2024-12-14T09:25:43.997Z level=INFO msg="Authenticated request!" app=local source=heroku-integration-service-mesh request-id=00Dxxxxxxxxxxxxxxx-d78d4596-87a2-4164-8682-bd23a77ea566
app/web.1 time=2024-12-14T09:25:43.997Z level=INFO msg="Forwarding request..." app=local source=heroku-integration-service-mesh request-id=00Dxxxxxxxxxxxxxxx-d78d4596-87a2-4164-8682-bd23a77ea566
app/web.1  {"level":30,"time":1734168344008,"pid":27,"hostname":"94b8d350-abb3-42ed-8275-e24e9ab9887b","reqId":"req-1","req":{"method":"GET","url":"/accounts","hostname":"127.0.0.1:3000","remoteAddress":"127.0.0.1","remotePort":40998},"msg":"incoming request"}
app/web.1  {"level":30,"time":1734168344017,"pid":27,"hostname":"94b8d350-abb3-42ed-8275-e24e9ab9887b","reqId":"req-1","msg":"GET /accounts: {}"}
app/web.1  {"level":30,"time":1734168344017,"pid":27,"hostname":"94b8d350-abb3-42ed-8275-e24e9ab9887b","reqId":"req-1","msg":"Querying org 00Dxxxxxxxxxxxxxxx Accounts..."}
heroku/router at=info method=GET path="/accounts" host=welcome-integration-43d0c9ef2b77.herokuapp.com request_id=00Dxxxxxxxxxxxxxxx-d78d4596-87a2-4164-8682-bd23a77ea566 fwd="141.163.209.254" dyno=web.1 connect=0ms service=732ms status=200 bytes=6839 protocol=https
app/web.1  {"level":30,"time":1734168344345,"pid":27,"hostname":"94b8d350-abb3-42ed-8275-e24e9ab9887b","reqId":"req-1","msg":"For org 00Dxxxxxxxxxxxxxxx, found the following Accounts: [{\"id\":\"001F700001j0eAqIAI\",\"name\":\"テストデータ01株式会社\"},{\"id\":\"001F700001j0eArIAI\",\"name\":\"テストデータ03株式会社\"},

Heroku側の処理としては、どうなっているのでしょう。
以下、主な箇所だけ抜き出しました。

fastify.get('/accounts', async function (request, reply) {
  const { event, context, logger } = request.sdk;

  // (略)...
  
  // Query invoking org's Accounts
  logger.info(`Querying org ${org.id} Accounts...`);
  const result = await org.dataApi.query('SELECT Id, Name FROM Account');
  const org = context.org;
  const accounts = result.records.map(rec => rec.fields);
  logger.info(`For org ${org.id}, found the following Accounts: ${JSON.stringify(accounts || {})}`);
});

requestオブジェクトの中に組み込まれたsdkオブジェクトには、Salesforceのデータ操作機能が含まれており、これを使うことでまるでApexでsoqlを書くかのようにデータをクエリすることができます。(その他DMLも)

従って、Heroku Integrationを使うことでsalesforceへの認証やAPIへのリクエストは不要になります。
また、場合によってはSalesforce側でApex REST APIを用意する必要があることもあったかと思いますが、上に示したようにクエリやDMLが容易に記述できるのであれば、Apexで実装する必要も減らせるのではないでしょうか。

公式でも紹介されていますが、Salesforceのデータを元にHeroku App側でPDFを生成して、それをSalesforceの元のレコードに添付する、といったことはもう少し楽に実現できそうですね。

7.その他気になった点など

  • API消費はカウントされてしまうのは惜しい
  • ユーザモードが3種用意されているようですので、レコードのアクセス制御が必要な場合にも対応できそう(と思う)
  • あくまで、Salesforceが起点となってHeroku側に実装された機能を利用する、という仕組みだと捉えてますが、外部からHerokuにリクエストが来て(どうにかして)Salesforce側に連携できるようになると良いな、と思いました。
    • そういったこともできるのかもしれないですが、よくわからず。
    • 詳しい人からのツッコミをお待ちします。
  • 料金体系はどうなるか...?

8.おわりに

Getting Startedまで進められた、ということでひとまず「完全に理解した」状態になりましたでしょうか?

Salesforce Functionsが、こういった形でカムバックしてきたのだと思いますが、これは非常に良いことだと思います。SalesforceとHerokuの両方を使っている方々におかれましては、積極的に使っていきましょう!

その他、Heroku Integrationに関する詳細は公式ドキュメント:Heroku Integrationも併せて参照ください。

なお、本記事はHeroku Advent Calendar 2024の15日目の記事になります。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?