CloudWatch Logs のサブスクリプションフィルタを使って特定文字列を検知したログをEメール通知する

# はじめに
**CloudWatch Logs** へ保存したログを、サブスクリプションフィルタを使って特定の文字列を検知し、その内容を **SNS** を使ってEメール通知するということを行いました。
また、今回の設定は全て **aws-cli** を利用して行っています。(確認として管理コンソールの画像も載せてます)


# 構成
まず、クライアントから **CloudWatch Logs** へログを送ります。
すると **サブスクリプションフィルタ** の検知をトリガーに **Lambda** が実行されます。
実行された **Lambda** が **SNS** を通してクライアントへ **Eメール** を送信します。
![Untitled.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82090/fdf0b468-568a-d662-9309-7c3130bca5aa.png)


# 設定
今回は **aws-cli** を利用して作成します。設定は構成図の番号の流れで行っていきます。

## ① SNSの設定
検知した際の通知先設定を行います。
まず、トピックの作成を行います。

```bash:トピックの作成
$ aws sns create-topic --name subscription-topic
{
    "TopicArn": "arn:aws:sns:ap-northeast-1:123456789012:subscription-topic"
}
```

作成したら次は通知先となるサブスクリプションの登録です。

```bash:サブスクリプションの登録
$ aws sns subscribe --topic-arn arn:aws:sns:ap-northeast-1:123456789012:subscription-topic --protocol email --notification-endpoint test@hogehoge.jp
{
    "SubscriptionArn": "pending confirmation"
}
```

登録先がEmailの場合、通知先となるメールアドレスに以下のような確認メールが届きます。
![スクリーンショット 2021-01-28 11.43.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82090/4b00b928-2789-27ec-117d-84719f28c953.png)
このままだとステータスが **「PendingConfirmation」** となっていて送れる状態になっていません。

```bash:サブスクリプションステータスの確認
$ aws sns list-subscriptions-by-topic --topic-arn arn:aws:sns:ap-northeast-1:123456789012:subscription-topic 
{
    "Subscriptions": [
        {
            "SubscriptionArn": "PendingConfirmation",
            "Owner": "123456789012",
            "Protocol": "email",
            "Endpoint": "test@hogehoge.jp",
            "TopicArn": "arn:aws:sns:ap-northeast-1:123456789012:subscription-topic"
        }
    ]
}
```
![スクリーンショット 2021-01-28 11.43.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82090/e65116b7-a3aa-36be-92a4-6395dac3a361.png)


届いたメールの **「Confirm subscription」** のリンクをクリックして承認します。
![スクリーンショット 2021-01-28 11.43.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82090/49146771-c03a-2423-f603-e57d5fd1b9ae.png)
すると、先ほど **「PendingConfirmation」** となっていた **SubscriptionArn** がサブスクリプションのARNに置き換わっているのが確認できます。

```bash:サブスクリプションステータスの確認
$ aws sns list-subscriptions-by-topic --topic-arn arn:aws:sns:ap-northeast-1:123456789012:subscription-topic 
{
    "Subscriptions": [
        {
            "SubscriptionArn": "arn:aws:sns:ap-northeast-1:123456789012:subscription-topic:62a143c8-2b3f-4a43-9fc2-7486f7eaf862",
            "Owner": "123456789012",
            "Protocol": "email",
            "Endpoint": "test@hogehoge.jp",
            "TopicArn": "arn:aws:sns:ap-northeast-1:123456789012:subscription-topic"
        }
    ]
}
```
![スクリーンショット 2021-01-28 11.43.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82090/c3ffa83c-88fa-44be-f191-eda9437c37e9.png)

これで送信可能な状態となりました。


## ② IAM の設定
ここでは Lambda が SNS を利用するためのIAMロールを作成します。
まず、Lambdaがログを出力するためのポリシーをjsonファイルに記載します。

```bash:ポリシーの作成
$ vim example-policy.json
```

```text:example-policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        }
    ]
}
```

定義したjsonファイルを利用してポリシー作成を行います。

