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)하여야 정상적으로 출력됩니다.
