[AWS] Slack Template으로 예쁜 AWS 알람 받기

1. 대상

  • 이미 Cloudwatch에서 알람을 Slack으로 받고 있지만, 더 꾸미고 싶다!
  • Javascript로 코드를 작성할 수 있다!

하는 분을 대상으로 글을 작성하였습니다.

2. Template 작성

const url = require("url");
const https = require("https");
// 1. URL 입력
const hookUrl =
"https://hooks.slack.com/services/<Your hook url>";
// 2. 식별자 입력: Instance ID, DB 식별자 등
// Cloudwatch Alarm 생성할 때 사용한 식별자 입력
let caseID = {
"database-1": "myDB",
"i-xxxxxxxxxxxx": "myInstance",
};

알람에서 식별자로 표시되는 경우, 사용자는 보기 어렵습니다.
우리가 이해하기 쉬운 식별자로 바꿔주기 위한 변수(caseID)를 선언합니다.

const processEvent = function (event, context) {
let message = JSON.parse(event.Records[0].Sns.Message);
let categorizaionCase, creationMessage;
try {
categorizaionCase = categorizeCase(message);
creationMessage = createMessage(message, categorizaionCase);
} catch (err) {
creationMessage = errorMessage(message);
} finally {
postMessage(creationMessage, function (response) {
if (response.statusCode < 400) {
console.info("Message posted!");
context.succeed();
} else if (response.statusCode < 500) {
console.error(
"4xx error occured when processing message: " +
response.statusCode +
" - " +
response.statusMessage
);
context.succeed(); // Don't retry when got 4xx cuz its request error
} else {
// Retry Lambda func when got 5xx errors
context.fail(
"Server error when processing message: " +
response.statusCode +
" - " +
response.statusMessage
);
}
});
}
};

cloudwatch에서 전달받은 message를 Parameter로 받아 처리하는 함수(processEvent)를 정의합니다.
Try – Catch – Finally 문을 활용하여 Slack에 보낼 message를 만들어 전달합니다.
Finally Block에서는 response의 statusCode에 따라 결과를 처리합니다.

const categorizeCase = function (message) {
let caseDiv = {
caseEmoji: {
OK: ":ok:",
ALARM: ":rotating_light:",
},
caseEvent: {
ALARM: {
CPU: "CPU 사용량이 기준을 초과하였습니다.",
Memory: "Memory 사용량이 기준을 초과하였습니다.",
Disk: "Disk 사용량이 기준을 초과하였습니다.",
EBS:
"I/O 지연 시간이 길어지고 있습니다. 애플리케이션이 프로비저닝한 것보다 많은 IOPS를 구동하려고 하고 있지 않은지 확인하십시오.",
Network: "수신받고 있는 패킷 수가 많습니다.",
Status: "EC2 상태 검사를 실패하였습니다.",
},
OK: {
CPU: "CPU 사용량이 정상으로 복구되었습니다.",
Memory: "Memory 사용량이 정상으로 복구되었습니다.",
Disk: "Disk 사용량이 정상으로 복구되었습니다.",
EBS: "I/O 지연 시간이 정상으로 복구되었습니다.",
Network: "Network가 정상으로 복구되었습니다.",
Status: "EC2 상태가 정상으로 복구되었습니다.",
},
},
casePlainText: {
ALARM: {
CPU: " CPU 사용량 초과",
Memory: " Memory 사용량 초과",
Disk: " Disk 사용량 초과",
EBS: " I/O 지연 시간 증가",
Network: " 수신받는 패킷수 확인 필요",
Status: " EC2 상태 검사 실패",
},
OK: {
CPU: " CPU 사용량 복구 완료",
Memory: " Memory 사용량 복구 완료",
Disk: " Disk 사용량 복구 완료",
EBS: " I/O 지연 시간 복구 완료",
Network: " Network 복구 완료",
Status: " EC2 상태 복구 완료",
},
},
};
let dimensionsName = ["InstanceId", "DBInstanceIdentifier"];
for (let dimensionValue of message.Trigger.Dimensions) {
if (dimensionsName.indexOf(dimensionValue.name) !== -1) {
caseDiv.caseInstance = dimensionValue.value;
break;
}
}
console.log(caseDiv.caseInstance);
switch (message.Trigger.MetricName) {
case "disk_used_percent":
message.Trigger.MetricName = "DiskUsedPercent";
break;
case "mem_used_percent":
message.Trigger.MetricName = "MemoryUsedPercent";
break;
}
if (caseDiv.caseInstance == undefined) {
throw new Error("Dimension name is not defined!");
}
if (message.AlarmName.search(new RegExp("CPU", "i")) !== -1) {
caseDiv.caseName = "CPU";
} else if (message.AlarmName.search(new RegExp("MEM", "i")) !== -1) {
caseDiv.caseName = "Memory";
} else if (message.AlarmName.search(new RegExp("DISK", "i")) !== -1) {
caseDiv.caseName = "Disk";
} else if (message.AlarmName.search(new RegExp("EBS", "i")) !== -1) {
caseDiv.caseName = "EBS";
} else if (message.AlarmName.search(new RegExp("NETWORK", "i")) !== -1) {
if (message.Trigger.MetricName == "NetworkPacketsIn") {
caseDiv.caseName = "Network";
} else {
throw new Error("Alarm Name is not defined!");
}
} else if (message.AlarmName.search(new RegExp("STATUS", "i")) !== -1) {
caseDiv.caseName = "Status";
} else {
throw new Error("Alarm Name is not defined!");
}
if (caseDiv.caseName == "Disk" && message.Trigger.Dimensions.length > 1) {
caseDiv.caseName += "( " + message.Trigger.Dimensions[0].value + " )";
}
return caseDiv;
};

