AWS APIGatewayのアクセスのログ(json)を作成し、Kinesis Firehose(S3)に投げる


iOSアプリでユーザがあるいくつかのページを見たかどうかが知りたくなり、元々使ってみたかったAWS周りのサービスを使ってアプリの起動ログを送るようにしてみた覚書。

※2018/03/01前後の情報です。
※備忘録代わりメモなので雑なところあります。

やりたかったこと

  • iOSアプリで特定ページを見たかどうかのログを取りたい
  • アプリ自体はクライアントだけで動作するものなので、バックエンドはなく、そのためだけに用意もしない
  • 今はログを取得したいだけだが、今後何か処理が入るかもしれない
  • 今後ログの用途が広がる可能性も考えて、S3に置きたい

やったこと

  • APIGatewayでエンドポイントを作成
  • iOSアプリからリクエストを送る
  • APIGatewayからKinesis Firehoseへログを送る

この記事で説明しないこと

  • Kinesis Firehoseの話 (既にdelivery streamがある前提です)
  • iOSアプリ側の実装 (特に今回の話に依存することはないので書きません)

APIGatewayでエンドポイントを作成

エンドポイントを用意する

APIGatewayのトップ APIs から Create API を選択する。

New API を選択し、必要情報を入力する。

API Name: 適当な判別可能な名前
Description: 必要なら説明
Endpoint Type: Regional or Edge Optimized を選ぶ、それぞれの詳細はこちら

情報入力後、画面右下の Create API を選択するとAPIが作成され、トップの APIs の下に今作成したAPIの名前が追加される。

リソースとメソッドの追加

追加されたAPIを選択すると、次のような画面が表示される。

ここから、作成したAPIにresourceとmethodを追加していく。

Actions から Create Resource を選択すと、Resource作成画面へ遷移するので、必要情報を入力する。
今回は単純にiOSアプリからリクエストを叩きたいだけなので、proxy resourceやCORSにはチェックを入れずに進みます。

試しに test というResourceを作成しました。

/test というResourceが追加されたのがわかります。
続いて、ここにMethodを追加します。再び Actions から Create Method を選択すると、次のようにセレクトリストが出現します。

このセレクトリストでMethodを選択することができます。今回はログを残したいだけなのでGETにします。リストからGETを選択して、リストの隣に出るチェックマークを押すと、設定が保存され次のような画面が表示されます。

APIGatewayへのリクエストをLambdaなどで処理したい場合はこのままで結構ですが、今は一旦リクエストを送れるところまで進むため、Mockを選択し、Saveします。

ここで、次のような画面になります。

これで一旦形としては出来ましたが、アプリから叩く場合はそのレスポンスが気になるため、仮でレスポンスを入れておきます。

上記画像でいう右下の、Integration Responseを選択します。
ここで、レスポンスのStatus CodeContent TypeHeaderBodyのテンプレートなどを設定することが出来ます。
今は一旦返ればなんでも良いとして、Body Mapping Templateを下記のようにしました。

テストとデプロイ

Resource > Method (GET) のページに戻ると、左上に TEST の項目があることがわかります。
ここでは、作成したAPIをテストすることができます。今回はMockでの作成であるため落ちることはないかと思いますが、試してみましょう。

遷移後の画面の下の方で TEST をクリック。

こんな感じで、ちゃんと動いていることがわかります。

問題なければ、外から叩くためにAPIをDeployしましょう。

Actions から Deploy API を選択すると、新たにウィンドウが表示されます。
Stageから [New Stage] を選択すると次のような画面になるため、必要情報を入力しましょう。
Stage はバージョンのようなもので、テストの場合は、 Beta などで良いと思います。

Deployすると、自動的にStagesのページに遷移すると思います。
トップに表示されている Invoke URL: がエンドポイントのもととなるURLです。

※実際に使用する場合は、ここままだと誰でもAPIを叩けてしまうため、API Keyを作成する、Authorizerを設定する、など何かしらの制限を設けましょう。

iOSアプリからリクエストを送る

ここはiOSアプリの実装になるので、アプリ実装の説明は割愛します。

先程までの設定で、決まった値を返すエンドポイントを作成することが出来ました。

https://xxxx.execute-api.ap-northeast-1.amazonaws.com/beta/test (stageがbeta, resourceがtestの場合)にリクエストを送り、Mockで設定した値が返ってくるかどうか確認してください。

APIGatewayからKinesis Firehoseへログを送る

Roleの作成

疎通の確認が取れたら、Mockで作っていたAPIをKinesis FirehoseにObjectをPUTするように作り変えます。

がその前に、APIGatewayからKinesis Firehoseを使えるように、roleを作ってあげないといけないため、別で作成しましょう。

詳細は割愛しますが、IAMを開き、

  • Policyから、Kinesis FirehoseにPut RecordPut Record BatchのActionを許可するようなPolicyを作成する
  • 作成したPolicyを持つRoleを作成する
  • 作成したRoleの Trusted entitiesapigateway.amazonaws.com を追加する

