kintone Webhook と AWS Lambda の連携で kintoneにコメントを書き込む

kintoneとAWS Lambda を連携させて、
[kintoneレコード追加] → [Webhook発火] → [API Gateway] → [AWS Lambda] → [AWS Lambda] → [kintoneレコードコメント書き込み] をやってみました。

# 仕組みイメージ

![kintoneWebhookAwsLambda.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/bd887b0b-84f2-57bb-50f7-a5b5578810e3.png)

# 実行環境

* macOS 10
* AWS Cloud9
* kintone

# 前提条件

* kintoneでWebhookを設定済み
* AWSアカウント取得済み
* API Gateway設定出来る
* AWS Lambda作成出来る
* AWS CloudWatch利用出来る
* JavaScript, Node.js書ける
* 何らかのSDKを使った経験がある
* AWSで課金されることを理解している

# どんなことが出来るか

Webhookを設定したkintoneアプリにレコードを追加します。
<img width="763" alt="スクリーンショット 2019-10-22 16.29.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/350fa58b-7b21-7ab6-8e98-16cd60def2e9.png">

Webhookが発火して、[AWS API Gateway]経由で1つ目のLambdaを実行します。1つ目のLambdaは直ぐにkintoneへレスポンスを返して、2つ目のLambdaを実行します。2つ目のLambdaでWebhook通知を発火したkintoneアプリのレコードにコメントを書き込みます。
<img width="1191" alt="スクリーンショット 2019-10-22 16.29.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/215c08ae-fbcb-e883-5587-debe4386dd15.png">
レコード詳細画面をリロードするとコメントを使う設定になっていれば、追加したレコードのコメントに書き込みがあるはずです。

# 事前準備

下記拙稿をご覧下さい。

