일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 스케일아웃
- 임베디드타입
- 트리맵
- findById
- 산업은행청년인턴
- DB replication
- SpringBatch
- JPA
- flyway
- 폰켓몬
- 그래프탐색
- fatch
- BFS
- 산업은행it
- springboot
- 백준
- CS
- 외래키제약조건위반
- 구현
- CPU스케줄링
- 프로그래머스
- 컴퓨터구조
- 2178
- 운영체제
- 프로젝트
- 파이널프로젝트
- 해시
- 트리셋
- 코테
- Spring JPA
- Today
- Total
나 JAVA 봐라
AWS Lambda에서 CloudWatch Logs 접근하기 본문
사용 기술
- AWS CloudWatch
- AWS SNS
- AWS Lambda
현재 진행 상황
- AWS CloudWatch Logs에 로그 그룹, 로그 스트림 생성
- 로그 그룹에 대한 지표 필터 생성
- 지표 필터에 대한 경보 생성
- AWS Lambda, SNS를 통해 슬랙 알람
먼저 기존의 슬랙 알람은 아래와 같다.
알람을 통해 바로 로그이벤트를 확인할 수 없는 문제가 있다. 링크에 접속해도, 로그 스트림을 확인할 수 있는 링크가 아니기 때문에 에러로그를 확인할 수 없다.
연동해둔 로그 이벤트는 아래와 같다. 이제 해당 로그 이벤트를 알람을 통해 바로 확인할 수 있도록 해보겠다.
1. 기존 Lambda에 Nodejs 레이어 추가
Lambda에서 CloudWatch logs에 접근하기 위해 aws-sdk를 사용해야한다.
이를 위해 Lambda 함수에 aws-sdk를 포함시킨다.
하지만 코드 상에서만 추가해두면 "Cannot find module 'aws-sdk'"
에러가 뜰 것이다.
이것은 Lambda 환경에 aws-sdk
모듈이 포함되어 있지 않아서 발생하는 문제이다.
따라서 Lambda의 layer에 aws-sdk
모듈을 추가해야한다.
먼저 로컬 터미널에서 aws-sdk을 설치한다.
(참고로 nodejs 안에 aws-sdk가 포함되어 있어, 나는 nodejs를 설치했다.)
npm install nodejs
설치가 되었다면 설치된 모듈을 .zip으로 압축한다.
참고로 각 언어 별로 지켜야하는 폴더 구조를 지켜서 압축해야한다.
nodejs는 아래와 같은 구조를 가져야한다.
Lambda의 계층 탭에서 Add a layer를 클릭한 후, zip파일을 업로드하고 계층을 생성한다.
2. IAM 권한 정책 추가
Lambda 함수 역할에 CloudWatch Logs에서 로그를 읽을 수 있는 권한을 줘야 로그에 접근할 수 있다.
따라서 Lambda 함수가 필요한 권한을 갖추도록 역할을 업데이트해야 한다.
먼저 AWS IAM에서 Lambda 함수에 해당하는 역할을 클릭한다.
권한 추가를 클릭하고, CloudWatchLogsReadOnlyAccess 권한을 추가한다.
이제 Lambda에서 log에 접근하기위한 모든 과정이 끝났다.
3. Lambda 함수 수정하기
우리 프로젝트에서는 자바스크립트를 사용했다.
그리고 SNS를 통해 받는 경보가 6개인데 그 중 ‘teachme_error’ 라는 이름을 가진 경보에 대해서만 로그 이벤트를 출력하도록 코드를 짰다.
전체 코드는 아래와 같다.
const AWS = require('aws-sdk');
// 구성 -> 환경변수로 webhook을 받도록 합니다.
const ENV = process.env
if (!ENV.webhook) throw new Error('Missing environment variable: webhook')
const webhook = ENV.webhook;
const https = require('https')
const logStreamName = 'boot-teachme-log';
const logGroupName = 'teachme_log';
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) => {
const snsMessage = event.Records[0].Sns.Message;
const parsedMessage = JSON.parse(snsMessage);
// [teachme_error] 경보에 대해서만 처리
if (parsedMessage.AlarmName === 'teachme_error') {
try {
// 로그 스트림 데이터 가져오기
const logStreamData = await exports.getLogStreamData(logGroupName, logStreamName);
// logStreamData를 원하는 형식으로 가공하여 사용
console.log('Log Stream Data:', logStreamData);
//const logStreamLink = exports.createLogStreamLink(logGroupName, logStreamName);
const postData = exports.buildSlackMessage(parsedMessage, logStreamData);
await exports.postSlack(postData, webhook);
} catch (error) {
console.error('Error processing log stream data:', error);
}
}
else{ // 다른 경보일 경우
const postData = exports.buildSlackMessage(parsedMessage);
await exports.postSlack(postData, webhook);
}
}
exports.buildSlackMessage = (data, logStreamData) => {
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);
const baseFields = [
{
title: '언제',
value: executeTime,
},
{
title: '설명',
value: description,
},
{
title: '원인',
value: cause,
},
{
title: '바로가기',
value: exports.createLink(data),
},
];
// 로그 스트림 데이터가 존재하는 경우에만 추가
if (logStreamData) {
baseFields.push({
title: '로그 스트림 데이터',
value: JSON.stringify(logStreamData), // 로그 스트림 데이터를 적절히 가공하여 사용
});
}
return {
attachments: [
{
title: `[${data.AlarmName}]`,
color: newState.color,
fields: baseFields,
},
],
}
}
// CloudWatch 알람 바로 가기 링크
exports.createLink = (data) => {
return `https://console.aws.amazon.com/cloudwatch/home?region=${exports.exportRegionCode(data.AlarmArn)}#alarm:alarmFilter=ANY;name=${encodeURIComponent(data.AlarmName)}`;
}
// LogStreamData 생성
exports.getLogStreamData = async (logGroupName, logStreamName) => {
const cloudwatchlogs = new AWS.CloudWatchLogs();
// 최근 3분 동안의 타임스탬프를 계산
const threeMinutesAgo = new Date(Date.now() - 3 * 60 * 1000);
const params = {
logGroupName: logGroupName,
logStreamName: logStreamName,
startTime: threeMinutesAgo.getTime(), // 최근 3분 이내의 로그만 가져오도록 설정
};
try {
const response = await cloudwatchlogs.getLogEvents(params).promise();
// 'ERROR'를 포함하는 로그만 필터링하여 반환
const errorEvents = response.events
.filter(event => event.message.includes('ERROR'))
.map(event => ({
timestamp: new Date(event.timestamp).toISOString(),
message: event.message.trim(),
}));
return errorEvents; // ERROR를 포함하는 로그 이벤트 배열 반환
} catch (error) {
console.error('Error fetching log stream data:', error);
throw error;
}
}
exports.exportRegionCode = (arn) => {
return arn.replace("arn:aws:cloudwatch:", "").split(":")[0];
}
exports.getCause = (data) => {
const trigger = data.Trigger;
const evaluationPeriods = trigger.EvaluationPeriods;
const minutes = Math.floor(trigger.Period / 60);
if(data.Trigger.Metrics) {
return exports.buildAnomalyDetectionBand(data, evaluationPeriods, minutes);
}
return exports.buildThresholdMessage(data, evaluationPeriods, minutes);
}
// 이상 지표 중 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) => {
reject(err);
});
req.write(JSON.stringify(data));
req.end();
});
}
결과
이제 teachme_error 경보가 발생하면 아래와 같이 slack 메시지가 전송된다.
참고
AWS lambda layer 사용하기 (node.js)
'DevOps, MLOps' 카테고리의 다른 글
AI 플랫폼에 쿠버네티스를 도입한다면? (0) | 2024.06.25 |
---|---|
kubeflow 란? (2) | 2024.06.06 |
AWS 서비스로 모니터링 환경 구축하기 (1) | 2024.06.05 |
Github Actions 로 배포 자동화하기 (0) | 2024.06.05 |
DockerHub를 통해 EC2에 Spring boot, mysql 올리기 (3) | 2024.06.05 |