CloudWatchアラートを重要度で色分けしてSlackへ通知する

### なぜ、アラートの重要度で色分けが必要なのか

現状、CloudWatchメトリクスでしきい値を超えるとSlackへアラートが飛ぶようになっています。
しかし、新たにSlackチャンネルへ入ってきた人は、通知を見ても何が重大なのかわからないのが実際のところです。
さらに、通知が来るたびにダッシュボードに確認しに行くのも手間です。
そもそも、必要ない通知があることも、、、

そこで、通知の精査と重要度分けが必要だなということで色分けすることにしました。

### なにを使っているのか

以下、構成

```
CloudWatchメトリクス → Simple Notification Service (SNS) → AWS Lambda Function (Lambda) → Slack通知
```

### どうやって構築するか

<img width="1093" alt="スクリーンショット 2018-12-08 23.28.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/259386/63456c8e-0442-f7f3-1d1d-6a19e1393b1d.png">


`SNSを重要度別に複数作成し、LambdaでどのSNSかを判定し、色分けしてSlackへ通知`というのがミソになります

#### Slack通知用のLambdaを作成

Slack通知に関しては基本的に以下を参考にしています。

- https://dev.classmethod.jp/cloud/aws/lambda-blueprint-slack-cloudwatch/

通知用のLambdaで実行するpythonコードです。
※ ちなみに、以下のコードではKMSを使っていません。

```python:lambda_function.py

from __future__ import print_function

import boto3
import json
import logging
import os

from base64 import b64decode
from urllib2 import Request, urlopen, URLError, HTTPError


HOOK_URL = os.environ['HookUrl']
SLACK_CHANNEL = os.environ['slackChannel']

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

AWS_REGION = os.environ['AWS_REGION']

def lambda_handler(event, context):
    logger.info("Event: " + str(event))
    message = json.loads(event['Records'][0]['Sns']['Message'])
    topicArn = str(event['Records'][0]['Sns']['TopicArn'])
    logger.info("Message: " + str(message))

    alarm_name = message['AlarmName']
    new_state = message['NewStateValue']
    reason = message['NewStateReason']

    # topicArnをeventからパースして、WarningかCriticalが含まれているかを判定しています
    if ("slack-alert-warning" in topicArn):
        color = "warning"
    elif ("slack-alert-critical" in topicArn):
        color = "danger"

    if (new_state == "ALARM"):
        new_state = ":exclamation: " + new_state
    elif (new_state == "OK"):
        new_state = ":white_check_mark: " + new_state
        color = "good"

    url = "<https://" + AWS_REGION + ".console.aws.amazon.com/cloudwatch/home?region=" + AWS_REGION + "#cw:dashboard=Home|Link to cloudwatch>"

    # attachmentsを使えばcolorで色付けできます
    slack_message = {
        'channel': SLACK_CHANNEL,
        'attachments': [{ "color": color, "text": "%s \n alarm_name: `%s` \n state is now %s: ```%s```" % (url, alarm_name, new_state, reason) }]
    }

    req = Request(HOOK_URL, json.dumps(slack_message))
    try:
        response = urlopen(req)
        response.read()
        logger.info("Message posted to %s", slack_message['channel'])
    except HTTPError as e:
        logger.error("Request failed: %d %s", e.code, e.reason)
    except URLError as e:
        logger.error("Server connection failed: %s", e.reason)

```

LambdaをCloudFormationで作る場合は以下を参考にしてください。
上記のURL通りに、手で作るのもOKです！

```yaml:alert-lambda.yaml

  LambdaFunction:
    Type: "AWS::Lambda::Function"
    Properties: 
      Code:
        S3Bucket: << ソースを置いているバケット名 >>
        S3Key: << バケットからソースへの相対パス >>
      FunctionName: Alert-message-to-slack
      Handler: lambda_function.lambda_handler
      MemorySize: 128
      Role: lambda_basic_execution
      Runtime: python3.6
      Environment:
        Variables:
          HookUrl: << IncomingWebのURL >>
          slackChannel: << アラートを流すSlackのChannel名 >>
```


