AWS CDKで作ったAPIを使ってSalesforceのWebサイトURLからドメインを取り出してみた

2022.10.14

クラスメソッド営業統括本部 CRMチーム 進地 です。 Salesforceに保持しているWebサイトURLからドメインを抜き出したいという要望がありましたので、諸々調査してみました!

やりたいこと

  1. AWS CDKを使ってAPI GatewayにREST APIを作成。このAPIはURLをパラメータで受け取って、そのドメインを抽出して返す。
  2. Salesforceにて取引先を作成するタイミングで1で作ったAPIをコールして、結果を受け取る。コールする時に「取引先.Webサイト(API参照名: Account.Website)」の値をパラメータとしてAPIに渡す。
  3. 受け取った結果をSalesforceの取引先に保存する。

このエントリーでは2までを実施してみます(3は結構やっかい。後日別エントリーで補完できるかな?)

やってみた

AWS CDKを使ってAPI GatewayにREST APIを作成する

AWS CDK Intro WorkshopのAPI Gatewayのページを参考にREST APIを作りました。

まず、CDKプロジェクトを新規に作ります。ここではプロジェクト名をdomain-extractorとしました。

$ cdk init domain-extractor
$ cd domain-extractor

次に、リソースを定義します。言語にはTypeScriptを選びました。lib/domain-extractor-stack.tsを次のように書きました。

import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigw from 'aws-cdk-lib/aws-apigateway';
import { ServicePrincipal } from 'aws-cdk-lib/aws-iam';

export class DomainExtractorStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const domainExtractor = new lambda.Function(this, 'DomainExtractorHandler', {
      runtime: lambda.Runtime.NODEJS_16_X,
      code: new lambda.AssetCode('lambda'),
      handler: 'domainExtractor.handler'
    });
    domainExtractor.grantInvoke(new ServicePrincipal('apigateway.amazonaws.com'));

    const api = new apigw.LambdaRestApi(this, 'domainExtractor', {
      handler: domainExtractor,
      proxy: false,
      deployOptions: {
        loggingLevel: apigw.MethodLoggingLevel.INFO,
        dataTraceEnabled: true,
        metricsEnabled: true,
      }
    });
    const extractor = api.root.addResource('extractor');
    extractor.addMethod('POST');
  }
}

Lambdaファンクションをまず定義し、次にそのLambdaファンクションを呼び出すAPI Gatewayを定義しています。

なお、

domainExtractor.grantInvoke(new ServicePrincipal('apigateway.amazonaws.com'));

の指定がないと、API Gatewayから定義したLambdaが呼び出せずPermissionエラーになります(ここは結構ハマった)。

[apigateway] grant lambda invoke permission when new stages are added #3983

次に、Lambdaファンクションの本体(DomainExtractorHandler)を作成します。

$ cd lambda

lambdaディレクトリに移動して、domainExtractor.jsを次のように書きました。

const url = require('url');
const psl = require('psl');

exports.handler = async function(event) {
  console.log("request:", JSON.stringify(event, undefined, 2));
  const body = JSON.parse(event.body);
  const parsed = psl.parse(url.parse(body.url).hostname);
  const returnValue = {
    domain: parsed.domain
  };
  return {
    statusCode: 200,
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(returnValue, null, 2)
  };
};

urlモジュールを使ってパラメータで受け取ったURLのホスト名(hostname)を抽出し、それをpslモジュールのparseメソッドに渡すことでドメインを抽出しています。 pslモジュールはlambdaディレクトリ直下でnpm installする必要があることに注意が必要です。

$ pwd
domain-extractor/lambda
$ npm install psl

あとは、デプロイして、

$ cdk deploy
:
DomainExtractorStack.domainExtractorEndpointEC1F9D14 = https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/

作成したAPIの動作確認をしてみます。

$ curl -X POST -d '{"url": "https://dev.classmethod.jp/"}' https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/extractor
:
{
  "domain": "classmethod.jp"
}

期待通りドメインが返りました。

Salesforceにて取引先を作成するタイミングで1で作ったAPIをコールして、結果を受け取る

手前味噌ですが、私が過去に書いた次のエントリーを参照して作成したAPIをコールします。

Amazon API Gateway を Salesforce からコールアウトする

Salesforceでの指定ログイン情報の作成は、参照先記事の方法で実施済みとします(名前をAWS_APIGWとして定義したものとします)。

取引先を作成するタイミングで実行したいので、取引先(Account)に対するApex Triggerを作成します。
triggers/AccountTrigger.triggerを次のように実装してみました。

trigger AccountTrigger on Account (before insert) {
    new AccountTriggerHandler().execute();
}

処理の実体はAccountTriggerHandlerというApexクラスの方で行います。

public with sharing class AccountTriggerHandler extends TriggerHandler {
    /**
     * 作成前処理
     * @param newList 作成した取引先
     */
    public override void beforeInsert(List<Sobject> newList) {
        // Webサイトの値を取り出し、APIをコールする
        for (Account acc : (List<Account>)newList) {
            if (acc.Website != null) {
                DomainExtractorCalloutAsync.execute('{"url": "' + acc.Website + '"}');
            }
        }
    }
}

TriggerHandlerはTrigger作成用の汎用抽象クラスです。作っておくと便利ですよ。

public abstract class TriggerHandler {
    public void execute() {
        switch on Trigger.operationType {
            when BEFORE_INSERT  { this.beforeInsert(Trigger.new); }
            when BEFORE_UPDATE  { this.beforeUpdate(Trigger.oldMap, Trigger.newMap); }
            when BEFORE_DELETE  { this.beforeDelete(Trigger.oldMap); }
            when AFTER_INSERT   { this.afterInsert(Trigger.newMap); }
            when AFTER_UPDATE   { this.afterUpdate(Trigger.oldMap, Trigger.newMap); }
            when AFTER_DELETE   { this.afterDelete(Trigger.oldMap); }
            when AFTER_UNDELETE { this.afterUndelete(Trigger.newMap); }
        }
    }

    protected virtual void beforeInsert(List<Sobject> newList) {}
    protected virtual void beforeUpdate(Map<Id, Sobject> oldMap, Map<Id, Sobject> newMap) {}
    protected virtual void beforeDelete(Map<Id, Sobject> oldMap) {}
    protected virtual void afterInsert(Map<Id, Sobject> newMap) {}
    protected virtual void afterUpdate(Map<Id, Sobject> oldMap, Map<Id, Sobject> newMap) {}
    protected virtual void afterDelete(Map<Id, Sobject> oldMap) {}
    protected virtual void afterUndelete(Map<Id, Sobject> newMap) {}
}

AccountTriggerHandlerの中から、実際のAPIのコールアウト処理はさらにDomainExtractorCalloutAsyncクラスで行っています。

global with sharing class DomainExtractorCalloutAsync implements Database.AllowsCallouts {
    @future(callout=true)
    public static void execute(String body) {
        Http http = new Http();
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:AWS_APIGW/prod/extractor');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setHeader('Accept', 'application/json');
        req.setBody(body);
        HttpResponse res = http.send(req);
        CalloutResponse cres = (CalloutResponse)JSON.deserializeStrict(res.getBody(), CalloutResponse.class);
        System.debug(cres.domain);
    }

    class CalloutResponse {
        String domain;
    }
}

Salesforceで取引先を新規作成してみて、開発者コンソールのログにAPIの実行結果(取引先のWebサイトから抽出したドメイン)が記載されていることを確認します。ここでは取引先のWebサイトにhttps://dev.classmethod.jp/を指定してみました。

期待する結果(classmethod.jp)が取れていますね。