```bash:ポリシーの作成
$ aws iam create-policy --policy-name subscription-policy --policy-document file://example-policy.json
{
    "Policy": {
        "PolicyName": "subscription-policy",
        "PolicyId": "ANPA2ZM2YJELWMNIB6FR5",
        "Arn": "arn:aws:iam::123456789012:policy/subscription-policy",
        "Path": "/",
        "DefaultVersionId": "v1",
        "AttachmentCount": 0,
        "PermissionsBoundaryUsageCount": 0,
        "IsAttachable": true,
        "CreateDate": "2021-01-27T13:04:30+00:00",
        "UpdateDate": "2021-01-27T13:04:30+00:00"
    }
}
```

次は、作成するロールが Lambda で利用できるように信頼ポリシーを作成します。

```bash:信頼ポリシーの作成
$ vim example-role-trust-policy.json
```

jsonファイルに以下の定義をします。

```json:example-role-trust-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
```

それではロールの作成を行います。

```bash:ロールの作成
$ aws iam create-role --role-name subscription-role --assume-role-policy-document file://example-role-trust-policy.json
{
    "Role": {
        "Path": "/",
        "RoleName": "subscription-role",
        "RoleId": "AROA2ZM2YJEL2EULM52BI",
        "Arn": "arn:aws:iam::123456789012:role/subscription-role",
        "CreateDate": "2021-01-27T13:05:27+00:00",
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": {
                        "Service": "lambda.amazonaws.com"
                    },
                    "Action": "sts:AssumeRole"
                }
            ]
        }
    }
}
```
以下のようにロールが作成されます。
![スクリーンショット 2021-01-27 22.31.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82090/1bd637f7-bf3c-51b8-2415-1bcb2cb98d87.png)
![スクリーンショット 2021-01-27 22.14.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82090/20c6825e-f5d6-f8e7-f719-9ca906c2718f.png)
次に先ほど作成したポリシーをロールヘアタッチします。

```bash:ロールへのアタッチ
$ aws iam attach-role-policy --role-name subscription-role --policy-arn "arn:aws:iam::123456789012:policy/subscription-policy"
```

次にSNSへのアクセス権限を付与します。今回は既存のAWS管理ポリシーを利用します。

```bash:ロールへのアタッチ
% aws iam attach-role-policy --role-name subscription-role --policy-arn "arn:aws:iam::aws:policy/AmazonSNSFullAccess" 
```
以下の2つのポリシーがアタッチされていることが確認できます。
![スクリーンショット 2021-01-27 22.27.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82090/b7a42039-c8a9-8fe6-a2fc-49f59c4f7d46.png)


## ③ Lambda 関数の作成
サブスクリプションフィルタで利用するLambdaの作成を行います。

```bash:ソースコードの作成
$ vim subscription.py
```

以下のコードを用意します。

```python:subscription.py
import base64
import json
import zlib
import datetime
import os
import boto3
from botocore.exceptions import ClientError

print('Loading function')

def lambda_handler(event, context):
    data = zlib.decompress(base64.b64decode(event['awslogs']['data']), 16+zlib.MAX_WBITS)
    data_json = json.loads(data)
    log_json = json.loads(json.dumps(data_json["logEvents"][0], ensure_ascii=False))

    print(log_json)

    try:
        sns = boto3.client('sns')

        #SNS Publish
        publishResponse = sns.publish(
            TopicArn = os.environ['SNS_TOPIC_ARN'],
            Message = log_json['message'],
            Subject = os.environ['ALARM_SUBJECT']
        )

    except Exception as e:
        print(e)
```
zipファイル化します。

```bash:zipファイル化
$ zip subscription.zip subscription.py
```

それではLambda関数を作成します。

```bash:Lambda関数の作成
$ aws lambda create-function --function-name subtest --role arn:aws:iam::123456789012:role/subscription-role --runtime python3.7 --handler subscription.lambda_handler --zip-file fileb://subscription.zip
{
    "FunctionName": "subtest",
    "FunctionArn": "arn:aws:lambda:ap-northeast-1:123456789012:function:subtest",
    "Runtime": "python3.7",
    "Role": "arn:aws:iam::123456789012:role/subscription-role",
    "Handler": "subscription.lambda_handler",
    "CodeSize": 581,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2021-01-29T13:47:14.979+0000",
    "CodeSha256": "tSwPi+21KmRkigZ7LupkcTlpla4eVmnIMR9FwI5TMAI=",
    "Version": "$LATEST",
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "fb42511e-0e72-458b-b725-22ebbf32c375",
    "State": "Active",
    "LastUpdateStatus": "Successful"
}
```

