Skip to content

Instantly share code, notes, and snippets.

@nonbeing
Last active March 1, 2022 15:02
Show Gist options
  • Save nonbeing/5cfb87afbaef57effdc2d3808e16855a to your computer and use it in GitHub Desktop.
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.
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