CloudWatchのログをSlack通知する方法

# はじめに
今回、CloudWatchのログをSlack通知する実装をしました。
こんなに簡単に実装できるんだ〜という感じでしたが、色んなサービスが連携して面白かったので記録しておきます。

### やりたいこと：
CloudWatchに溜められるlogの中に、FATAL、ERROR、WARNINGの情報がある場合、slackのログ監視用チャンネルにログの内容を出力する

### 前提：
・slack通知したいログがCloudWatchに出力されている
・slack通知用のアプリを作成している
・slack通知用のチャンネルを用意している（アプリをインテグレーションしている）

# CloudWatchのログをSlack通知する全体像
<img width="1000" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2644489/6e0b2825-cdbd-b75b-3ff6-2468e15c75d9.png">

【参考】

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

# ログデータの流れまとめ
1. AWS CloudWatchの特定のロググループに出力される
2. サブスクリプションフィルターのパターンに一致した場合、紐づけたLambda関数に送られる
3. Lambdaの中で整形される
4. slackにポストされる

# 実装手順

### 1. ログをslackにポストする用のLambda関数を作成
### 2. 出力対象ログが属するロググループにサブスクリプションフィルターを設定
・ログの送信先として、先ほど作成したLambda関数を選択する
・サブスクリプションフィルターパターンにFATAL、ERROR、WARNINGをフィルタするように設定する


<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2644489/ea12446e-a4c8-5516-1f06-81f7e8a79029.png">

<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2644489/8479a8a3-0192-47a8-38f2-84e3f5255464.png">


※CloudWatchに出力されているログの形に合わせてパターンを設定する
例：JSON形式でログが出力されている場合のパターン↓
`{ ( $.level = "FATAL" ) || ( $.level = "WARN" ) || ( $.level = "ERROR" ) ) }`

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

### 3. System Manegerのパラメータストアでパラメータを新規作成し、slackアプリのトークンを保存
※ 安全な文字列のタイプを指定し、文字列を暗号化する
<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2644489/087bf811-1526-09bf-a659-f453ef71081d.png">

### 4. LambdaのIAMロールにSSM読み取り用ポリシー追加
※System Manegerのパラメータストアに登録した内容を取り出すために、Lambdaに紐づくIAMロールにポリシー（iam-ssm-get-parameter）を追加
<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2644489/9f39b7d9-340d-4358-c2f7-8214df6137a7.png">

### 5. Lambdaの中身を書いていく
・ログの情報を取り出す
・メッセージを整形
・postMessageする

```ruby
require 'json'
require 'base64'
require 'zlib'
require 'aws-sdk-ssm'
require 'aws-sdk-kms'
require 'slack-ruby-client'

def lambda_handler(event:, context:)
  data = JSON.parse(Zlib::GzipReader.new(StringIO.new(Base64.decode64(event["awslogs"]["data"]))).read)

  # 集計するログの基本情報
  log_group = data["logGroup"]
  log_stream = data["logStream"]
  log_events = data["logEvents"]

  # ログの基本情報
  base_message = "■対象ロググループ:" + log_group + " > " + log_stream + "\n"

  # ログの本文
  logs = []
  log_events.each { |log_event|
    logs_json = JSON.parse(log_event["message"])
    logs.push(logs_json)
  }

  # slackに送信
  post_slack(base_message, logs)
end

#
# slackに送信
#
def post_slack(base_message, logs)
  # 送信するテキスト：ログの基本情報
  text = "<!here>\n"
  text += base_message

  # アタッチメントテキスト：ログの内容
  attachments = []
  attachment_texts = []

  logs.each do |log_json|
    timestamp = log_json["time"]
    attachment_text = "■発生時刻: " + timestamp + "\n"
    attachment_text += "■ログメッセージ: " + message + "\n"
    attachment_texts.push(attachment_text)
  end

  attachments.push({ fallback: 'Error', text: attachment_texts.join("\n\n"), color: 'danger' })
  slack_client.chat_postMessage(channel: 'チャンネル名', text: text, attachments: attachments, as_user: true)
end

#
# slackクライアント
#
def get_slack_client
  ssm_client = Aws::SSM::Client.new
  get_token_request = {
    name: 'xxx', # パラメータ名
    with_decryption: true # 暗号化されている場合は復号し、暗号化されていない場合は何もしない
  }

  Slack.configure do |config|
    config.token = ssm_client.get_parameter(get_token_request).parameter.value
  end
  Slack::Web::Client.new
end
```    

# 最後に
最近AWSを触り始めた初学者ですが、AWSについてもっと知りたい欲が出てきました。
また、面白いことがあれば記事にしたいと思います！