CloudWatch Logs のサブスクリプションフィルタを使用し、Lambda で Slack に エラー通知

# はじめに
エラーログ（今回はlaravelのログ）出力時、CloudWatch Logsサブスクリプションフィルタを使用し、slackに通知する仕組みを作ります。
CloudWatch Logsの収集対象としているログにErrorという文字列が出力されたらLambdaが起動し、Slackに通知します。
<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/da056bd4-a77b-d6fb-6a08-488fd6d5bb8b.png">



# 事前に準備
・EC2サーバーでwebページを構築済み
・統合 CloudWatch agent についての理解
統合 CloudWatch agentとは、EC2などのサーバーにインストールする CloudWatchのagent (パッケージ) のことです。
インストールすることでカスタムメトリクスが取得できるようになったり、CloudWatch Logs にログを送信できるようになります。

# 流れ
①EC2アタッチ用IAMロールの作成
②EC2にCloudWatch agentをインストール
③CloudWatch agent設定ファイルの作成
④CloudWatchエージェントの起動
⑤SlackのWebhookURLを作成
⑥Lambda作成
⑦CloudWatch Logsサブスクリプションフィルタを設定

①~④までは今回、ssmを使用した方法ですが、ssmを使用せず、下記の記事のec2内で設定する方法もあります。

https://business.ntt-east.co.jp/content/cloudsolution/column-try-28.html

# EC2アタッチ用IAMロール作成
1.下記のポリシーを付与したIAMロールを作成します。

・AmazonSSMManagedInstanceCore（Systems ManagerでCloudWatchエージェントをインストール、起動する際に必要）
・CloudWatchAgentAdminPolicy（Systems Manager パラメータストアに書き込むために必要）
<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/44749caf-bafd-6fe1-16a1-5d5d92f42e27.png">

2.作成後、EC2にアタッチします。
EC2コンソール画面→EC2を選択しアクション→セキュリティ→IAMロールの変更からアタッチできます。

# EC2にCloudWatch agentをインストール
CloudWatch agentをインストールする方法は、２パターンあります。それぞれの方法を解説します
①Systems Managerを使用してインストール
②EC2にSSH接続してインストール

### ①Systems Managerを使用してインストール
1.Systems Managerコンソールを開き、Runcommandを選択
2.AWS-ConfigureAWSPackageを選択
<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/7d14c68f-a90d-eb1f-787b-46e167714909.png">
3.コマンドパラメータは以下を選択する
  Action:Install
  Installation Type:Uninstall and reinstall
  Name: AmazonCloudWatchAgent  
4.ターゲットは、インスタンスを手動で選択する→EC2インスタンスを選択します。
##### 注意：ターゲットのインスタンスが出てこない場合
EC2インスタンスにIAAMロールをアタッチして反映されるまでに5分程度かかることがありますので、お待ち下さい。それでも反映されない場合、`②EC2にSSH接続してインストール`の方法をためしてください。

5.ほかは、デフォルトのまま実行をクリックします。（S3 バケットへの書き込みを有効化はしなくても構いません）
6.コマンドのステータスが成功しているか確認します。

### ②EC2にSSH接続してインストール
EC2インスタンスへSSH接続して、下記コマンドでパッケージをダウンロードして、その後インストールします。
リージョンが東京でなければ、コマンドの`ap-northeast-1`の箇所を変更してください

```
$ wget https://s3.ap-northeast-1.amazonaws.com/amazoncloudwatch-agent-ap-northeast-1/amazon_linux/amd64/latest/amazon-cloudwatch-agent.rpm

$ sudo rpm -U ./amazon-cloudwatch-agent.rpm
```

# CloudWatch agent設定ファイルの作成
EC2インスタンスへSSH接続して、下記コマンドで設定ファイルの作成します。質問に答える形式です。
`default choice:`と同じ選択肢であれば、Enterキーを押すだけでよいです。

```
$ sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-config-wizard
```

