Amazon DynamoDBにEvent Webhookのイベントデータを保存する方法

サポートエンジニアの佐藤(@awwa500)です。本ブログではこれまで、Event Webhookのイベントデータを保存する方法をいくつかご紹介してきました。今回は、AWSを使ったイベントデータの保存方法の一つとして、Amazon DynamoDBを利用する方法をご紹介します。

構成

以下の構成でイベントデータをDynamoDBに保存します。

イベントデータをDynamoDBに保存

各サービスが担う役割とポイントとなる設定について以下にまとめます。設定手順の都合上、図の右端から順に解説していきます。

  • DynamoDB
    イベントデータの保存先となるデータベースです。プライマリキーを「sg_event_id(文字列)」とする以外、設定は任意です。sg_event_id以外の項目はデータ保存時に自動的に作成されます。他に、検索条件に応じてセカンダリインデックスを設定しておくことをお勧めします。
     
    イベントデータの保存先
  • AWS Lambda
    API Gatewayで受けたイベントデータをDynamoDBに保存するために、AWS Lambdaを利用します。イベントデータはJSON配列で送信されますが、そのままではDynamoDBに保存できないため、データ変換後DynamoDBに対する保存処理(putItem)を呼び出します。今回は、Node.js 6.10ランタイムを前提とし、「dynamodb-doc」というパッケージを利用してデータの変換処理を行っています。Lambda関数のコードは以下のとおりです。
    const doc = require('dynamodb-doc');
    const dynamoDb = new doc.DynamoDB();
    
    function putItem(element, index, array) {
        const params = {
            TableName: 'sendgrid_events',   // DynamoDBのテーブル名
            Item: element
        };
        return new Promise((resolve, reject) => {
           dynamoDb.putItem(params, (err, data) => {
               if (err) {
                   reject(err);
               } else {
                   resolve('Success to putItem to the DynamoDB');
               }
           });
        });
    }
    
    exports.handler = (event, context, callback) => {
        // 保存処理を繰り返し呼び出す
        var promises = [];
        for (let item of event) {
            promises.push(putItem(item));
        }
        Promise.all(promises).then(results => {
            callback(null, '');
        }).catch(errors => {
            // 1件でもエラーが発生したら、API Gatewayにステータスコード500を返す
            console.log(errors);
            callback(JSON.stringify({status: 500, message: 'Internal Server Error'}));
        })
    };
    

    このコードは、配列に格納されているイベントデータをDynamoDBに保存して、すべての保存処理が成功したら正常応答を返します。一方、1件でもエラーが発生したらステータスコード500を返します。
     
    作成したLambda関数には、データの保存先となるDynamoDBテーブルに対するputItem権限を与えるのを忘れないでください。

  • API Gateway
    Event WebhookのHTTPリクエストはAPI Gatewayで受けます。適当な名前でリソースを作成後、POSTメソッドを作成してください。
     
    OSTメソッドを作成
    次に、統合リクエスト設定で、統合タイプを「Lambda関数」に変更し、さきほど作成したLambda関数のLambdaリージョンLambda関数名を設定します。マッピングテンプレート含めその他の設定は不要です。
     
    Lambda関数名を設定
    次に、メソッドレスポンス設定で、HTTPステータスに「500」を追加します。レスポンスヘッダーやレスポンス本文の設定は不要です。
     
    HTTPステータスに「500」を追加
    最後に、統合レスポンス設定で、Lambdaのエラーとメソッドレスポンスのステータスをマッピングします。
     
    Lambdaエラーの正規表現は次のように設定します。この設定は、Lambda関数が出力するエラーに含まれる文字列とメソッドレスポンスのステータスコードをマッピングするための条件です。

    .*"status" *: *500.*
    

    メソッドレスポンスのステータスコードは「500」を選択します。
    マッピングテンプレートContent-Typeに「application/json」を追加し、以下のテンプレートを設定します。

    #set($errorObj = $util.parseJson($input.path('$.errorMessage')))
    {
        "error" : {
            "message" : "$errorObj.message"
        }
    }
    

    まとめると、設定画面は次のようなイメージになります。
     
    まとめ
    以上でAPI Gatewayの設定は完了です。最後にAPIをデプロイし、エンドポイントのURLを確認します。
     
    本記事では詳細は割愛しますが、API Gatewayに対する不正アクセスを防止する場合、API GatewayにBasic認証を設定します。具体的には、API Gatewayのメソッドリクエスト設定で、カスタムオーソライザーの設定を行い、認証用のLambda関数を呼び出すようにします。Basic認証を利用するのは、Event Webhookで対応している認証方式がBasic認証のみのためです。

  • Event Notification(SendGrid)
    Event WebhookはSendGridダッシュボードの「Settings > Mail Settings > Event Notification」で設定します。HTTP POST URLにさきほどデプロイしたAPI GatewayのURLを設定します。SELECT ACTIONSで受け取りたいイベントのチェックボックスをONにして設定を保存します。
     
    受け取りたいイベントのチェックボックスをONにして設定
    Event Notification設定画面で「Test Your Integration」ボタンを選択して、DynamoDBにテストデータが保存されることが確認できたら設定は完了です。
     
    DynamoDBでテストデータの保存を確認

