Trusted Advisorのステータス変更をSlackに通知するLambda関数をserverlessで作成する

# 概要
この記事では、AWS Trusted Advisorのステータス変更をSlackに通知するために、PythonでLambda関数を記述し、serverless frameworkでデプロイする方法を記載する。


# AWS Trusted Advisorのステータス変更を通知する意義
AWS Trusted AdvisorはAWSのインフラストラクチャの構成を最適化するために、「コストの最適化」や「サービス制限」、「セキュリティ」などの項目に対して適切なアドバイスをおこなってくれるサービスである。

例えば、RDSの手動スナップショットのデフォルトの制限数は100なので、手動スナップショットの数が80に達するとAWS Trusted Advisorが「サービス制限」の項目に対して警告を行う。

管理者はAWSマネージメントコンソールで詳細を確認でき、Emailで週次のチェック結果を受信することができる。

しかし、セキュリティやサービス制限など運用に影響する項目に関しては、警告が発せされたら即座に通知されるようにしておく事で、共有や対応のスピードが速くなる。

AWSはTrusted Advisorのステータス変更を検知するCloudWatch Eventsを用意してくれている([Trusted Advisor チェック結果を Amazon CloudWatch Eventsで監視する](https://docs.aws.amazon.com/ja_jp/awssupport/latest/user/cloudwatch-events-ta.html))。したがって、このCloudWatch Eventsを利用して通知システムを構築するのが良い。

> [AWS Trusted Advisor](https://aws.amazon.com/jp/premiumsupport/trustedadvisor/)
> [Trusted Advisor チェック結果を Amazon CloudWatch Eventsで監視する](https://docs.aws.amazon.com/ja_jp/awssupport/latest/user/cloudwatch-events-ta.html)

# Lambda関数を作ってみよう！

## 作業
### CloudWatch EventsサンプルJSONの確認
CloudWatch EventsのJSONを確認して、通知するメッセージはどうするか考える必要がある。
[Trusted Advisor チェック結果を Amazon CloudWatch Eventsで監視する](https://docs.aws.amazon.com/ja_jp/awssupport/latest/user/cloudwatch-events-ta.html)の「Trusted Advisor 用の CloudWatch イベントルールを作成するには:」の手順を進める。下記の画像のように入力する。

(私の場合はステータスが「OK」の時は通知しないようにしている。)
<img width="585" alt="スクリーンショット 2018-09-03 1.28.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/158743/8e9c4426-8774-2cdb-02da-4b9008eba1ed.png">

サンプルのJSONファイルが表示されるので、コピーして開発とテストで使用する。
<img width="584" alt="スクリーンショット 2018-09-03 1.29.01.png" src="https://qiita-image-store.s3.amazonaws.com/0/158743/e9d40468-a016-08a0-382d-045bc00251d0.png">

有用そうな項目は、個人的には下記の通り。

|JSONキー名|説明|
|---|---|
|`status`|変更後のステータス|
|`resource_id`|リソースのID|
|`detail.check-name`|チェック項目名|
|`detail.check-item-detail`|チェックの詳細|

### Slack通知用のコード作成
実際にSLackに通知するためのコードを書いていく。
私はPythonに慣れているので、Pythonで書いた。


#### 必要なライブラリのインストール
必要なライブラリは`requests`で、あらかじめpipでインストールしておく。

    $ pip install requests
    $ pip freeze > requirements.txt # デプロイ時に使用

```requirements.txt
certifi==2018.8.24
chardet==3.0.4
idna==2.7
requests==2.19.1
urllib3==1.23
```

#### 実行コード

```handler.py
# -*- coding: utf-8 -*-

import json
import requests
import os


def webhook_icon(status=''):
    if status == 'INFO':
        return ':information_source:'
    elif status == 'WARN':
        return ':warning:'
    else:
        return ':rotating_light:'


def attachement_color(status=''):
    if status == 'INFO':
        return '#CCCCCC'  # 灰
    elif status == 'WARN':
        return '#FFFF00'  # 黄
    else:
        return '#FF0000'  # 赤


def alert(event, _context):
    detail = event.get('detail', {})
    status = detail.get('status', 'ERROR')
    resource = detail.get('resource_id')
    check_name = detail.get('check-name')
    check_detail = detail.get('check-item-detail')

    requests.post(os.getenv('SLACK_WEBHOOK_URL'), data=json.dumps({
        "text": "<!channel> *AWS Trusted Advisorのステータス変更を検知しました*",
        "icon_emoji": webhook_icon(status=status),
        "attachments": [
            {
                "fallback": check_name,
                "color": attachement_color(status=status),
                "author_name": "Trusted Advisor",
                "author_link": "https://console.aws.amazon.com/trustedadvisor/home?#/dashboard",
                "title": check_name,
                "fields": [
                    {
                        "title": "ステータス",
                        "value": status,
                        "short": False
                    },
                    {
                        "title": "リソース",
                        "value": resource,
                        "short": False
                    }
                ]
            },
            {
                "title": '詳細',
                "fallback": check_name,
                "color": attachement_color(status=status),
                "text": "```\n{0}\n```".format(check_detail)
            }
        ]
    }))
```


WebhookのアイコンはTrusted Advisorのステータスに応じて、

- `ERROR`であれば:rotating_light:
- `WARN`であれば:warning:
- `INFO`であれば:information_source:

にしています。
また、通知時のSlackのラインの色を

- `ERROR`であれば<font color="#FF0000">赤</font>
- `WARN`であれば<font color="#FFFF00">黄</font>
- `INFO`であれば<font color="#CCCCCC">灰</font>

としている。

> メッセージ作成で参考にしたサイト
> [Message Formatting](https://api.slack.com/docs/messages/builder)
> [Attaching content and links to messages | Slack](https://api.slack.com/docs/message-attachments)

### serverless設定

[Serverless FrameworkでAWS Lamda関数を作成する](https://qiita.com/Esfahan/items/736d09f732fa619d2410)という記事がとても参考になりました。
serverlessのインストールなどはこちら参考にしていただければ。。

ここではserverlessの設定ファイル`serverless.yml`のみ記載する。

```yml
service: trusted-advisor

provider:
  name: aws
  runtime: python3.6
  logRetentionInDays: 7 # Lambda関数のログの保持期間は7日とする

  stage: prod
  region: us-east-1 # リージョンは米国東部 (バージニア北部)にしなければならない

plugins:
  # デプロイ前に`npm install serverless-python-requirements`しておく必要あり
  # pipでインストールしたパッケージをrequirements.txtに記載しておくことで、デプロイパッケージに含めてくれるプラグイン
  - serverless-python-requirements

functions:
  alert:
    handler: handler.alert
    description: Trusted Advisorのステータス変更をSlackに通知する
    environment:
      SLACK_WEBHOOK_URL: https://hooks.slack.com/services/XXXXXXXXX # Slack WebhookURLを設定
    events:
      - cloudwatchEvent: # CloudWatch Eventの指定
          event:
            source:
              - "aws.trustedadvisor"
            detail-type:
              - "Trusted Advisor Check Item Refresh Notification"
            detail:
              status: # 通知するステータスを指定
                - ERROR
                - INFO
                - WARN
```


### デプロイ

    $ serverless deploy -v # ymlの設定間違っていないか確認
    $ serverless deploy # デプロイ実行！


### Lambda関数のテストで通知を確認
デプロイが完了したらAWSマネジメントコンソールのLambdaのページで、サンプルJSONでテスト実行をおこない、通知されるか確認する。

ステータスが`ERROR`の場合、下記のような通知が来た。
<img width="360" alt="IMG_D45686D5F40A-1.jpeg" src="https://qiita-image-store.s3.amazonaws.com/0/158743/36e4b2b0-0167-9f7a-85f2-88a032e09a81.jpeg">

これで完成。
serverlessのCLoudWatch Eventsの設定をカスタマイズすれば、通知条件を絞ったりできるので、
試してみるといいかも。