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` を選択する。

![API_Gateway.png](https://qiita-image-store.s3.amazonaws.com/0/49409/469274f2-47d0-9454-e51a-376e9a0e26e6.png)

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

`API Name`: 適当な判別可能な名前
`Description`: 必要なら説明
`Endpoint Type`: Regional or Edge Optimized を選ぶ、それぞれの[詳細はこちら](https://aws.amazon.com/jp/about-aws/whats-new/2017/11/amazon-api-gateway-supports-regional-api-endpoints/)

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

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

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

![スクリーンショット 2018-03-11 14.08.27.png](https://qiita-image-store.s3.amazonaws.com/0/49409/53ce9264-4518-46de-308d-ccd62a3eff5f.png)

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

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

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

![スクリーンショット 2018-03-11 14.15.00.png](https://qiita-image-store.s3.amazonaws.com/0/49409/82263e02-a913-efb7-0558-033d4c835400.png)

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

![スクリーンショット 2018-03-11 14.14.49.png](https://qiita-image-store.s3.amazonaws.com/0/49409/6c8c5cf8-9fa7-dc0e-7a31-785fded47c3a.png)

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

![スクリーンショット 2018-03-11 14.18.46.png](https://qiita-image-store.s3.amazonaws.com/0/49409/444b63fe-051d-2e94-6d42-704b8d1d869b.png)

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

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

![スクリーンショット 2018-03-11 14.21.09.png](https://qiita-image-store.s3.amazonaws.com/0/49409/6498a94c-95dc-5680-dc99-2271836c1103.png)

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

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

![スクリーンショット 2018-03-11 14.26.09.png](https://qiita-image-store.s3.amazonaws.com/0/49409/bc0968b0-f841-2e2b-b28f-6b4d62b863ff.png)

## テストとデプロイ

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

![API_Gateway.png](https://qiita-image-store.s3.amazonaws.com/0/49409/7246d9a4-731e-d7d7-f08d-ef122b6081ea.png)

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

![API_Gateway.png](https://qiita-image-store.s3.amazonaws.com/0/49409/3e96c3dd-e2c5-6aa3-da7f-c8311c3cd57c.png)

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

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

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

![API_Gateway.png](https://qiita-image-store.s3.amazonaws.com/0/49409/24df1c09-4e20-2434-a68d-b9e70d89eff8.png)

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

![API_Gateway.png](https://qiita-image-store.s3.amazonaws.com/0/49409/43d72aa8-06f5-cdd5-e95a-b57d68e96719.png)

※実際に使用する場合は、ここままだと誰でも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 Record` と `Put Record Batch`のActionを許可するようなPolicyを作成する
- 作成したPolicyを持つRoleを作成する
- 作成したRoleの `Trusted entities` に `apigateway.amazonaws.com` を追加する

で出来ると思います。

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

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

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

![API_Gateway.png](https://qiita-image-store.s3.amazonaws.com/0/49409/b6a274f5-0c45-a5e4-74d3-48c9eaec1ac0.png)

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

```json
{
  "__type": "SerializationException"
}
```

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

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

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

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

![API_Gateway.png](https://qiita-image-store.s3.amazonaws.com/0/49409/d1736a79-c22c-76ba-dc06-478be8906e94.png)

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

![API_Gateway.png](https://qiita-image-store.s3.amazonaws.com/0/49409/4abbb452-210f-7a38-5193-4512d3a488a0.png)

ここに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 でアクセスログ記録をサポート](https://aws.amazon.com/jp/about-aws/whats-new/2017/11/amazon-api-gateway-supports-access-logging/)
[【新機能】Amazon API Gateway でアクセスログを記録する #reinvent](https://dev.classmethod.jp/server-side/serverless/aws-reinvent2017-api-gateway-access-log/)

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

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