본문 바로가기
Infra, Cloud/AWS

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

by J4J 2022. 10. 22.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 CloudWatch의 경보를 Slack으로 전달받는 방법에 대해 적어보는 시간을 가져보려고 합니다.

 

 

반응형

 

 

들어가기에 앞서...

 

서버가 특정 조건에 대한 기준점을 넘었을 때 경보를 발생시키기 때문에 사용 중인 EC2 인스턴스가 있어야 합니다.

 

인스턴스를 새롭게 만드시려는 분은 다음 글들을 참고 부탁드립니다.

 

 

 

 

 

경보를 Slack으로 전달받는 이유

 

먼저 CloudWatch에서 경보를 등록해두는 이유는 서버에 특정 문제가 발생되기 전에 어떤 문제가 발생될 수 있는지에 대해 확인하기 위함입니다.

 

대표적인 케이스가 다음과 같이 있습니다.

 

  • CPU 사용률이 특정 기준을 넘어갔는지
  • 서버가 전달받는 요청 양이 특정 기준을 넘어갔는지

 

 

 

이런 부분을 사전에 인지하여 대응을 하게 된다면 갑자기 서버에 문제가 발생하여 서비스가 동작되지 않는 불상사를 막을 수 있게 도와줍니다.

 

 

 

그리고 이런 경보를 Slack으로 전달받는 이유는 어떤 문제가 발생되고 있는지에 대한 알림을 받기 위해서입니다.

 

물론 CloudWatch를 통해서도 정해둔 기준점이 넘어갔는지에 대해 시각적으로 확인할 수 있지만 매일 CloudWatch만 볼 수 없을 것입니다.

 

그렇기 때문에 CloudWatch에 경보가 발생될 때 Slack과 같이 업무를 위해 자주 사용되는 플랫폼에 알림을 전달받도록 설정하면 즉각적으로 확인할 수 있기에 발 빠르게 대응하는데 도움을 줍니다.

 

 

 

추가로 꼭 Slack이어야 할 필요는 없습니다.

 

Slack 외에도 이메일, SMS 등에도 알림을 갈 수 있게 설정할 수 있으니 참고해주시면 될 것 같습니다.

 

 

 

 

Slack 연동하기 (1) - SNS 주제 생성

 

[ 1. Simple Notification Service (SNS) 접속 ]

 

SNS 접속

 

 

 

[ 2. 주제 생성 ]

 

좌측에서 주제 메뉴를 선택해주시고 주제 생성 버튼을 클릭해주시면 됩니다.

 

주제 생성

 

 

 

[ 3. 주제 생성 정보 입력 ]

 

유형은 표준, 이름은 자유롭게 입력하신 후 아래에 있는 주제 생성 버튼을 클릭하여 주제를 생성해주시면 도비니다.

 

주제 생성 정보 입력

 

 

 

 

Slack 연동하기 (2) - CloudWatch 경보 생성

 

[ 1. CloudWatch 접속 ]

 

CloudWatch 접속

 

 

 

[ 2. 경보 생성 ]

 

좌측에서 경보 상태 메뉴를 클릭한 뒤 경보 생성 버튼을 클릭해줍니다.

 

경보 생성

 

 

 

[ 3. 지표 선택 ]

 

먼저 지표 선택을 클릭해줍니다.

 

지표 선택

 

 

 

이번 글에서는 EC2 인스턴스의 CPU 사용률에 대해 등록해볼 것이기 때문에 아래에 있는 EC2를 클릭해줍니다.

 

EC2 클릭

 

 

 

다음으로는 인스턴스별 지표를 클릭해줍니다.

 

인스턴스별 지표 클릭

 

 

 

마지막으로 원하는 인스턴스의 CPUUtilization을 선택한 뒤 오른쪽 아래에 있는 지표 선택을 클릭해줍니다.

 

지표 선택

 

 

 

 

[ 4. 지표 정보 및 조건 입력 ]

 

다음으로 통계, 기간 등의 정보들을 원하는 데로 입력해줍니다.

 

지표정보 입력

 

 

 

그리고 아래에 있는 조건도 원하는데로 입력해준 뒤 다음을 클릭해줍니다.

 

조건 입력

 

 

 

[ 5. 알림 정보 입력 ]

 

작업 구성에서는 알림 정보를 먼저 입력해줍니다.

 

경보가 발생될 때 알림을 원한다면 "경보 상태"를 선택해주고 "기존 SNS 주제 선택"을 선택한 뒤 위에서 생성해둔 sns를 알림 전송에 등록해줍니다.

 

알림 정보 입력

 

 

 

알림 정보외에 아래에 있는 부가적인 정보들은 필요하시다면 추가 설정해주시고 다음을 클릭해주시면 됩니다.

 

 

 

[ 6. 이름 및 설명 입력 ]

 

다음으론 경보 이름과 설명을 입력해준 뒤 다음 버튼을 클릭해주시면 됩니다.

 

이름 및 설명 입력

 

 

 

그러면 미리 보기가 나올 텐데 설정한 정보가 모두 맞다면 아래에 있는 경보 생성을 클릭하여 최종적으로 경보를 생성해주시면 됩니다.

 

 

 

 

Slack 연동하기 (3) - Slack WebHook URL 생성

 

[ 1. Slack 앱 추가 ]

 

Slack에 접속하여 알림 받기를 원하는 채널에 들어가준 뒤 상단의 채널 명을 클릭해줍니다.

 

채널명 클릭

 

 

 

그리고 통합 탭으로 넘어가서 앱 추가 버튼을 클릭해줍니다.

 

앱 추가

 

 

 

[ 2. Incoming WebHooks 설치 ]

 

