AWS Lambda から Salesforce のカスタムオブジェクトにレコードを追加する

## はじめに

社内のとある取り組みにおいて、AWS の Lambda から Salesforce のカスタムオブジェクトにデータを入れる仕組みが必要になったので、その仕組みを実装するための覚え書きです。

本稿では、Node.js、AWS、Salesforce を使用するので、前提としてこの辺りの知識が必要になります。
が、筆者は上記の知識ゼロだったので、なくてもなんとかなる。はず。（すばらしい記事を残してくれている方々に感謝


## 手順

1. 作業環境・用意するもの
2. Node.js で Salesforce へ接続
3. AWS の準備
4. Lambda関数の作成
5. Lambda から Salesforce に接続
6. Salesforce カスタムオブジェクトにレコードを追加する

## 作業環境・用意するもの
【作業環境】

- Windows7、Node.js 7.1.0（本稿作業時）

【用意するもの】

- AWS と Salesforce アカウント
- AWS S3、IAMロール、ポリシーなどの設定
- データを入れるための Salesfoce オブジェクト
- Salesforce 接続アプリケーション 

## Node.js で Salesforce へ接続
こちらの記事が大変参考に。
[OAuth2 JWT Bearer Token フローを使ってSalesforceへアクセスする](http://qiita.com/stomita/items/4542ce1b48e5fa849ef1)

必要に応じてモジュールをインストールする。上記の記事では下記のモジュールを使用する。

- fs
- jsonwebtoken
- request
- jsforce

## AWS の準備
前準備として今回使用する IAM ロール、ポリシー、S3 バケットなどの作成、設定をしておく。

##  Lambda 関数の作成
AWS で Lambda 関数を作成する。
本稿では下記設定で作成した。

- 「Lambda」ページの 「Lambda 関数の作成」をクリックすると設計図の選択画面に遷移する。
- フィルターに「S3」と入れ、「s3-get-object」を選択。
- 「バケット」項目は 3 で作成したバケットを選択
- 「イベントタイプ」は「put」を選択
- プレフィックス、サフィックスは空欄で OK
- 「トリガーの有効化」にチェックを入れて「次へ」

[LambdaでS3にデータを格納する](http://qiita.com/masamasa/items/8b1ffad9fb55ca6e7c10)
### 関数の設定
関数の名前と、必要に応じて説明を入力。ランタイムは Node.js。

### Lambda 関数のコード
とりあえずデフォルトのままで OK。

### Lambda 関数ハンドラおよびロール
 - ハンドラ：デフォルト（ index.handler ）で OK。
 - ロール：「既存のロールを選択」で 3 で作成したロールを選択

ここまで入力して関数を作成する。

### 作成した関数をテストして動作を確認
- 事前に S3 になんかファイルを入れておく
- 「アクション」から「テストイベントの設定」を選択
- サンプルイベントテンプレートから「S3 Put」を選択。
- サンプルコード 14 行目辺りにある「"key": "HappyFace.jpg"」の値を先ほど入れておいたファイル名に
- サンプルコード 19 行目辺りにある「"name": "sourcebucket"」の値を準備したバケット名に変更
- 「保存してテスト」をクリックしてテスト。

##  Lambda から salesforce に接続
手順4で作成したLambda関数のコードをコピーし、手順2で作成したJSファイルと合体させる。
Lambda や Salesforce 接続に必要なモジュールなどは最初に読み込んでおいてOK。

```js:index.js

const...
const...

exports.handler = (event, context, callback) => {
 // 2 で作成の salesforce に接続するプログラムをここにぶち込む
};

```

とするだけ。
これで S3 にファイルがアップロードされた時、2 で作成した salesforce 接続処理が走る。
ここで合体した JS ファイル（仮に index.js）を関数パッケージとして Lambda にアップロードする。

- index.js
- pemファイル
- node_module

これらをまとめて ZIP にする。
Lambda 関数画面のコードタブ、コード エントリ タイプから「ZIP ファイルをアップロード」を選び、
作成した ZIP ファイルをアップロードする。

（pem ファイルもまとめてここでアップロードするため、JS 内で呼び出す PEM ファイルのパスに注意）
ZIP をアップロードしたら「テスト」をクリックし、ログを確認して 2 の動作ログが出ていれば OK。

### Salesforce カスタムオブジェクトにレコードを追加する
Salesforce にカスタムオブジェクトを作成し、手順2で作成したコードの

```js:index.js

conn.query('SELECT Id, Name FROM Account LIMIT 5').then(function(qr) {
 console.log('Done:', qr.done);
 console.log('Fetched Records:', qr.records);
});

```

の部分を

```js:index.js

conn.sobject(" オブジェクト名 ").create({
 " 項目名 ": 追加する値
});

```
とすることでカスタムオブジェクトにレコードを追加することができる。


最後に、ここまでの内容を元に、S3に保存した画像を Amazon Rekognition（DetectFaces）に送り、
解析結果を Salesforce のオブジェクトに保存する実装例をメモしておく。

```js:index.js

'use strict';
const aws = require('aws-sdk');
const fs = require('fs');
const s3 = new aws.S3({apiVersion: '2006-03-01'});
const jwt = require('jsonwebtoken');
const request = require('request');
const jsforce = require('jsforce');
const rekognition = new aws.Rekognition();
const TOKEN_ENDPOINT_URL = 'https://login.salesforce.com/services/oauth2/token';
const ISSUER = '********'; // Salesforce 接続アプリのコンシューマ鍵（client_id)
const AUDIENCE = 'https://login.salesforce.com'; // 固定
const cert = fs.readFileSync('./key/myapp.pem'); // 秘密鍵の読み込み
exports.handler = (event, context, callback) =>{

    const bucket = event.Records[0].s3.bucket.name;
    const key = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, ' '));
    const params = {
        Bucket: bucket,
        Key: key,
    };

    // rekognition に送るデータ
    const rekogParams = {
        Image: {
            S3Object: {
                Bucket: bucket,
                Name: key
            }
        },
        Attributes: ["ALL"]
    };

    rekognition.detectFaces(rekogParams, function (err, data) {
        if (err) {
            console.log(err, err.stack); // an error occurred
        } else {

            // JWTに記載されるメッセージの内容
            const claim = {
                iss: ISSUER,
                aud: AUDIENCE,
                sub: '**********', // 接続するSalesforceのユーザアカウント名
                exp: String(Date.now() + 3 * 60 * 1000) //現在時刻から3分間のみ有効
            };

            // JWTの生成と署名
            var token = jwt.sign(claim, cert, {algorithm: 'RS256'});

            // JWT Bearer Token フローによるアクセストークンのリクエスト
            request({
                method: 'POST',
                url: TOKEN_ENDPOINT_URL,
                form: {
                    grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
                    assertion: token
                }
            }, function (err, response, body) {
                if (err) {
                    return console.error(err);
                }
                var ret = JSON.parse(body);
                console.log(ret)
                var conn = new jsforce.Connection({
                    accessToken: ret.access_token,
                    instanceUrl: ret.instance_url
                });

                // Salesforce オブジェクト「rekognitionData」に Rekognition の解析結果を追加
                for (var i in data.FaceDetails) {
                    conn.sobject("rekognitionData__c").create({
                        "AgeRangeLow__c": data.FaceDetails[i].AgeRange.Low,
                        "AgeRangeHigh__c": data.FaceDetails[i].AgeRange.High,
                        "Gender__c": data.FaceDetails[i].Gender.Value,
                        "Smile__c": data.FaceDetails[i].Smile.Value,
                        "Eyeglasses__c": data.FaceDetails[i].Eyeglasses.Value,
                        "Sunglasses__c": data.FaceDetails[i].Sunglasses.Value,
                        "Beard__c": data.FaceDetails[i].Beard.Value,
                        "Mustache__c": data.FaceDetails[i].Mustache.Value,
                        "EyesOpen__c": data.FaceDetails[i].EyesOpen.Value,
                        "Emotions1__c": data.FaceDetails[i].Emotions[0].Type,
                        "Emotions2__c": data.FaceDetails[i].Emotions[1].Type,
                        "Emotions3__c": data.FaceDetails[i].Emotions[2].Type,
                    }, function (err, ret) {
                        if (err || !ret.success) {
                            return console.error(err, ret);
                        }
                    });
                }
            });
            callback(null, data.FaceDetails);
        }
    });
}
;

```

上記コードとオブジェクトの項目が少し違いますが、こんな感じでレコードが追加されます。

![sfobj.jpg](https://qiita-image-store.s3.amazonaws.com/0/182824/c74ad9a9-7066-5c1a-26d7-4d427b39e6fa.jpeg)