作成したら環境変数の設定を行います。

```bash:環境変数の設定
$ aws lambda update-function-configuration --function-name subtest --environment Variables='{SNS_TOPIC_ARN="arn:aws:sns:ap-northeast-1:123456789012:subscription-topic",ALARM_SUBJECT="TEST"}'
{
    "FunctionName": "subtest",
    "FunctionArn": "arn:aws:lambda:ap-northeast-1:123456789012:function:subtest",
    "Runtime": "python3.7",
    "Role": "arn:aws:iam::123456789012:role/subscription-role",
    "Handler": "subscription.lambda_handler",
    "CodeSize": 525,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2021-01-31T13:52:09.511+0000",
    "CodeSha256": "yHLp4MvKbGfdizOKxRKOlKKwj+zCY1FJpBQWZMizuCg=",
    "Version": "$LATEST",
    "Environment": {
        "Variables": {
            "SNS_TOPIC_ARN": "arn:aws:sns:ap-northeast-1:123456789012:subscription-topic",
            "ALARM_SUBJECT": "TEST"
        }
    },
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "19d21114-bf2c-415c-9d6e-996acbe6895a",
    "State": "Active",
    "LastUpdateStatus": "Successful"
}
```

## ④ CloudWatch Logs の設定
ログの保存先となるロググループを作成します。

```bash:ロググループの作成
$ aws logs create-log-group --log-group-name test-subscriptionfilter
```
![スクリーンショット 2021-01-24 22.23.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82090/bdc2ca8c-8648-9461-fe07-cfd102f4fd99.png)

次にログストリームを作成します。

```bash:ログストリームの作成
$ aws logs create-log-stream --log-group-name test-subscriptionfilter --log-stream-name test-stream
```
![スクリーンショット 2021-01-24 22.24.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82090/e19a863b-1af0-dfae-17f7-1c6285779d3b.png)

ログ送信を送るのに **creationTime** の情報が必要になるので確認します。

```bash:ログストリームの確認
$ aws logs describe-log-streams --log-group-name test-subscriptionfilter
{
    "logStreams": [
        {
            "logStreamName": "test-stream",
            "creationTime": 1611494667587,
            "arn": "arn:aws:logs:ap-northeast-1:123456789012:log-group:test-subscriptionfilter:log-stream:test-stream",
            "storedBytes": 0
        }
    ]
}
```

ログの送信テストをします。
その際、 **timestamp** は確認した値を入れます。

```bash:ログの送信
$ aws logs put-log-events --log-group-name test-subscriptionfilter --log-stream-name test-stream --log-events timestamp=1611494667587,message="This is Test"
{
    "nextSequenceToken": "49614471546104629625117997422005678002374743753152879794"
}
```
![スクリーンショット 2021-01-24 22.25.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82090/ba3e3a21-e18d-3a24-0f8d-386841a17219.png)

**msessage** で指定した`This is Test`が保存されているのが確認できました。
これでログ保存先の用意は完了です。

２回目以降のログ送信はコマンド実行した際の **nextSequenceToken** の値が必要になります。
後でも確認できますが面倒でなければメモしておくとスムーズかもしれないです。

それではいよいよサブスクリプションフィルタの作成を行います。

```bash:サブスクリプションフィルタの作成
$ aws logs put-subscription-filter --log-group-name test-subscriptionfilter --filter-name subscription-filter --filter-pattern test --destination-arn arn:aws:lambda:ap-northeast-1:123456789012:function:subtest

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.
```
エラーが発生してしまいました。。。どうやら `CloudWatch Logs` が Lambda を実行できないそうです。
コンソールからサブスクリプションフィルタを作成する場合は気にする必要ないのですが、cliで設定する場合は事前にLambda側でパーミッションの追加をしておく必要があります。

