CloudWatch Logs Insights の分析結果をLambdaで監視する

#はじめに
CloudWatch Insightsの分析結果を監視するLambdaを作成しました。

#CloudWatch Logs Insightsとは
CloudWatch Logs Insightsとは、2018年のre:Inventで発表されたサービスです。
詳細に関しては、以下のサイトをご覧いただければと思いますが、一言で説明すれば**CloudWatch Logs内のログデータを簡単に集計・分析できるサービス**です。
ロググループに対する専用のクエリ言語がサポートされており、そのクエリを投げることで簡単にログの集計・分析が行え、さらにグラフを作成して結果を可視化することもできます。これらの分析結果をCloudWatchダッシュボードに追加することもできます。
[新機能 – Amazon CloudWatch Logs Insights – 高速でインタラクティブなログ分析](https://aws.amazon.com/jp/blogs/news/new-amazon-cloudwatch-logs-insights-fast-interactive-log-analytics/)
[CloudWatch Logs Insights を使用したログデータの分析](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/AnalyzingLogData.html)

#やったこと
今回、CloudWatch Insightsで得られた分析結果を監視するLambdaを作成しました。
具体的には、**APIGatwayのアクセスログのステータスコードに400以上のものがある場合にSlackに通知する**
ということを行いました。
注：「LambdaでInsightsの分析結果を使う」ことをしたかったため、そのままCloudWatchのメトリクスを使えばもっと簡単にできます。

以下、構成図です。
APIGatewayのアクセスログをCloudWatch Logsに吐き出し、
LambdaでCloudWatch Logs Insightsのクエリを実行し分析結果を取得、
その分析結果の内容にしたがってSlackに通知するということを行います。
なお、2019年8月現在、Insightsの分析結果をCloudWatch Alermのメトリクスに指定することはできなかったため、LambdaはCloudwatch Eventsによって5分ごとに動かすようにしました。
<img width="638" alt="スクリーンショット 2019-08-10 14.49.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/429159/e16c5253-2136-7c9e-f3bf-d4642cd1f2af.png">


#事前準備
Lambdaを作成する前に、いくつかの事前準備を行います。

##SlackWebhookのURLを取得する。
Slack通知を行う場合、SlackWebhookのURLを取得しておいてください。

##IAMロールの作成
今回使用するIAMロールは以下の2つあるので、それらを作成します。
**1.APIGateway用IAMロール**
後述しますが、APIGatewayはデフォルト状態ではアクセスログをCloudWatch Logsに書き出す仕様ではないためその設定を行う必要があります。その際、APIGatewayがCloudWatch Logsにログを書き込む権限が必要となるためそのIAMロールを作成します。
デフォルトで存在するポリシー「AmazonAPIGatewayPushToCloudWatchLogs」をアタッチしたロールを作成し、ARNをコピーしておいてください。

**2.Lambda用IAMロール**
Lambdaに割り当てるロールを作成します。今回、CloudWatchLogsへのアクセス権限が必要なため、以下のIAMポリシーをアタッチしたロールを作成しました。

```json:IAMポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "logs:*"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
}

```

##ロググループの作成
APIGatewayのアクセスログを書き出すためのCloudWatch Logsのロググループを作成しておきます。


##APIGatewayの設定
APIGatewayの対象APIのアクセスログの設定を行います。デフォルト状態では、アクセスログは取得しない設定になっているためアクセスログをCloudWatch Logsに吐き出す設定を行います。
サービス > APIGateway > 設定
でARNにAPIGatway用に作成したIAMロールのARNを設定します。

アクセスログを取得するAPIを選択 > ステージを選択 > ログのトレース
CloudWatch ログを有効化し、アクセスログを有効化、ログの形式を設定 (今回は、JSONにしました)。

これでAPIGatwayの設定は完了です。

#Lambdaの設定
##環境変数
今回、以下の環境変数を設定しました。
**CHANNEL_LIST**　　　：通知するSlackのチャンネル名をカンマ区切りで設定。
**LOGGROUP_NAMES**　：　監視するCloudWatchLogsのロググループをカンマ区切りで設定。



```Python:Insightsの結果をLambdaで監視する
import os
import datetime
import time
import ast
import slackweb
import boto3

at_color = "danger"
client = boto3.client('logs')
channel_list = os.environ.get('CHANNEL_LIST').split(',')
logGroupNames = os.environ.get('LOGGROUP_NAMES').split(',')
slack_api = "Slack WebhookのURLを記載"

def lambda_handler(event, context):
    five_minutes_ago = datetime.datetime.now() - datetime.timedelta(minutes = 5)
    startTime = five_minutes_ago.replace(second = 0, microsecond = 0)
    endTime = startTime + datetime.timedelta(minutes = 5) - datetime.timedelta(milliseconds = 1)
    queryString = 'fields @timestamp, @message | sort @timestamp desc | filter (status >= 400)'
    
    for logGroupName in logGroupNames:
        error_logs = results(logGroupName, startTime, endTime, queryString)
        if len(error_logs) > 0:
            for error_log in error_logs:
                dict_message = ast.literal_eval(error_log[1]['value'])
                text = str(logGroupName) + '\n' + str(dict_message)
            for channel in channel_list:
                try:
                    post_slack(channel,text)
                except Exception as e:
                    post_slack(channel,text)
    
def results(logGroupName, startTime, endTime, queryString):
    start_query_res = client.start_query(
        logGroupName=logGroupName,
        startTime=int(startTime.timestamp()),
        endTime=int(endTime.timestamp()),
        queryString=queryString
    )
    queryId = start_query_res['queryId']

    get_results_res = client.get_query_results(
        queryId=queryId
    )
    
    while get_results_res['status'] == 'Running':
        time.sleep(5)
        get_results_res = client.get_query_results(
           queryId=queryId
        )
    return get_results_res['results']

def post_slack(channel,text):
    slack = slackweb.Slack(url=slack_api)
    slack.notify(
        username="API-accesslog",
        channel=channel,
        attachments = [{
            "color" : at_color,
            "text" : text
        }]
    )


```


#参考サイト
[Boto3 Docs 1.9.205 dociumentation](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html#)