Amazon Linux2を使用していますので、1を選択します

```
On which OS are you planning to use the agent?
1. linux
2. windows
default choice: [1]:
```
CloudWatchエージェントはrootで動かします

```
Which user are you planning to run the agent?
1. root
2. cwagent
3. others
default choice: [1]:
```
メトリクスは必要ではありません、ログを収集するので下記の質問は全て2を選択。

```
Do you want to turn on StatsD daemon?
1. yes
2. no
default choice: [1]:
2
Do you want to monitor metrics from CollectD?
1. yes
2. no
default choice: [1]:
2
Do you want to monitor any host metrics? e.g. CPU, memory, etc.
1. yes
2. no
default choice: [1]:
2
Do you have any existing CloudWatch Log Agent (http://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/AgentReference.html) configuration file to import for migration?
1. yes
2. no
default choice: [2]:
2
```

Laravelのログのパス、ロググループ、ログストリーム名を指定します。
今回は下記のようにしました。ログのパスは、自身の環境に合わせてください。
ログのパス`/home/ec2-user/logs/laravel*.log`
ロググループ`laravel-logs`
ログストリーム名`{instance_id}`

```
Do you want to monitor any log files?
1. yes
2. no
default choice: [1]:
1
Log file path:
/home/ec2-user/logs/laravel*.log 
Log group name:
default choice: [laravel*.log]
laravel-logs
Log stream name:
default choice: [{instance_id}]
{instance_id}
```

追加でログ収集しないので2を選択します。

```
Do you want to specify any additional log files to monitor?
1. yes
2. no
default choice: [1]:
2
```


設定ファイルは作成されますが、 Systems Manager parameter storeで設定ファイルを管理するので1を指定します。

```
Edit it manually if needed.
Do you want to store the config in the SSM parameter store?
1. yes
2. no
default choice: [1]:
1
```

パラメータストア名を設定します。
AmazonCloudWatch-で始まる名前にする必要があるので注意してください。
今回はAmazonCloudWatch-laravel-logsとしました。

```
What parameter store name do you want to use to store your config? (Use 'AmazonCloudWatch-' prefix if you use our managed AWS policy)
default choice: [AmazonCloudWatch-linux]
AmazonCloudWatch-laravel-logs
```

リージョン、クレデンシャル情報はデフォルトを使用します。
クレデンシャル情報は、IAMコンソール画面→ユーザー選択→認証情報タブ→アクセスキーで確認できます。
表示されているアクセスキーと異なれば、アクセスキーを作成し、クレデンシャル情報は`2. Other`を選択します。

```
Trying to fetch the default region based on ec2 metadata...
Which region do you want to store the config in the parameter store?
default choice: [ap-northeast-1]

Which AWS credential should be used to send json config to parameter store?
1. XXXXXXXXXXXXXXXXXXXX(From SDK)
2. Other
default choice: [1]:

Please make sure the creds you used have the right permissions configured for SSM access.
Which AWS credential should be used to send json config to parameter store?
1. XXXXXXXXXXXXXXXXXXXX(From SDK)
2. Other
default choice: [1]:

Successfully put config to parameter store AmazonCloudWatch-laravel-logs.
Program exits now.
```

設定ファイルが作成され、Systems Manager parameter storeに保存されました。

