AWS LambdaでBoxのWebhookを処理する

## 概要

BoxのフォルダにWebHookを設定し、AWS Lambdaに送信されたデータを取得/操作するまでの基本的な手順についてまとめています。
(2019年3月時点)

## 参考

- [Webhookのファイルワークフロー (Box Developer site)](https://ja.developer.box.com/docs/file-workflow-with-webhooks)


## Lambda 実行ロールの作成

Lambda関数の実行時には、「AWSLambdaBasicExecutionRole」が付与されたIAM Roleが必要です。
以下の手順でIAM Roleを作成しておき、以後Lambda関数を作成する際に
Roleを割り当てることとします。


IAM > ロールの作成

![lambda_box_webhook_01.png](http://archive.kowloonet.org/blog/lambda_box_webhook_01.png)

「AWSサービス」を選択し、次に「Lambda」を選択後、
「次のステップ:アクセス権限」をクリックします。

![lambda_box_webhook_02.png](http://archive.kowloonet.org/blog/lambda_box_webhook_02.png)
「ポリシーのフィルタ」欄に「AWSLambda」と入力し、候補表示の中から
「AWSLambdaBasicExecutionRole」にチェックして「次のステップ:タグ」をクリックします。

![lambda_box_webhook_03.png](http://archive.kowloonet.org/blog/lambda_box_webhook_03.png)
※Lambda関数を実行するだけなら、選択するポリシーは
「AWSLambdaBasicExecutionRole」だけとなりますが、その他に
- S3へのアクセス
- RDBへのアクセス

などもLambda関数から実行する場合には、この画面で適宜追加のポリシーも選択しておく必要があります。

タグの追加(オプション)画面で、このIAM Roleに割り当てるタグを指定できます。
たとえば、このRoleを所有しているユーザ名や、プロジェクト名、管理部署名など必要に応じ入力します。
今回は空欄のまま「次のステップ:確認」をクリックして進めます。


「ロール名」にロールの名称を指定し(本例では"Role-LambdaBasicExec")、
「ロールの作成」をクリックします。

![lambda_box_webhook_04.png](http://archive.kowloonet.org/blog/lambda_box_webhook_04.png)


ロールの一覧画面にて、今回作成したロールが表示されていることを確認します。

![lambda_box_webhook_05.png](http://archive.kowloonet.org/blog/lambda_box_webhook_05.png)


## Lambda関数の作成

「関数の作成」画面から、「設計図の使用」
キーワードに「microservice-http」と入力し、候補から
「microservice-http-endpoint-python3」を選択します。

![lambda_box_webhook_06.png](http://archive.kowloonet.org/blog/lambda_box_webhook_06.png)

![lambda_box_webhook_07.png](http://archive.kowloonet.org/blog/lambda_box_webhook_07.png)

関数の作成に必要な情報を入力します。

**基本的な情報**

関数名
本例では"boxWebHookTest"としました。

実行ロール
「既存のロールを使用する」を選択し、プルダウンから前掲の手順で作成したIAM Roleを選択します。
(本例では「Role-LambdaBasicExec」)

![lambda_box_webhook_08.png](http://archive.kowloonet.org/blog/lambda_box_webhook_08.png)


**API Gatewayトリガー**

API
新規APIの作成

セキュリティ
今回は認証なしでAPI Gatewayを呼び出せるように、「オープン」を選択します。

API名
APIの識別名を指定します。
デフォルトで
`関数名-API`
の命名規則で生成されますので、今回はそのまま
boxWebHookTest-API
としておきます。

デプロイされるステージ
APIのデプロイ先を「ステージ」指定により切り替えることが可能ですが、defaultのままにしておきます。

![lambda_box_webhook_09.png](http://archive.kowloonet.org/blog/lambda_box_webhook_09.png)

**関数のコード**
デフォルトのPythonコードが表示されていますが、
そのままにしておきます。

![lambda_box_webhook_10.png](http://archive.kowloonet.org/blog/lambda_box_webhook_10.png)

「関数の作成」を実行します。

Lambda関数が作成された旨のメッセージが表示され、関数とAPI Gatewayの設定画面が表示されます。

![lambda_box_webhook_11.png](http://archive.kowloonet.org/blog/lambda_box_webhook_11.png)

Designer画面で「API Gateway」のパネルを選択すると、下の画面にAPI EndpointのURLが表示されます。
このURLは後でWebHookの飛ばし先として使いますので、メモ帳に貼り付けておきます

![lambda_box_webhook_12.png](http://archive.kowloonet.org/blog/lambda_box_webhook_12.png)

---

## WebHookの作成

開発者コンソールにて、新規アプリケーションを作成します。
- カスタムアプリ
- 標準OAuth2.0(ユーザ認証)

![lambda_box_webhook_13.png](http://archive.kowloonet.org/blog/lambda_box_webhook_13.png)

![lambda_box_webhook_14.png](http://archive.kowloonet.org/blog/lambda_box_webhook_14.png)

![lambda_box_webhook_15.png](http://archive.kowloonet.org/blog/lambda_box_webhook_15.png)

アプリケーションスコープで「Webhookを管理」にチェックを入れて「変更を保存」します。
※このアプリのトークンを使って、Webhookを生成しますので、Webhook管理権限が必要です。

![lambda_box_webhook_16.png](http://archive.kowloonet.org/blog/lambda_box_webhook_16.png)

Webhookを登録する際に使用するトークンを生成します。

[OAuthのサンプルコード](https://github.com/BoxJapan-SE/oauth-samples)をいずれか実行し、
アクセストークンを取得しておきます。

このとき生成したアクセストークンは1時間だけ有効です。
後続のWebhook作成を1時間以内に完了できなかった場合は、
再度、アクセストークンを取り直して下さい。

※DeveloperトークンでWebhookを作成すると、約1日経過後に当該のWebhookが正しく機能しなくなる事象が出ましたため、
OAuth 3legged認証を経て入手したアクセストークンを使う必要があります。
(事象については本記事下部の[「翌日以降のWebhookのBody内容が不正(Anonymous User扱い)となる」](https://blog.kowloonet.org/2019/03/aws-lambda%E3%81%A7box%E3%81%AEwebhook%E3%82%92%E5%87%A6%E7%90%86%E3%81%99%E3%82%8B/#%E7%BF%8C%E6%97%A5%E4%BB%A5%E9%99%8D%E3%81%AEwebhook%E3%81%AEbody%E5%86%85%E5%AE%B9%E3%81%8C%E4%B8%8D%E6%AD%A3-anonymous-user%E6%89%B1%E3%81%84-%E3%81%A8%E3%81%AA%E3%82%8B)参照)


テスト用のフォルダをBox上に作成します。
そのフォルダをBoxのWebUIで開き、フォルダIDをメモ帳に貼り付けておきます。

`https://boxpocsite.app.box.com/folder/xxxxxxxxxxxxxxx`
URLの末尾、/folder/の後に続く数値部分がフォルダIDです。

![lambda_box_webhook_18.png](http://archive.kowloonet.org/blog/lambda_box_webhook_18.png)

このフォルダにファイルがアップロードされたタイミングで実行されるWebhookを作成します。

ここまでの手順で、以下の情報が手元に揃っているはずです。

- Box上に作成した、テスト用のフォルダID
- AWS LambdaのAPI Endpoint URL
- BoxアプリのDeveloperトークン

上記の値を代入して、Webhook作成を行います。
Curlコマンドの場合は以下の構文になります。

**構文**
```
$ curl https://api.box.com/2.0/webhooks \
> -H "Authorization: Bearer xxxx(Developerトークン)xxxx" \
> -H "Content-Type: application/json" -X POST \
> -d '{"target": {"id": "テスト用フォルダのID値", "type": "folder"}, "address": "AWS LambdaのAPI Endpoint URL", "triggers": ["トリガーのイベント"]}'
```

例として、登録用の値が

- テスト用のフォルダID : 1234567890
- AWS LambdaのAPI Endpoint URL : https://xxx.amazonaws.com/default/boxWebHookTest
- BoxアプリのDeveloperトークン : zzzzzzzzzzzzzzzzzz
- トリガーのイベント : FILE.UPLOADED

の場合は、以下となります。

```
$ curl https://api.box.com/2.0/webhooks \
> -H "Authorization: Bearer zzzzzzzzzzzzzzzzzz" \
> -H "Content-Type: application/json" -X POST \
> -d '{"target": {"id": "1234567890", "type": "folder"}, "address": "https://xxx.amazonaws.com/default/boxWebHookTest", "triggers": ["FILE.UPLOADED"]}'
```

Webhook作成のAPIコールが成功すると、以下の構文で戻り値が返ってきます。

```
{"id":"WebhookのID","type":"webhook","target":{"id":"フォルダID","type":"folder"},"created_by":{"type":"user","id":"Webhook作成を実行したユーザID","name":"ユーザ名","login":"ログイン用メールアドレス"},"created_at":"生成時刻(太平洋時間)","address":"Webhookの飛ばし先URL","triggers":["Webhookをトリガーするイベント"]}
```

## Webhookの到達確認

Webhookを実際にトリガーし、AWS LambdaのAPI Endpointまで到達するかを確認します。

Lambda関数のコンソールにて、Lambdaイベントの中身をそのままPrintするPythonコードを作成します。


![lambda_box_webhook_19.png](http://archive.kowloonet.org/blog/lambda_box_webhook_19.png)

```python:lambda_function.py
import json

def lambda_handler(event, context):
    print(json.dumps(event, indent=4))
    # JSON形式の戻り値を設定する
    return {
    'statusCode' : 200,
    'headers' : {
    'content-type' : 'text/html'
    },
    'body' : '<html><body>OK</body></html>'
}
```


コードの入力が完了したら、右上の「保存」をクリックします。
※AWS Lambdaコンソールでは、設定変更の都度「保存」が必要です

Boxのテストフォルダに何かファイルを1つアップロードします。
(どんなファイルでも構いません。)

ファイルをアップロードすることでWebhookイベントが発生し、AWS LambdaのAPI Endpoint URLめがけてPOSTメソッドが実行されます。

ファイルのアップロード完了後、Lambda関数の設定画面から「モニタリング」を選択します。

![lambda_box_webhook_20.png](http://archive.kowloonet.org/blog/lambda_box_webhook_20.png)


続いて「CloudWatchのログを表示」をクリックします。

![lambda_box_webhook_21.png](http://archive.kowloonet.org/blog/lambda_box_webhook_21.png)

ログストリームが生成されているので、リンクをクリックして開きます。

![lambda_box_webhook_22.png](http://archive.kowloonet.org/blog/lambda_box_webhook_22.png)


ログを上から見ていくと、Header情報などの管理情報に続いて、POSTのbody部分を確認できます。

![lambda_box_webhook_23.png](http://archive.kowloonet.org/blog/lambda_box_webhook_23.png)
![lambda_box_webhook_24.png](http://archive.kowloonet.org/blog/lambda_box_webhook_24.png)
![lambda_box_webhook_25.png](http://archive.kowloonet.org/blog/lambda_box_webhook_25.png)


この"body"部分にWebhookの本体が格納されています。

![lambda_box_webhook_26.png](http://archive.kowloonet.org/blog/lambda_box_webhook_26.png)

```
"body": "{\"type\":\"webhook_event\",\"id\":\"eb92204d-dcc6-4(省略)
```

## WebhookのBody内容

POSTされたWebhookのデータ部分は、以下のJSON形式となっています。

参考:
https://developer.box.com/reference#webhooks-v2


```JavaScript
{
"type":"webhook_event",
"id":"webhookイベントのID",
"created_at":"2019-03-18T01:34:02-07:00",
"trigger":"FILE.UPLOADED",
"webhook":{
"id":"WebhookのID(Box上での識別ID)",
"type":"webhook"
},
"created_by":{
"type":"user",
"id":"BoxのユーザID",
"name":"Boxのユーザ名",
"login":"BoxユーザのログインMailアドレス"
},
"source":{
"id":"Webhookをトリガーしたコンテンツ(ファイルなど)に付与されたID",
"type":"コンテンツの種別(file or folderが入る)",
"file_version":{
"type":"file_version",
"id":"ファイルバージョンID",
"sha1":"ファイルのSHA1ハッシュ値"
},
"sequence_id":"0",
"etag":"0",
"sha1":"ファイルのSHA1ハッシュ値",
"name":"ファイル/フォルダ名",
"uniq":"486d66f9a8b64f8af37bfd2eff9d0d4e",
"key_ref":null,
"description":"",
"size":0,
"path_collection":{
"total_count":2,
"entries":[
{
"type":"folder",
"id":"0",
"sequence_id":null,
"etag":null,
"name":"最上位のフォルダ名"
},
{
"type":"folder",
"id":"2階層目のフォルダID",
"sequence_id":"1",
"etag":"1",
"name":"webhook_lambda_test"
}
]
},
"created_at":"2019-03-18T01:34:02-07:00",
"modified_at":"2019-03-18T01:34:02-07:00",
"trashed_at":null,
"purged_at":null,
"content_created_at":"2019-03-13T23:12:56-07:00",
"content_modified_at":"2019-03-13T23:12:56-07:00",
"created_by":{
"type":"user",
"id":"コンテンツを作成したBoxのユーザID",
"name":"Boxのユーザ名",
"login":"BoxユーザのログインMailアドレス"
},
"modified_by":{
"type":"user",
"id":"コンテンツを最終更新したBoxのユーザID",
"name":"Boxのユーザ名",
"login":"BoxユーザのログインMailアドレス"
},
"owned_by":{
"type":"user",
"id":"コンテンツ所有者のBoxユーザID",
"name":"Boxのユーザ名",
"login":"BoxユーザのログインMailアドレス"
},
"shared_link":null,
"parent":{
"type":"folder",
"id":"Webhookをトリガーしたコンテンツを格納しているフォルダのID",
"sequence_id":"1",
"etag":"1",
"name":"webhook_lambda_test"
},
"item_status":"active"
},
"additional_info":[

]
}
```

### 補足

フォルダ名/ファイル名などが2バイト文字の場合、値としてUnicode変換したものが代入されます。
本例では、Boxの最上位のフォルダ名としてWebhookに入っていた値は
`\u3059\u3079\u3066\u306e\u30d5\u30a1\u30a4\u30eb`という文字列でした。
これを変換すると
文字列「`すべてのファイル`」になります。

フォルダ名/ファイル名に2バイト文字が全く含まれていない場合は、オリジナルの名前がそのまま入ります(Unicode化されない)。
本例では、2階層目のフォルダ名は`webhook_lambda_test`でしたので、
Bodyの中でも

```
"name":"webhook_lambda_test"
```
と、そのままの名前で記載されています。

## Webhook受信時のログを整形する

上掲のPythonコードでは、受け取ったLambdaイベントの中身を未加工のまま出力しているので、
以下の問題があり非常に読みづらいです。

* 改行されていない
* Jsonの階層に従ったインデントがなされていない
* 2バイト文字部分がUnicode変換されている

修正前のCloudwatchログ
![lambda_box_webhook_28.png](http://archive.kowloonet.org/blog/lambda_box_webhook_28.png)

そこで、Lambda上のログを読みやすく出力するようにコードを少し改良します。

* 生のJsonデータをパースして、Unicodeから変換し、インデント出力する関数「print_json」を定義
* 関数「print_json」にWebhookのBody部を渡す

```python:lambda_function.py
import json
import codecs

# 受け取ったJsonを整形して出力する関数を定義
def print_json(data):
    print(codecs.decode(json.dumps(data, indent=4, separators=(',', ';')), 'unicode-escape'))

def lambda_handler(event, context):
    # 受け取ったBodyを整形してCloudwatchに出力(デバッグ・確認用)
    print_json(event)
    
    # JSON形式の戻り値を設定する
    return {
        'statusCode' : 200,
        'headers' : {
            'content-type' : 'text/html'
        },
        'body' : '<html><body>OK</body></html>'
    }
```

コードを更新して保存後、再度Webhookを飛ばし、
Cloudwatchのログで確認すると、

* 改行
* インデント
* 2バイト文字

が解決されて、読みやすくなったことが分かります。

修正後のCloudwatchログ
![lambda_box_webhook_29.png](http://archive.kowloonet.org/blog/lambda_box_webhook_29.png)

## Webhookから任意の値を取得する

実際にWebhookをトリガーとするアプリケーションを作る際には、
WebhookのBody部分から目当ての値を取りだして、Pythonの変数に格納して
処理をする必要があります。

そこで、Body部分から値を取得して変数として取り扱うためのコードを追加します。

```python:lambda_function.py
import json
import codecs

# 受け取ったJsonを整形して出力する関数
def print_json(data):
    print(codecs.decode(json.dumps(data, indent=4, separators=(',', ';')), 'unicode-escape'))


def lambda_handler(event, context):
    # 受け取ったBodyを整形してCloudwatchに出力(デバッグ・確認用)
    print_json(event)
    
    # eventのbody部分を取得して、Jsonとして解析
    body = json.loads(event['body'])
    
    # body部分の任意の値を取り出せるか確認
    print('type = ' + body['type'])
    print('トリガーのファイル名 = ' + body['source']['name'])

    # JSON形式の戻り値を設定する
    return {
        'statusCode' : 200,
        'headers' : {
            'content-type' : 'text/html'
        },
        'body' : '<html><body>OK</body></html>'
    }
```

関数lambda_handler()の中に

* WebhookのBody部を取得してjson.loads()に渡す
* Body部分のJsonの中から、特定のフィールドを取り出してログ出力

を追加しました。

このコードに更新して保存後、再度Webhookを飛ばし、
Cloudwatchのログで確認すると、

* Webhookのタイプ
* Webhookをトリガーしたファイル名

をBodyから取得できていることが確認できます。

修正後のCloudwatchログ

![lambda_box_webhook_30.png](http://archive.kowloonet.org/blog/lambda_box_webhook_30.png)

あとは、Pythonの変数に格納して好きな処理に渡すだけです。


## Webhookの有効期限

- 最後のWebhook実行(実行結果が成功)から、いちどもWebhookが使われていない状態で2週間が経過
「使われていない」とは、Webhookイベントが発火していない、ということ。

- 最後の実行(実行結果がFail)から2週間が経過

上記、いずれかの条件を満たすと、そのWebhookは削除されます。
削除されたWebhookは再度作り直す必要があります。

## ハマった/やらかしました集

### 翌日以降のWebhookのBody内容が不正(Anonymous User扱い)となる

翌日、2個目のファイルを同じフォルダにアップロードしたところ、
Webhookは起動してPOSTが行われたが、BODY部を見ると

- trigger : NO_ACTIVE_SESSION
- ユーザID :2
- ユーザ名 : Anonymous User

となっていました。
1回目の成功したときと同じユーザーでBoxにログインし直しても、事象は改善せず。

```
{
"type":"webhook_event",
"id":"9c6f708d-1393-49c0-9b4a-96f93ead0b58",
"created_at":"2019-03-19T03:01:47-07:00",
"trigger":"NO_ACTIVE_SESSION",
"webhook":{
"id":"151732426","type":"webhook"
},
"created_by":{
"type":"user",
"id":"2",
"name":"Anonymous User",
"login":""
},
"source":{
"id":"424148311576",
"type":"file"
},
"additional_info":[

]
}
```

当該のWebhookの状態を確認すると、生きているように見えます。

```
{
"id":"151732426",
"type":"webhook",
"target":{
"id":"70394214504",
"type":"folder"
},
"created_by":{
"type":"user","id":"ユーザID",
"name":"ユーザ名",
"login":"ログインメールアドレス"
},
"created_at":"2019-03-18T01:00:54-07:00",
"address":"AWS LambdaのAPI Endpoint URL",
"triggers":["FILE.UPLOADED"]
}
```

LambdaがWebhookサーバ側にStatus Codeを返していない可能性を考えましたが、
正しく200を返していることも確認できました。

![lambda_box_webhook_26.png](http://archive.kowloonet.org/blog/lambda_box_webhook_27.png)

#### (切り分け)アプリで生成したトークンで再度WebHook作成

開発者トークンでWebhookを作成したことが悪さしている可能性を考慮して、
OAuth認証アプリで生成したトークンを使い、Webhookを再作成

トークン生成に使用したアプリケーション名称:
GLENN-OAUTH-SAMPLE-PYTHON

作成時刻: 2019/03/27 13:46
作成したWebhook
```
{
"id": "153930739",
"type": "webhook",
"target": {
"id": "70394214504",
"type": "folder"
},
"created_by": {
"type": "user",
"id": "xxxx",
"name": "xxxx",
"login": "xxxx"
},
"created_at": "2019-03-26T21:46:22-07:00",
"address": "https://xxxxxx.ap-northeast-1.amazonaws.com/default/boxWebHookTest",
"triggers": [
"FILE.DOWNLOADED",
"FILE.UPLOADED"
]
}
```

2019/03/27 15:08
Webhookの作成から1時間以上経過後、ファイルを再度Upload
→正常なWebhookが返ることを確認

切り分けから、DeveloperトークンでWebhookを作成したことが原因と考えられます。


### 自前のWebサーバ宛てのWebhookが失敗する

本ページはAWS LambdaをWebhookの宛先として使用していますが、
これより前に、自前でNginxのサーバを立てて、安いSSLサーバ証明書を買って
[Flask](http://flask.pocoo.org/)でWebhookを受け取る仕組みを作ろうとして、
無事失敗しました。

**発生事象**
Webhbookは問題無くトリガーされたが、Webサーバ側のアクセスログには何も記録されない。
TCPDumpを取ったところ、SSL Handshakeの過程で、BoxのWebhook送信元サーバが
自発的にSSL Handshakeを切ってしまっていた。
Reason Codeは 「Unknown CA」。

**原因**
自前で立てたWebサーバ側の設定漏れ。
サーバ証明書は入れていたが、中間証明書を入れ忘れていた。
そのため、BoxのWebhookサーバ側で信頼チェインの検証に失敗していた。

- https://whatsmychaincert.com/
- https://www.ssllabs.com/ssltest/index.html

上記のサイトに自前Webサーバのドメイン名を入れてテストしたところ、
Chainのエラーとなったことで気づくことができました。

**対応**
サーバ証明書に中間証明書を追加。
Nginxなので、証明書ファイル1つの中に順に追記するだけでよいです。

```
-----BEGIN CERTIFICATE-----
サーバSSL証明書
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
SSL中間証明書1
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
SSL中間証明書2
-----END CERTIFICATE-----
```

参考:
https://www.sslbox.jp/support/man/interca_coressl.php
https://tsunokawa.hatenablog.com/entry/2014/09/24/114014

nginxをリスタート後、

- 上記のSSLチェックサイトのテスト結果良好
- Webhook受信時のSSL Handshakeが成功
- Webhookが受け取れた

を確認できました。