[Salesforce] リード作成時にコールアウトを使ってJiraにチケットを作る

Salesforceから外部サービスのRest APIをコールアウトする方法について記述しました。 認証プロバイダ、指定ログイン情報の利用でコールアウト処理を簡素化することと、 一例としてリード作成時にJiraにIssueを作る方法を取り上げました。
2021.02.26

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Salesforceには、外部APIを呼び出すコールアウトの仕組みがあります。

Apex を使用したコールアウトの呼び出し

いま、問い合わせを受け付けるフォームによってSalesforceにてリードが自動作成されているとします。 そして、リードに対応するチケットを Jira Cloud に自動で作りたいとします。

この要件をコールアウトを使用して実現したいと思います。

Jira API を呼び出すコールアウトの実装

設定

Salesforce のコールアウトを実装するために、Salesforceの認証プロバイダ、指定ログイン情報の設定、またJiraにAppを設定します。

Salesforce 認証プロバイダの設定

Jiraの認証プロバイダを設定します。認証プロバイダと後述する指定ログイン情報を使うことで、認証周りのコードをApexで書かなくてよくなります。

認証プロバイダ設定画面

認証プロバイダに設定する各値は次の通りです。

項目 設定値
プロバイダタイプ Open ID Connect
名前 Jira1
URL 接尾辞 auth
コンシューマ鍵 後述するJira Appの設定から取得してセットする
コンシューマの秘密 後述するJira Appの設定から取得してセットする
承認エンドポイント URL https://auth.atlassian.com/authorize?audience=api.atlassian.com
トークンエンドポイント URL https://auth.atlassian.com/oauth/token
デフォルトの範囲 read:jira-work manage:jira-project manage:jira-configuration read:jira-user write:jira-work manage:jira-data-provide offline_access
ヘッダーでアクセストークンを送信 チェックする
API 応答にコンシューマの秘密を含める チェックする
他の項目 入力およびチェックしない

設定して、認証プロバイダを保存すると自動的に「Salesforce 設定」ブロックの各URLが生成されます。 このうちの「コールバック URL」を後述するJira Appの設定で使いますので控えておきます。

Jira Appの作成

https://developer.atlassian.com/console/myapps/にアクセスして、Jira Appを作成します。

Jira Appの一覧画面

ここでは「SalesforceSync」というAppを作成しました。Appを選択し詳細設定を行います。

Jira App Permissions設定

左メニューから「Permissions」を選びます。 Permissionsの一覧から「Jira platform REST API」を「Add」し、「Configure」ボタンをクリックします。

Jira App Permissions スコープ設定

REST API で使えるスコープのリストが表示されるので、必要なスコープ(権限)を「Add」します。 私はここでは全スコープをAddしました。

次に左メニューから「Authorization」を選びます。

Jira App Authorization設定

Authorization type 「OAuth 2.0 (3LO)」の「Configure」ボタンをクリックします。

Jira App Authorization コールバックURL設定

Callback URLを設定する画面が表示されますので、認証プロバイダの設定で控えておいた「コールバック URL」を設定します。

次に左メニューから「Settings」を選びます。

Jira App ClientID と Secretの取得

ページの下部に「Client ID」と「Secret」が取得できますので控えます。 そして先に作成した認証プロバイダの「コンシューマ鍵」に「Client ID」を、「コンシューマの秘密」に「Secret」をそれぞれ設定します。

Salesforce 指定ログイン情報の設定

Jiraの指定ログイン情報を設定します。

Salesforce 指定ログイン情報設定

指定ログイン情報に設定する各値は次の通りです。

項目 設定値
表示ラベル Jira
名前 Jira
URL https://<お使いのサブドメイン>.atlassian.net
ID 種別 指定ユーザ
認証プロトコル OAuth 2.0
認証プロバイダ Jira2
認証ヘッダーを生成 チェックする
保存時に認証フローを開始3 チェックする
他の項目 入力、チェックしない

ここまでの設定が正しければ、JiraとSalesforce間の認証を求める画面が表示されますので、承諾して認証してください。 「認証状況」が「認証済み」になれば、コールアウトを使う準備は完了です。

接続検証

Salesforceの開発者コンソールを立ち上げて[Debug] > [Open Execute Anonymous Window]を選びます。

立ち上がったウィンドウで次のApexコードを記述し、「Execute」します。

ユーザ情報取り出し接続検証Apex

HttpRequest req = new HttpRequest();
req.setEndpoint('callout:Jira/rest/api/3/user?accountId=<取得したいJiraアカウントのアカウントID>');
req.setMethod('GET');
Http http = new Http();
HttpResponse res = http.send(req);
System.debug(res);
System.debug(res.getBody());

ログを確認して、ステータスコード200が返ってきていれば成功です。

接続確認結果

細かな認証の取り回しを実装しなくても、

req.setEndpoint('callout:<指定ログイン情報名><Rest API パス>');

でRest APIを簡単に呼び出すことができます。

実装

それでは、Salesforceでリード作成時にJiraにチケットを作成する処理を実装します。