今回は、詳細なメトリクスは設定しませんでしたが、[CloudWatchでEC2のメモリ・ディスク使用率を監視する](https://engineers.weddingpark.co.jp/aws-cloudwatch-ec2/)場合、こちらの記事で、設定ファイルを作成できます。
その際は、EC2にcollectd (サーバの統計情報を収集するためのソフトウェア) のアプリケーションが必要ですので、インストールする必要があります。

```
sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
sudo yum install -y collectd
```

# CloudWatchエージェントの起動
SSMのRunCommandでエージェントを起動します。
1.Systems Managerコンソールを開き、Runcommandを選択
2.コマンドドキュメントリストでAmazonCloudWatch-ManageAgentを選択
<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/9c10d7df-634e-8991-5388-03af93331422.png">
3.コマンドパラメータは、以下を選択
  Action: configure
  Mode: ec2
  Optional Configuration Source:ssm
  Optional Configuration Location:AmazonCloudWatch-laravel-logs（パラメータストア名を指定）
  Optional Open Telemetry Collector Configuration Source:ssm
  Optional Open Telemetry Collector Configuration Location:空白
  Optional Restart: yes
<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/b6e31702-11ef-3f5d-facc-0327c4cac73e.png">

4.ターゲットは、インスタンスを手動で選択する→EC2インスタンスを選択します。
5.ほかは、デフォルトのまま実行をクリックします。（S3 バケットへの書き込みを有効化はしなくても構いません）
6.コマンドのステータスが成功しているか確認します。
7.Cloudwatch Logsグループに`laravel-logs`が作成されていることを確認します
　反映に数分かかることがあります。それでも確認されない場合、サーバー内のlogファイルに書き込みをしてみてください。
<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/21030aef-bad4-f80e-fcae-a059dcc9d5f4.png">

## Slack通知したいエラーログのパスが複数ある場合
複数のログをCloudWatchLogsに出力したい場合、マイパラメータを編集する必要があります。
マイパラメータの名前をクリックします。
<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/af799669-2a4d-f4d2-2ab2-ba784442ab19.png">

編集をクリックします。

<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/7888880e-a233-c357-3dc0-9673f07928a7.png">


```json:編集前
{
  "agent": {
    "run_as_user": "root"
  },
  "logs": {
    "logs_collected": {
      "files": {
        "collect_list": [
          {
            "file_path": "/home/ec2-user/logs/laravel*.log",
            "log_group_name": "laravel-logs",
            "log_stream_name": "{instance_id}"
          }
        ]
      }
    }
  }
}
```
`collect_list`の[]内に複数入れることで、ログを複数CloudWatchLogsに出力されます。

```json:編集後
{
  "agent": {
    "run_as_user": "root"
  },
  "logs": {
    "logs_collected": {
      "files": {
        "collect_list": [
          {
            "file_path": "/home/ec2-user/logs/laravel*.log",
            "log_group_name": "laravel-logs",
            "log_stream_name": "{instance_id}"
          },
          {
            "file_path": "/batch/logs/*.log",
            "log_group_name": "batch-logs",
            "log_stream_name": "{instance_id}"
          }
        ]
      }
    }
  }
}

```

メトリクスも入れる場合

```json:メトリクスも追加
{
  "agent": {
    "run_as_user": "root"
  },
  "logs": {
    "logs_collected": {
      "files": {
        "collect_list": [
          {
            "file_path": "/home/ec2-user/logs/laravel*.log",
            "log_group_name": "laravel-logs",
            "log_stream_name": "{instance_id}"
          },
          {
            "file_path": "/batch/logs/*.log",
            "log_group_name": "batch-logs",
            "log_stream_name": "{instance_id}"
          }
        ]
      }
    }
  },
  "metrics": {
    "append_dimensions": {
      "AutoScalingGroupName": "${aws:AutoScalingGroupName}",
      "ImageId": "${aws:ImageId}",
      "InstanceId": "${aws:InstanceId}",
      "InstanceType": "${aws:InstanceType}"
    },
    "metrics_collected": {
      "collectd": {
        "metrics_aggregation_interval": 60
      },
      "disk": {
        "measurement": ["used_percent"],
        "metrics_collection_interval": 60,
        "resources": ["*"]
      },
      "mem": {
        "measurement": ["mem_used_percent"],
        "metrics_collection_interval": 60
      },
      "statsd": {
        "metrics_aggregation_interval": 60,
        "metrics_collection_interval": 10,
        "service_address": ":8125"
      }
    }
  }
}
```
編集後のパラメータを使い、RunCommandで再度実行すると、パス`/batch/logs/*.log`もCloudWatchLogsに出力されます。


# SlackのWebhookURLを作成
1.slackのAppを選択し、`Incoming WebHooks`を追加します
<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/6273b12f-3fd2-e669-5a9f-a3f69bbab580.png">
2.Webページに遷移し、Slackに追加をクリックします
<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/5054e0bb-aaad-a1d4-19db-cb888e169783.png">
3.通知したチャンネル名を選択し、`Incoming WebHooks`を追加をクリックします
<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/3108d73f-5dc8-da62-5cc8-1af198f20eb5.png">
4．チャンネルへの投稿を選択し、`Webhook URL`をコピーし、設定を保存します
<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/14fc37ab-dac3-6e55-2500-187f4e3a7763.png">
5.設定を保存すると、チャンネルに下記画像のメッセージが表示されます。
<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/aafc0a3c-36bc-d3ad-7be3-82d182844249.png">

# Lambda作成
1. 下記の設定でLambda作成
・タイプ：一から作成
・関数名：Laravel-ErrorLogs-to-Slack
・ランタイム：python3.8

2. 設定→環境変数を設定します
- キー：`ProjectName`、値：`HOGE`にします。
- キー：`WebhookURL`、値：`webhookのURL`にします。
コード内で`os.environ['WebhookURL']`とすると値を参照できます。


```python:Laravel-ErrorLogs-to-Slack
import json
import logging
import os
import base64
import gzip
import urllib.request

HOOK_URL = os.environ['WebhookURL']
PROJECT_NAME = os.environ['ProjectName']

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):

    # CloudWatchLogsからのデータはbase64エンコードされているのでデコード
    decoded_data = base64.b64decode(event['awslogs']['data'])

    # バイナリに圧縮されているため展開
    json_data = json.loads(gzip.decompress(decoded_data))
    
    # CloudWatch Logsに複合化＆解凍したログを出力
    logger.info("Event: " + json.dumps(json_data))
    
    error_message = json_data['logEvents'][0]['message']
    
    logger.info("Message: " + str(error_message))

    #　環境ごとに環境名を記載。logGroupはロググループ名を記載。
    if json_data['logGroup'] == "laravel-logs":
        environment = "本番"
    elif json_data['logGroup'] == "laravel-logs-dev":
        environment = "開発"

    # Slackへのメッセージを作成
    send_data = {
		"type": "mrkdwn",
		"text": "<!channel> %s Laravel %s環境:exclamation::exclamation:\nエラーログを検出しました。ログを確認してください。\n ```%s``` \n確認中は:eyes:、完了後は:white_check_mark:をお願いします" % (PROJECT_NAME, environment, error_message)
	}

    send_text = json.dumps(send_data)

    request = urllib.request.Request(
        HOOK_URL, 
        data=send_text.encode('utf-8'), 
        method="POST"
    )
    with urllib.request.urlopen(request) as response:
        response_body = response.read().decode('utf-8')

```
下記は、本番と開発環境でロググループの名前を分けている場合に、使用するとよいでしょう。
なければ削除してよいです。

```py:ロググループ名によって環境名を変える
    #　環境ごとに環境名を記載。logGroupはロググループ名を記載。
    if json_data['logGroup'] == "laravel-logs":
        environment = "本番"
    elif json_data['logGroup'] == "laravel-logs-dev":
        environment = "開発"
```


ログデータの取得については、こちらを参照してください。
[AWS Lambda のサブスクリプションフィルターのevent構造](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/SubscriptionFilters.html#LambdaFunctionExample)

メッセージは、[絵文字チートシート](https://www.webfx.com/tools/emoji-cheat-sheet/)と、[Slack Fromatting tex](https://api.slack.com/reference/surfaces/formatting#block-formatting)t(View this exampleをクリックすると、`Block Kit Builder`に遷移して、メッセージを調整します。)を参考にしました。
<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/1c95c16a-ad90-3e01-afa0-32f0bb1b040f.png">
<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/b62e15f6-9f2c-6388-ec60-db0fd34bb91f.png">

`Block Kit Builder`でメッセージを作成したら、右側のコードをコピーします。
ただし、コピーしたコードの`"blocks"`の箇所を削除してから、Lambdaにコードを貼り付けます。

##### Block Kit Builderで作成時

```python:Block_Kit_Builderで作成時
{
　　"blocks": [
	{
		"type": "section",
		"text": {
			"type": "mrkdwn",
			"text": "<!channel> Laravel 本番環境:exclamation::exclamation:\nLaravelのエラーログを検出しました。ログを確認してください。"
		}
	}
　]
}
```

##### Lambdaに貼り付けるコード

```python:Lambdaに貼り付けるコード
{
	"type": "mrkdwn",
	"text": "<!channel> Laravel 本番環境:exclamation::exclamation:\nLaravelのエラーログを検出しました。ログを確認してください。\n ```%s``` " % (error_message)
}
```
エラーログの箇所のみをマークダウンする場合、`\n ```%s``` " % (error_message)`を付け加えます。








# CloudWatch Logsサブスクリプションフィルタを設定
1.CloudWatch Logsのロググループ`laravel logs`を選択します。
タブのサブスクリプションフィルターから、`Lambda サブスクリプションフィルターを作成`をクリックします。
<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/00aff940-f2a9-967a-7ed2-539fcadfcffd.png">
<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/180bf7a8-ce63-63b8-de06-a153c9817e29.png">

2.下記の通り設定します。
・送信先：先程作成したLambda
・ログの形式：その他
・フィルターパターン：`?ERROR ?error`
　（slackに通知したい文字を選択します。複数選択するときは、半角スペースを開けます。全角スペースはだめです。）

https://qiita.com/shimajiri/items/81a4ed0fe39fe337fedb

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html

<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/0bc69a26-b47f-fa6b-d087-461be02d3757.png">
・サブスクリプションフィルター名：お好きな名前

3.パターンをテストできます。問題なければ`ストリーミング開始`をクリック

# 動作確認
laravelのログファイルにエラー文を加えると、以下の通り、slackに通知が来ました。

<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/0779aff8-7b98-c3f7-e506-4b41fba6d956.png">

ログデータは下記のようなデータとなります。

```json
{
    "messageType": "DATA_MESSAGE",
    "owner": "658663129401",
    "logGroup": "laravel-logs",
    "logStream": "i-0000000000000",
    "subscriptionFilters": [
        "ERROR"
    ],
    "logEvents": [
        {
            "id": "36722748203695186877044706505754336186020163251740803078",
            "timestamp": 1646704981236,
            "message": "[2021-01-11 11:11:11] production.ERROR: エラーテストです。"
        }
    ]
}
```

# 参考
・[実行中の EC2 Linux インスタンスに CloudWatch Logs エージェントをインストールして設定する](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/QuickStartEC2Instance.html)
・[CloudWatch LogsでLaravelのログを収集してみた](https://tec.tecotec.co.jp/entry/2020/05/27/090000#anchor6)
・[【CloudWatch・サブスクリプションフィルタ・Slack】出力されるログの一部を抽出してSlackにわかりやすいメッセージ出力してみた](https://bftnagoya.hateblo.jp/entry/2021/06/14/190641)
・[AWSで監視してアラートをメール送信する（4選）](https://qiita.com/kusano00/items/f1f2f857bd73d9ebc312#awlogs%E3%83%88%E3%83%AA%E3%82%AC%E3%83%BC%E3%81%AE%E7%9B%A3%E8%A6%96)
・[Emoji cheat Sheet(lambda用)](https://www.webfx.com/tools/emoji-cheat-sheet/)
・[AWS CloudWatchのログフィルタパターンを分かりやすく解説](https://qiita.com/shimajiri/items/81a4ed0fe39fe337fedb)