CloudFormationテンプレート(JSON) - SNS トリガで Lambda を呼び出して Slack へ

#1. 概要
* CloudWatch Alarm の通知を Slack へ送るための設定を CloudFormation 一発で。
* Lambdaの完成形はこんな感じ<br />
![スクリーンショット 2018-01-16 20.47.58.png](https://qiita-image-store.s3.amazonaws.com/0/148868/60521592-edf6-6783-7bde-a165cf98b0d1.png)
 * SNS：トリガ
 * Amazon CloudWatch Logs：Lambda の実行ログをを CloudWatch Logs へ送るため
 * Amazon S3：S3バケットにある Lambda 関数のファイルを get するため
* CloudFormationで作成するリソース
 * IAM Role
 * SNS Topic + Subscription
 * Lambda Function + Lambda Permission

#2. 事前に行なうこと
* 通知を受けるSlack チャンネルの incoming-webhook URL を発行する
* Lambda 関数を S3 へ put する

#3. Lambda 関数
* SNS から受け取った CloudWatch Alarm の内容を Slack へ投げる Python3.6 コード

```slack-syscrit.py
import os
import json
import logging
from datetime import datetime, timedelta
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError

Slack_Webhook_URL = os.environ['SLACK_WEBHOOK_URL']

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

def lambda_handler(event, context):
    logger.info("Event: " + str(event))
    message = json.loads(event['Records'][0]['Sns']['Message'])
    logger.info("Message: " + str(message))
    
    NewStateValue = message['NewStateValue']
    NewStateReason = message['NewStateReason']
    AlarmName = message['AlarmName']
    StateChangeTime = (datetime.strptime(message['StateChangeTime'][:-9], '%Y-%m-%dT%H:%M:%S') + timedelta(hours=9)).strftime("%Y/%m/%d %H: %M :%S")
    SlackText = "[" + NewStateValue + "] " + AlarmName
    SlackAttachments = message['AlarmDescription'] + "\n" + message['NewStateReason'] + "\n" + StateChangeTime

    color = "danger"
    icon = ":scream:"
    if NewStateValue == "OK":
        color = "good"
        icon = ":wink:"
    
    slack_message = {
        'username': "AWS CloudWatch Alarm",
        'text': SlackText,
        'icon_emoji': icon,
        'attachments': [
            {
                "color": color,
                "text": SlackAttachments
            }
        ]
    }
    
    req = Request(Slack_Webhook_URL, json.dumps(slack_message).encode('utf-8'))
    try:
        response = urlopen(req)
        response.read()
        logger.info("Message posted to %s", Slack_Webhook_URL)
    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)
```
* これをこのまま Zip で固めて、「slack-syscrit.py.zip」ファイルを S3 のバケットに put する
 * 今回は、バケット「lambda-function-AWSアカウントID」配下に「slack-syscrit.py.zip」を置いた

#4. CloudFormation
```SNS-Lambda-Slack.json
{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Description": "CloudWatch Alarm -> SNS -> Lambda -> Slack #syscrit",
    "Resources": {
        "IAMRole" : {
            "Type": "AWS::IAM::Role",
            "Properties": {
                "AssumeRolePolicyDocument": {
                    "Version": "2012-10-17",
                    "Statement": [
                        {
                            "Effect": "Allow",
                            "Principal": {
                                "Service": "lambda.amazonaws.com"
                            },
                            "Action": "sts:AssumeRole"
                        }
                    ]
                },
                "ManagedPolicyArns": [
                    "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
                    "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
                ],
                "Path": "/",
                "RoleName": "lambda-slack-syscrit"
            }
        },
        "LambdaFunction" : {
            "DependsOn" : [ "IAMRole" ],
            "Type" : "AWS::Lambda::Function",
            "Properties" : {
                "Code" : {
                    "S3Bucket" : "lambda-function-AWSアカウントID",
                    "S3Key" : "slack-syscrit.py.zip"
                },
                "Environment" : { "Variables" : { "SLACK_WEBHOOK_URL": "通知を受けるSlack チャンネルの incoming-webhook URL" } },
                "FunctionName" : "slack-syscrit",
                "Handler" : "slack-syscrit.lambda_handler",
                "Role" : { "Fn::GetAtt" : [ "IAMRole", "Arn" ] },
                "Runtime" : "python3.6",
                "Timeout" : 10
            }
        },
        "SNSTopic": {
            "DependsOn" : [ "LambdaFunction" ],
            "Type" : "AWS::SNS::Topic",
            "Properties" : {
                "DisplayName" : "slack syscrit",
                "TopicName" : "slack-syscrit",
                "Subscription": [
                    {
                        "Endpoint" : { "Fn::GetAtt" : [ "LambdaFunction", "Arn" ] },
                        "Protocol": "lambda"
                    }
                ]
            }
        },
        "LambdaPermission" : {
            "Type" : "AWS::Lambda::Permission",
            "Properties" : {
                "Action" : "lambda:InvokeFunction",
                "FunctionName" : { "Fn::GetAtt" : [ "LambdaFunction", "Arn" ] },
                "Principal" : "sns.amazonaws.com",
                "SourceArn" : { "Ref" : "SNSTopic" }
            }
        }
    },
    "Outputs" : {
        "IAMRole" : {
            "Description" : "IAM Role Name",
            "Value" : { "Ref" : IAMRole }
        },
        "SNSTopic" : {
            "Description" : "SNS Topic Name",
            "Value" : { "Fn::GetAtt" : [ "SNSTopic", "TopicName" ] }
        },
        "LambdaFunction" : {
            "Description" : "Lambda Function Name",
            "Value" : { "Ref" : LambdaFunction }
        }
    }
}
```
* ２箇所を適宜変更
 * "S3Bucket" : "<font color="Red">lambda-function-AWSアカウントID</font>"
 * "Environment" : { "Variables" : { "SLACK_WEBHOOK_URL": "<font color="Red">通知を受けるSlack チャンネルの incoming-webhook URL</font>" } },


#5. 参考
* https://iangilham.com/2016/03/22/Sns-trigger-lambda-via-cloudformation.html
 * SNS トリガの場合は、AWS::Lambda::Permission　の プロパティに SourceAccount を入れてはいけない。