CloudWatch LogsのKFHサブスクリプションフィルターを理解する

AWSのサービスの中には、CloudWatch Logs（以下CWL）にしかログを吐き出せないものが結構ある。
これはこれで使いではあるが、S3にログを集約したい、といった場合は、Kinesis Data Firehose（以下KFH）を用いたサブスクリプションフィルターを設定する必要があり、結構面倒くさい（しかも、Kinesis系のサブスクリプションフィルターだけマネジメントコンソールから設定出来なかったりする）。

面倒くさいステップが少しでも腹落ちするよう、**トンネル開通**に例えて、一連の方法をまとめておく。
（手順自体は[公式](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/SubscriptionFilters.html#FirehoseExample)に準ずる）。

## イメージ
**人 --> 入り口 --> ( === トンネル === ) --> 出口**

こういうイメージで、

**ログレコード --> CWLサブスクリプションフィルター --> ( === KFH === ) --> S3**

これを作ります。

## ステップバイステップ
### 1. 入り口と出口を作る
- 何でもいいのでCWLロググループを一つ選ぶ。

これがトンネルの入り口となる。
量を試したければVPC Flow Logsが使いやすい。
ここでは、ABC順で最初に目に付いたGlueのジョブアウトプット（`/aws-glue/jobs/output`）を使うことにする。

- 最終目的地用のS3バケットを作成する。
これがトンネルの出口となる。

```bash
% aws s3api create-bucket --bucket kfhtest2021 --create-bucket-configuration LocationConstraint=ap-northeast-1
{
    "Location": "http://kfhtest2021.s3.amazonaws.com/"
}
```

### 2. 施工許可証を作る
- KFHストリーム用のIAMロールを作成する。

これがトンネルを作るためのいわば施工許可証になる。
マネジメントコンソールだとKFHストリーム作成時に自動作成してくれるが、IAMロール・信頼関係・IAMポリシーの相互関係を改めて整理するのに意外にちょうどよかったので、あえて公式通りCLI方式で書いておく。

まずはKFHがこのロールをAssumeRoleするための**信頼関係**をJSONファイルで用意する。

```json:~/trust-kfh.json
{
  "Statement": {
    "Effect": "Allow",
    "Principal": { "Service": "firehose.amazonaws.com" },
    "Action": "sts:AssumeRole",
    "Condition": { "StringEquals": { "sts:ExternalId":"AWSアカウントID" } }
  }
}
```

次にKFHに何の権限が許可されるかを記述した**IAMポリシー**を同じくJSONファイルで用意する（バケット名はここでは`kfhtest2021`）。余談だが、このIAMポリシーは後続のIAMロール専用の、いわゆるインラインポリシーという位置づけになる。

```json:~/policy-kfh.json
{
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [ 
          "s3:AbortMultipartUpload", 
          "s3:GetBucketLocation", 
          "s3:GetObject", 
          "s3:ListBucket", 
          "s3:ListBucketMultipartUploads", 
          "s3:PutObject" ],
      "Resource": [ 
          "arn:aws:s3:::kfhtest2021", 
          "arn:aws:s3:::kfhtest2021/*" ]
    }
  ]
}
```

**IAMロール**を作る。**信頼関係**はこの時紐付ける。

```bash
% aws iam create-role \
    --role-name FirehosetoS3Role \
    --assume-role-policy-document file://~/trust.json
```
```bash:結果
{
    "Role": {
        "Path": "/",
        "RoleName": "FirehosetoS3Role",
        "RoleId": "AROAQH2ZXUWM2YY5UEUHI",
        "Arn": "arn:aws:iam::AWSアカウントID:role/FirehosetoS3Role",
        "CreateDate": "2021-02-03T12:25:39+00:00",
        "AssumeRolePolicyDocument": {
            "Statement": {
                "Effect": "Allow",
                "Principal": {
                    "Service": "firehose.amazonaws.com"
                },
                "Action": "sts:AssumeRole",
                "Condition": {
                    "StringEquals": {
                        "sts:ExternalId": "AWSアカウントID"
                    }
                }
            }
        }
    }
}
```

作った**IAMロール**に**IAMポリシー**を紐付けて完成。

```bash
% aws iam put-role-policy --role-name FirehosetoS3Role \
    --policy-name Permissions-Policy-For-Firehose \
    --policy-document file://~/policy-kfh.json
```

これで、「何ができるか（IAMポリシー）」を表面に、「誰がこの権限を使うことができるか（信頼関係）」を裏面に書いた、"施工許可証"であるところのIAMロールができた。
この後作るKFHストリームにこの施工許可証を渡してやれば、KFHストリームは表面に書かれた権限でもって、出口であるS3バケットに向かって堂々と掘り抜けることができることになる（許可証の利用許可は裏面に書いてある）。

### 3. トンネルを掘る
- CWLとS3の間を繋ぐ、KFHストリームを作成する。

これが実際にログストリームが通過するトンネルになる。
一点注意点は、KFHの圧縮オプションを無効にすること。CWLはそれ自身がGZIP圧縮するので、二重に圧縮がかかってしまい後で使う時に無駄に悩む羽目になる。まあ、デフォルトは「圧縮なし」なので、以下の通りやれば特に問題はない。

```bash
% aws firehose create-delivery-stream \
    --delivery-stream-name 'my-delivery-stream' \
    --s3-destination-configuration \
    '{"RoleARN": "arn:aws:iam::AWSアカウントID:role/FirehosetoS3Role", "BucketARN": "arn:aws:s3:::kfhtest2021"}'
```
```bash:結果
{
    "DeliveryStreamARN": "arn:aws:firehose:ap-northeast-1:AWSアカウントID:deliverystream/my-delivery-stream"
}
```

ここまででトンネルは完成。出口にも繋がっている。
最後に、"トンネル利用許可証"を用意した上で、入口を開通させる作業が残っている。

### 4. トンネル利用許可証を作る
- CWLがKFHを使うためのIAMロールを作成する。

先程作った建築許可証はKFHストリームがS3という"出口"に掘り抜けるためのものだが、今回作るのは入り口であるCWLロググループがKFHストリームという"トンネル"を使っていいよという、いわばトンネル利用許可証である。

やることはほぼ同じ。
まずは**信頼関係**。

```json:trust-cwl.json
{
  "Statement": {
    "Effect": "Allow",
    "Principal": { "Service": "logs.ap-northeast-1.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }
}
```

次いで**IAMポリシー**。

```json:policy-cwl.json
{
    "Statement":[
      {
        "Effect":"Allow",
        "Action":["firehose:*"],
        "Resource":["arn:aws:firehose:ap-northeast-1:AWSアカウントID:*"]
      }
    ]
}
```

**IAMロール**作成。

```bash
% aws iam create-role \
    --role-name CWLtoKinesisFirehoseRole \
    --assume-role-policy-document file://~/trust-cwl.json
```
```bash:結果
{
    "Role": {
        "Path": "/",
        "RoleName": "CWLtoKinesisFirehoseRole",
        "RoleId": "AROAQH2ZXUWM6SCQSYNSV",
        "Arn": "arn:aws:iam::AWSアカウントID:role/CWLtoKinesisFirehoseRole",
        "CreateDate": "2021-02-03T13:29:15+00:00",
        "AssumeRolePolicyDocument": {
            "Statement": {
                "Effect": "Allow",
                "Principal": {
                    "Service": "logs.ap-northeast-1.amazonaws.com"
                },
                "Action": "sts:AssumeRole"
            }
        }
    }
}
```

最後に**IAMロール**と**IAMポリシー**を紐付けて完成。

```bash
% aws iam put-role-policy --role-name CWLtoKinesisFirehoseRole \
    --policy-name Permissions-Policy-For-CWL \
    --policy-document file://~/policy-cwl.json
```

利用許可証ができ、CWLがトンネルであるところのKFHストリームに接続する準備ができた。

### 5. 入り口とトンネルを繋ぐ
- 準備したKFHストリームを指定して、CWLロググループのサブスクリプションフィルターを作成する。

これで、入り口 -> トンネル -> 出口が繋がり、ログがS3バケットに出力され始める。 

まず、先程作ったトンネル（KFHストリーム）の識別名であるところのARNを取得する。

```bash
% aws firehose describe-delivery-stream --delivery-stream-name "my-delivery-stream" --query 'DeliveryStreamDescription.DeliveryStreamARN'
```
```bash:結果
"arn:aws:firehose:ap-northeast-1:AWSアカウントID:deliverystream/my-delivery-stream"
```

トンネル利用許可証（IAMロール）の識別名であるARNも取得。

```bash
% aws iam get-role --role-name CWLtoKinesisFirehoseRole --query 'Role.Arn'
```
```bash:結果
"arn:aws:iam::AWSアカウントID:role/CWLtoKinesisFirehoseRole"
```

最後に、これらの情報をかき集めてサブスクリプションフィルターを作る。
フィルターパターンはログによりまちまちだが、ここでは全件出力するので""で指定。

```bash
% aws logs put-subscription-filter \
    --log-group-name "/aws-glue/jobs/output" \
    --filter-name "Destination" \
    --filter-pattern "" \
    --destination-arn "arn:aws:firehose:ap-northeast-1:AWSアカウントID:deliverystream/my-delivery-stream" \
    --role-arn "arn:aws:iam::AWSアカウントID:role/CWLtoKinesisFirehoseRole"
```

ようやく、目指すサブスクリプションフィルターが作成された。

![スクリーンショット 2021-02-03 午後10.59.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/609876/496ad85a-8219-a98d-397c-625bb490e2ec.png)

## 6. 動確
- あとはログを出力させ、S3に吐き出されることを確認するだけ。

今回はGlueジョブのOutputログなので、Output出力の設定をしたジョブを適当に走らせてみる。

```bash
% aws glue start-job-run --job-name job_orcl2s3
```

待つことしばし。
KFHストリーム経由でS3バケットにログ出力されたことが分かる。
![スクリーンショット 2021-02-03 午後11.25.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/609876/e3bda130-ed2a-1b8a-e26a-455dbfa1017e.png)


せっかくなので、S3 Selectでクエリーしてみる。
CWLのログはJSONフォーマット、GZIP圧縮なので、以下の通り指定する。

| 大項目 | 小項目 | 値 |
|:-----------|:------------|:------------|
|入力設定|形式|JSON|
||圧縮|GZIP|﻿
|出力設定|形式|JSON|﻿

![スクリーンショット 2021-02-03 午後11.37.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/609876/21e6b323-958b-d0b4-5a83-a1e2dcd18416.png)

...複数行が結合されているように見えるが、いったんそれは措くとして、とりあえず"トンネル"は無事開通したようだ。

## 余談
2020年のアップデートで、これまで1つだった、ロググループあたりのサブスクリプションフィルター数が「2」に増えた。
KFH経由でS3に送りつつ、Elasticsearch Serviceにも送る、みたいなことが、fluentdのようなログルーターを介さずともCWL単体でできるようになったことになる。