```bash:パーミッションの追加
$ aws lambda add-permission --function-name subtest --statement-id "lambdapermission" --principal "logs.ap-northeast-1.amazonaws.com" --action "lambda:InvokeFunction" --source-arn "arn:aws:logs:ap-northeast-1:123456789012:log-group:*:*" --source-account 123456789012
{
    "Statement": "{\"Sid\":\"lambdapermission\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"logs.ap-northeast-1.amazonaws.com\"},\"Action\":\"lambda:InvokeFunction\",\"Resource\":\"arn:aws:lambda:ap-northeast-1:123456789012:function:subtest\",\"Condition\":{\"StringEquals\":{\"AWS:SourceAccount\":\"123456789012\"},\"ArnLike\":{\"AWS:SourceArn\":\"arn:aws:logs:ap-northeast-1:123456789012:log-group:*:*\"}}}"
}
```
ということで、改めてサブスクリプションフィルタの作成を行います。

```bash:サブスクリプションフィルタの作成
$ aws logs put-subscription-filter --log-group-name test-subscriptionfilter --filter-name subscription-filter --filter-pattern Test --destination-arn arn:aws:lambda:ap-northeast-1:123456789012:function:subtest
```
無事作成できました。


# テスト
それでは実際にサブスクリプションフィルタの情報がメールで通知されるか試してみます。

```bash:ログの送信
$ aws logs put-log-events --log-group-name test-subscriptionfilter --log-stream-name test-stream --log-events timestamp=1611453737087,message="This is Test" --sequence-token 49614471546104629625117997422005678002374743753152879794
{
    "nextSequenceToken": "49614471546104629625118392286855075160199013121852005554"
}
```
もし **put-log-events** で以下のようなエラーが発生した場合、 **--sequence-token** の値をエラー内の記載されている値(ここだと`49614471546104629625118452176870929254188121824069508274`)に置き換えて改めて送信してみてください。

```text:送信エラー
An error occurred (InvalidSequenceTokenException) when calling the PutLogEvents operation: The given sequenceToken is invalid. The next expected sequenceToken is: 49614471546104629625118452176870929254188121824069508274
```
メールが届けば成功です。

![スクリーンショット 2021-01-31 23.02.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82090/628767a2-ae63-e3c4-ff6c-d37cc0c94d03.png)

先ほど Lambda のところで **update-function-configuration** で設定した `ALARM_SUBJECT` の値が件名で表示され、本文には **put-log-events** で送信した `message` の内容が表示されます。


# おわりに
メトリクスフィルターの方が簡単に設定できますが、文字列を検知したことの通知しかできません。
ログ本文を通知内容に含めたい場合はサブスクリプションフィルターを利用することで解決できます。


# 参考URL
- AWS CLI Command Reference - SNS
[create-topic](https://docs.aws.amazon.com/cli/latest/reference/sns/create-topic.html)
[subscribe](https://docs.aws.amazon.com/cli/latest/reference/sns/subscribe.html)
[list-subscriptions-by-topic](https://docs.aws.amazon.com/cli/latest/reference/sns/list-subscriptions-by-topic.html)
- AWS CLI Command Reference - IAM
[create-policy](https://docs.aws.amazon.com/cli/latest/reference/iam/create-policy.html)
[create-role](https://docs.aws.amazon.com/cli/latest/reference/iam/create-role.html)
[attach-role-policy](https://docs.aws.amazon.com/cli/latest/reference/iam/attach-role-policy.html)
- AWS CLI Command Reference - Lambda
[create-function](https://docs.aws.amazon.com/cli/latest/reference/lambda/create-function.html)
[update-function-configuration](https://docs.aws.amazon.com/cli/latest/reference/lambda/update-function-configuration.html)
[add-permission](https://docs.aws.amazon.com/cli/latest/reference/lambda/add-permission.html)
・AWS CLI Command Reference - CloudWatchLogs
[create-log-group](https://docs.aws.amazon.com/cli/latest/reference/logs/create-log-group.html)
[create-log-stream](https://docs.aws.amazon.com/cli/latest/reference/logs/create-log-stream.html)
[put-log-events](https://docs.aws.amazon.com/cli/latest/reference/logs/put-log-events.html)
- AWS公式
[AWS CLI での Amazon SNS の使用](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-services-sns.html)
[AWS CLI を使用して IAM ロールを引き受ける方法を教えてください。](https://aws.amazon.com/jp/premiumsupport/knowledge-center/iam-assume-role-cli/)
- Developers.IO
[CloudWatch Logs を文字列検知してログ内容をメールを送信してみた サブスクリプションフィルター版](https://dev.classmethod.jp/articles/notification_cloudwatchlogs_subscriptoinfilter/)