ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • EC2 CPU 상승 알림 (SNS + Lambda + CloudWatch)
    공부/AWS 2024. 7. 28. 21:18

    서비스를 운영하다 보면 CPU가 상승하는 현상을 자주 확인할 수 있습니다. 제한된 리소스로 최대의 효율을 내고, 안정적으로 운영할 수 있도록 하는 것이 항상 생각하고 가져가야 할 의무라고 생각합니다.

    오늘은 EC2의 CPU 상승을 확인하고, 특정 임계치를 넘는 경우 알림을 받을 수 있도록 하는 기능을 설명하려고 합니다.

    (참고에 작성하였지만, 거의 이분 블로그 를 참고하였다.)


    1. SNS 주제 생성

    - 알림을 수집하고 보내기 위해서는 SNS의 주제를 생성해야 합니다.

    - 표준으로 설정합니다.

    - 이름, 표시이름 2가지 모두 작성합니다.

     

    2. Lambda 함수 생성

    - 람다는 Cloud Watch의 경보가 SNS로 전달이 된 후 Lambda를 실행하여 알림을 보낼 수 있도록 합니다.

     

     2-1. Lambda 함수 코드 추가

    // 구성 -> 환경변수
    const ENV = process.env;
    const webhook = ENV.webhook; // slack webhook url을 등록
    if(!ENV.webhook) throw new Error('Missing enviroment variable: webhook');
    
    const https = require('https');
    
    // CloudWatch 경보 상태 3가지
    const statusCodeColorsAndMessage = {
      ALARM: {"color": "danger", "message":"위험"},
      INSUFFICIENT_DATA: {"color": "warning", "message":"데이터 부족"},
      OK: {"color": "good", "message":"정상"}
    }
    
    //  CloudWatch 경보 임계치 설정 4가지
    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 = statusCodeColorsAndMessage[data.NewStateValue];
      const oldState = statusCodeColorsAndMessage[data.OldStateValue];
      const executeTime = exports.toDateTemplate(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
              },
              {
                title:'현재 상태',
                value:`*${newState.message}*`
              },
              {
                title:'바로가기',
                value: exports.createLink(data)
              }
            ]
          }
        ]
      }
    }
    
    // CloudWatch 알람 바로 가기 링크
    exports.createLink = (data)=>{
      const region = exports.exportRegionCode(data.AlarmArn);
      return `https://ap-northeast-2.console.aws.amazon.com/cloudwatch/home?region=${region}#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)
    }
    
    // 이상 지표 중 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}`;
    }
    
    exports.toDateTemplate = (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.option(slackUrl),message);
    }
    
    exports.option = (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();
      })
    }

     

    2-2. webhook 환경변수 추가

     

    2-3. 테스트 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: \"EC2-CPUUtilization-high\" in Asia Pacific (Seoul)",
            "Message": "{\"AlarmName\":\"TEST!!!\",\"AlarmDescription\":\"EC2 CPU 알람 (10% 이상 시)\",\"AlarmArn\":\"arn:aws:cloudwatch:ap-northeast-2:123123:alarm:ant-man-live-ALB-RequestCount-high\",\"AWSAccountId\":\"123412351\",\"NewStateValue\":\"ALARM\",\"NewStateReason\":\"Threshold Crossed: 1 datapoint (10.0) was greater than or equal to the threshold (1.0).\",\"StateChangeTime\":\"2024-07-24T07:41:50.708+0000\",\"Region\":\"Asia Pacific (Seoul)\",\"OldStateValue\":\"OK\",\"Trigger\":{\"MetricName\":\"CPUUtilization\",\"Namespace\":\"AWS/EC2\",\"StatisticType\":\"Statistic\",\"Statistic\":\"MAXIMUM\",\"Unit\":null,\"Dimensions\":[{\"value\":\"i-0e3e982bf1c7f0910\",\"name\":\"EngineName\"}],\"Period\":300,\"EvaluationPeriods\":1,\"ComparisonOperator\":\"GreaterThanOrEqualToThreshold\",\"Threshold\":1.0}}",
            "Timestamp": "2024-07-24T06:20:39.536Z",
            "SignatureVersion": "1",
            "MessageAttributes": {}
          }
        }
      ]
    }

     

    3. Slack Webhook 생성

    - 슬랙 로그인 후 Your apps 선택

     

    기존에 앱이 있다면, 기존 앱을 선택하시고 없다면 생성하셔야 합니다. 저 같은 경우 기존에 생성했던 앱으로 실행하였습니다.

    앱 생성 방법은 아래 참고블로그를 확인해주세요.

    - Your Apps 접속하면, 생성했었던 앱을 확인할 수 있습니다.

     

    - 좌측에서 Incoming Webhooks 선택합니다.

    - 현재 webhook으로 사용 중인 채널들을 확인할 수 있습니다.

    - 여기서 하단에 Add New Webhook to Workspace를 선택하여 알림 받을 채널을 추가합니다.

     

    - 웹훅을 추가한 후 다시 Lambda의 환경변수에 Webhook URL을 등록합니다. 그 후 테스트를 시도해 봅니다.

     

    4. Lambda + SNS 연결

    4-1. 트리거 추가 선택합니다.

    4-2. SNS 선택 후, SNS 주제에 이전에 생성한 SNS를 선택합니다.

    (이미지에 보이면 안 되는 개인정보가 있어서 해당 부분을 제거하고 올렸습니다. 양해 부탁드립니다.)

     

    5. SNS + CloudWatch 경보 연결

    5-1. 경보 상태 or 모든 경보 선택 후 경보 생성 선택합니다.

     

    5-2. 지표 선택 클릭 후 하단 지표에서 인스턴스 ID를 입력하면 더 빨리 찾을 수 있습니다.

    5-3. 지표와 조건을 설정합니다.

    - 위와 같이 설정하면, 15분 동안 CPUUtilization이 평균 20% 넘는 기준이 3번 발생하는 경우 알림이 전송되게 설정하였습니다.

     

    5-4. 알림 설정

    - 경보 상태 알람 설정합니다. 다음으로 알림 전송은 이전에 생성했던 SNS 주제를 선택합니다.

     

    - 경보를 했으면, 정상으로 돌아왔을 때도 알림도 확인이 필요합니다.

    - 동일하게 SNS를 선택합니다.

     

    6. 경보 상태 시작하기

    6-1. 모든 경보에서 내가 만든 경보 검색 후 클릭하기

    6-2. 데이터 부족 상태임으로 정상(실행) 상태로 만들기 위해서 데이터 부족이라는 막대바를 클릭하여 정상으로 전환하기.

    (초록색 바를 클릭하면 됩니다. 처음이라면 회색바일 것입니다.)

     

    이제는 서버의 특이사항을 슬랙으로 확인할 수 있습니다.

     

     

    검색하며 찾아보고 작성한 글입니다. 혹시라도 부정확한 정보를 전달드릴 수 있습니다. 틀린 부분이 있으면 댓글을 남겨주세요.

    Ref.

    https://velog.io/@king/slack-incoming-webhook

     

    Slack Incoming Webhook 2가지 방법

    Slack Incoming Webhook(인커밍웹훅) 설정에는 2가지 방법이 있습니다. 앱 생성(추천), 앱 추가(비추) 를 각각 알아봅니다.

    velog.io

    https://velog.io/@wngud4950/AWS-Lambda%EB%A1%9C-Slack-%EC%9E%90%EB%8F%99%EC%95%8C%EB%A6%BC-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0

     

    [AWS] Lambda + CloudWatch + SNS 로 Slack 자동알림 생성하기

    개요 AWS Lambda를 이용해 CloudWatch에서 나오는 경보를 Slack으로 자동알림을 생성해줘보았다. CPU 사용률이 갑자기 튀어 서버가 먹통되는 경우, 신속히 처리하기 위해 알림을 주게 되었다. CPU 사용률

    velog.io

    https://jojoldu.tistory.com/586

     

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

    AWS 서비스를 이용하면 CloudWatch를 통해 서비스의 이상 지표를 손쉽게 확인할 수 있습니다. 이를테면 다음과 같은 경우인데요. 평소보다 로드밸런서로 들어오는 요청양이 2배이상 높다거나 RDS의 C

    jojoldu.tistory.com

     

    댓글

Designed by Tistory.