CloudWatchLogsのサブスクリプションフィルタをAWSCLIから設定する

# はじめに
[前回の記事](https://qiita.com/Mouflon_127000/items/a55d207b46d0a2730241)でAWS CLIコマンドを利用してCloudWatchAlermを設定する方法を調査・検証した。
せっかくなので、Alermと一緒に大量に設定する必要のあるCloudWatchLogsのロググループと
サブスクリプションフィルタをAWS CLIから設定する方法を調査・検証した。

# 環境
CloudShellからの実行を想定。記事作成時点のCloudShell上でのバージョンは以下の通り。
- AWS CLI:2.15.21
- jq:1.6(設定確認時に利用)

# 準備:SNS連携用Lambda作成
なんでもよいのでサブスクリプションフィルタの送信先となるLambda関数を作っておく。
今回はログ監視を想定し、サブスクリプションフィルタに指定した文字列を検知した場合、
メール整形用Lambdaへ連携、LmabdaからSNSのトピックを呼びメールを送信するLambda関数を用意する。

Lambda関数作成にあたり参考にさせていただいた記事

https://dev.classmethod.jp/articles/cwl-lambda-sns-publish/

下記のコードを利用する場合は環境変数`SNS_TOPIC_ARN`にメール通知先となるSNSのARNを設定すること。
また、LambdaのIAMロールに`sns:Publish`の許可があること。

```py:lambda
import json
import logging
import base64
import gzip
import os
from datetime import datetime, timezone, timedelta
import boto3

# logger初期化
logger = logging.getLogger()
logger.setLevel(logging.INFO)

# タイムスタンプ時刻変換
def convert_time(timestamp_unix):
    # UNIXミリ秒から秒への変換
    timestamp_sec = timestamp_unix / 1000
    dt = datetime.fromtimestamp(timestamp_sec, timezone(timedelta(hours=9)))
    # ISO:8601形式に変換
    timestamp_jst = dt.isoformat()

    return timestamp_jst

# SNSへの連携
def publish_sns(loggroup, logstream, logfiltername, logtimestamp, message):
    sns_client = boto3.client('sns')
    sns_topic_arn = os.environ['SNS_TOPIC_ARN']

    sns_client.publish (
        TopicArn = sns_topic_arn,
        Subject = "【HOGEHOGEシステム】ログ監視アラーム",
        Message =(
            "========================================" + "\n" +
            "ログ グループ名: " + loggroup + "\n" +
            "ログ ストリーム名: " + logstream + "\n" +
            "ログ フィルタ名: " + logfiltername + "\n" +
            "ログ 検知日時(JST): " + logtimestamp + "\n" +
            "ログ 内容: " + message + "\n" +
            "========================================"
        )
    )

def lambda_handler(event, context):
    # CWlogsサブスクリプションフィルタ連携データからJSONデータ取り出し
    logger.info("LOAD Function: " + context.function_name)
    decode_data = base64.b64decode(event['awslogs']['data'])
    json_data = json.loads(gzip.decompress(decode_data))

    # データ取得
    log_group = json_data['logGroup']
    log_stream = json_data['logStream']
    log_filtername = json_data['subscriptionFilters'][0]
    log_timestamp = json_data['logEvents'][0]['timestamp']
    log_message = json_data['logEvents'][0]['message']

    # タイムスタンプ時刻変換
    log_timestamp = convert_time(log_timestamp)

    # SNSへの連携
    publish_sns(log_group, log_stream, log_filtername, log_timestamp, log_message)

    logger.info("END Function: " + context.function_name)
```

# 設定コマンド
`aws logs put-subscription-filter`を利用すればサブスクリプションフィルタを設定できる。

https://awscli.amazonaws.com/v2/documentation/api/latest/reference/logs/put-subscription-filter.html

```bash
aws logs put-subscription-filter \
--log-group-name <ロググループ名> \
--filter-name <フィルタ名> \
--filter-pattern <フィルタパターン> \
--destination-arn <連携先>
```

### 注意点
GUI上からサブスクリプションフィルタを設定する場合はAWS側で自動的にやってくれるため意識しなかったが、
Lambda側のリソースポリシーでCloudWatchLogsからアクセスできるように設定しておかないと以下のエラーが出る。

> An error occurred (InvalidParameterException) when calling the PutSubscriptionFilter operation: Could not execute the lambda function. Make sure you have given CloudWatch Logs permission to execute your function.

以下のようにCloudWatchLogsからのlambda:InvokeFunctionを許可しておくこと。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/561546/e054c56d-7314-9814-b629-906057b73e99.png)


## コマンドサンプル

```bash
aws logs put-subscription-filter \
--log-group-name "messages" \
--filter-name "gp-dev-log-messages" \
--filter-pattern "?\".err>\" ?\".crit>\" ?\".alert>\" ?\".emerg>\"" \
--destination-arn "arn:aws:lambda:ap-northeast-1:xxxxxxxxxx"
```
`--filter-pattern`内でダブルクォーテーション使う場合はエスケープしないとエラーになる点は注意

## 動作確認
EC2から/var/log/messagesをロググループに出力しているため、
今回はloggerコマンドでsyslogにエラーを出力する。
AWS CLIから`aws log put-log-events`でロググループにログを送信しても良い。

設定コマンド実行後GUI確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/561546/3b08a3b2-eebf-493e-a07f-9b143ada7196.png)

メール受信確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/561546/675b7809-3f16-4b70-76d2-1f5f0a41da5b.png)

# サブスクリプションフィルタ設定確認
`aws logs describe-subscription-filters`を利用すればサブスクリプションフィルタ設定を取得できる。

https://awscli.amazonaws.com/v2/documentation/api/latest/reference/logs/describe-subscription-filters.html

オプションとして`--log-group-name`の指定が必須となる。
CloudWatchAlermの時と違い、AWS CLIコマンド単体では全てのロググループと
それに紐づくサブスクリプションフィルタを一括で取得できないのは残念。
一気に取得する場合はロググループ名を読み込んでループさせる
シェルスクリプトを作る必要がありそう。
JSON形式で取得できるので、jqに渡して成形・出力する。

### コマンドサンプル
```bash
aws logs describe-subscription-filters --log-group-name "messages" | jq -c '.subscriptionFilters[] | [.logGroupName, .filterName, .filterPattern, .destinationArn]'
```

### 実行結果サンプル

```bash
["messages","gp-dev-log-messages","?\".err>\" ?\".crit>\" ?\".alert>\" ?\".emerg>\"","arn:aws:lambda:ap-northeast-1:xxxxxxxxxx"]
```

以上。
CLIでの設定より連携先Lambdaの中身を書く方が大変でした。