알람 경보 설정(CPU, Memory, Disk)

하드웨어의 각 리스소 사용량이 설정 기준치를 넘었을 시 대쉬보드를 통한 시각적 경보 효과 및 이메일, Slack을 통한 알람을 구현하는 과정을 정리한다.

  • 경보 환경 세팅
    • window/ubuntu 서버 모두 다양한 방법으로 하드웨어 부하를 테스트 할 수 있고, 그 중 가장 확인하기 쉬운 ubuntu서버에서 cpu부하를 통해 테스트 환경 설정.
      우분투 서버에서(ec2 인스턴스 원격 연결된) sudo yum install stress -y 명령어로 테스트 패키지 설치.
      그 다음 grep -c processor /proc/cpuinfo 명령어로 인스턴스의 cpu 코어 갯수 확인
    • 그 다음 아래 명령어 양식대로 cpu 부하 설정을 위한 코어 갯수 및 부하 기간 설정
      stress --cpu 코어갯수 --timeout 시간(초)
    • top 명령어를 통해 cpu 사용량이 100%까지 올라간 것을 확인.

cpu코어 수
20분동안 cpu util 100%부하
cpu사용량이 100%에 근접

  • 경보 위젯 확인
    • 데이터가 기준치를 넘었을 시 경보 목록과 대쉬보드 페이지에 경보가 생성됨.

 

 Email을 통한 알람 설정

  • SNS topic/subscription 설정: 이메일/Slack 채널 알람을 위해 sns 주제/구독 설정
  • 이메일 알람 확인: cloudwatch 콘솔 -> 경보 탭 '모든 경보' -> 경보 생성
  • 경보 생성 지표를 생성 후 지표 탭에서 경보 확인 기간을 설정(20분)
  • 참고) 기간은 경보를 생성하기 위해 지표를 평가하는 기간으로, 20분으로 설정 시 경보가 20분 당 한 번씩 평가함.(경보가 지속되는 상황에서는 최초 한번의 알람만 울림)
  • 이메일 경보 설정 예시

  • 그리고 경보 -> 조건 탭에서 경보 이벤트를 설정할 데이터 기준치를 설정

5분마다 한번씩 확인

 

cpu사용량이 60%이상일 때 알람설정

  • 추가 구성 탭 에서는 경보를 알릴 데이터 포인트를 설정하는데, default는 1/1로 되어있음.

왼쪽은 데이터 포인트 체크 횟수, 오른쪽은 기간*횟수를 의미함.

 

  • 실제 테스트 결과, 5분에 한 번 꼴로 알람이 울림.
    참고) https://docs.aws.amazon.com/ko_kr/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html
  • 그리고 그림에서 볼 수 있듯, 데이터 누락에 관한 경보도 설정할 수 있는데,
    다음-> 알림 탭에서 경보상태 트리거를 설정 및 알림 전송 엔드포인트(아까 생성한 SNS토픽 이름)선택-> 경보 이름, 설명 저장 후 경보 생성 완료-> 경보 환경 세팅 후 이메일 확인
  • 예시

Slack 알림 메시지 설정
이메일을 통한 알람 수신은 매우 번거롭고 확인하기 힘들기 때문에 팀에서 사용 중인 slack채널을 통해 빠르게 알람 수신할 수 있도록 설정할 수 있음.

특히 이메일 알람의 경우 기본 format을 편집할 수 없어서 불필요한 정보도 함께 전달되며, 즉각적으로 확인할 수 없음.
Slack알람 순서: 알림 받을 slack 채널 생성-> 수신 webhook 설정-> SNS 주제, 구독 생성-> lambda 함수 설정-> 동작 확인

  • 알람 수신용 채널 생성 및 설정경보 알림 전용 채널을 새로 생성(본인에게 알람 수신 원할 시 별도 생성은 필요하지 않음)

  • 해당 채널로 입장-> 상단 더보기-> 앱 클릭-> 'incoming webhooks' 검색 및 추가

  • 해당 채널의 웹훅 URL(aws에서 이벤트 발생 시 슬랙채널로 알림을 보낼 때 사용하는 주소)이 생성되고, 경보를 수신할 채널 이름, 사용자 지정 후 설정 저장
  • AWS Lambda의 알람 설정 함수가 해당 URL로 알람 전송->slack에서 확인

  • 설정 후 채널에 알림 문구가 도착한 것을 확인

  • 이제 aws sns topic/subscription를 설정해야 하는데, 그 전에 lambda에서 알림 전송용 함수를 생성함.(ARN 주소를 사용하기 위해 람다를 생성해야하므로 함수만 만들어놓고 topic생성하러 가기)

  • 람다 함수의 언어를 선택하고, 본인이 원하는 알람 포맷으로 코딩하여 테스트 진행하고 함수 ARN를 복사 후 새 탭을 열어서 aws sns 페이지로 이동.(python, node js 예제가 많이 있습니다.) 그다음 환경 변수에서 channel이름과 slack채널의 webhook URL을 입력
  • 앞서 진행 했듯 aws sns 주제/구독을 생성하되 구독 생성 시 프로토콜을 lambda로, 엔드포인트를 방금 생성한 lambda 함수 ARN을 입력.
    • 주의) 최초 topic생성의 경우 새 topic를 생성하지만, 앞서 이미 이메일 알람을 위한 topic 생성이 완료된 경우는 subscription만 생성.

  • 설정이 완료된 lambda상태

