おれは"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
こんな感じにプロジェクトが作成されます。
ちょっと一息(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にインポートした後の動作検証で期待通りに動かなくなります。
この時点で余計なカンマは外しておきましょう。
--- (ちょっと一息(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
というラベルが付いてますね)
次に、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 に指定した値が名前として登録されるようです。
さらに、上記コマンドで以下の権限セットが作成されるので、この外部サービスを利用したいユーザに割り当てておきます。
- 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消費はカウントされてしまうのは惜しい
- Heroku Integration > 制約事項
- でもまぁ仕方ないかな、というところもありますね
- ユーザモードが3種用意されているようですので、レコードのアクセス制御が必要な場合にも対応できそう(と思う)
- あくまで、Salesforceが起点となってHeroku側に実装された機能を利用する、という仕組みだと捉えてますが、外部からHerokuにリクエストが来て(どうにかして)Salesforce側に連携できるようになると良いな、と思いました。
- そういったこともできるのかもしれないですが、よくわからず。
- 詳しい人からのツッコミをお待ちします。
- 料金体系はどうなるか...?
8.おわりに
Getting Startedまで進められた、ということでひとまず「完全に理解した」状態になりましたでしょうか?
Salesforce Functionsが、こういった形でカムバックしてきたのだと思いますが、これは非常に良いことだと思います。SalesforceとHerokuの両方を使っている方々におかれましては、積極的に使っていきましょう!
その他、Heroku Integrationに関する詳細は公式ドキュメント:Heroku Integrationも併せて参照ください。
なお、本記事はHeroku Advent Calendar 2024の15日目の記事になります。