Slack에 보낼 message를 만들 때 사용할 내용을 정리한 변수(caseDiv)를 만들어 return하는 함수(categorizeCase)를 정의합니다.

const unitAdd = function (message, metricName) {
let caseUnit = {};
if (metricName.search(new RegExp("free", "i")) !== -1) {
try {
caseUnit.old =
+(
Math.round(
+message.NewStateReason.match(
/([0-9]*)+(["."])+([0-9]*)+E+([0-9]*)/g
)[1] /
Math.pow(10, 9) +
"e+2"
) + "e-2"
) + " GB";
caseUnit.new =
+(
Math.round(
+message.NewStateReason.match(
/([0-9]*)+(["."])+([0-9]*)+E+([0-9]*)/g
)[0] /
Math.pow(10, 9) +
"e+2"
) + "e-2"
) + " GB";
} catch (err) {
console.log(err);
caseUnit.old =
+(
Math.round(
+message.NewStateReason.match(/([0-9]*)+(["."])+([0-9]*)/g)[1] +
"e+2"
) + "e-2"
) + " %";
caseUnit.new =
+(
Math.round(
+message.NewStateReason.match(/([0-9]*)+(["."])+([0-9]*)/g)[0] +
"e+2"
) + "e-2"
) + " %";
}
} else if (metricName.search(new RegExp("packet", "i")) !== -1) {
caseUnit.old = message.NewStateReason.match(/\([0-9]+/g)[1].split("(")[1];
caseUnit.new = message.NewStateReason.match(/\[[0-9]+/g)[0].split("[")[1];
} else if (metricName.search(new RegExp("Status", "i")) !== -1) {
caseUnit.old = message.NewStateReason.match(/\([0-9]+/g)[1].split("(")[1];
caseUnit.new = message.NewStateReason.match(/\[[0-9]+/g)[0].split("[")[1];
} else {
caseUnit.old =
+(
Math.round(
+message.NewStateReason.match(/([0-9])+(["."])+([0-9])/g)[1] + "e+2"
) + "e-2"
) + " %";
caseUnit.new =
+(
Math.round(
+message.NewStateReason.match(/([0-9])+(["."])+([0-9])/g)[0] + "e+2"
) + "e-2"
) + " %";
}
return caseUnit;
};
const createMessage = function (message, caseDiv) {
let newTime = new Date(
Date.parse(message.StateChangeTime.slice(0, 23) + "-09:00")
).toISOString();
let slackMessage = {
text:
"[" +
(caseID[caseDiv.caseInstance] != undefined
? caseID[caseDiv.caseInstance]
: caseDiv.caseInstance) +
"]" +
caseDiv.casePlainText[message.NewStateValue][
caseDiv.caseName.match(/[A-Za-z]+/g)[0]
] +
"\n ",
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text:
"현재 상태: " +
caseDiv.caseEmoji[message.NewStateValue] +
" \n" +
"발생시간: " +
newTime.slice(0, 10) +
" " +
newTime.slice(11, 19),
},
},
{
type: "divider",
},
{
type: "section",
fields: [
{
type: "mrkdwn",
text:
"*해당 서버:*\n>" +
(caseID[caseDiv.caseInstance] != undefined
? caseID[caseDiv.caseInstance]
: caseDiv.caseInstance) +
"\n ",
},
{
type: "mrkdwn",
text:
"*기준:*\n>" +
unitAdd(message, message.Trigger.MetricName).old +
"\n ",
},
{
type: "mrkdwn",
text: "*대상:*\n>" + caseDiv.caseName + "\n ",
},
{
type: "mrkdwn",
text:
"*현재:*\n>" +
unitAdd(message, message.Trigger.MetricName).new +
"\n ",
},
{
type: "mrkdwn",
text: "*지표:*\n>" + message.Trigger.MetricName + "\n ",
},
{
type: "mrkdwn",
text:
"*이벤트:*\n>" +
caseDiv.caseEvent[message.NewStateValue][
caseDiv.caseName.match(/[A-Za-z]+/g)[0]
],
},
],
},
{
type: "divider",
},
{
type: "actions",
elements: [
{
type: "button",
text: {
type: "plain_text",
text: "Monitor",
emoji: true,
},
url: "https://IloveMonitoring.com",
},
{
type: "button",
text: {
type: "plain_text",
text: "Console",
emoji: true,
},
url: "https://console.aws.amazon.com",
},
],
},
],
};
return slackMessage;
};

함수(categorizeCase)의 return 값으로 Slack에 보낼 message를 생성하는 함수(createMessage)를 정의합니다.
message에 단위를 추가할 수 있는 함수(unitAdd)를 활용합니다.

const errorMessage = function (message) {
let newTime = new Date(
Date.parse(message.StateChangeTime.slice(0, 23) + "-09:00")
).toISOString();
let slackMessage = {
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text:
"현재 상태: " +
message.NewStateValue +
" \n" +
"발생시간: " +
newTime.slice(0, 10) +
" " +
newTime.slice(11, 19),
},
},
{
type: "divider",
},
{
type: "section",
fields: [
{
type: "mrkdwn",
text: "*알람 이름:*\n>" + message.AlarmName + "\n ",
},
{
type: "mrkdwn",
text: "*수집 항목:*\n>" + message.Trigger.MetricName + "\n ",
},
{
type: "mrkdwn",
text: "*이벤트:*\n>" + message.NewStateReason + "\n ",
},
],
},
{
type: "divider",
},
{
type: "actions",
elements: [
{
type: "button",
text: {
type: "plain_text",
text: "Monitor",
emoji: true,
},
url: "https://IloveMonitoring.com",
},
{
type: "button",
text: {
type: "plain_text",
text: "Console",
emoji: true,
},
url: "https://console.aws.amazon.com",
},
],
},
],
};
return slackMessage;
};