→ SNS topic인 HW_resource_alarm 트리거가 생성되어있음.

  • aws sns 구독 상태
    • 현재 이메일/slack 알람을 위한 두개의 엔드포인트가 등록되어 있음.

마찬가지로 경보를 생성하고 cpu 부하를 테스트한 뒤 60%가 넘어가면 아래와 같이 슬랙에 알림이 전송됨.

이메일 알람과 slack알람 비교

  • 이메일: 가독성 떨어지고 즉각 수신이 불가, 이메일 format변경 불가
  • 슬랙: 즉각 알람 가능, 알람현황 및 대시보드 공유url을 추가하여 바로 모니터링 할 수 있음.
  • 전체 Flow

내가 사용한 슬랙 알람 Lambda 함수 코드

직접 실습해 보실 수 있도록 제가 사용했던 lambda코드를 첨부합니다.(물론 구글링이 90%..)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
// 구성 -> 환경변수로 webhook을 받도록 합니다.
const ENV = process.env
if (!ENV.webhook) throw new Error('Missing environment variable: webhook')
 
const webhook = ENV.webhook;
const https = require('https')
 
const statusColorsAndMessage = {
    ALARM: {"color""danger""message":"위험"},
    INSUFFICIENT_DATA: {"color""warning""message":"데이터 부족"},
    OK: {"color""good""message":"정상"}
}
 
const comparisonOperator = {
    "GreaterThanOrEqualToThreshold"">=",
    "GreaterThanThreshold"">",
    "LowerThanOrEqualToThreshold""<=",
    "LessThanThreshold""<",
}
 
exports.handler = async (event=> {
    await exports.processEvent(event);
}
 
exports.processEvent = async (event=> {
    console.log('Event:', JSON.stringify(event))
    const snsMessage = event.Records[0].Sns.Message;
    console.log('SNS Message:', snsMessage);
    const postData = exports.buildSlackMessage(JSON.parse(snsMessage))
    await exports.postSlack(postData, webhook);
}
 
exports.buildSlackMessage = (data) => {
    const newState = statusColorsAndMessage[data.NewStateValue];
    const oldState = statusColorsAndMessage[data.OldStateValue];
    const executeTime = exports.toYyyymmddhhmmss(data.StateChangeTime);
    const description = data.AlarmDescription;
    //const cause = exports.getCause(data);
 
    return {
        attachments: [
            {
                title: `[${data.AlarmName}]`,
                color: newState.color,
                fields: [
                    {
                        title: '시간',
                        value: executeTime
                    },
                    {
                        title: '설명',
                        value: description
                    },
                    // {
                    //     title: '원인',
                    //     value: cause
                    // },
                    {
                        title: '이전 상태',
                        value: oldState.message,
                        shorttrue
                    },
                    {
                        title: '현재 상태',
                        value: `*${newState.message}*`,
                        
                        shorttrue
                    },
                    {
                        //title: '바로가기',
                        title: '알람설정 현황',
                        value: exports.createLink(data)
                    },
                    {
                        title:'대시보드 현황',
                        value: exports.create1Link(data)
                    }
                ]
            }
        ]
    }
}
 
// CloudWatch 알람 바로 가기 링크
exports.createLink = (data) => {
    return `알람 세부 현황 url`;
}
exports.create1Link = (data) => {
    return ` 대시보드 url`;
}
 
exports.exportRegionCode = (arn) => {
    return  arn.replace("arn:aws:cloudwatch:""").split(":")[0];
}
 
 
 
// 이상 지표 중 Band를 벗어나는 경우
exports.buildAnomalyDetectionBand = (data, evaluationPeriods, minutes) => {
    const metrics = data.Trigger.Metrics;
    const metric = metrics.find(metric => metric.Id === 'm1').MetricStat.Metric.MetricName;
    const expression = metrics.find(metric => metric.Id === 'ad1').Expression;
    const width = expression.split(',')[1].replace(')''').trim();
 
    return `${evaluationPeriods * minutes} 분 동안 ${evaluationPeriods} 회 ${metric} 지표가 범위(약 ${width}배)를 벗어났습니다.`;
}
 
// // 이상 지표 중 Threshold 벗어나는 경우 
// exports.buildThresholdMessage = (data, evaluationPeriods, minutes) => {
//     const trigger = data.Trigger;
//     const threshold = trigger.Threshold;
//     const metric = trigger.MetricName;
//     const operator = comparisonOperator[trigger.ComparisonOperator];
//     return `${evaluationPeriods * minutes}분 동안 ${evaluationPeriods}회 이상 ${metric} ${operator} ${threshold}`;
    
// }
 
// 타임존 UTC -> KST
exports.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());
}
 
exports.postSlack = async (message, slackUrl) => {
    return await request(exports.options(slackUrl), message);
}
 
exports.options = (slackUrl) => {
    const {host, pathname} = new URL(slackUrl);
    return {
        hostname: host,
        path: pathname,
        method: 'POST',
        headers: {
            'Content-Type''application/json'
        },
    };
}
 
function 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) => {
            console.error(err);
            reject(err);
        });
 
        req.write(JSON.stringify(data));
        req.end();
    });
    
}
 
 
 
cs

 

Reference

https://docs.aws.amazon.com/ko_kr/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html

+ Recent posts