public inherited sharing class JiraIssueGenerator {
    private Fields fields;
    private final static String CRLF = '\r\n';

    @InvocableVariable
    public List<Id> leadIds;

    public JiraIssueGenerator(Id leadId) {
        Lead lead = [SELECT Name, Company, Email, Phone FROM Lead WHERE Id = :leadId LIMIT 1];
        String summary = lead.Name + ' からのお問い合わせ';
        Description description = new Description(lead.Name + ' からお問い合わせがありました。' + CRLF + '連絡先はメールアドレスは ' + lead.Email + '、電話番号は ' + lead.Phone + ' です。');
        this.fields = new Fields(summary, description, new Project('TEST'), new IssueType('10001'), new Assignee('6033128f2de69b006aed7155'));
    }

    @InvocableMethod(Label='JiraにIssueを作成する')
    public static void generate(List<Id> leadIds) {
        callHTTPRequest(leadIds);
    }

    @future(callout=true)
    public static void callHTTPRequest(List<Id> leadIds){
        for (Id leadId : leadIds) {
            JiraIssueGenerator jig = new JiraIssueGenerator(leadId);
            HttpRequest req = new HttpRequest();
            req.setEndpoint('callout:Jira/rest/api/3/issue');
            req.setMethod('POST');
            req.setHeader('Content-Type', 'application/json');
            req.setHeader('Accept', 'application/json');
            req.setBody('{"fields": ' + JSON.serialize(jig.fields) + '}');
            system.debug(req.getBody());
            Http http = new Http();
            HttpResponse res = http.send(req);
            System.debug(res);
            System.debug(res.getBody());    
        }
    }

    private class Fields {
        private String summary;
        private Description description;
        private Project project;
        private IssueType issuetype;
        private Assignee assignee;

        private Fields(String summary, Description description, Project project, IssueType issuetype, Assignee assignee) {
            this.summary = summary;
            this.description = description;
            this.project = project;
            this.issuetype = issuetype;
            this.assignee = assignee;
        }
    }

    private class Description {
        private String type;
        private Integer version;
        private List<ParagraphContent> content; 

        private Description(String text) {
            this.type = 'doc';
            this.version = 1;
            this.content = new List<ParagraphContent>();
            this.content.add(new ParagraphContent(text));
        }
    }

    private class ParagraphContent {
        private String type;
        private List<TextContent> content;

        private ParagraphContent(String text) {
            this.type = 'paragraph';
            this.content = new List<TextContent>();
            this.content.add(new TextContent(text));
        }
    }

    private class TextContent {
        private String type;
        private String text;

        private TextContent(String text) {
            this.type = 'text';
            this.text = text;
        }
    }

    private class Project {
        private String key;
        private Project(String key) {
            this.key = key;
        }
    }

    private class IssueType {
        private String id;
        private IssueType(String id) {
            this.id = id;
        }
    }

    private class Assignee {
        private String id;
        private Assignee(String id) {
            this.id = id;
        }
    }

}

大まかに以下の流れの処理を行っています。

  1. generateメソッドで対象リードのSalesforce Id(のリスト)を受け取る
  2. callHTTPRequestメソッドにリードIdのリストを渡して呼び出す
  3. 各リードId毎に当該リードの情報をSOQLで取り出す
  4. 取り出したリード情報からJiraのIssueを作成するためのJSONを作る
  5. 4で作成したJSONをbodyにセットして、Issue作成の/rest/api/3/issueエンドポイントにPOSTを実行する
  6. 3〜5を2で受け取ったリストに含まれるIdの個数分繰り返す

求められるJSONのフォーマットは公式ドキュメントを参照してください。

プロセスビルダーからのApex起動

リード作成時に起動するプロセスをプロセスビルダーで作成します。

プロセスビルダー Jira Issue作成プロセス概要

アクションの設定でApexのgenerateメソッドをコールし、引数として作成したリードのIdを渡しています。

先に作成したApexでgenerateメソッドに @InvocableMethod(Label='JiraにIssueを作成する') と設定しているのでプロセスビルダーでは「JiraにIssueを作成する」を選ぶことでgenerateメソッドをコールできます。 また、 leadIdsに @InvocableVariable と設定しているので、プロセスビルダーからleadIdsに対して先述の通りリードのIdを渡すことができます。

Apexコールのアクション設定

ちなみに、generateメソッドはcallHTTPRequestメソッドに受け取ったパラメータを渡しているだけなので一纏めにできそうですが、generateメソッドの中でコールアウトを行おうとするとプロセスビルダーから呼んだ時に

System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling

のエラーが発生します。そのため、コールアウト処理は別メソッドに括り出して @future(callout=true) をつけて非同期で呼び出しています。

動作確認

Salesforceでリードを新規作成してみて、Jiraにチケットが作成されていることを確認します。

テストリード作成

Jiraを見てみると…

JiraにてIssueがリード情報から作成されている

Issueが作成されています!

まとめ

Salesforce から Jira の Rest APIをコールアウトすることで、リード作成時に Jira の Issue を自動で作成することができました。

また、Salesforce からコールアウトを行う時には認証プロバイダと指定ログイン情報を使うことで認証周りの複雑な取り回しをコードで実装しなくても済むこともおわかりいただけたかと思います。Apexコードがすっきりしますので是非ご活用ください。

参考資料


  1. 任意の値で大丈夫です。 
  2. 先に設定した認証プロバイダを指定します。 
  3. 「保存時に認証フローを開始」は更新画面だけに現れます。