Salesforceの外部サービスを使ってAWSのLambdaを実行してみる

[Ateam Brides Inc. Advent Calendar 2021](https://qiita.com/advent-calendar/2021/ateam-brides)の6日目は
株式会社エイチームブライズ エンジニア
新卒2年目になった@takaHALが担当します！

# これは何？

Lambdaで作ったAPIをSalesforceの外部サービスに登録し
SalesforceのフローからLambdaを実行してみます。

# 使うもの

- AWS
    - Lambda
    - IAM
    - APIGateway
    - CloudWatch
        - 意識して使うのはこの4つ

- ServerlessFramework
- Salesforce
    - 外部サービス
    - 指定ログイン情報

利用するSalesforce環境はtrailheadのplayground環境です。
trailheadに登録すれば誰でも利用できます。

# 外部サービスとは

https://trailhead.salesforce.com/ja/content/learn/modules/external-services

>![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/445600/b591df2d-30d7-d097-44c3-d5647565a63e.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/445600/17c48d59-6df3-51a2-6859-4cd6558ef572.png)

https://swagger.io/resources/open-api/

OpenAPIのJSONファイルを登録することで、フロー内でOpenAPIに書かれた定義でREST APIを実行できます。
ノーコードでAPIを実行するフローアクションが作成できるので非常に便利なものになってます

レスポンスも受け取ることができるため複雑なロジックもSalesforceの外部に置けるのもいいポイントです。

先に`trailhead`をやっておくとイメージがしやすいと思います。

では早速やっていきましょう

# AWS側の準備

ServerlessFrameworkを利用します。

詳しい説明は[Serverless Framework の使い方を初心者にも分かりやすく説明する](https://qiita.com/mkin/items/0a82c84df084496544c6)で書いてくださっているのでお勧めです。

::::note warn
デプロイに必要なIAMやAWSは準備してある前提で進めます
あくまでもメインはSalesforceなのでAWSとServerlessFramework部分はサクッと済ませます。
::::


## 1. プロジェクトを作成

開発環境

```bash
$ serverless --version
Framework Core: 2.68.0 (local)
Plugin: 5.5.1
SDK: 4.3.0
Components: 3.18.1

$ node -v
v17.1.0
```

```bash
$ mkdir salesforce-lambda-test
$ cd salesforce-lambda-test
$ serverless create --template aws-nodejs-typescript --name salesforce-lambda-test
```

![スクリーンショット 2021-12-05 11.07.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/445600/af0b1c6f-2d05-21b8-c6c1-83264cf7ca7a.png)

## 2. AWSにデプロイ

今回利用したプロジェクトのテンプレートバージョンは以下になります

https://github.com/serverless/serverless/tree/v2.68.0/lib/plugins/create/templates/aws-nodejs-typescript

```bash
$ npm install
$ sls deploy
service: salesforce-lambda-test
stage: dev
region: us-east-1
stack: salesforce-lambda-test-dev
resources: 13
api keys:
  None
endpoints:
  POST - https://{hoge}.us-east-1.amazonaws.com/dev/hello
functions:
  hello: salesforce-lambda-test-dev-hello
layers:
  None
```

## 3. 外部サービスに利用する openapiのjsonファイルを用意する

Salesforceの`OpenAPI 2.0 スキーマ定義`は少しクセがあるので、今回はサクッと検証するためにtrailheadのサンプルを少しいじって利用させていただきます。
実際はAPIGatewayの画面からエクスポートしたり`documentation`として登録するといいと思います。

https://www.npmjs.com/package/serverless-v2-aws-documentation

https://trailhead.salesforce.com/ja/content/learn/modules/external-services/register-an-external-service


https://qiita.com/rllllho/items/53a0023b32f4c0f8eabb

https://help.salesforce.com/s/articleView?id=sf.enhanced_external_services_considerations.htm&type=5

```json
{
  "swagger": "2.0",
  "info": {
    "version": "1.0",
    "title": "Salesforce test"
  },
  "paths": {
    "/hello": {
      "post": {
        "operationId": "apiHello",
        "consumes": ["application/json"],
        "produces": ["application/json"],
        "parameters": [
          {
            "name": "accountName",
            "in": "formData",
            "required": true,
            "type": "string",
            "description": "Name of the account"
          },
          {
            "name": "accountType",
            "in": "formData",
            "required": true,
            "type": "string",
            "description": "The type of account"
          }
        ],
        "responses": {
          "201": {
            "description": "The response when the account does not already exist and we can create one",
            "schema": { "$ref": "#/definitions/accountDetails" }
          },
          "409": {
            "description": "The response when the account already exists and we cannot create one",
            "schema": { "$ref": "#/definitions/accountDetails" }
          },
          "400": {
            "description": "Error response if the account name parameter is less than minimum characters",
            "schema": { "$ref": "#/definitions/errorModel" }
          },
          "404": {
            "description": "Error response if the account is not supported by service or account is not found",
            "schema": { "$ref": "#/definitions/errorModel" }
          }
        }
      }
    }
  },
  "definitions": {
    "accountDetails": {
      "required": ["id", "name", "type", "availableBal"],
      "properties": {
        "id": { "type": "string", "description": "id" },
        "name": { "type": "string", "description": "name" },
        "type": { "type": "string", "description": "type" },
        "availableBal": { "type": "string", "description": "availableBal" }
      }
    },
    "errorModel": {
      "required": ["errorCode", "errorMessage"],
      "properties": {
        "errorCode": {
          "type": "string",
          "description": "A service-specific error code."
        },
        "errorMessage": {
          "type": "string",
          "description": "A service-specific error code."
        }
      }
    }
  }
}

```

## 4. IAMを作成する

SalesforceからAPIGatewayを通じでLambdaを実行するために使用するIAMを作成します。

![スクリーンショット 2021-12-05 13.00.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/445600/8c477582-24ee-2518-bd88-ec1e74b60434.png)

![スクリーンショット 2021-12-05 13.01.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/445600/b5063c92-fa91-80ac-f16a-b76981a9208a.png)

`AmazonAPIGatewayInvokeFullAccess`を今回はとりあえず付与しました。

![スクリーンショット 2021-12-05 13.03.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/445600/0220093d-f477-8d0a-627b-cee9be0ded5b.png)

作成したらアクセスキーとシークレットアクセスキーをメモしておきましょうあとで使います。

これでAWS側の設定等の操作は終了です。

# Salesforceで外部サービスを登録する

## 1. 指定ログイン情報の作成

![スクリーンショット 2021-12-05 13.10.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/445600/03076e9e-e295-3229-1816-02b0c330ea3a.png)

|項目|内容|
|---|:---|
|URL|デプロイしたAPIのエンドポイントを記入|
|ID種別|AWS 署名バージョン4 を選択|
|AWSアクセスキーID|作成したIAMのアクセスID|
|AWSシークレットキー|作成したIAMのシークレットキー|
|AWSリージョン|デプロイしたAPIのリージョン 例）　us-east-1 |
|AWS サービス|今回はAPIGatewayにアクセスするので　`apigateway `と記載します |
|コールアウトオプション|全部オフです|

そのまま好きな名前で保存でOKです。
しかし、リージョンとサービスにそのまま　文字列入れるの微妙にいけてないですね。。。。

そもそもここら辺は早く[Hyperforce](
https://www.salesforce.com/jp/products/platform/hyperforce/)でデフォルトでLambdaとかと繋げれるようにしてくれると楽なので今後に期待ですね。

## 2. 外部サービスの登録

### 外部サービスを新規作成でAPI仕様からを選択

![スクリーンショット 2021-12-05 13.25.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/445600/eb6f456f-3da9-db24-7557-9bccd7d7f1ad.png)

###　外部サービスを設定

`指定ログイン情報`に先程登録したものを選択し、`JSON形式の完全なサービススキーマ`を選択

[AWS側の準備](#aws側の準備)で準備したものを使います


![スクリーンショット 2021-12-05 13.26.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/445600/37506524-d0a0-c86f-ecfb-93ca303b9ba2.png)
![スクリーンショット 2021-12-05 14.11.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/445600/1e454e71-2800-5912-5bf6-3c9f3b4c3205.png)



そのまま登録でOK

# フローで外部サービスを使ってみる

## 1. フローを新規作成

今回はデバッグ実行で動作確認するのみなのでどれでもいいです。今回はスケジュールトリガーフローにしてみます。

![スクリーンショット 2021-12-05 13.32.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/445600/fae36979-bcdd-b072-cc68-c35e4b1b3799.png)

## 2.　アクションを追加

外部サービスとして登録したものがフローのアクションとして選択できるので選択します。

入力値で選択できるのは`parameters`に記載したものになってます。

```json
"parameters": [
          {
            "name": "accountName",
            "in": "formData",
            "required": true,
            "type": "string",
            "description": "Name of the account"
          },
          {
            "name": "accountType",
            "in": "formData",
            "required": true,
            "type": "string",
            "description": "The type of account"
          }
        ],
```

![スクリーンショット 2021-12-05 14.14.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/445600/b97ae724-231b-3dab-c483-bd341f9e9e11.png)


ちなみに詳細の部分ではresponsesに記載した内容が取得できることがわかります。これでフローを200の時の値を使用したりエラーハンドリングもできますね

```json
"responses": {
          "201": {
            "description": "The response when the account does not already exist and we can create one",
            "schema": { "$ref": "#/definitions/accountDetails" }
          },
          "409": {
            "description": "The response when the account already exists and we cannot create one",
            "schema": { "$ref": "#/definitions/accountDetails" }
          },
          "400": {
            "description": "Error response if the account name parameter is less than minimum characters",
            "schema": { "$ref": "#/definitions/errorModel" }
          },
          "404": {
            "description": "Error response if the account is not supported by service or account is not found",
            "schema": { "$ref": "#/definitions/errorModel" }
          }
        }
```



## 3. デバッグして確認してみる

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3434353630302f30643761656534612d643338332d373739622d353062622d3536373932663331383531382e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/445600/c70cb6d2-f224-f663-0917-f4bd4bc32508.png)


実行できていますね

CloudWatch側でも確認が取れました。

![スクリーンショット 2021-12-05 15.25.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/445600/c6b630f6-f36f-a13f-0296-ffd6fcaad442.png)

今回は`"in": "formData"`で送信しているので `accountName=hoge&accountType=fuga`みたいな形式でデータが送られます。

```json
"parameters": [
          {
            "name": "accountName",
            "in": "formData",
            "required": true,
            "type": "string",
            "description": "Name of the account"
          },
          {
            "name": "accountType",
            "in": "formData",
            "required": true,
            "type": "string",
            "description": "The type of account"
          }
        ],
```

https://help.salesforce.com/s/articleView?id=release-notes.rn_forcecom_ext_services_specify_mime_types.htm&type=5&release=232

>リクエストボディパラメータがメソッド POST、PUT、PATCH について定義されていない場合、フォームデータリクエストパラメータがリクエストボディで application/x-www-form-urlencoded として送信されます

::::note warn
`application/json`でデータを送信したい場合は下記のように"in": "body",で記載すると送信できます。
その際はフロー内で変数を作成する際に種別でApex定義を選択しましょう"type": "object",の中身が個別設定できる変数になってくれます。
::::

```json

        "consumes": ["application/json"],
        "produces": ["application/json"],
        "parameters": [
          {
            "name": "requests",
            "in": "body",
            "required": true,
            "schema": { "$ref": "#/definitions/Requests" },
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "405": {
            "description": "Invalid input"
          }
        }
      }
    }
  },
  "definitions": {
    "Requests": {
      "type": "object",
      "required": ["requestId", "name"],
      "properties": {
        "requestId": {
          "type":"integer"
        },
        "name": {
          "type": "string"
        },
      }
    }
  }
}

```

# テストで作成したAWSのリソースを削除

削除する場合は以下のコマンドです

```bash
$ sls remove
```

# 参考にさせてもらった記事

https://trailhead.salesforce.com/ja/content/learn/modules/external-services/register-an-external-service

https://dev.classmethod.jp/articles/callout-from-salesforce-to-amazon-api-gateway/

# 最後に

Salesforce関係の開発を触り始めて日が浅いですが、フローを使って外部APIと連携してみたりデータ連携をコードをほぼ書かなくてもいいようにできないかな？と思っていたところ`外部サービス`というものを見つけたので試してみました。

通常のopenapiの記述よりも癖はありますが、使いこなすと非常に便利な機能だと思いますので自分でもマスターしようと思います。

外部サービスをうまく使えば以下のようなことがSalesforce内でApexを書くことなく実現できます。

- 複雑な計算ロジックをSalesforceに持ち込むことなく利用できる
- Salesforce外の自社サービスのデータとコラボレーションしやすい

などいろいろできそうです。

外部サービスとは違いますが、Apexで`@InvocableMethod`を使うとフローアクションを作成できるのでそちらもお勧めです。

`sObject `型を受け取ることができるので、レコードの更新をトリガーにしてBigQueryにオブジェクトのデータを送るとかさまざまな用途で使えると思います。

https://developer.salesforce.com/docs/atlas.ja-jp.232.0.apexcode.meta/apexcode/apex_classes_annotation_InvocableMethod.htm

https://help.salesforce.com/s/articleView?id=sf.flow_build_extend_apex.htm&type=5

もう少し記事を更新していくつもりではいますが、質問やこっちの方がいいよ！みたいな意見があれば是非コメントにていただきたいです。



[Ateam Brides Inc. Advent Calendar 2021](https://qiita.com/advent-calendar/2021/ateam-brides)の7日目は、@rf_pがお送りします！！どんなネタを用意してくるのか楽しみです！！