で出来ると思います。

とりあえず送れるようにする

roleを作成できたら、先程Mockの選択した Integration Request の設定画面へ行きます。
先程Mockにした部分を、AWS Serviceに変更し、各項目を次のように埋めます。

execution roleには、先程作成したroleのARNを入れてください。

Saveしたあとは、一旦テストしてみましょう。おそらく次のようなExceptionが返ってきます。

{
  "__type": "SerializationException"
}

これは、Kinesis FirehoseにPUT RECORDする際は、次の形式で送らないといけないためです。

{
  "DeliveryStreamName": "[Kinesis Firehose上のdelivery streams名]",
  "Record": {
    "Data": "[データのオブジェクト(Base64エンコード)]"
  }
}

この形式にするために、Integration Requestの下の方にある設定の、Body Mapping Templateを編集します。

まずはAdd mapping template から、Content-Typeに application/json を指定します。

追加した application/json を選択すると、更に下にスクロールできるようになり、Mapping Templateを編集できます。

ここにTemplateを記入していくわけですが、詳しくは下記を参照すると良いです。

API Gateway のマッピングテンプレートリファレンス
https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html

テストの場合は、簡単に次のような形式で送ってみると良いと思います。

#set($data = "{ ""key"": ""property""}")
{
  "DeliveryStreamName": "xxxxx",
  "Record": {
    "Data": "$util.base64Encode($data)"
  }
}

ここまで出来たら動作確認しつつ、欲しい形式にTemplateを変えていきましょう。
動作確認では、まずはAPI GatewayでのTest機能で確認し、エラーが出なければ実際にリクエストを送るもの(今回はアプリ)から確認すると良いでしょう。

設定に問題がなければ、少しの時間の後にKinesis Firehoseの方にデータが送られます。
ここでは解説していませんが、実際はそこからS3などにデータを吐き出していると思いますので、そちらで意図した出力になっているか確認してください。

最終的なMapping Template

今回は、基本的にはログ目的だったので、resource名や時間、あればrequest parameterなどを拾えれば良いということで、最終的に下記のようになりました。(もっとスマートなやり方があるかもしれません...)

#set($allParams = $input.params())
#set($params = $allParams.get('querystring'))
#set($headers = $allParams.get('header'))
#set($json = "{ 
    ""requestId"":""$context.requestId"", 
    ""ip"": ""$context.identity.sourceIp"", 
    ""user"":""$context.identity.user"",
    ""requestTime"":""$context.requestTime"", 
    ""requestTimeEpoch"":""$context.requestTimeEpoch"",
    ""httpMethod"":""$context.httpMethod"",
    ""resourcePath"":""$context.resourcePath"", 
    ""queryParams"": {
    #foreach($paramName in $params.keySet())
    ""$paramName"":""$util.escapeJavaScript($params.get($paramName))""#if($foreach.hasNext),
    #end
    #end
    },
    ""queryStrings"": ""#foreach($paramName in $params.keySet())$paramName=$util.escapeJavaScript($params.get($paramName))#if($foreach.hasNext)&#end#end"",
    ""protocol"":""$context.protocol""
}")
#set($data = $json.replaceAll(" ","").replaceAll("\n","")+"
")
{
  "DeliveryStreamName": "xxxxx",
  "Record": {
    "Data": "$util.base64Encode($data)"
  }
}

各変数やメソッドはリファレンスを参照していただくとして、他の部分を少し説明します。

#set($json = "{ (略

ここで、 $json にログとして残したい各パラメータをjson形式で無理やり作成しています。

#set($data = $json.replaceAll(" ","").replaceAll("\n","")+"
")

ここでは、ログを一行一レコードで保存したいので、先程作成した $json の無駄な余白や改行を削除し、最後に無理やり改行を加えています。
(改行周りが "\n"などで上手くいかなかったので、+のあとに改行の入力を無理やりいれています...。)

その後、最後の実際に送るjsonのブロックで、Kinesis Firehoseの形式に合わせつつ、$dataをbase64エンコードしたものを追加しています。

おまけ

CloudWatch Logsを使わなかった理由

最初はAPI Gateway > Kinesis Firehose > S3 ではなく、API GatewayにはCloudWatch Logsと連携して簡単にアクセスログを取得できることから、そちらを利用する方法を考えていました。

参考記事:
Amazon API Gateway でアクセスログ記録をサポート
【新機能】Amazon API Gateway でアクセスログを記録する #reinvent

ただし今回は、ユーザがendpointを叩いたときのget parameterもログとして取得したいという前提があり、その実現方法が分からなかったため断念しました。
単純にアクセスログを残したいだけであれば、CloudWatch Logsを使うほうが楽だと思います。

以上です。何か間違いあればコメント頂けると幸いです。