Last active
March 1, 2022 15:02
-
-
Save nonbeing/5cfb87afbaef57effdc2d3808e16855a to your computer and use it in GitHub Desktop.
AWS Lambda Function to handle CloudWatch SNS events for AutoScaling and post notifications to Slack.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var AWS = require('aws-sdk'); | |
var url = require('url'); | |
var https = require('https'); | |
var hookUrl, slackChannel; | |
hookUrl = 'https://hooks.slack.com/services/ABC/DEF/XYZ'; // Enter your Slack hook URL here | |
slackChannel = 'MySlackChannel'; // Enter the Slack channel to send a message to | |
var postMessage = function(message, callback) { | |
var body = JSON.stringify(message); | |
var options = url.parse(hookUrl); | |
options.method = 'POST'; | |
options.headers = { | |
'Content-Type': 'application/json', | |
'Content-Length': Buffer.byteLength(body) | |
}; | |
var postReq = https.request(options, function(res) { | |
var chunks = []; | |
res.setEncoding('utf8'); | |
res.on('data', function(chunk) { | |
return chunks.push(chunk); | |
}); | |
res.on('end', function() { | |
var body = chunks.join(''); | |
if (callback) { | |
callback({ | |
body: body, | |
statusCode: res.statusCode, | |
statusMessage: res.statusMessage | |
}); | |
} | |
}); | |
return res; | |
}); | |
postReq.write(body); | |
postReq.end(); | |
}; | |
var processEvent = function(event, context) { | |
// log the full SNS JSON - very very useful for debugging/troubleshooting/development | |
console.info('Received event:', JSON.stringify(event, null, 2)); | |
var message = JSON.parse(event.Records[0].Sns.Message); | |
console.info("SNS Message: ", JSON.stringify(message, null, 2)) | |
var alarmName, asgName, event, reason, cause, instanceId, oldState, newState; | |
if (typeof(message.Details) != 'undefined' && typeof(message.Details.InvokingAlarms) != 'undefined') { | |
// message type 1 - usual AutoScaling launch/terminate | |
console.info("Processing Message Type 1") | |
asgName = message.AutoScalingGroupName; | |
alarmName = message.Details.InvokingAlarms[0].AlarmName; | |
event = message.Event; | |
reason = message.Details.InvokingAlarms[0].NewStateReason; | |
oldState = message.Details.InvokingAlarms[0].OldStateValue; | |
newState = message.Details.InvokingAlarms[0].NewStateValue; | |
instanceId = message.EC2InstanceId; | |
cause = message.Cause; | |
description = message.Description; | |
} | |
else if (typeof(message.Details) != 'undefined' && typeof(message.Details.InvokingAlarms) === 'undefined'){ | |
// message type 2 - generic SNS events | |
// example: EC2 Instance Termination via ELB Health Checks | |
// contains 'Details', but 'Details' aren't useful | |
// doesn't contain 'InvokingAlarms' (because there aren't any InvokingAlarms) | |
console.info("Processing Message Type 2") | |
asgName = message.AutoScalingGroupName; | |
alarmName = "AutoScaling SNS for '" + asgName + "'"; | |
event = message.Event; | |
reason = 'None'; | |
oldState = 'None'; | |
newState = 'None'; | |
instanceId = message.EC2InstanceId; | |
cause = message.Cause; | |
description = message.Description; | |
} | |
else { | |
// message type 3: Just OK->ALARM transitions : NOISE | |
// neither contains 'Details' nor InvokingAlarms' | |
// doesn't necessarily correspond to AutoScaling launches/terminations | |
// just log to console and return | |
console.warn("IGNORING noise: OK->ALARM transistion"); | |
context.succeed(); | |
return; | |
/*alarmName = message.AlarmName; | |
reason = message.NewStateReason; | |
oldState = message.OldStateValue; | |
newState = message.NewStateValue;*/ | |
} | |
var now = Math.round((new Date).getTime()/1000); | |
var slackMessage = { | |
"channel": slackChannel, | |
"text": "*" + alarmName + " Invoked*", | |
"mrkdwn": true, | |
"attachments": [{ | |
"footer": "CloudCover Alert", | |
"footer_icon": "https://s3-us-west-2.amazonaws.com/slack-files2/avatars/2016-05-31/46880103908_d935a3be9933b2345829_88.png", | |
"ts": now, | |
"fields": [ | |
// Alarm OldState and NewState are kinda redundant and useless | |
// because NewState is almost always == 'ALARM', OldState == 'OK' | |
// No need to include them in the SlackMessage | |
{ | |
"title": "Event", | |
"value": event, | |
"short": true | |
}, | |
{ | |
"title": "Cause", | |
"value": cause, | |
"short": false | |
} | |
] | |
}] | |
}; | |
// if the SNS message had a 'reason' then append it: | |
if (typeof(reason) != 'undefined') { | |
slackMessage['attachments'][0]['fields'].push( | |
{ | |
"title": "Reason", | |
"value": reason, | |
"short": false | |
} | |
); | |
} | |
// if the SNS message had an 'instanceId' then append it: | |
if (typeof(instanceId) != 'undefined') { | |
slackMessage['attachments'][0]['fields'].push( | |
{ | |
"title": "Instance Id", | |
"value": instanceId, | |
"short": true | |
} | |
); | |
} | |
/* Take action based on the AutoScaling Event Type | |
There are only 4 types of AutoScaling events | |
(http://docs.aws.amazon.com/autoscaling/latest/userguide/ASGettingNotifications.html): | |
1) autoscaling:EC2_INSTANCE_LAUNCH | |
Successful instance launch | |
2) autoscaling:EC2_INSTANCE_LAUNCH_ERROR | |
Failed instance launch | |
3) autoscaling:EC2_INSTANCE_TERMINATE | |
Successful instance termination | |
4) autoscaling:EC2_INSTANCE_TERMINATE_ERROR | |
Failed instance termination | |
*/ | |
// EC2_INSTANCE_TERMINATE_ERROR or EC2_INSTANCE_LAUNCH_ERROR | |
if (event.toLowerCase().search("error") > -1) { | |
slackMessage['attachments'][0]['fallback'] = slackMessage['attachments'][0]['pretext'] = description; | |
slackMessage['attachments'][0]['color'] = "danger"; | |
slackMessage['attachments'][0]['fields'].push( | |
{ | |
"title": "Priority", | |
"value": "Critical", | |
"short": true | |
} | |
); | |
} | |
else if (event.toLowerCase().search("terminate") > -1) { | |
// SCALE-IN (contains 'terminate', doesn't contain 'error') | |
slackMessage['attachments'][0]['fallback'] = slackMessage['attachments'][0]['pretext'] = description; | |
slackMessage['attachments'][0]['color'] = "good"; | |
slackMessage['attachments'][0]['fields'].push( | |
{ | |
"title": "Priority", | |
"value": "Normal", | |
"short": true | |
} | |
); | |
} | |
else if (event.toLowerCase().search("launch") > -1) { | |
// SCALE-OUT (contains 'launch', doesn't contain 'error') | |
slackMessage['attachments'][0]['fallback'] = slackMessage['attachments'][0]['pretext'] = description; | |
slackMessage['attachments'][0]['color'] = "danger"; | |
slackMessage['attachments'][0]['fields'].push( | |
{ | |
"title": "Priority", | |
"value": "Critical", | |
"short": true | |
} | |
); | |
} | |
else { // unexpected event: report error | |
console.error("alarmName: '" + alarmName + "' was unexpected"); | |
slackMessage['attachments'][0]['pretext'] = "[ERROR] Alarm: '*" + alarmName + "*' was unexpected"; | |
slackMessage['attachments'][0]['text'] = "Report this error to ABC, DEF, XYZ"; | |
slackMessage['attachments'][0]['mrkdwn_in'] = ["text", "pretext"]; | |
} | |
postMessage(slackMessage, function(response) { | |
if (response.statusCode < 400) { | |
console.info('Message posted successfully'); | |
context.succeed(); | |
} else if (response.statusCode < 500) { | |
console.error("Error posting message to Slack API: " + response.statusCode + " - " + response.statusMessage); | |
context.succeed(); // Don't retry because the error is due to a problem with the request | |
} else { | |
// Let Lambda retry | |
console.error("5xx server error posting message to Slack API: " + response.statusCode + " - " + response.statusMessage); | |
context.fail("5xx server error when processing message: " + response.statusCode + " - " + response.statusMessage); | |
} | |
}); | |
}; | |
exports.handler = function(event, context) { | |
if (hookUrl) { | |
processEvent(event, context); | |
} else { | |
context.fail('Hook URL has not been set.'); | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment