카테고리 없음

[우당탕탕 AWS] AWS - Slack Notification

ooin 2024. 10. 26. 16:32
반응형

지난번 RDS 스케일 다운 작업을 진행하면서 낮아진 스펙에서의 장애 예방 및 장애대응을 할 수 있는 알림이 더욱 더 필요하다고 생각되었습니다.

 

RDS 스케일 다운과 관련된 내용은 아래 글에서 확인할 수 있습니다.

https://threezerosin.tistory.com/38

 

[우당탕탕 AWS]RDS 인스턴스 스케일 따운!

AWS 관련 서비스의 이전을 완료하고 유지보수를 진행하면서 비용관련 부분을 확인하던 중 눈에띄는 점이 있어 자세히 들여다 보았습니다. 전체 AWS 에서 발생하는 비용 중 53% 이상의 비율이 RDS(Re

threezerosin.tistory.com

 

이를 위해 Slack을 활용하여 24시간 모니터링 환경을 구축하기로 결정하였습니다.

 

지켜보고있다

 

아키텍쳐

모니터링 환경을 구축하기 위한 아키텍쳐 및 알림 프로세스는 아래와 같습니다.

출처: https://www.lgcns.com/blog/cns-tech/aws-ambassador/41065/

 

프로세스

1. EC2 (혹은 기타 AWS 인스턴스) 의 모니터링 할 수치를 CloudWatch 에서 일정 시간마다 모니터링

2. 모니터링 하면서 일정 수치(=정해놓은 수치)가 넘어가거나 미달되면 SNS 를 통해 알림 이벤트를 생성

3. 해당 SNS를 구독하고 있는 Lambda에서 알림발생시 정해놓은 트리거(Slack 알람) 실행

4. 관련 담당자는 Slack에서 시스템 관련 알람을 확인

 

위와 같은 프로세스로 24시간 모니터링 환경을 구축하며, 해당 서비스들을 어떻게 구성하는지 알아보도록 하겠습니다.

 


EC2(혹은 RDS) 모니터링 환경 구성

 

0. 기존에 이용하던 EC2 인스턴스가 없다면 인스턴스를 구성해줍니다. 

 

 

AWS Simple Notification Service(SNS)

  • 대시보드로 이동

 

 

  • SNS 주제를 생성

 

 

유형은 표준을 선택 및 이름 입력 후 생성

 

CloudWatch

  • CloudWatch 서비스 대시보드로 이동

 

  • 경보 생성

 

 

 

 

EC2의 CPUUtilization(CPU 사용량) 의 경보를 생성하고, 테스트를 위해 우선 낮은 수치인 4%을 입력하여 기능이 정상적으로 작동하는지 확인해보겠습니다. 

 

 

입력한 값에 맞춰 기준선이 생겨 기준선보다 높은 CPU 사용량이을 확인할 수 있습니다.

 

 

 

경보생성 버튼을 클릭해 경보를 생성해줍니다.

 

 

Slack 에서 APP 추가

 

  • '앱 찾아보기' 또는 'Slack 마켓플레이스' 를 클릭

 

이동한 웹에서 '수신 웹후크' 또는 'incoming webhook'를 검색하여 Slack에 추가합니다.

 

알림을 받을 채널을 선택하고, 이때 생성되는 웹 후크 URL을 별도로 복사해둡니다.

 

 

Lambda

  • lambda 대시보드로 이동

 

 

 

  • 신규 lambda 함수 생성

 

아래의 코드를 입력 후 Deploy

 

const ENV = process.env;
if (!ENV.webhook) throw new Error('Missing environment variable: webhook');

const webhook = ENV.webhook;
import https from 'https';

const statusColorsAndMessage = {
    ALARM: {"color": "danger", "message":"위험"},
    INSUFFICIENT_DATA: {"color": "warning", "message":"데이터 부족"},
    OK: {"color": "good", "message":"정상"}
};

const comparisonOperator = {
    "GreaterThanOrEqualToThreshold": ">=",
    "GreaterThanThreshold": ">",
    "LowerThanOrEqualToThreshold": "<=",
    "LessThanThreshold": "<",
};

// 함수를 export
export const handler = async (event) => {
    await processEvent(event);
};

export const processEvent = async (event) => {
    const snsMessage = event.Records[0].Sns.Message;
    const postData = buildSlackMessage(JSON.parse(snsMessage));
    await postSlack(postData, webhook);
};

export 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: [
            {
                title: `[${data.AlarmName}]`,
                color: newState.color,
                fields: [
                    {
                        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)
                    }
                ]
            }
        ]
    };
};

// CloudWatch 알람 바로 가기 링크
export const createLink = (data) => {
    return `https://console.aws.amazon.com/cloudwatch/home?region=${exportRegionCode(data.AlarmArn)}#alarm:alarmFilter=ANY;name=${encodeURIComponent(data.AlarmName)}`;
};

export const exportRegionCode = (arn) => {
    return arn.replace("arn:aws:cloudwatch:", "").split(":")[0];
};

export const getCause = (data) => {
    const trigger = data.Trigger;
    const evaluationPeriods = trigger.EvaluationPeriods;
    const minutes = Math.floor(trigger.Period / 60);

    if(data.Trigger.Metrics) {
        return buildAnomalyDetectionBand(data, evaluationPeriods, minutes);
    }

    return buildThresholdMessage(data, evaluationPeriods, minutes);
};

export const 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}배)를 벗어났습니다.`;
};

export const 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}`;
};


export 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());
};

export const postSlack = async (message, slackUrl) => {
    return await request(options(slackUrl), message);
};

export const 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();
    });
}

 

 

Slack Webhook URL 등록

 

  • 환경변수에 이전에 복사해두었던 URL을 등록

 

  • lambda 트리거 추가

 

 

 

  • 생성한 AWS-Slack-Alert SNS 를 트리거에 추가

하면 Slack 모니터링 환경 구성이 완료되었습니다.

 


테스트

위에 구성한 모니터링 환경에서 알림이 정상적으로 오는지 확인해보겠습니다.

 

 

lambda 테스트 코드

 

  • lambda 테스트로 이동

 

  • 이벤트 JSON에 아래 코드와 같이 입력후 테스트 버튼 클릭

//JSON
{
  "Records": [
    {
      "EventSource": "aws:sns",
      "EventVersion": "1.0",
      "EventSubscriptionArn": "arn:aws:sns:ap-northeast-2:981604548033:alarm-topic:test",
      "Sns": {
        "Type": "Notification",
        "MessageId": "test",
        "TopicArn": "arn:aws:sns:ap-northeast-2:123123:test-alarm-topic",
        "Subject": "ALARM: \"RDS-CPUUtilization-high\" in Asia Pacific (Seoul)",
        "Message": "{\"AlarmName\":\"Aurora PostgreSQL CPU 알람 (60%이상시)\",\"AlarmDescription\":\"Aurora PostgreSQL CPU 알람 (60%이상시)\",\"AWSAccountId\":\"981604548033\",\"NewStateValue\":\"ALARM\",\"NewStateReason\":\"Threshold Crossed: 3 out of the last 3 datapoints [8.891518474692088 (14/07/21 23:18:00), 9.72 (14/07/21 23:17:00), 9.18241509182415 (14/07/21 23:16:00)] were greater than or equal to the threshold (7.0) (minimum 3 datapoints for OK -> ALARM transition).\",\"StateChangeTime\":\"2021-07-14T23:20:50.708+0000\",\"Region\":\"Asia Pacific (Seoul)\",\"AlarmArn\":\"arn:aws:cloudwatch:ap-northeast-2:981604548033:alarm:Aurora PostgreSQL CPU 알람 (60%이상시)\",\"OldStateValue\":\"OK\",\"Trigger\":{\"MetricName\":\"CPUUtilization\",\"Namespace\":\"AWS/RDS\",\"StatisticType\":\"Statistic\",\"Statistic\":\"MAXIMUM\",\"Unit\":null,\"Dimensions\":[{\"value\":\"aurora-postgresql\",\"name\":\"EngineName\"}],\"Period\":60,\"EvaluationPeriods\":3,\"ComparisonOperator\":\"GreaterThanOrEqualToThreshold\",\"Threshold\":7,\"TreatMissingData\":\"- TreatMissingData:                    ignore\",\"EvaluateLowSampleCountPercentile\":\"\"}}",
        "Timestamp": "2021-06-07T11:31:17.380Z",
        "SignatureVersion": "1",
        "MessageAttributes": {}
      }
    }
  ]
}

 

 

테스트 결과 아래와 같이 Slack에 알림이 오는 것을 확인합니다.

 

 

 

 

해당 테스트로는 lambda 트리거가 정상적으로 실행되는지만 확인되기때문에 실제 EC2 인스턴스의 CPU 사용량이 일정수치 이상으로 올라가면 알림이 오는지 확인해보겠습니다.

 

k6를 이용한 부하테스트

 

오픈소스 부하테스트 도구인 k6를 활용해 CPU 이용량을 늘려 구축한 환경이 정상적으로 작동하는지 확인해보겠습니다.

k6 script

 

k6 실행

 

slack에 알림

 

CPU 사용량이 4% 를 넘어가자 Slack을 통해 알림이 왔습니다. 이렇게 정상적으로 모니터링 알림 환경이 구축된것까지 확인을 완료하였습니다. CPU 사용량을 이후 95%로 수정하여 운영중입니다.

 


아래 블로그들을 참고하였습니다.

 

참고

https://jforj.tistory.com/293

 

[AWS] CloudWatch의 경보를 Slack으로 전달받기

안녕하세요. J4J입니다. 이번 포스팅은 CloudWatch의 경보를 Slack으로 전달받는 방법에 대해 적어보는 시간을 가져보려고 합니다. 들어가기에 앞서... 서버가 특정 조건에 대한 기준점을 넘었을 때

jforj.tistory.com


https://www.lgcns.com/blog/cns-tech/aws-ambassador/41065/

 

SLACK을 활용한 AWS 모니터링 환경 구성하기 - LG CNS

1. 개요 AWS Cloudwatch에서는 AWS 내 Resource에 대한 모니터링 지표의 데이터가 수집되고 있습니다. 이런 모니터링 지표 데이터는 사용자의 필요에 따라 경보를 설정하고 AWS SNS(Simple Notification Service)를

www.lgcns.com

 

반응형