Salesforce B2C Commerce の SFRA にてジョブを利用して SFTP で AWS S3 にファイルアップロードしてみる


※ これから記載する事項は、私が所属する会社とは一切関係のない事柄です。

本記事では B2C Commerce のジョブと AWS の Transfer Family を利用して AWS S3 にファイルをアップロードしたいと思います


## ジョブの概要

まず少し、ジョブについて説明します。ジョブを作成するときはジョブステップを組み合わせながらフローを作成していきます。
![スクリーンショット 2022-07-12 18.23.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/3dd9fe4f-0a25-ef69-0ebe-4fa74d90a9bb.png)




上記の画像 A、B、C のジョブステップで構成されたフローを「(並列、順次)同位フロー」と呼びます。デフォルトではそれぞで同時に（並列に）実行されますが、左から順次実行するように設定することも可能です。並列実行するリソースがなかったり、並列実行できないステップが混ざっていると、順次実行されます。

Aの中にも複数のステップを設定できますが、これらは順次実行されます。

画像の上から下への矢印のように同位フローが実行が終了されたら一つ下の同位フローへと順次実行されます。これを「順次フロー」

#### デフォルトでは下記のようになります。

A、B、C が同時に実行される。
↓
D、F が同時に実行される
↓
F が実行される


### その他できること

