2018. 10. 24
Notify CloudWatchAlarm of Slack with Metrics Graph.
Introduction
Hello everyone. You can now get CloudWatch's graph with API.
Until now, even if the alarm flies, it was necessary to access the management console and check the dashboard for the specific data transition situation.
I created a serverless application for notifying Slack of contents of CloudWatchAlarm with graph. Personally, I think that adding this function would make monitoring of AWS a lot easier.
https://github.com/k-masatany/cloudwatch_snapshot_graph_to_slack
Operation flow
- Pass over the threshold of CloudWatchAlarm
- SNS messages that has alarm details are published to Topic
- Invoke Lambda
- Get alarm details in SNS Topic and get CloudWatchMetrics images since 1 hour ago to now with
getMetricsGraphFromCloudWatch
API - Put the acquired data to S3
- Create slack webhook payload using SNS information and the URL of the put image
- Notification to Slack
CloudFormation
To deploy this application, use AWS SAM.
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
cloudwatch_snapshot_graph_to_slack
Cloudwatch alarm notification to Slack with snapshot graph.
Parameters:
SNSTopic:
Type: String
Default: 'alarm-notice-topic'
AssetsBucketName:
Type: String
Default: 'image-upload-bucket'
WebHookURL:
Type: String
Default: 'https://example.com'
Channel:
Type: String
Default: 'slack-channel'
Username:
Type: String
Default: 'SlackBot'
IconEmoji:
Type: String
Default: ':slack:'
Resources:
Function:
Type: AWS::Serverless::Function
Properties:
Description: 'post sns messages to slack incoming webhooks'
CodeUri: cloudwatch_snapshot_graph_to_slack/
Handler: app.lambdaHandler
Runtime: nodejs8.10
Timeout: 30
MemorySize: 512
Events:
SNS:
Type: SNS
Properties:
Topic: !Ref Topic
Environment:
Variables:
SLACK_WEBHOOK_URL: !Ref WebHookURL
SLACK_CHANNEL: !Ref Channel
SLACK_USERNAME: !Ref Username
SLACK_ICONEMOJI: !Ref IconEmoji
BACKET_NAME: !Ref AssetsBucketName
TZ: Asia/Tokyo
Role: !GetAtt IamRole.Arn
AssetsBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref AssetsBucketName
AccessControl: PublicRead
LifecycleConfiguration:
Rules:
- Status: Enabled
ExpirationInDays: 7
WebsiteConfiguration:
IndexDocument: index.html
ErrorDocument: error.html
BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref AssetsBucket
PolicyDocument:
Statement:
- Action:
- 's3:GetObject'
Effect: 'Allow'
Resource: !Sub arn:aws:s3:::${AssetsBucket}/*
Principal: '*'
Topic:
Type: 'AWS::SNS::Topic'
Properties:
TopicName: !Ref SNSTopic
IamRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: 'sts:AssumeRole'
Policies:
- PolicyName: 'cloudwatch_snapshot_graph_to_slack'
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: 'Allow'
Action:
- 'autoscaling:Describe*'
- 'cloudwatch:Describe*'
- 'cloudwatch:Get*'
- 'cloudwatch:List*'
- 'logs:Get*'
- 'logs:List*'
- 'logs:Describe*'
- 'logs:TestMetricFilter'
- 'logs:FilterLogEvents'
- 'sns:List*'
- 'sns:Get*'
Resource: '*'
- Effect: 'Allow'
Action:
- 'logs:CreateLogGroup'
- 'logs:CreateLogStream'
- 'logs:PutLogEvents'
Resource: 'arn:aws:logs:*:*:*'
- Effect: 'Allow'
Action:
- s3:PutObject
Resource: !Sub arn:aws:s3:::${AssetsBucket}/*
Edit Parameter for your own environment and use it. Since Makefile is prepared, it can also be overwritten with environment variable.
In order to deploy SAM, you need S3 bucket ahead of time, so please make it in advance. Once deployed, SNS, Lambda, and S3 buckets for publishing are created, Set CloudWatchAlarm notification destination to created SNS.
Lambda code
The code of Lambda running on this serverless application is as follows. The runtime is Node.js 8.10.
\"use strict\"
const axios = require(\"axios\")
var AWS = require(\"aws-sdk\")
const BASE_URL = process.env.SLACK_WEBHOOK_URL
async function getMetricsGraphFromCloudWatch(MessageId, message) {
const props = {
width: 320,
height: 240,
start: \"-PT1H\",
end: \"PT0H\",
timezone: \"+0900\",
view: \"timeSeries\",
stacked: false,
metrics: [
[
message.Trigger.Namespace,
message.Trigger.MetricName,
message.Trigger.Dimensions[0].name,
message.Trigger.Dimensions[0].value
]
],
stat:
message.Trigger.Statistic.charAt(0).toUpperCase() +
message.Trigger.Statistic.slice(1).toLowerCase(),
period: message.Trigger.Period
}
const widgetDefinition = {
MetricWidget: JSON.stringify(props)
}
const cloudwatch = new AWS.CloudWatch()
const s3 = new AWS.S3()
try {
const cw_res = await cloudwatch
.getMetricWidgetImage(widgetDefinition)
.promise()
const res = await s3
.putObject({
Bucket: process.env.BACKET_NAME,
Key: `${MessageId}.png`,
Body: cw_res.MetricWidgetImage,
ContentType: \"image/png\"
})
.promise()
return res
} catch (err) {
console.error(err)
}
}
async function createPayload(records) {
let attachments = []
for (const record of records) {
const sns = record.Sns
const message = JSON.parse(sns.Message)
let color = \"good\"
if (message.NewStateValue == \"ALARM\") {
color = \"danger\"
} else if (message.NewStateValue != \"OK\") {
color = \"warning\"
}
await getMetricsGraphFromCloudWatch(sns.MessageId, message)
let attachment = {
fallback: sns.Subject,
title: message.AlarmName,
pretext: sns.Subject,
color: color,
text: message.NewStateReason,
image_url: `https://s3-ap-northeast-1.amazonaws.com/${
process.env.BACKET_NAME
}/${sns.MessageId}.png`
}
attachments.push(attachment)
}
return {
channel: process.env.SLACK_CHANNEL,
username: process.env.SLACK_USERNAME,
icon_emoji: process.env.SLACK_ICONEMOJI,
attachments: attachments
}
}
function createOptions(payload) {
let options = {
method: \"post\",
baseURL: BASE_URL,
headers: {
\"Content-Type\": \"application/x-www-form-urlencoded; charset=UTF-8\"
},
data: `payload=${JSON.stringify(payload)}`
}
return options
}
exports.lambdaHandler = async (event, context) => {
const payload = await createPayload(event.Records)
const options = createOptions(payload)
try {
const res = await axios.request(options)
response = {
statusCode: 200,
body: JSON.stringify({
message: \"hello world\",
location: res.data.trim()
})
}
} catch (err) {
console.log(err)
return err
}
return response
}
Result
With this serverless application, Slack will be notified like this.
Since it is possible to check the time series data together with the alert, it is very easy because you do not have to check the dashboard every time and check the data transition.
Conclusion
This time I tried to notify the content of CloudWatchAlarm with Slack with image. Since I feel that this is very convenient by looking at the actual notice content, I would like to refine the application during the course of using.