Try Block에서 categorizeCase 함수 또는 createMessage 함수를 실행할 때 Error가 발생하는 경우,
간단한 내용만 정리하여 전달하는 함수(errorMessage)를 정의합니다.

const postMessage = function (message, callback) {
let body = JSON.stringify(message);
let options = url.parse(hookUrl);
options.method = "POST";
options.headers = {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(body),
};
let postReq = https.request(options, function (res) {
let chunks = [];
res.setEncoding("utf8");
res.on("data", function (chunk) {
return chunks.push(chunk);
});
res.on("end", function () {
let body = chunks.join("");
if (callback) {
callback({
body: body,
statusCode: res.statusCode,
statusMessage: res.statusMessage,
});
}
});
return res;
});
postReq.write(body);
postReq.end();
};
exports.handler = function (event, context) {
if (hookUrl) {
processEvent(event, context);
} else {
context.fail("Missing Slack Hook URL.");
}
};

만들어진 message를 Slack으로 전달하는 함수(postMessage)를 정의합니다.

마지막으로 알람을 만들 때 주의사항은 Cloudwatch에서 알람을 생성할 때
CPU, MEM, DISK, EBS, NETWORK 중 하나의 단어만 반드시 포함(대소문자 구분 X)하여야 정상적으로 출력됩니다.

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다