エラーハンドリング

Event Webhookは受信側が2xx以外のステータスコードを返した場合、24時間再送を試みます。今回ご紹介した実装は、例えば、Lambdaの同時実行数制限を超えた場合など、受信側のどこで問題が発生しても、2xx以外のステータスコードを返すようになっています。こうすることで、イベントデータの喪失を防ぐことができます。

コスト

では、AWSのコストはどれくらいかかるのでしょうか?月間10万通送信した場合と、月間1000万通送信した場合に想定される金額をそれぞれ計算してみます。計算の前提となる条件は以下のとおりです。

  • AWS東京リージョンを利用
  • メール1通あたり3件のイベントが発生
  • Webhookの1リクエストに含まれるイベント数は10件
  • データサイズは1イベントあたり1KB

AWSの各サービスの想定利用料金(月額)は以下のとおりです。

API Gateway

https://aws.amazon.com/jp/api-gateway/pricing/
1回のリクエストに対するレスポンスのサイズ 0.3KB(今回の構成における実測値)

月間10万通送信 月間1000万通送信
APIコール数による費用 0.128 USD(3万回) 12.75 USD(300万回)
データ転送量による費用 1.26 USD(9GB) 126 USD(900GB)
1.388 USD 138.75 USD

AWS Lambda

https://aws.amazon.com/jp/lambda/pricing/
1回の実行で使用する想定メモリ量 128MB(今回の構成における実測値)
1回の実行で想定される所要時間  2400ミリ秒(今回の構成における実測値)

月間10万通送信 月間1000万通送信
リクエスト数による費用
※無料枠100万回
0.0 USD(3万回) 0.4 USD(300万回)
所要時間による費用
※無料枠40万GB-秒
0.0 USD(9,000 GB-秒) 8.335 USD(900,000 GB-秒)
0.0 USD 8.735 USD

DynamoDB

https://aws.amazon.com/jp/dynamodb/pricing/

月間10万通送信 月間1000万通送信
データストレージによる費用
※無料枠25GB
0.00 USD(0.3GB) 1.425 USD(30GB)
書き込みキャパシティーユニットによる費用
※無料枠25ユニット
0.0 USD(25ユニット) 0.0 USD(25ユニット)
読み込みキャパシティーユニットによる費用
※無料枠25ユニット
0.0 USD(25ユニット) 0.0 USD(25ユニット)
0.00 USD 1.425 USD

以上の計算結果から、AWS LambdaやDynamoDBと比較して、API Gatewayの利用料金が比較的大きくなると考えられます。料金の大部分は、データ転送量(インターネットへのデータ転送)による費用が占めています。この部分は実体としては、API Gatewayのレスポンスデータですが、ボディ部を0KBにしてもヘッダ部は一定のサイズが確保されてしまうようです。

大量メール送信時のイベント処理性能

つぎに、今回の構成の処理性能を見ていきます。AWSの各サービスはデフォルトの制限が定められており、影響を受けると考えられる制限は次のとおりです。

  • API Gateway:10,000リクエスト/秒
  • AWS Lambda:同時実行数1,000
  • DynamoDB:書き込みキャパシティーユニット数 10,000

これらの制限値は上限緩和申請をすることで引き上げも可能なようですが、上限緩和申請をせずにデフォルトの制限値で利用した場合に、1時間あたりどの程度のリクエストを処理できるか計算してみます。計算条件はコスト計算で使用したものと同じです。

これらの制限のうち、最も厳しいのはAWS Lambdaの同時実行数です。そこで、Lambdaが1時間あたりに処理できるリクエスト数から、そのリクエストを発生させるメールの通数を逆算してみました。計算結果は以下のとおりです。

計算項目 結果
1回のLambda関数(10イベントを処理)実行にかかる時間 2400ms
同時実行数1でLambda関数をシーケンシャル実行した場合、1時間あたりの実行可能回数 1500回
同時実行数1000で1時間あたりに実行可能な回数 150万回
Lambda関数150万回の実行で処理できるイベント数 1500万イベント
1500万イベントを発生させるメールの通数 500万通

あくまでも概算ではありますが、1時間あたり500万通のメール送信により発生するイベントを処理できそうなことがわかります。SendGridには1時間あたりに送信可能な通数に制限はないため、メールの送信通数に見合ったイベント処理性能を用意しておくようにしてください。

まとめ

いかがでしたでしょうか?AWSを利用することで、簡単な設定や実装で大量のイベントデータを安全かつ安価に保存できることがご理解いただけたかと思います。元々、DynamoDBは非常に信頼性の高いサービスですが、DynamoDB含めその他のサービスで予期しない問題が発生してもデータを失いにくい設計にしてみました。イベントデータ保存の例として参考にしていただけますと幸いです。