こんにちは。
Heroku & Salesforce でプロダクト、サービス開発をする仕事をしております、白石です。
今回はHeroku Advent Calendar 2020 の 15日目の記事ということで
「Salesforce Functions (Pilot版)」 環境構築をやってみた結果を皆様にもシェアします。
はじめに
「Salesforce Functions」はHerokuに非常に関わりの深いサービスです。
昨年のDreamforceイベントで発表され、主に開発者界隈で大いに盛り上がりました。
※発表時は「Evergreen」という名称でしたね。
Salesforce Functionsは、Heroku開発チーム・Salesforce開発チームが一体となって作ったサービスであると言われています。
実際に昨年のDreamforceのブースで見せてもらったSalesforce Functionsのデモは、URLに heroku
という文字列が入っていたことを記憶しています。
最近の発表では、Herokuの機能であるビルドパック、アドオン、プライベートスペース、パブリックスペースなどもSalesforce Functions上で提供される予定であるともいわれています。
「Salesforceプラットフォーム上で、Herokuの機能が使用できる。」
そんな夢いっぱいなサービスのリリースを心待ちにしていたところ、
セールスフォースの中の人からPilot版を使用させていただく機会をいただきました。
今回、環境構築までをやってみましたので、簡単ですがご紹介できればと思ってます。
※ Pilot版なので正式リリース時には本記事と異なる箇所があることが予想されます。あくまでもPilot版地点の参考情報として読んでいただけると幸いです。
正式にリリースされたときには本記事内容をアップデートしたいと思ってます。
Salesforce Functions の特徴
Salesforceプラットフォームを拡張するための関数(JavaScriptおよびTypeScript)を記述して、Salesforce上のデータと対話しながらあらゆる処理を行うことができるようになります。
- Apexを介して呼び出すことができます。
- Apexコードを使用できる場所ならどこでも呼び出し可能
- フロー内のApexアクション
- バッチジョブ
- Apexトリガー
- Apexプラットフォームイベントサブスクリプション を使用するワークフロー内
- Apexコードを使用できる場所ならどこでも呼び出し可能
- 負荷に応じて自動的にスケールアップおよびスケールダウンします。
- ネットワークで分離されたシングルテナント環境の専用コンピューティングインフラストラクチャで実行されます。
Salesforce FunctionsのリリースによってApexはどうなるの?と気になっていた方もいらっしゃるかもしれませんが、Apexはなくならないと思っています。
Salesforce FunctionsとApexのそれぞれの特徴が活かせるように連携させながら開発していくことが重要となりそうです。
Salesforce Functions の動作環境
現状のSalesforce Functions Pilot版においては以下の前提条件をもとに動作するものとなっています。
- Salesforce CLIでサポートされるシステム:Windows 10 64ビット以降、MacOS 10.11以降、またはUbuntu14.0.4以降。
- Node.js
- npmがインストールおよび更新されたSFDXCLI
- Salesforce FunctionsSFDXプラグインのインストール
- Dockerデスクトップ
- スクラッチ環境での開発のみをサポート
初期設定方法
ここからは簡単にSalesforce Functionsの初期設定方法についてまとめます。
Salesforce Functions Pilotの設定手順を解説したドキュメントが公開されており、
その手順をもとに設定を進めたものになります。
Salesforce CLI コマンドインターフェースが利用できる必要があるので、
事前にインストールしておきましょう。
1. Salesforce Functionsのインストール
以下のコマンドを実行し、SFDXのバージョンが7.74以降、SalesforceFunctionsプラグインのバージョンが32.0以降になっていればOKです。
$ sfdx update
$ sfdx plugins:install evergreen
2. DeveloperHubの認証
以下のコマンドを実行します。
$ sfdx force:auth:web:login -a my-devhub -d
Salesforceログイン画面が立ち上がるのでログインしましょう。
アクセスを許可します。
3.DevHub組織の有効化
[設定] > [Dev Hub]より「Dev Hubを有効化」スイッチをオンにします。
4. functionsの有効化
[設定] > [機能]→「機能を有効化」を行います。
Salesforce Functions環境のログイン情報を入力します。
アクセスを許可します。
Salesforceの機能アカウントに接続されたと表示されていることを確認します。
5. SFDX CLIとSalesforce functionsの接続を確認する
SFDX CLIは、DevHub組織の接続に使用したのと同じSalesforceFunctionsアカウントにも接続する必要があります。
以下のコマンドを実行します。
$ sfdx evergreen:auth:login
先程と同じ画面が立ち上がるのでログインします。
6. SFDX CLIとDevHub組織が同じSalesforce Functionsアカウントに接続されていることを確認する
以下のコマンドを実行します。
$ sfdx evergreen:whoami
Account IDがSalesforce機能画面と一致していることを確認します。
7.DXプロジェクトの作成
functionsの開発を開始するには、Salesforce functionsプロジェクトを配置するディレクトリに新しいDXプロジェクトを作成します。
$ sfdx force:project:create -n MyFunctionProject
8. スクラッチ組織の作成
スクラッチ組織は、Salesforceコードとメタデータのソース駆動型の使い捨てデプロイメントです。スクラッチ組織を使用して、SalesforceFunctionsのすべての開発とローカルテストを行います。
新しいDXプロジェクトのルートディレクトリに切り替えます。
$ cd MyFunctionProject
config/project-scratch-def.jsonファイルを編集して「functions」機能を有効にして保存し、以下に書き換えます。
{
"orgName": "acme company",
"edition": "Developer",
"features": ["Functions"]
}
以下のコマンドを実行し、新しいスクラッチ組織を作成します。
$ sfdx force:org:create -s -f config/project-scratch-def.json -a MyScratchOrgAlias
9. functionsの作成
以下のコマンドを実行します。
$ sfdx evergreen:function:create MyFunction -l javascript
- MyFunction :関数の名前
-
-l
言語(javascriptまたはtypescript):スクリプトのテンプレートと、作成される追加ファイル(index.json for Javascriptや index.ts forなど)を定義します。
以下のファイルが作成されます。
.
├── README.md
├── config
│ └── project-scratch-def.json
├── forceapp
│ └── main
│ └── default
│ ├── ...various directories...
│ ├── classes
│ └── permissionsets
├── functions
│ └── MyFunction
│ ├── README.md
│ ├── function.toml
│ ├── index.js
│ ├── package.json
│ └── test
│ └── unitTests.js
├── package.json
├── scripts
└── sfdx-project.json
-
functions/MyFunction/function.toml
:メタデータ情報。Salesforce functionsとSalesforce組織の間で共有されます。関数をデプロイするときは、このファイルを使用して、このfunctions統合に固有の構成設定を指定します。 -
functions/MyFunction/package.json
:functionsに必要な依存関係を指定するために使用します。Salesforce組織内のデータへのアクセスに使用できるSalesforceSDKへの参照がすでに含まれていて、Salesforce組織と通信するためのFunctions用に事前設定されたクラスにアクセスができます。
functions/MyFunction/package.json
{
"name": "my-function-function",
"version": "0.0.1",
"author": "TODO",
"description": "TODO",
"license": "UNLICENSED",
"main": "index.js",
"repository": {
"type": "git"
},
"engines": {
"node": ">=10.0.0"
},
"scripts": {
"lint": "eslint . --ext .js --config .eslintrc",
"test": "node_modules/mocha/bin/mocha 'test/**/*.js'"
},
"dependencies": {
"@salesforce/salesforce-sdk": "^1.4.0"
},
"devDependencies": {
"@types/chai": "^4.1.7",
"@types/chai-as-promised": "^7.1.0",
"@types/mocha": "^5.2.5",
"@types/node": "^10.12.18",
"@types/sinon": "^7.0.4",
"chai": "^4.2.0",
"eslint": "^6.7.2",
"mocha": "^5.2.0",
"sinon": "^7.2.3"
}
}
-
functions/MyFunction/index.js
:プライマリスクリプトファイル。受信リクエストの情報やSalesforce組織のデータとやり取りするために使用できるメソッドパラメータで事前設定されています。のデフォルト関数はindex.js、受信したペイロードをログに記録し、アカウントデータのクエリを発行し、クエリ結果をログに記録します。
functions/MyFunction/index.js
const sdk = require('@salesforce/salesforce-sdk');
module.exports = async function (event, context, logger) {
logger.info(`Invoking MyFunction with payload ${JSON.stringify(event.data || {})}`);
const results = await context.org.data.query('SELECT Id, Name FROM Account');
logger.info(JSON.stringify(results));
return results;
}
- このデフォルトのfunctions
index.js
は、受信したペイロードをログに記録し、アカウントデータのクエリを発行し、クエリ結果をログに記録します。
パラメータ | 説明 |
---|---|
event | トリガーイベントを記述し、イベントデータを含むEventオブジェクト。パイロットの場合、イベントはApexから取得できます。このパラメータは、「payload(ペイロード)」と呼ばれることもあります。 |
context | Salesforceとの間で読み取りおよび書き込みを行うためのSalesforce組織コンテキスト。この情報の多くは、すでに事前設定されています。 |
logger | 関数のSalesforceロガー。 |
index.js を変更して、event、context、logger引数を追加します。
functions/MyFunction/index.js
// functions/MyFunction/index.js
const sdk = require('@salesforce/salesforce-sdk');
/**
* Describe MyFunction here.
*/
module.exports = async function (event, context, logger) {
logger.info('Invoking MyFunction...');
// You can set a breakpoint here to inspect the variables that are passed in.
// Insert a new Account with the name given in the event payload.
const acct = new sdk.SObject('Account');
const name = event.data.AccountToCreate__c || 'MyAccount'
acct.setValue('Name', `${name}-${Date.now()}`);
const insertResult = await context.org.data.insert(acct);
logger.info(JSON.stringify(insertResult));
// Query Accounts to verify that our new Account was created.
const fields = event.data.fields ? event.data.fields.join(', ') : 'Id, Name, CreatedDate'
const soql = `SELECT ${fields} FROM Account ORDER BY CreatedDate DESC LIMIT 5`;
logger.info(soql);
const queryResults = await context.org.data.query(soql);
logger.info(JSON.stringify(queryResults));
return queryResults;
}
- この例は、Salesforce Functions Node.js SDKを使用して取引先を作成し、指定された取引先項目でクエリを実行します。新しい取引先の名前は、ペイロードの
AccountToCreate__c
項目から入力されます。
10. Salesforceにアクセスするための権限の設定
権限セットを更新します。
functions設定ページを介して組織をSalesforce functionsに接続すると、最小限の権限で「functions」権限セットが自動的に作成されます。
force-app/main/default/permissionsets/Functions.permissionset-meta.xml
を作成し、以下をコピーしてディレクトリ配下にファイルを作成します。
<?xml version="1.0" encoding="UTF-8"?>
<PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">
<hasActivationRequired>true</hasActivationRequired>
<label>Functions</label>
<objectPermissions>
<allowCreate>true</allowCreate>
<allowDelete>false</allowDelete>
<allowEdit>true</allowEdit>
<allowRead>true</allowRead>
<modifyAllRecords>false</modifyAllRecords>
<object>Account</object>
<viewAllRecords>false</viewAllRecords>
</objectPermissions>
<objectPermissions>
<allowCreate>false</allowCreate>
<allowDelete>false</allowDelete>
<allowEdit>true</allowEdit>
<allowRead>true</allowRead>
<modifyAllRecords>false</modifyAllRecords>
<object>FunctionInvocationRequest</object>
<viewAllRecords>false</viewAllRecords>
</objectPermissions>
</PermissionSet>
- この権限の更新は、組織に接続されているすべての機能に自動的に適用されます。
11. 権限セットデプロイ
以下のコマンドを実行します。
$ sfdx force:source:push -f
12. functionsをローカルで実行する
以下のコマンドを実行します。
$ cd functions/MyFunction
$ sfdx evergreen:function:start --verbose
以下のように表示されたら準備完了です。
2020-09-30T12:53:56 [develop] Build Complete
2020-09-30T12:53:56 [develop] Listening for file modifications [*/package.json, *.ejs, *.ts, *.js]
2020-09-30T12:53:56 Starting streaming-http-adapter 0.5.4 2153adc7da97497cf041de3c70f02da229c32851
2020-09-30T12:53:56 Debugger listening on ws://0.0.0.0:9222/06d05cec-c6b6-46bb-94a5-5bd0cf184aac
2020-09-30T12:53:56 For help, see: https://nodejs.org/en/docs/inspector
2020-09-30T12:53:57 Ready to process signals
13. functionsの呼び出し
ペイロード用のファイルを作成します。
functions/MyFunction/payload.json
{
"id" : 2345,
"service" : "MyService"
}
新しいターミナルウィンドウのDXプロジェクトディレクトリで、次の関数を呼び出します。
$ sfdx evergreen:function:invoke http://localhost:8080 --payload='@functions/MyFunction/payload.json'
POST http://localhost:8080... 200
{
"totalSize": 1,
"done": true,
"records": [
{
"attributes": {
"type": "Account",
"url": "/services/data/v47.0/sobjects/Account/0011g00000jmra5AAA"
},
"Id": "0011g00000jmra5AAA",
"Name": "MyAccount-1582832731577"
}
]
}
14. functionsのデプロイ
Salesforce組織から呼び出すことができるようにSalesforce functionsにデプロイします。
以下のコマンドを実行します。
$ cd functions/MyFunction
$ sfdx evergreen:function:deploy -t MyTarget -u MyScratchOrgAlias
以下のコマンドで確認をとります。
$ sfdx evergreen:function:list -t MyTarget
15. スクラッチ組織をアプリにバインドする
functionsがデプロイされたら、スクラッチ組織をfunctionsがデプロイされたSalesforceFunctionsアプリとスペースにバインドする必要があります。
$ sfdx evergreen:org:bind -u MyScratchOrgAlias -t MyTarget
16. Apexから関数を呼び出す
以下のコマンドを実行し、Apexクラスを作成します。
$ cd ../..
$ sfdx force:apex:class:create -n FunctionApex -d force-app/main/default/classes
FunctionApex.cls
public with sharing class FunctionApex {
public static void test() {
System.debug('Invoking MyFunction');
functions.Function myFunction = functions.Function.get('MyFunction');
functions.FunctionInvocation invocation = myFunction.invoke('{"fields":["Id","Name"]}');
String jsonResponse = invocation.getResponse();
System.debug('Response from MyFunction ' + jsonResponse);
}
}
- 開発者パイロットの場合、Apexを使用してfunctionsを(同期的または非同期的に)呼び出すことができます。functionsは、Apexコードを使用できる場所ならどこからでも呼び出すことができます。したがって、たとえば、フロー内のApexアクション 、バッチジョブ 、Apexトリガー 、およびApexプラットフォームイベントサブスクリプション を使用するワークフロー内で呼び出したり、非同期でキューに入れたりすることができます。
- 上記Apexは
functions.Function.get()
:functionsを名前で検索するメソッドです。 -
Function.invoke(payload)
:非同期 - Apexを介して関数を直接呼び出すことは、Apexコールアウトを作成することに似ています。関数が戻るのに2分以上かかる場合、リクエストはタイムアウトになるようです。これを回避するには、同期Function.invoke(payload, callback)メソッドではなく非同期メソッドを使用して、関数を非同期で呼び出します。
pushします。
$ sfdx force:source:push
スクラッチ組織の開発者コンソールを開いて、TraceFlagレコードを作成します。
$ sfdx force:org:open -p /_ui/common/apex/debug/ApexCSIPage
Apexを呼び出します。
$ echo "FunctionApex.test();" | sfdx force:apex:execute -f /dev/stdin
Compiled successfully.
Executed successfully.
50.0 APEX_CODE,FINEST;APEX_PROFILING,INFO;CALLOUT,INFO;DB,INFO;NBA,INFO;SYSTEM,DEBUG;VALIDATION,INFO;VISUALFORCE,INFO;WAVE,INFO;WORKFLOW,INFO
Execute Anonymous: FunctionApex.test();
07:56:00.64 (64335218)|USER_INFO|[EXTERNAL]|0053B000004dO6R|test-ye25wyxgkzea@example.com|(GMT-08:00) Pacific Standard Time (America/Los_Angeles)|GMT-08:00
07:56:00.64 (64417452)|EXECUTION_STARTED
07:56:00.64 (64437324)|CODE_UNIT_STARTED|[EXTERNAL]|execute_anonymous_apex
07:56:00.64 (64769200)|HEAP_ALLOCATE|[79]|Bytes:3
07:56:00.64 (64874501)|HEAP_ALLOCATE|[84]|Bytes:152
07:56:00.64 (64897890)|HEAP_ALLOCATE|[399]|Bytes:408
07:56:00.64 (64924979)|HEAP_ALLOCATE|[412]|Bytes:408
07:56:00.64 (64944004)|HEAP_ALLOCATE|[520]|Bytes:48
07:56:00.64 (64989426)|HEAP_ALLOCATE|[139]|Bytes:6
07:56:00.64 (65045788)|HEAP_ALLOCATE|[EXTERNAL]|Bytes:1
07:56:00.64 (65203659)|STATEMENT_EXECUTE|[1]
07:56:00.64 (65210808)|STATEMENT_EXECUTE|[1]
07:56:00.64 (65310761)|HEAP_ALLOCATE|[52]|Bytes:5
07:56:00.64 (65364818)|HEAP_ALLOCATE|[58]|Bytes:5
07:56:00.64 (65383670)|HEAP_ALLOCATE|[66]|Bytes:7
07:56:00.64 (65421976)|SYSTEM_MODE_ENTER|false
07:56:00.64 (65481776)|HEAP_ALLOCATE|[1]|Bytes:5
07:56:00.64 (79861992)|HEAP_ALLOCATE|[1]|Bytes:1
07:56:00.64 (79886018)|HEAP_ALLOCATE|[1]|Bytes:3
07:56:00.64 (79981294)|METHOD_ENTRY|[1]|01p3B000006sogN|FunctionApex.FunctionApex()
07:56:00.64 (79996753)|STATEMENT_EXECUTE|[1]
07:56:00.64 (80007201)|STATEMENT_EXECUTE|[1]
07:56:00.64 (80026585)|HEAP_ALLOCATE|[1]|Bytes:5
07:56:00.64 (80068168)|METHOD_EXIT|[1]|FunctionApex
07:56:00.64 (80703234)|PUSH_TRACE_FLAGS|[1]|01p3B000006sogN|FunctionApex|APEX_CODE,DEBUG;APEX_PROFILING,INFO;CALLOUT,INFO;DB,INFO;NBA,INFO;SYSTEM,DEBUG;VALIDATION,INFO;VISUALFORCE,INFO;WAVE,INFO;WORKFLOW,INFO
07:56:00.64 (81008498)|USER_DEBUG|[3]|DEBUG|Invoking MyFunction
07:56:00.64 (552615875)|FUNCTION_INVOCATION_REQUEST|[26]|Function.invoke[Name=MyFunction, Invocation ID=00D3B0000002Bm5UAE-4_0GkVqilaChbTmlpukKa--MyFunction-2020-12-13T15:56:01.117143Z, Namespace=, Request ID=4_0GkVqilaChbTmlpukKa-, Payload length=24]
07:56:03.268 (3268013841)|FUNCTION_INVOCATION_RESPONSE|[26]|Function.invoke[Name=MyFunction, Invocation ID=00D3B0000002Bm5UAE-4_0GkVqilaChbTmlpukKa--MyFunction-2020-12-13T15:56:01.117143Z, Namespace=, Request ID=4_0GkVqilaChbTmlpukKa-, Response length=197]
07:56:03.268 (3268308115)|USER_DEBUG|[9]|DEBUG|Response from MyFunction {"totalSize":1,"done":true,"records":[{"attributes":{"type":"Account","url":"/services/data/v50.0/sobjects/Account/0013B00000gZmeeQAC"},"Id":"0013B00000gZmeeQAC","Name":"MyAccount-1607874962918"}]}
07:56:03.268 (3268536659)|POP_TRACE_FLAGS|[1]|01p3B000006sogN|FunctionApex|APEX_CODE,FINEST;APEX_PROFILING,INFO;CALLOUT,INFO;DB,INFO;NBA,INFO;SYSTEM,DEBUG;VALIDATION,INFO;VISUALFORCE,INFO;WAVE,INFO;WORKFLOW,INFO
07:56:03.268 (3268580469)|SYSTEM_MODE_EXIT|false
07:56:03.275 (3275918284)|CUMULATIVE_LIMIT_USAGE
07:56:03.275 (3275918284)|LIMIT_USAGE_FOR_NS|(default)|
Number of SOQL queries: 0 out of 100
Number of query rows: 0 out of 50000
Number of SOSL queries: 0 out of 20
Number of DML statements: 0 out of 150
Number of Publish Immediate DML: 0 out of 150
Number of DML rows: 0 out of 10000
Maximum CPU time: 0 out of 10000
Maximum heap size: 0 out of 6000000
Number of callouts: 0 out of 100
Number of Email Invocations: 0 out of 10
Number of future calls: 0 out of 50
Number of queueable jobs added to the queue: 0 out of 50
Number of Mobile Apex push calls: 0 out of 10
07:56:03.275 (3275918284)|CUMULATIVE_LIMIT_USAGE_END
07:56:03.268 (3275998822)|CODE_UNIT_FINISHED|execute_anonymous_apex
07:56:03.268 (3276023524)|EXECUTION_FINISHED
まとめ
いかがだったでしょうか?
実際に設定内容を見ていただくとSalesforce Functionsの特徴や機能が具体的にイメージできるようになったのではないかと思います。
正式リリースはいつか、サービスの価格はいくらか、パッケージ化できるのか、ガバナ制限はどうなるのか、JavaとNode.js以外でもコーディングできるのか、など実際のプロジェクトで使用するためには確認しておくべき事項がいくつかあるので、引き続き調査して判明したことがあれば紹介できればと思ってます。
それでは、よいお年をお迎えください。