AWS LambdaでBoxのWebhookを処理する

概要

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

参考

Lambda 実行ロールの作成

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

IAM > ロールの作成

lambda_box_webhook_01.png

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

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

lambda_box_webhook_03.png ※Lambda関数を実行するだけなら、選択するポリシーは 「AWSLambdaBasicExecutionRole」だけとなりますが、その他に - S3へのアクセス - RDBへのアクセス

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

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

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

lambda_box_webhook_04.png

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

lambda_box_webhook_05.png

Lambda関数の作成

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

lambda_box_webhook_06.png

lambda_box_webhook_07.png

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

基本的な情報

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

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

lambda_box_webhook_08.png

API Gatewayトリガー

API 新規APIの作成

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

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

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

lambda_box_webhook_09.png

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

lambda_box_webhook_10.png

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

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

lambda_box_webhook_11.png

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

lambda_box_webhook_12.png


WebHookの作成

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

lambda_box_webhook_13.png

lambda_box_webhook_14.png

lambda_box_webhook_15.png

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

lambda_box_webhook_16.png

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です。

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": ["トリガーのイベント"]}'

例として、登録用の値が

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

$ 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

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

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

lambda_box_webhook_21.png

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

lambda_box_webhook_22.png

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

lambda_box_webhook_23.png lambda_box_webhook_24.png lambda_box_webhook_25.png

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

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

{
"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コードでは、受け取ったPOSTの中身を未加工のまま出力しているので、 以下の問題があり非常に読みづらいです。

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

修正前のCloudwatchログ lambda_box_webhook_28.png

そこで、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バイト文字

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

修正後のCloudwatchログ lambda_box_webhook_29.png

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ログ

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

(切り分け)アプリで生成したトークンで再度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が受け取れた

を確認できました。