* [AWS Cloud9 で AWS Lambda を開発する環境を構築してみる](https://qiita.com/sy250f/items/093405f206b551e03502)
* [AWS Cloud9 で 作成した AWS Lambda 関数で kintone Webhook 通知を受信する](https://qiita.com/sy250f/items/afa3e80db16ee99f1579)
* [AWS Lambda 関数から 別の Lambda 関数を呼び出す方法](https://qiita.com/sy250f/items/7452a3067be2781035d6)

# 処理内容の説明

処理の流れに沿って、処理の中の重要な点を説明します。

## kintoneのレコードを追加したことをWebhookで通知する

kintoneには[Webhook](https://jp.cybozu.help/k/ja/user/app_settings/set_webhook/webhook.html)機能があります。

以下参照（[kintoneヘルプより](https://jp.cybozu.help/k/ja/user/app_settings/set_webhook/webhook.html)）
> Webhookは、複数のWebサービスを連携するための仕組みです。
> kintoneでWebhookを設定すると、次の操作が行われたときに、そのことを外部のWebサービスに通知できます。
> 
> レコードの追加
> レコードの編集
> レコードの削除
> コメントの書き込み
> レコードのステータスの変更

今回このWebhookの仕組みを利用して、通知の内容を AWS Lambda でログに出力して、受信が成功したことを再度kintoneのレコードコメントに書き込みしています。
ただし、コメントの書き込みに対する処理はループしてしまうので、操作種類を見てコメント書き込みの時は、再度kintoneのレコードに書き込む処理を省いています。

## Webhook通知を [AWS API Gateway] で受信する

API Gateway は AWS が提供する Web API 作成のサービスです。
REST API サービスを簡単に用意することが出来ます。

今回は最初のLambda関数を実行させる入り口として利用しています。
Webhookの設定で、API Gateway の API エンドポイント(URL) を登録することで、作成した API と紐づけた Lambda関数が実行されます。

## 最初のLambda関数が実行される

Lambda関数でWebhook通知内の操作の種類を判別して、レコードコメント書き込み以外の場合は、次のLambda関数を実行して、そうで無い場合はそのまま成功ステータスを返しています。

```javascript
const aws = require('aws-sdk');

"use strict";
let sc;
let result;
let response;

exports.handler = async (event, context, callback) => {
  try {
    const kintonePost = JSON.parse(event.body);
    console.log(kintonePost);
    const operationType = kintonePost.type;
    console.log(operationType);

    if (operationType !== 'ADD_RECORD_COMMENT') {

      sc = 200;
      result = "kintone comment POST success!";
      response = {
        statusCode: sc,
        headers: { "Content-type": "application/json" },
        body: JSON.stringify( result )
      };

      let lambda = new aws.Lambda({apiVersion: '2015-03-31'});
      const params = {
        FunctionName: "<arn>",
        InvocationType: "RequestResponse",
        Payload: JSON.stringify(event)
      };
      console.log(params);
      lambda.invoke(params, (err, data) => {
        let res = data;
        if (err) {
          console.log("invoke error.");
          console.log(err);
          context.done(err, err);        
        } else {
          console.log("invoke success.");
          console.log("response:", res);
          context.done(null, res);
        }
      });
    } else {
      sc = 200;
      result = "kintone Record POST success!";
      response = {
        statusCode: sc,
        headers: { "Content-type": "application/json" },
        body: JSON.stringify( result )
      };
    }
  } catch(err) {
    sc = 400;
    result = err.message;
    response = {
      statusCode: sc,
      headers: { "Content-type": "application/json" },
      body: JSON.stringify( result )
    };
  }
  callback(null, response);
};
```

## 2つ目のLambdaでkintoneのレコードコメントに書き込み

2つ目のLambda関数に処理が渡った際に、Webhookが発火したkintoneアプリの同じレコードのレコードコメントに kintoneのREST APIを叩いてコメントを書き込んでいます。

```javascript
let requestPromise = require('request-promise');
"use strict";
const env = process.env;
const url = 'https://' + env.SUBDOMAIN + '/k/v1/';
const headers = {'X-Cybozu-API-Token': env.APITOKEN, 'Content-Type': 'application/json'};
let sc; // status code
let result; // Response payload

exports.handler = async (event, context, callback) => {
    console.log(event);
    const kintonePost = JSON.parse(event.body);
    console.log(kintonePost);
    const operationType = kintonePost.type;
    
    const kintonePostComment = (kintonePost) => {
        const req_body = {
            "app": kintonePost.app.id,
            "record": kintonePost.record['$id'].value,
            "comment": {
                "text": "操作種類:" + operationType + "\nAWS Lambdaからのコメント書き込みです。\nご確認をお願いします。"
            }
        };
        const option = {
            url: url + 'record/comment.json',
            method: 'POST',
            headers: headers,
            json: req_body
        };
        return requestPromise(option)
        .then((response)=>{
            context.done(null, {text: "kintone comment POST success!"});
            sc = 200;
            result = "kintone comment POST success!";
            let callback = {
              statusCode: sc,
              headers: { "Content-type": "application/json" },
              body: JSON.stringify( result )
            };
            return callback;
        })
        .catch((error)=>{
            console.error('kintone Post Comment failed:', error);
            sc = 400;
            result = error;
            let callback = {
              statusCode: sc,
              headers: { "Content-type": "application/json" },
              body: JSON.stringify( result )
            };
            return callback;
        });
    };

    let kintonePostResponse;
    if (operationType !== 'ADD_RECORD_COMMENT') {
        kintonePostResponse = await kintonePostComment(kintonePost);
        console.log(kintonePostResponse);
    } else {
        sc = 200;
        result = "kintone Record POST success!";
        let callback = {
          statusCode: sc,
          headers: { "Content-type": "application/json" },
          body: JSON.stringify( result )
        };
        kintonePostResponse = callback;
    }
    callback(null, kintonePostResponse);
};
```

# あとがき

普段 VS Code を利用してコードを書きますが、
Lambdaの開発については、Cloud9を利用した方が捗るような気がしました。
Cloud9についてはもう少し調べて、出来ればチームで使えるようにしていきたい。

---

# 参考リンク

色々と参考にさせていただきました。ありがとうございます。

## AWS関連

* [AWS SAMを使う前にCloudFormationテンプレートを書こう](https://qiita.com/izanari/items/78258251cced2f713b33)
* [Webhook の受信を契機として複数の API を叩く Lambda 関数を Node.js で書きたいときのためのメモ](https://muziyoshiz.hatenablog.com/entry/2018/06/30/083957)
* [Node.js での AWS Lambda Context オブジェクト](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/nodejs-prog-model-context.html)
* [AWS-SDK AWS.Lambda](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Lambda.html)
* [AWS-SDK AWS.Lambda invoke](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Lambda.html#invoke-property)


## Node関連

* [Node.jsのrequestモジュールを使ってHTTPSでPOSTリクエストを行う](https://qiita.com/penta515/items/074b5c7694b9bcec1043)
* [Request-Promise](https://github.com/request/request-promise)

## kintone関連

* [レコードコメントの投稿](https://developer.cybozu.io/hc/ja/articles/209758903)
* [Slackからkintoneへレコード登録する方法 ～AWS Lambda でつなぐ～ 後編](https://developer.cybozu.io/hc/ja/articles/210014653-Slack%E3%81%8B%E3%82%89kintone%E3%81%B8%E3%83%AC%E3%82%B3%E3%83%BC%E3%83%89%E7%99%BB%E9%8C%B2%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95-AWS-Lambda-%E3%81%A7%E3%81%A4%E3%81%AA%E3%81%90-%E5%BE%8C%E7%B7%A8)
* [Webhookを設定する](https://jp.cybozu.help/k/ja/user/app_settings/set_webhook/webhook.html)


## Cloud9

* [MacOS デフォルトキー割り当てリファレンス](https://docs.aws.amazon.com/ja_jp/cloud9/latest/user-guide/keybindings-default-apple-osx.html)

## その他

* [draw.io](https://about.draw.io/)