下記のように他にもできることは多くありますが、それらについては [Trailhead](https://trailhead.salesforce.com/ja/content/learn/modules/b2c-admin-create-and-manage-jobs) か [Infocenter](https://documentation.b2c.commercecloud.salesforce.com/DOC1/topic/com.demandware.dochelp/content/b2c_commerce/topics/jobs/b2c_job_steps.html?resultof=%22%6a%6f%62%22%20) をご覧ください。

* ジョブ実行中にオブジェクトをロックして、他のユーザからの修正や他のジョブから修正されないようにできます
* スケジュールでの実行ができます
* ジョブパラメータを利用して複数のジョブステップで共通のパラメータを利用することができます
* あらかじめ作成されてすぐ利用できるシステムジョブステップがあります
* 自作のカスタムジョブステップを作成できます
* ジョブ実行結果の通知





## 実装内容

Job を利用して注文情報と顧客情報の XML ファイルを Transfer Famify を通して SFTP で S3 にアップロードします。
※リソースのロックやスケジュール実行、通知については取り扱いません。
※カスタムジョブステップは [Chunk-Oriented Script Module](https://documentation.b2c.commercecloud.salesforce.com/DOC1/topic/com.demandware.dochelp/content/b2c_commerce/topics/jobs/b2c_chunk_oriented_script_modules.html) と [Task-Oriented Script Module](https://documentation.b2c.commercecloud.salesforce.com/DOC1/topic/com.demandware.dochelp/content/b2c_commerce/topics/jobs/b2c_task_oriented_script_modules.html) がありますが、今回作成するのは Task-Oriented Script Module のみです。


![スクリーンショット 2022-07-12 18.42.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/dc6637e3-eb76-1954-f36c-ba6cc4efa0f4.png)






## 仕様

* 「ExportCustomers」システムジョブステップで顧客情報を B2C Commerce のストレージ内の `/Impex/src/sample/customers/20220711.xml`に出力
* 「Custom.SampleUploadModule」というカスタムジョブを作成し、`/Impex/src/sample/customers/20220711.xml`　に出力されたファイルを SFTP で S3 バケット「cc-sd-job-test」 内の `Import/RefArch/customers/20220711.xml`にアップロード
* 「ExportOrders」システムジョブステップで注文情報を B2C Commerce のストレージ内の `/Impex/src/sample/orders/20220711.xml`に出力
* 「Custom.SampleUploadModule」というカスタムジョブを作成し、`/Impex/src/sample/orders/20220711.xml`　に出力されたファイルをSFTPでS3バケット「cc-sd-job-test」 内の `Import/RefArch/orders/20220711.xml`にアップロード




## 実装手順

### 1. AWS Transfer FamilyとS3の設定

[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/transfer/latest/userguide/getting-started.html)があるので、こちらをステップ２まで実行してください。


### 2. SFTP認証のための秘密鍵と公開鍵の作成

下記のコマンドをローカルで上から順番に実行してください。
すると 「key」「key.p12」「cert.cer」「key.pub」をいう４つのファイルがカレントディレクトリに生成されます。
この際に設定したパスワードは後ほど使うので覚えておいてください

```sh
ssh-keygen -t rsa -b 2048 -m PEM -f key
openssl req -new -x509 -key key -out cert.cer -days 365
openssl pkcs12 -export -out key.p12 -inkey key -in cert.cer
```




### 3. 秘密鍵の保管

Business Manager の **管理 > オペレーション > 秘密鍵と証明書** に遷移し、上記で作成した「key.p12」秘密鍵ファイルをインポートします。
この際、上記で設定したパスワードを入力してください。
また、任意のエイリアスを設定できますので、その値も覚えておいてください。

![image (10).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/5da2ad5e-e643-c239-3854-081893b60d44.png)




### 4. AWS Transfer Familyへのユーザの設定

[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/transfer/latest/userguide/getting-started.html)のステップ３を実行してください。
その際、手順８の「[SSH public key] (SSH 公開キー) に SSH キーペアの SSH 公開キー部分を入力します。」で上記で生成した「key.pub」のファイルの中身を入力してください。
また、ユーザ名も後ほど利用するのでメモしておいてください。
今回は「sample-user」というユーザ名のユーザを作成しました。


![image (32).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/9610ab73-b00e-4f34-281f-0be459c42d33.png)





### 5. AWS Transfer Family のドメインとユーザの取得

AWS のコンソールからドメインとユーザ名をコピーしておきます。

![スクリーンショット 2022-07-12 19.13.49 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/0cf06283-31ff-92cb-f3b8-1fd179c70210.png)




### 6. Service の作成

サービスの作成を行います。
詳細については、下記のヘルプを参照してください。

* [Create a Web Service Configuration](https://documentation.b2c.commercecloud.salesforce.com/DOC1/topic/com.demandware.dochelp/content/b2c_commerce/topics/web_services/b2c_create_web_service_config.html?resultof=%22%73%65%72%76%69%63%65%22%20)
* [Create a Web Service Profile](https://documentation.b2c.commercecloud.salesforce.com/DOC1/topic/com.demandware.dochelp/content/b2c_commerce/topics/web_services/b2c_create_web_service_profile.html?resultof=%22%73%65%72%76%69%63%65%22%20)
* [Create a Web Service Credential](https://documentation.b2c.commercecloud.salesforce.com/DOC1/topic/com.demandware.dochelp/content/b2c_commerce/topics/web_services/b2c_create_web_service_credential.html?resultof=%22%73%65%72%76%69%63%65%22%20)


認証情報の設定の際に上記で取得したドメインとユーザ名を入力する必要があります。
今回は「sample.job.test」というサービスを作成しました。下記に設定した内容の画像を添付します。


#### サービスの設定
![image (12).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/bfe02422-dc60-3df1-9a2a-cd6c6bb204cc.png)




#### プロフィールの設定
![image (13).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/c20a137d-51d6-171a-e577-da973262958c.png)




#### 認証情報の設定
![スクリーンショット 2022-07-12 19.12.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/10e4038e-afa0-b1d3-1777-f0605c08f8d2.png)




### 7. S3にパスを設定しておく

今回作成した S3 バケットは「cc-sd-job-test」です。
このバケット内にあらかじめ下記のフォルダを作成しておきます。

* Import/RefArch/customers
* Import/RefArch/orders

![image (14).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/fa1d2ae9-e1b9-459c-9631-47dfbecd3229.png)




### 8. カスタムジョブステップの作成

カートリッジ内にカスタムジョブステップを作成し、 B2C Commerce にアップロードし、カートリッジパスを設定します。
カスタムジョブステップの作成方法の詳しい作り方は[Trailhead](https://trailhead.salesforce.com/ja/content/learn/modules/b2c-admin-create-and-manage-jobs/b2c-admin-create-custom-job-steps)か[Infocenter](https://documentation.b2c.commercecloud.salesforce.com/DOC1/index.jsp?topic=%2Fcom.demandware.dochelp%2Fcontent%2Fb2c_commerce%2Ftopics%2Fjobs%2Fb2c_create_custom_job_step.html)をご覧ください。
※ カートリッジの作成方法やアップロード方法については割愛します。詳しくは [Trailhead](https://trailhead.salesforce.com/ja/content/learn/modules/b2c-cartridges)、[Infocenter](https://documentation.b2c.commercecloud.salesforce.com/DOC1/topic/com.demandware.dochelp/content/b2c_commerce/topics/site_development/b2c_cartridges.html?resultof=%22%63%61%72%74%72%69%64%67%65%22%20)、[以前紹介した Qiita 記事](https://qiita.com/syunyama/items/21c71ac9c49b332e126d#1-%E3%82%AB%E3%83%BC%E3%83%88%E3%83%AA%E3%83%83%E3%82%B8%E3%81%AE%E4%BD%9C%E6%88%90) をご参照ください

今回は「app_job_test」というカートリッジに下記の2つのファイルを作成します。



* `samplejob` は秘密鍵を Business Manager に保管した際のエイリアスです。今回はコード内にベタ書きですが、これはジョブパラメータで受け取るようにしてもいいかと思います。
* `dirName` や `destFilePath` でパスを設定しています。要件に合わせてパスを変更したり、ファイル名を自動生成にしたり、アーカイブにファイルを移動させるなどしてください。

```js:scripts/jobs/upload.js
"use strict";

const File = require("dw/io/File");
const LocalServiceRegistry = require("dw/svc/LocalServiceRegistry");
const Status = require("dw/system/Status");
const KeyRef = require("dw/crypto/KeyRef");

function sftpUpload(params, stepExecution) {
    var sftpService = registerSFTP(params.SFTPServiceID);
    var siteID = require("dw/system/Site").current.ID;
    var dirName = File.IMPEX + File.SEPARATOR + "src";
    var sourceFile = new File(
        dirName + File.SEPARATOR + params.SourceFilePath + ".xml"
    );
    var destFilePath =
        File.SEPARATOR +
        "cc-sd-job-test" +
        File.SEPARATOR +
        "Import" +
        File.SEPARATOR +
        siteID +
        File.SEPARATOR +
        params.DestinationFilePath +
        ".xml";

    var returnStatus;
    try {
        var uploadStatus = sftpService.call({
            sourceFile: sourceFile,
            destFilePath: destFilePath,
        });

        if (uploadStatus.ok) {
            returnStatus = new Status(Status.OK);
        } else {
            returnStatus = new Status(
                Status.ERROR,
                uploadStatus.status,
                uploadStatus.errorMessage
            );
        }
    } catch (e) {
        returnStatus = new Status(Status.ERROR, "EXCEPTION", e.toString());
    }
    return returnStatus;
}

function registerSFTP(serviceID) {
    return LocalServiceRegistry.createService(serviceID, {
        createRequest: function (svc, params) {
            svc.client.setIdentity(new KeyRef("samplejob"));
            svc.setOperation(
                "putBinary",
                params.destFilePath,
                params.sourceFile
            );
        },
        parseResponse: function (svc, uploadStatus) {
            return uploadStatus;
        },
        mockCall: function (svc, params) {},
    });
}

exports.sftpUpload = sftpUpload;
```




```json:steptypes.json
{
    "step-types": {
        "script-module-step": [
            {
                "@type-id": "custom.SampleUploadModule",
                "@supports-parallel-execution": "true",
                "@supports-site-context": "true",
                "@supports-organization-context": "false",
                "description": "Upload Molule",
                "module": "app_job_test/cartridge/scripts/jobs/upload.js",
                "function": "sftpUpload",
                "transactional": "false",
                "timeout-in-seconds": "900",
                "parameters": {
                    "parameter": [
                        {
                            "@name": "SFTPServiceID",
                            "@type": "string",
                            "@required": "true",
                            "description": "SFTP Service ID",
                            "default-value": "test"
                        },
                        {
                            "@name": "DestinationFilePath",
                            "@type": "string",
                            "@required": "false",
                            "@trim": "false",
                            "description": "Target Path and Filename after /cc-sd-job-test/Import/{Site ID}/"
                        },
                        {
                            "@name": "SourceFilePath",
                            "description": "Source Path and Filename after /IMPEX/src/",
                            "@type": "string",
                            "@required": "false",
                            "@trim": "false"
                        }
                    ]
                },
                "status-codes": {
                    "status": [
                        {
                            "@code": "ERROR",
                            "description": "Used when the step failed with an error."
                        },
                        {
                            "@code": "FINISHED",
                            "description": "Used when the step finished successfully."
                        },
                        {
                            "@code": "FINISHED_WITH_WARNINGS",
                            "description": "Used when the step finished with warnings."
                        }
                    ]
                }
            }
        ]
    }
}
```


![スクリーンショット 2022-07-13 13.02.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/700a27c8-e35c-1bb5-fc60-278a6e3debe1.png)




### 9. ジョブの設定

今回は注文情報のエクスポートとアップロードと顧客情報のエクスポートとアップロードを並列で実行したいと思います。
今回は「test」というジョブを作成しました。
「SampleCustomerExport」と「SampleOrderExport」はそれぞれ「ExportCustomers」と「ExportOrders」というシステムジョブステップを利用しています。
システムジョブステップの利用方法は[一覧](https://documentation.b2c.commercecloud.salesforce.com/DOC1/index.jsp?topic=%2Fcom.demandware.dochelp%2FDWAPI%2Fjobstepapi%2Fhtml%2Fapi%2FjobStepList.html)をご覧ください。

![image (15).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/d600ab8a-7823-ba0c-192a-2598a7ecd5f1.png)




#### それぞれのジョブステップの説明

##### 共通で利用できるパラメータ

* customerExportFilePath
    * 顧客情報を出力する先の B2C Commerce のストレージの `/IMPEX/src/` 以降のパス
* orderExportFilePath
    * 注文情報を出力する先の B2C Commerce のストレージの `/IMPEX/src/` 以降のパス
* SFTPServiceID
    * SFTP に使用するサービスの ID


![image (16).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/3e7ee0b4-5da9-fbdc-7b95-890f86f31b8a.png)




##### SampleCustomerExport のポイント

* ExportFile に customerExportFilePath パラメータを設定


![image (28).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/df3809e8-c462-2f84-7d09-7fe5cc940afc.png)



##### SampleCustomerUpload のポイント

* SFTPServiceID に SFTPServiceID パラメータを設定
* DestinationFilePath に S3 にアップロードする際の`/cc-sd-job-test/Import/{Site ID}/` 以降のパスとファイル名を指定
* SourceFilePath には customerExportFilePath パラメータを設定


![image (29).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/bb69e391-8bf0-0b32-a808-254b815e2bfa.png)



##### SampleOrderExport のポイント

* ExportFile に orderExportFilePath パラメータを設定


![image (30).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/5a5de0cc-b630-644e-f254-83b4514eae49.png)




##### SampleOrderUpload のポイント

* SFTPServiceID に SFTPServiceID パラメータを設定
* DestinationFilePath に S3 にアップロードする際の`/cc-sd-job-test/Import/{Site ID}/` 以降のパスとファイル名を指定
* SourceFilePath には orderExportFilePath パラメータを設定


![image (31).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/6d905a71-eec9-ab70-e125-b113282089c6.png)




## 実行してみる

今回は一回のみ手動で実行してみたいと思います。
ジョブのページの右上の「ただちに実行」をクリックすると実行されます。実行結果は「スケジュールと履歴」タブで表示されます。

![スクリーンショット 2022-07-13 14.04.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/a689503a-378f-7fad-28c8-ee5281a4538b.png)



ステータスが OK なのを確認したらちゃんと S3 にファイルが上がっているかを確認します。

`Import/RefArch/customers/20220711.xml`
![スクリーンショット 2022-07-13 14.06.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/ddb786f3-20fd-3121-c9a0-2ef4f645d7e2.png)




`Import/RefArch/orders/20220711.xml`
![スクリーンショット 2022-07-13 14.06.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/579391d7-49e4-8bfe-2ff9-b9263da48101.png)