概要
BoxのフォルダにWebHookを設定し、AWS Lambdaに送信されたデータを取得/操作するまでの基本的な手順についてまとめています。
(2019年3月時点)
参考
Lambda 実行ロールの作成
Lambda関数の実行時には、「AWSLambdaBasicExecutionRole」が付与されたIAM Roleが必要です。
以下の手順でIAM Roleを作成しておき、以後Lambda関数を作成する際に
Roleを割り当てることとします。
IAM > ロールの作成
「AWSサービス」を選択し、次に「Lambda」を選択後、
「次のステップ:アクセス権限」をクリックします。
「ポリシーのフィルタ」欄に「AWSLambda」と入力し、候補表示の中から
「AWSLambdaBasicExecutionRole」にチェックして「次のステップ:タグ」をクリックします。
※Lambda関数を実行するだけなら、選択するポリシーは
「AWSLambdaBasicExecutionRole」だけとなりますが、その他に
- S3へのアクセス
- RDBへのアクセス
などもLambda関数から実行する場合には、この画面で適宜追加のポリシーも選択しておく必要があります。
タグの追加(オプション)画面で、このIAM Roleに割り当てるタグを指定できます。
たとえば、このRoleを所有しているユーザ名や、プロジェクト名、管理部署名など必要に応じ入力します。
今回は空欄のまま「次のステップ:確認」をクリックして進めます。
「ロール名」にロールの名称を指定し(本例では"Role-LambdaBasicExec")、
「ロールの作成」をクリックします。
ロールの一覧画面にて、今回作成したロールが表示されていることを確認します。
Lambda関数の作成
「関数の作成」画面から、「設計図の使用」
キーワードに「microservice-http」と入力し、候補から
「microservice-http-endpoint-python3」を選択します。
関数の作成に必要な情報を入力します。
基本的な情報
関数名
本例では"boxWebHookTest"としました。
実行ロール
「既存のロールを使用する」を選択し、プルダウンから前掲の手順で作成したIAM Roleを選択します。
(本例では「Role-LambdaBasicExec」)
API Gatewayトリガー
API
新規APIの作成
セキュリティ
今回は認証なしでAPI Gatewayを呼び出せるように、「オープン」を選択します。
API名
APIの識別名を指定します。
デフォルトで
関数名-API
の命名規則で生成されますので、今回はそのまま
boxWebHookTest-API
としておきます。
デプロイされるステージ
APIのデプロイ先を「ステージ」指定により切り替えることが可能ですが、defaultのままにしておきます。
関数のコード
デフォルトのPythonコードが表示されていますが、
そのままにしておきます。
「関数の作成」を実行します。
Lambda関数が作成された旨のメッセージが表示され、関数とAPI Gatewayの設定画面が表示されます。
Designer画面で「API Gateway」のパネルを選択すると、下の画面にAPI EndpointのURLが表示されます。
このURLは後でWebHookの飛ばし先として使いますので、メモ帳に貼り付けておきます
WebHookの作成
開発者コンソールにて、新規アプリケーションを作成します。
- カスタムアプリ
- 標準OAuth2.0(ユーザ認証)
アプリケーションスコープで「Webhookを管理」にチェックを入れて「変更を保存」します。
※このアプリのトークンを使って、Webhookを生成しますので、Webhook管理権限が必要です。
Webhookを登録する際に使用するトークンを生成します。
OAuthのサンプルコードをいずれか実行し、
アクセストークンを取得しておきます。
このとき生成したアクセストークンは1時間だけ有効です。
後続のWebhook作成を1時間以内に完了できなかった場合は、
再度、アクセストークンを取り直して下さい。
※DeveloperトークンでWebhookを作成すると、約1日経過後に当該のWebhookが正しく機能しなくなる事象が出ましたため、
OAuth 3legged認証を経て入手したアクセストークンを使う必要があります。
(事象については本記事下部の「翌日以降のWebhookのBody内容が不正(Anonymous User扱い)となる」参照)
テスト用のフォルダをBox上に作成します。
そのフォルダをBoxのWebUIで開き、フォルダIDをメモ帳に貼り付けておきます。
https://boxpocsite.app.box.com/folder/xxxxxxxxxxxxxxx
URLの末尾、/folder/の後に続く数値部分がフォルダIDです。
このフォルダにファイルがアップロードされたタイミングで実行される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コードを作成します。
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関数の設定画面から「モニタリング」を選択します。
続いて「CloudWatchのログを表示」をクリックします。
ログストリームが生成されているので、リンクをクリックして開きます。
ログを上から見ていくと、Header情報などの管理情報に続いて、POSTのbody部分を確認できます。
この"body"部分にWebhookの本体が格納されています。
"body": "{\"type\":\"webhook_event\",\"id\":\"eb92204d-dcc6-4(省略)
WebhookのBody内容
POSTされたWebhookのデータ部分は、以下のJSON形式となっています。
参考:
https://developer.box.com/reference#webhooks-v2
{
"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変換されている
そこで、Lambda上のログを読みやすく出力するようにコードを少し改良します。
- 生のJsonデータをパースして、Unicodeから変換し、インデント出力する関数「print_json」を定義
- 関数「print_json」にWebhookのBody部を渡す
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バイト文字
が解決されて、読みやすくなったことが分かります。
Webhookから任意の値を取得する
実際にWebhookをトリガーとするアプリケーションを作る際には、
WebhookのBody部分から目当ての値を取りだして、Pythonの変数に格納して
処理をする必要があります。
そこで、Body部分から値を取得して変数として取り扱うためのコードを追加します。
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ログ
あとは、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を返していることも確認できました。
(切り分け)アプリで生成したトークンで再度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でWebhookを受け取る仕組みを作ろうとして、
無事失敗しました。
発生事象
Webhbookは問題無くトリガーされたが、Webサーバ側のアクセスログには何も記録されない。
TCPDumpを取ったところ、SSL Handshakeの過程で、BoxのWebhook送信元サーバが
自発的にSSL Handshakeを切ってしまっていた。
Reason Codeは 「Unknown CA」。
原因
自前で立てたWebサーバ側の設定漏れ。
サーバ証明書は入れていたが、中間証明書を入れ忘れていた。
そのため、BoxのWebhookサーバ側で信頼チェインの検証に失敗していた。
上記のサイトに自前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が受け取れた
を確認できました。