webhooks를 검색창에 입력하면 다음과 같이 나오는데 여기에 보이는 Incoming Webhooks를 설치해줍니다.

 

Incoming WebHooks 설치

 

 

 

[ 3. 수신 웹후크 Slack에 추가 ]

 

웹 페이지가 하나가 나올 텐데 여기서는 Slack에 추가 버튼을 클릭해주시면 됩니다.

 

수신 웹후크 Slack에 추가

 

 

 

페이지가 넘어가면 아래쪽에 적용을 원하는 채널을 선택해주고 수신 웹후크 통합 앱 추가를 클릭해줍니다.

 

수신 웹후크 통합 앱 추가

 

 

 

그러면 다음과 같이 수신 웹후크가 추가되며 웹후크 URL을 확인할 수 있습니다.

 

웹후크 URL 확인

 

 

 

 

Slack 연동하기 (4) - Lambda 함수 생성

 

[ 1. Lambda 접속 ]

 

Lambda 접속

 

 

 

[ 2. 함수 생성 ]

 

좌측 함수메뉴에 접속하여 함수 생성 버튼을 클릭해주시면 됩니다.

 

함수 생성

 

 

 

[ 3. 함수 생성 정보 입력 ]

 

함수 이름, 런타임 아키텍처를 먼저 입력 및 선택해주시면 됩니다.

 

또한 기본 실행 역할 변경에서는 상황에 맞게 설정해주시면 되는데 저는 새 역할을 자동으로 생성하도록 하겠습니다.

 

모든 입력이 완료되면 오른쪽 아래에 있는 함수 생성을 클릭해주시면 됩니다.

 

함수 생성 정보 입력

 

 

 

새 역할 생성을 선택한 뒤 함수 생성을 끝마쳤다면 IAM의 역할 메뉴를 들어가 보면 다음과 같이 lambda 역할이 자동적으로 하나 생성되어 있을 것입니다.

 

lambda 역할 생성 확인

 

 

 

 

[ 4. 함수 코드 추가 ]

 

함수 코드 추가는 CloudWatch 이상 지표를 슬랙 알람으로 받기 (feat. SNS, Lambda)를 참고했습니다.

 

개인적으로 글에 있는 함수 코드가 너무 좋다고 생각하여 거의 똑같이 활용을 했습니다.

 

// 구성 -> 환경변수로 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) => {
    const snsMessage = event.Records[0].Sns.Message;
    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,
                        short: true
                    },
                    {
                        title: '현재 상태',
                        value: `*${newState.message}*`,
                        short: true
                    },
                    {
                        title: '바로가기',
                        value: exports.createLink(data)
                    }
                ]
            }
        ]
    }
}

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

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

 

 

 

위의 코드를 다음과 같이 코드 탭의 index.js 파일에 넣어주고 Deploy 버튼을 클릭해줍니다.

 

코드 등록

 

 

 

 

[ 5. 환경 변수 추가 ]

 

다음은 소스코드에 사용될 환경 변수를 등록해줘야 합니다.

 

위에 사용되는 소스코드에서는 webhook가 있습니다.

 

여기서의 webhook는 위에서 만들어둔 webhook url을 등록해주면 됩니다.

 

 

 

상단의 구성 탭 → 좌측 환경 변수 메뉴에서 편집 버튼을 클릭해줍니다.

 

환경 변수 편집

 

 

 

그리고 다음과 같이 키에는 webhook, 값에는 위에서 만든 웹후크 url을 입력해준 뒤 아래 저장 버튼을 클릭해줍니다.

 

webhook url 등록

 

 

 

[ 6. sns 트리거 추가 ]

 

상단에 트리거 추가를 클릭해줍니다.

 

트리거 추가

 

 

 

트리거 대상으로 sns를 선택해주시고 sns 주제로는 위에서 만들어둔 sns 주제를 선택한 뒤 추가 버튼을 클릭해주시면 됩니다.

 

sns 트리거 추가

 

 

 

 

Slack 연동하기 (5) - Lambda 테스트

 

위의 설정들이 모두 완료되었다면 Lambda 코드가 정상적으로 수행되었을 때 Slack에 전달해주는지 확인해 보겠습니다.

 

입력할 테스트 코드는 다음과 같습니다.

 

{
  "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": {}
      }
    }
  ]
}

 

 

 

위의 코드를 테스트 탭에서 이벤트 JSON에 입력한 뒤 오른쪽 위에 있는 테스트 버튼을 클릭해줍니다.

 

테스트 코드 입력

 

 

 

그러면 Slack에 다음과 같이 메시지가 넘어오는 것을 확인할 수 있습니다.

 

Slack 메시지 확인

 

 

 

 

Slack 연동하기 (6) - 부하 테스트

 

부하 테스트는 JMeter를 이용하여 해보겠습니다.

 

JMeter를 이용하여 부하 테스트를 원하시는 분은 [AWS] JMeter를 이용하여 EC2 부하 테스트하기를 참고해주시면 됩니다.

 

 

 

JMeter를 이용하여 서버에 부하가 발생하도록 했고 CloudWatch에서 다음과 같이 기준점이 넘어 경보 상태가 된 것을 확인할 수 있었습니다.

 

CloudWatch 경보 상태

 

 

 

그리고 Slack에는 해당 경보에 대한 알림 메시지가 다음과 같이 오는 것을 확인할 수 있었습니다.

 

slack 경보 알림 메시지

 

 

 

 

참조

 

CloudWatch 이상 지표를 슬랙 알람으로 받기 (feat. SNS, Lambda)

 

 

 

 

 

 

 

 

이상으로 CloudWatch의 경보를 Slack으로 전달받는 방법에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형

댓글