LoginSignup
5
7

More than 5 years have passed since last update.

AWS LambdaでBoxのWebhookを処理する

Last updated at Posted at 2019-05-23

概要

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

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

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

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

修正前のCloudwatchログ
lambda_box_webhook_28.png

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

  • 生のJsonデータをパースして、Unicodeから変換し、インデント出力する関数「print_json」を定義
  • 関数「print_json」にWebhookのBody部を渡す
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

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

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

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

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

あとは、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が受け取れた

を確認できました。

5
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
7