#### CloudFormationでSNSを作成

```yaml:sns-topic.yaml

---
AWSTemplateFormatVersion: '2010-09-09'

  Parameters:
    LambdaFunction:
      Type: String
      Default: << lambda fuction の ARN >>

  Resources:
# SNS Topic
    WarningLevelAlert:
      Type: AWS::SNS::Topic
      Properties:
        DisplayName: slack alert
        TopicName: slack-alert-warning
        Subscription:
          - Endpoint: ! Ref LambdaFunction
            Protocol: lambda
    CriticalLevelAlert:
      Type: AWS::SNS::Topic
      Properties:
        DisplayName: slack alert
        TopicName: slack-alert-critical
        Subscription:
          - Endpoint:  ! Ref LambdaFunction
            Protocol: lambda
# Lambda Permission
    WarningLevelAlertPermission:
      Type: AWS::Lambda::Permission
      Properties:
        Action: lambda:InvokeFunction
        FunctionName: !GetAtt LambdaFunction.Arn
        Principal: sns.amazonaws.com
        SourceArn: !Ref WarningLevelAlert
    CriticalLevelAlertPermission:
      Type: AWS::Lambda::Permission
      Properties:
        Action: lambda:InvokeFunction
        FunctionName: !GetAtt LambdaFunction.Arn
        Principal: sns.amazonaws.com
        SourceArn: !Ref CriticalLevelAlert

  Outputs:
    WarningLevelAlertSNSTopic:
      Description: SNS Topic Name
      Value: !Ref WarningLevelAlert
      Export: 
        Name: WarningLevelAlertSNSTopic
    CriticalLevelAlertSNSTopic:
      Description: SNS Topic Name
      Value: !Ref CriticalLevelAlert
      Export: 
        Name: CriticalLevelAlertSNSTopic
```


#### 試しに、lambda function作成時にアラームを設定

LambdaのメトリクスにSNSを登録する部分は共通部分になるので、１つのyamlに分けS3へアップロードしておく。

```yaml:cloudwatch-lambda-alert.yaml

---
AWSTemplateFormatVersion: '2010-09-09'
Description: Lambda Cloudwatch
Parameters:
  FunctionName:
    Type: String
    Default: << lambda function の名前 >>
  Errors:
    Type: Number
    Default: 1

Resources:
  ErrorsAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName:
        Fn::Join:
          - ''
          - - !Ref FunctionName
            - '/Lambda/Errors'
      AlarmDescription:
        Fn::Join:
          - ''
          - - 'Lambda/Errors/'
            - !Ref FunctionName
            - '/'
            - !Ref Errors
            - 'over'
      AlarmActions:
        - !ImportValue CriticalLevelAlertSNSTopic
      MetricName: Errors
      Namespace: AWS/Lambda
      Statistic: Sum
      Period: '60'
      EvaluationPeriods: '1'
      Threshold: !Ref Errors
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value: !Ref FunctionName

```

Lambda作成時に、LambdaのメトリクスにSNSを登録する

```yaml:lambda.yaml

# 省略

  LambdaFunction:
    Type: "AWS::Lambda::Function"

# 省略

  CloudWatch:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL:
        Fn::Join:
          - ''
          - - 'https://s3.amazonaws.com/'
            - << S3 バケット名 >>
            - '/cloudwatch-lambda-alert.yaml'
      Parameters:
        FunctionName: !Ref LambdaFunction

```


### 結果

Slackのアラート通知がわかりやすくなった、、かな
Linkの名前が変わっているのはご愛嬌ということでｗ

Warningのアラート
<img width="753" alt="スクリーンショット 2018-12-08 23.23.45.png" src="https://qiita-image-store.s3.amazonaws.com/0/259386/b790f607-c1fa-3498-2394-627fa83ed1df.png">

Criticalのアラート
<img width="786" alt="スクリーンショット 2018-12-08 23.25.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/259386/0a3ba75a-4fc7-a748-6969-88d43be29336.png">