AWS CloudWatchのログを監視し、Slackに通知する(CloudWatch+Lambda+Ruby+Slack)

# 概要
CloudWatchのログを監視し、特定のログが出力された場合に、
その特定のログの内容をSlackに通知する方法を紹介します。

# 利用するサービス
- AWS
    - CludWatch
    - Lambda
    - Amazon Systems Manager
- Slack(Incoming Webhook)

# 手順
以下の順序で作業を行っていきます。

1. SlackのWorkspaceにIncoming Webhookを登録
2. AWS Systems ManagerにSlackにPOSTするためのURLを登録
3. Lambdaでログ内容をSlackにPOSTするプログラムを作成
4. CloudWatchのロググループのサブスクリプションフィルターにLambdaを設定する
5. 連携確認テスト

## 1. SlackのWorkspaceにIncoming Webhookを登録 
まず、ログを通知したいチャンネルをSlackで作成。
次に、[slack app directory](https://my.slack.com/apps)で`Incoming Webhook`を検索し、`Slackに追加`をクリック。
`チャンネルへの投稿`で作成したSlackチャンネルを選択し、`Incoming Webhook インテグレーションの追加`をクリック。
`Webhook URL`の内容をコピーして控えておく。

## 2. AWS Systems ManagerにSlackにPOSTするためのURLを登録
Lambdaのコードに直接URLを記入して、Slackに通知することはできるのですが、
セキュリティの面で心配なので、Systems Managerを利用することにします。

まず、AWS Systems Managerのコンソールを開く。
パラメーターストア>>パラメータの作成をクリック。
以下のように`Incoming Webhook`のURLを設定しパラメータを作成する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1568031/8bdf7581-7032-45a1-ed30-bcc57d95135c.png)

## 3. Lambdaでログ内容をSlackにPOSTするプログラムを作成
まず、Lambdaのコンソールを開く。
関数>>関数の作成をクリック。
関数名に`log_to_slack`と指定し、ランタイムに`Ruby`を選択して関数を作成する。
作成した関数に対応するロールを編集して`AmazonSSMFullAccess`を追加。
コードソースに以下を指定。

```rb
require 'json'
require 'base64'
require 'zlib'
require 'net/http'
require 'uri'
require 'aws-sdk-ssm'

def lambda_handler(event:, context:)
   
    # Base64変換+zip解凍+JSON変換
    json_log_data = JSON.parse(Zlib::GzipReader.new(StringIO.new(Base64.decode64(event["awslogs"]["data"]))).read)
    log_events = json_log_data["logEvents"]
    output = ''
    log_events.each do |log_event|
      output += log_event["message"]
      output += "\n"
    end
    
    # slackに通知
    slack_post(output)
end

def slack_post(message)
  uri  = URI.parse(get_slack_url)
  ssm_client = Aws::SSM::Client.new
  request = {
    name: 'test-slack-url', # パラメータ名
    with_decryption: true   # 暗号化されている場合は復号し、暗号化されていない場合は何もしない
  }
  response = ssm_client.get_parameter(request)
  slack_url = response.parameter.value
  
  params = { text: '```' + message + '```' }
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true
  http.start do
    request = Net::HTTP::Post.new(uri.path)
    request.set_form_data(payload: params.to_json)
    http.request(request)
  end
end

def get_slack_url
  # このソースにURLをベタ打ちするのはセキュリティの問題があるため、
  # aws systems managerにwebhookのスラックURLを登録した。
  # aws systems managerに登録したurlを取得
  ssm_client = Aws::SSM::Client.new
  request = {
    name: 'test-slack-url',
    with_decryption: true
  }
  response = ssm_client.get_parameter(request)
  response.parameter.value
end
```
`Deploy`ボタンを押してデプロイしておく。

## 4. CloudWatchのロググループのサブスクリプションフィルターにLambdaを設定する
### CloudWatchにログを出力するためテストLambdaを作成
まず、Lambdaのコンソールを開く。
関数>>関数の作成をクリック。
関数名に`lambda_test`と指定し、ランタイムに`Ruby`を選択して関数を作成する。
コードソースに以下を指定。

```rb
require 'json'

def lambda_handler(event:, context:)
    # TODO implement
    { statusCode: 200, body: JSON.generate('Hello from Lambda!') }
    pp "INFO:AAAAA"
    pp "DEBUG:BBBB"
    pp "ERROR:CCCC"
end
```

この`lambda_test`をテスト実行することでCloudWatchの
`lambda_test`に対応したロググループに意図的に
「INFO:AAAA」
「DEBUG:BBBB」
「ERROR:CCCC」
といったログを出力できるようになる。
`Deploy`ボタンを押してデプロイし、
`Test`ボタンをおしてCloudWatchのコンソールからロググループをクリックし、
`lambda_test`のロググループが作成されていることを確認する。
### lambda_testのロググループのサブスクリプションフィルターを設定する
lambda_testのロググループを開き、サブスクリプションフィルタータブをクリック。
作成>>`Lambda サブスクリプションフィルターを作成`を選択。
Lambda関数で`log_to_slack`を選択。
ログの形式で`その他`を選択。
サブスクリプションフィルターのパターンで`ERROR`と入力。
　※`lambda_test`の`ERROR:CCCC`を検知したタイミングで`log_to_slack`が動作するように設定。
サブスクリプションフィルター名で`error_filter`と入力。
`ストリーミングを開始`をクリック。

## 5. 連携確認テスト
`lambda_test`のコードを開き、`Test`をクリックし、
Slackの今回作成したチャンネルに`ERROR:CCCC`が通知されていることを確認する。

お疲れさまでした。最後までお付き合いいただき、ありがとうございます。

## 参考記事
[AWS Lambdaで作るSlack bot (Incoming Webhook)](https://qiita.com/yokoc1322/items/553ad147b82277b2beca)
[Ruby でWebhook にPOST する方法](https://qiita.com/yulii/items/c156e88211a58466e7bc)
[【Ruby】AWS SSMパラメータストアの値を取得する](https://qiita.com/kou_pg_0131/items/1fa538c6fb7171e010e4)