BETA

[テスト投稿] AWS LambdaでBoxのWebhookを処理する

投稿日:2020-01-11
最終更新:2020-01-11

概要

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

例として、登録用の値が

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

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

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

修正前のCloudwatchログ

そこで、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ログ

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が受け取れた

を確認できました。

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

@B8joK0B5mCsmwqT5の技術ブログ

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう