Route 53을 이용하여 도메인의 상태를 모니터링하는 방법
AWS에서 특정 도메인 혹은 IP의 상태를 실시간으로 모니터링하고 장애 발생시 Slack으로 알림을 주는 시스템을 구축해보자.
Route 53의 모니터링 기능
일반적으로 Route 53은 AWS에서 도메인주소를 호스팅하는 도구로써 더 유명하지만 특정 도메인이나 IP 주소가 유효한지 검사할 수 있는 상태 검사 기능으로써의 역할도 수행할 수 있다. 이를 CloudWatch와 연계하면 검사 대상에게서 상태 이상이 감지되었을 때 경보를 발생시킬 수 있다.
사전 지식
Amazon SNS
AWS Lambda
선행 작업
이 글은 모니터링 경보 메시지를 Slack으로 수신받기 위해 Slack Webhook URL을 사전 발급해야한다.
작업 순서
- Route 53 상태 검사 구성
- SNS 생성
- CloudWatch 경보 생성
- Lambda 트리거 연결 및 소스코드 작성
1. Route 53 상태 검사 구성
AWS Route 53 메인페이지로 접속하여 상태 검사 생성 버튼을 클릭한다.
모니터링 대상을 엔드포인트로 선택하고 모니터링의 이름을 짓는다.
모니터링 할 엔드포인트의 프로토콜, 주소, 포트, 경로를 지정한다. (AWS에서 호스팅중이 아닌 엔드포인트도 지정 가능)
고급 구성에서는 모니터링의 동작을 커스터마이징 할 수 있는데 기능마다 요금이 추가되니 적당히 쓰도록 한다.
경보 생성은 나중에 별도로 연결할 예정이니 아니오를 선택하고 상태 검사 생성 버튼을 클릭한다.
생성이 완료되면 목록에 방금 추가한 모니터링 이름이 표시된다. 첫 상태는 알 수 없음으로 표기되며, 1분 이후부터는 엔드포인트의 상태가 제대로 표시된다.
2. SNS 생성
us-east-1 리전의 SNS 페이지에서 접속하여 주제 생성 버튼을 클릭한다.
유형은 표준으로 선택하고 주제 이름을 지어준다. 주제 생성 버튼 클릭
SNS 주제가 만들어졌다. 이제 CloudWatch에 연결할 수 있는 상태가 되었다.
3. CloudWatch 경보 생성
us-east-1 리전의 CloudWatch 페이지에 접속하여 경보 생성 버튼을 클릭한다.
지표 선택 버튼을 클릭한다.
찾아보기 탭의 지표 목록에서 Route 53을 클릭한다.
X축 데이터로 사용할 HealthCheckPercentageHealthy 를 선택한다.
HealthCheckPercentageHealthy 지표는 모니터링중인 엔드포인트의 상태를 정상으로 보고한 Route 53 상태 검사기 비율을 나타낸다. 예를 들어 4개의 검사기중 3개만 정상으로 보고했다면 지표의 값은 75(단위 %)가 된다.
그래프로 표시된 지표 탭에서 HealthCheckPercentageHealthy 레이블을 선택하고 지표 선택 버튼을 클릭한다.
지표 이름을 지어주고 통계는 평균으로 선택한다. 기간(Periods)은 지표 수집 간격을 의미하는데 높은 정확성을 위해서 짧은 주기인 1분을 선택한다.
임계값 유형을 정적으로 선택하고 임계값은 80으로 지정한다. 경보 조건은 < 임계값으로 선택한다.
추가 구성에서는 경보의 민감도를 조절할 수 있도록 데이터포인트 수와 평가 기간을 조절한다.
위 이미지대로 설정하면 5분 동안 60초 간격으로 기록된 데이터포인트에서 임계값 80 미만인 횟수가 5번 감지되는 경우 경보가 발생하게 된다.
다음 버튼을 클릭한다.
엔드포인트의 비정상 상태를 알리기 위한 경보이므로 트리거 조건은 경보 상태로 선택하고 경보 알람은 아까 만들어두었던 SNS를 선택한다.
마지막으로 CloudWatch 경보의 이름과 설명을 지어준다. 이름과 설명 정보는 SNS 알람을 통해 전달되므로 적절하게 작성하자.
4. Lambda 트리거 연결 및 소스코드 작성
us-east-1 리전에 Lambda를 생성하고 트리거 추가 버튼을 클릭한다. 여기서는 엔드포인트의 비정상 경보 메시지를 가공 및 Slack으로 전달하는 역할을 할 것이다.
이 Lambda는 SNS 알람 발생이 감지될 때 실행되도록 아까 만들어두었던 SNS를 트리거로 추가한다. 이제 이 Lambda는 SNS 알람이 발생하면 자동으로 실행된다.
구성 탭으로 돌아와서 환경 변수를 편집해야한다. 사전에 미리 발급해둔 Slack webhook URL을 환경 변수로 추가해준다.
import https from 'https';
// 구성 -> 환경변수로 webhook을 받도록 합니다.
const ENV = process.env
if (!ENV.webhook) throw new Error('Missing environment variable: webhook')
const webhook = ENV.webhook;
const statusColorsAndMessage = {
ALARM: {"color": "danger", "message":"위험"},
INSUFFICIENT_DATA: {"color": "warning", "message":"데이터 부족"},
OK: {"color": "good", "message":"정상"}
}
const processEvent = async (event) => {
const snsMessage = event.Records[0].Sns.Message;
const postData = buildSlackMessage(JSON.parse(snsMessage))
await postSlack(postData, webhook);
}
const buildSlackMessage = (data) => {
const newState = statusColorsAndMessage[data.NewStateValue];
const oldState = statusColorsAndMessage[data.OldStateValue];
const executeTime = toYyyymmddhhmmss(data.StateChangeTime);
const description = data.AlarmDescription;
const cause = getCause(data);
return {
attachments: [
{
pretext: '서버 경보가 발생하였습니다.',
color: newState.color,
fields: [
{
title: '경보 이름',
value: data.AlarmName
},
{
title: '발생 시각',
value: executeTime
},
{
title: '설명',
value: description
},
{
title: '원인',
value: cause
},
{
title: '이전 상태',
value: oldState.message,
short: true
},
{
title: '현재 상태',
value: `*${newState.message}*`,
short: true
},
{
title: '바로가기',
value: createLink(data)
}
],
"footer": "AWS CloudWatch"
}
]
}
}
// Generate CloudWatch link
const createLink = (data) => {
return `https://console.aws.amazon.com/cloudwatch/home?region=${exportRegionCode(data.AlarmArn)}#alarm:alarmFilter=ANY;name=${encodeURIComponent(data.AlarmName)}`;
}
// Export region code from arn
const exportRegionCode = (arn) => {
return arn.replace("arn:aws:cloudwatch:", "").split(":")[0];
}
const getCause = (data) => {
const trigger = data.Trigger;
const evaluationPeriods = trigger.EvaluationPeriods;
const datapointsToAlarm = trigger.DatapointsToAlarm;
const minutes = Math.floor(trigger.Period / 60);
return `${evaluationPeriods * minutes}분 동안 ${trigger.Period}초 간격의 ${datapointsToAlarm}개 데이터포인트에서 임계값 이탈 감지`;
}
// Convert timezone UTC -> KST
const toYyyymmddhhmmss = (timeString) => {
if(!timeString){
return '';
}
const kstDate = new Date(new Date(timeString).getTime() + 32400000);
function pad2(n) { return n < 10 ? '0' + n : n }
return kstDate.getFullYear().toString()
+ '-'+ pad2(kstDate.getMonth() + 1)
+ '-'+ pad2(kstDate.getDate())
+ ' '+ pad2(kstDate.getHours())
+ ':'+ pad2(kstDate.getMinutes())
+ ':'+ pad2(kstDate.getSeconds());
}
const postSlack = async (message, slackUrl) => {
return await request(options(slackUrl), message);
}
// Build HTTP request options
const options = (slackUrl) => {
const { host, pathname } = new URL(slackUrl);
return {
hostname: host,
path: pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
};
}
// HTTP request send
const request = (options, data) => {
return new Promise((resolve, reject) => {
const req = https.request(options, (res) => {
res.setEncoding('utf8');
let responseBody = '';
res.on('data', (chunk) => {
responseBody += chunk;
});
res.on('end', () => {
resolve(responseBody);
});
});
req.on('error', (err) => {
reject(err);
});
req.write(JSON.stringify(data));
req.end();
});
}
export const handler = async (event) => {
await processEvent(event);
}
위 소스코드는 SNS에서 전달된 event(경보 메시지) 객체를 보기 좋게 가공하여 webhook url로 메시지를 post하는 로직이다.
해당 내용을 코드 탭에서 작성하고 Deploy 버튼을 눌러 최종 배포하도록 하자.
{
"Records": [
{
"EventSource": "aws:sns",
"EventVersion": "1.0",
"EventSubscriptionArn": "arn:aws:sns:us-east-1:{AWS 계정코드}:{경보 이름}:{경보 id}",
"Sns": {
"Type": "Notification",
"MessageId": "{메시지 id}",
"TopicArn": "arn:aws:sns:us-east-1:{AWS 계정코드}:{경보 이름}",
"Subject": "ALARM: \"{SNS 이름}\" in US East (N. Virginia)",
"Message": "{\"AlarmName\":\"{SNS 이름}\",\"AlarmDescription\":\"{SNS 설명}\",\"AWSAccountId\":\"{AWS 계정코드}\",\"AlarmConfigurationUpdatedTimestamp\":\"2023-10-23T01:29:47.051+0000\",\"NewStateValue\":\"ALARM\",\"NewStateReason\":\"Threshold Crossed: 5 out of the last 5 datapoints were less than the threshold (80.0). The most recent datapoints which crossed the threshold: [0.0 (23/10/23 04:22:00), 0.0 (23/10/23 04:21:00), 0.0 (23/10/23 04:20:00), 0.0 (23/10/23 04:19:00), 10.416666666666666 (23/10/23 04:18:00)] (minimum 5 datapoints for OK -> ALARM transition).\",\"StateChangeTime\":\"2023-10-23T04:23:02.911+0000\",\"Region\":\"US East (N. Virginia)\",\"AlarmArn\":\"arn:aws:cloudwatch:us-east-1:{AWS 계정코드}:alarm:{SNS 이름}\",\"OldStateValue\":\"OK\",\"OKActions\":[],\"AlarmActions\":[\"arn:aws:sns:us-east-1:{AWS 계정코드}:{경보 이름}\"],\"InsufficientDataActions\":[],\"Trigger\":{\"MetricName\":\"HealthCheckPercentageHealthy\",\"Namespace\":\"AWS/Route53\",\"StatisticType\":\"Statistic\",\"Statistic\":\"AVERAGE\",\"Unit\":null,\"Dimensions\":[{\"value\":\"{지표 id}\",\"name\":\"HealthCheckId\"}],\"Period\":60,\"EvaluationPeriods\":5,\"DatapointsToAlarm\":5,\"ComparisonOperator\":\"LessThanThreshold\",\"Threshold\":80.0,\"TreatMissingData\":\"missing\",\"EvaluateLowSampleCountPercentile\":\"\"}}",
"Timestamp": "2023-10-23T04:23:02.961Z",
"SignatureVersion": "1",
"Signature": "{인증값}",
"SigningCertUrl": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-{SNS id}.pem",
"UnsubscribeUrl": "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:{AWS 계정코드}:{경보 이름}:{경보 id}",
"MessageAttributes": {}
}
}
]
}
예시 샘플 데이터
※ https://jojoldu.tistory.com/586 의 소스코드를 수정하였습니다.
실제로 경보가 발생하면?
CloudWatch가 감시중인 HealthCheckPercentageHealthy 지표에서 임계값을 이탈하면 CloudWatch는 SNS로 신호를 전달하며 해당 SNS를 구독중인 Lambda가 경보 메시지를 전달받아 가공하여 Slack으로 최종 전달하게 된다.
이로써 엔드포인트에 장애가 발생했을 시 팀 Slack에 빠르게 공유하여 신속한 장애대응을 할 수 있도록하는 시스템을 구축해보았다.
'Operation > AWS' 카테고리의 다른 글
[AWS] EC2 인스턴스 구축하기 (1) | 2023.11.28 |
---|---|
[AWS] EC2 인스턴스로 AMI를 만드는 방법 (0) | 2023.11.01 |
[AWS] VPC 구축하기 (0) | 2022.12.26 |
댓글