You are currently viewing AWS Lambda + Amazon EventBridge Scheduler로 커스텀 알람 만들기

AWS Lambda + Amazon EventBridge Scheduler로 커스텀 알람 만들기

Intro

지난글에서, 간단히 AWS Chatbot을 통해 Cloudwatch Alarm을 Slack으로 받아보았다.

그러나 글의 말미에 언급하였던 것처럼, 간단한 구축은 장점이나 커스텀요소가 많이 부족하다는 아쉬움이 있었다.

이글에서는 Amazon EventBridge를 사용해 주기적으로 AWS Lambda를 트리거 하고, 지표 확인 및 알람 대상을 평가해 Slack으로 발송하는 과정을 공유하고자 한다.

기존의 Cloudwath Alarm + AWS Lambda글들을 살펴보면, 단순히 Alarm Event를 Lambda를 통해 Parsing하여 Slack으로 전달하는 사용 예시가 많았다. 이 글에서는 직접 메트릭을 조회하고 상태를 감지하여 알람을 만들고 전송하는 과정을 정리해두고자 한다.


Overview

구축과정을 상술하면 내용이 길어질듯 하여, 먼저 결과물(메세지 알람)을 보면서 알람이 어떤식으로 작동하는지, 무엇을 고려하였는지 설명하고자 한다.

예시

  • 알람 수신화면 (모바일)
  • 알람 수신화면 (Slack)

위의 알람설정은 CPU 사용량을 대상으로 매 5분 마다, 지난 3번의 평가 시점을 확인하여 3번 중 2번 이상 기준 값 (75%) 이상일 경우 알람 메세지를 보낸 경우의 예시이다.

개선점

지난 AWS Chatbot의 알람의 경우, 실제 알람 대상확인의 어려움, 메세지 템플릿 변경불가, 알람의 현재상태 파악 불가등의 문제점이 있었다.

지금의 알람 메세지에서는 다음의 방식으로 개선이 되었다.

  • 메세지 템플릿 변경 불가
    • Slack block builder를 사용해 직접 메세지 템플릿 작성, 한글 메세지!
  • 실제 알람 대상 확인의 어려움
    • 평가대상에 대해 각각의 Slack message 템플릿을 작성해 메세지를 보낸다.
    • 가령 3대의 인스턴스가 평가대상인 경우 각 인스턴스대상 메트릭 이미지와 함께 3번의 메세지가 발송된다.
  • 알람의 현재 상태 파악 불가
    • 알람의 상태가 전환될때가 아닌, 매 5분마다 실행되어 알람 재상기 가능
    • 단순 1번의 지표가 아닌, 최근 3번의 지표를 첨부함으로 추이파악 가능

고려사항

  • 신속함 vs 정확함
    • 경보가 발생했을때 바로 알아채기 VS 진짜 경보가 필요할때 확인하기
    • 신속함보다는 정확함을 선택하였다.
      • 신속함을 구체적으로 표현하면 1분에 한번, 지표가 기준치를 넘으면(1/1) 바로 경보 메세지를 발송하는것이라고 할 수 있겠다.
      • 그러나 이 경우 False Alarm의 우려가 있다. 가령 지표가 기준치 근처의 상태일 경우, 알람을 받고 확인하면 정상치로 복구되어있는 경우가 그렇다.
      • 정확함을 구체적으로 표현하면, 2~3회의 평가를 통해 기준치 이상이 확실할 경우 경보 메세지를 발송하고 대응하는 것이다.
      • 또한 경보 메세지에서 지표 추이를 같이 발송함으로써 알람 메세지를 통해 상태가 부하 증가/유지/감소 중인지 추이를 바로 확인할 수 있다.
  • 유지보수를 위한 관련 링크 삽입
    • 알람을 운영하다보면, 구축 이후 운영시 알람은 날라오지만 어디서 발생되는 알람인지, 어디를 수정할지 파악하기 힘든 경우가 있었다.
    • 일관적인 알람관련 리소스의 명명 규칙을 잡고 작업함으로써 관련 구성요소의 링크를 바로 찾아갈 수 있게되었다.

Slack 설정

Webhook vs API

Slack을 통해 메세지를 발송할때, Webhook을 통해 메세지를 발송하는 방법Token을 사용한 API 호출을 통해 메세지를 발송하는 방법 두가지 방법이 있다.

Webhook의 경우 간단히 webhook으로 별도의 번거로운 인증절차 없이 해당 훅으로 메세지 body를 보내면 작동한다는 간편함이 장점이다.

그러나 발송아이콘/이름, 파일첨부, 사용자/그룹 멘션등 디테일한 설정은 불가능하다는 단점이 존재한다.


이 커스텀 알람에서는 해당 기능 모두 사용할것이기 때문에 Token 기반 API환경이 필요하다, Slack App 생성 및 토큰 발급, 권한 부여, Workspace에 App초대 등의 사전 작업을 수행한다.

Slack App 생성 및 설정

전반적으로 공식문서의 Slack App 생성하기워크스페이스에 앱 추가를 참고하면 좋다.

상세한 구축과정은 잘 정리한 다른 블로그들을 참고하는것이 좋다. (링크)

Slack api페이지를 통해 새로운 앱을 생성한 뒤, 좌측의 Features에서 OAuth & Permissions 설정이 필요하다.



S3 Bucket 생성

Slack 메세지에 메트릭 이미지를 첨부하기 위해 S3 bucket의 생성이 필요하다.

해당 버킷은 public access 허용권한이 필요하기에 별도의 버킷을 생성하는것이 권장되고, 버킷내 특정 디렉토리에 이미지를 저장해두고 기간 후 만료 처리를 할 것이다.


Bucket의 생성 이후, 퍼블릭 액세스 허용 권한이 필요하다.

  • bucket 권한 > ACL(액세스 제어 목록) > 다음과 같이 설정


또한, 저장된 Image를 영구적으로 사용할 목적이 아니라면, 수명 주기를 통해 적정기간 이후 bucket내 이미지를 제거할 수 있다. (예시에서는 1년 설정)

  • bucket 관리 > 수명 주기 규칙 > 다음과 같이 설정



AWS Lambda 설정 + 권한

기반 메트릭을 조회하고, 평가하여, Slack 메세지를 보내는 Lambda를 생성, 설정 및 테스트 알람을 발송하는 과정까지 수행할 것이다.

생성 및 설정

alarm_base 라는 리소스명의 AWS Lambda 함수를 생성한다

Python 기반의 소스코드를 작동시킬 예정이므로 런타임으로는 Python을 선택한다. (예시는 3.12) 그외에는 기본값을 적용한다.


웹 콘솔로 기본 생성시 제한 시간은 3초이다. 메트릭 대상의 수에 따라 3초로는 부족하기 때문에 대상의 수에 따라 적절히 제한 시간을 설정한다. 제한 시간은 Lambda 실행시간의 Timeout을 설정하는것으로, 실제 과금은 제한 시간내에서 작동시간 만큼 과금된다.

  • 구성 > 일반 구성 > 편집 > 제한 시간 변경


IAM 권한 부여

기본 생성된 Lambda Role에는 Lambda LogGroup을 생성하고 LogEvents를 전송하는 권한만 부여되어있다.

람다 함수 작동을 위해 다음의 권한을 추가로 부여한다. 테스트를 위해 임시로 FullAccess를 부여할것이며, 아래의 필요성에 따라 필요한 권한으로 최적화할 수 있다.

  • EC2FullAccess > EC2 status, tags 조회를 위해 부여
  • S3FullAccess > 경보 발생시, 메트릭 이미지를 S3 버킷에 저장하기 위해 부여
  • CloudWatchFullAccess > Cloudwatch metric 조회 및 LogGroup 생성


Lambda Layer 추가

사용할 Pythoncode에는 Lambda에서 기본 사용하는 라이브러리외에, datetime, slack sdk등의 추가 라이브러리 활용이 필요하다.

AWS Lambda에서는 기본 라이브러리외에 추가 종속성을 사용하려면 해당 패키지를 Layer로 추가하여야한다. 해당 라이브러리를 별도로 구성한 뒤, .zip로 묶어 업로드하고, Layer를 추가한다. 참고자료

터미널에서 다음의 명령어를 실행하여 현재경로에 boto3, requests, datetime, slack_sdk 패키지가 포함된 lambda_layer.zip 파일을 만든다

# 라이브러리 설치할 디렉토리 구성
mkdir python; cd python

# boto3, requests, datetime, slack_sdk 패키지 현재 경로에 설치 
pip install boto3 requests datetime slack_sdk -t .

# 설치된 패키지를 lambda_layer로 압축 후 이동
zip -r ../lambda_layer.zip .
 ; cd ..


Lambda 웹 콘솔에서 좌측의 추가리소스 > 계층 > 계층 생성을 클릭한다.

다음과 같이 적당한 이름, .zip파일을 업로드한 뒤, 생성한 Lambda 함수의 아키텍처와 런타임을 설정한다. 다르면 계층 적용이 불가능하다.


이후 Lambda 콘솔로 돌아와, 생성한 함수 하단의 계층 추가를 클릭한 뒤, 사용자 지정 계층을 선택해 방금 추가한 레이어를 추가한다.

Source code

Slack, S3, IAM, Layer등의 설정이 끝나면 Lambda 함수 실행 준비가 완료되었다.

먼저 아래의 소스코드를 code source의 lambda_function.py에 붙여넣고, Deploy를 눌러 업데이트사항을 배포한다.

python code

import boto3
import json
import os
from urllib.error import URLError, HTTPError
from urllib.request import Request, urlopen
from datetime import datetime, timedelta, timezone
from slack_sdk import WebClient

# Boto3 클라이언트 생성
ec2 = boto3.client('ec2')
cloudwatch = boto3.client('cloudwatch')
s3 = boto3.client('s3')
ssm = boto3.client('ssm')

def lambda_handler(event, context):
    # 변수 설정
    slack = event["slack"]
    condition = event["condition"]
    metric = event["metric"]

    alarm_list,missing_list=get_alarm_list(condition, metric)
    image_url = None
    if alarm_list:
        image_url = capture_metric_image(metric)
    slack_messages=build_slack_message(alarm_list, missing_list, condition, metric, image_url)

    send_slack_message(slack, slack_messages)
    print("image_url", image_url)
    print("alarm:",alarm_list)
    print("missing:",missing_list)


# 인스턴스들을 조회해서 알람이 필요한 ec2 정보 return
def get_alarm_list(condition, metric):
    # 변수 설정
    utc_time = datetime.now(timezone.utc)
    end_time = utc_time - timedelta(minutes=1)
    start_time = end_time - timedelta(minutes=condition["check_interval"])
    ec2_filters = [
        {
            'Name': 'tag:ENV',
            'Values': ['production']
        },
        {
            'Name': 'tag:MONITORING',
            'Values': ['true']
        },
        {
            'Name': 'instance-state-name',
            'Values': ['running']
        }
    ]

    # 알람을 저장할 리스트 초기화
    alarm_list = metric["alarm_list"]
    missing_list = metric["missing_list"]

    # 각 인스턴스에 대해 CloudWatch에서 메트릭 데이터 조회
    ec2_responses = ec2.describe_instances(Filters=ec2_filters)
    for reservation in ec2_responses['Reservations']:
        for instance in reservation['Instances']:
            instance_id = instance['InstanceId']

            for tag in instance.get('Tags', []):
                if tag['Key'] == 'Name':
                    instance_name = tag['Value']

            cloudwatch_response = cloudwatch.get_metric_data(
                MetricDataQueries=[
                    {
                        'Id': 'm1',
                        'MetricStat': {
                            'Metric': {
                                'Namespace': metric["namespace"],
                                'MetricName': metric["metric"],
                                'Dimensions': [
                                    {
                                        'Name': 'InstanceId',
                                        'Value': instance_id
                                    }
                                ]
                            },
                            'Period': 300,
                            'Stat': 'Average'
                        },
                        'ReturnData': True
                    }
                ],
                StartTime=start_time,
                EndTime=end_time
            )

            # cloudwatch_response에서 values만 가져오기
            # ex) values = [93.71751850254701, 75.10669500813984, 51.36778787465036]
            values = cloudwatch_response['MetricDataResults'][0].get('Values', [])
            threshold_count = 0
            print(values)
            # 시스템 메트릭의 경우 5분당 1회, 총 3개의 데이터를 조회해 2개이상 비어있으면 missing으로 판단
            if len(values) <= 1:
                missing_list.append({'instance_id': instance_id, 'name': instance_name, 'metric': values})
            # 그 외의 경우, 개별 값을 기준치와 비교하여 카운터 상승
            else:
                for value in values:
                    if value >= condition["threshold"]:
                        threshold_count += 1

            # 3개중 2번 이상 기준치를 넘었을 경우 소수점 둘째자리만 남기고 3개 값 모두 저장(추이 파악용) 
            if threshold_count >= 2:
                    str_values = ", ".join([f"{value:.2f}%" for value in values])
                    alarm_list.append({'instance_id': instance_id, 'name': instance_name, 'metric': str_values})

    return alarm_list, missing_list

# 문제있는 metric image 캡쳐
def capture_metric_image(metric):
    # 변수설정
    kst_offset = timezone(timedelta(hours=9))
    kst_time = datetime.now(kst_offset)
    upload_time = kst_time.strftime('%y%m%d_%H%M%S')
    image_loc = f'cloudwatch-graph/cloudwatch_metric_chart_{upload_time}.png'
    metric_sources = {
    "metrics": [
        metric["image_query"]
    ],
    "view": "timeSeries",
    "stacked": False,
    "stat": "Maximum",
    "period": 300,
    "title": f'{metric["namespace"]}-{metric["metric"]}',
    "width": 800,
    "height": 400,
    "yAxis": {
        "left": {
            "showUnits": False
        },
        "right": {
            "showUnits": False
        }
    },
    "timezone": "+0900"
    }

    # metric 이미지 생성
    metric_sources = json.dumps(metric_sources)
    response = cloudwatch.get_metric_widget_image(MetricWidget=metric_sources)
    data = response['MetricWidgetImage']
    # 생성된 이미지 s3 업로드
    s3.put_object(ACL='public-read', Body=data, Bucket=metric["image_s3_bucket"], Key=image_loc)
    image_url = f'https://s3-ap-northeast-2.amazonaws.com/{metric["image_s3_bucket"]}/{image_loc}'
    return image_url

# 대상 ec2와 image url을 포함한 슬랙메세지 작성
def build_slack_message(alarm_list, missing_list, condition, metric, image_url):
    kst_offset = timezone(timedelta(hours=9))
    kst_time = datetime.now(kst_offset)
    resource_name=metric["metric"]
    ec2_base="https://ap-northeast-2.console.aws.amazon.com/ec2/home?region=ap-northeast-2"
    cloudwatch_base="https://ap-northeast-2.console.aws.amazon.com/cloudwatch/home?region=ap-northeast-2"
    cloudwatch_query=metric["metric_query"]
    lambda_base="https://ap-northeast-2.console.aws.amazon.com/lambda/home?region=ap-northeast-2"
    scheduler_base="https://ap-northeast-2.console.aws.amazon.com/scheduler/home?region=ap-northeast-2"
    loggroup_base="https://ap-northeast-2.console.aws.amazon.com/cloudwatch/home?region=ap-northeast-2#logsV2:log-groups/log-group/$252Faws$252Flambda$252F"

    slack_messages = []
    for alarm in alarm_list:
        print(alarm)
        slack_message = {
            "text": f"<{ec2_base}#Instances:tag:Name={alarm['name']};v=3;|:rotating_light: {alarm['name']}/{resource_name} {alarm['metric']} 경보>",
            "attachments": [
                {
                    "color": "danger",
                    "fields": [
                        {"title": "설명", "value": f"{alarm['name']} EC2 서버에서 최근 {str(condition['check_interval'])}분 동안 {resource_name} {alarm['metric']}\n<@U0587R99T2Q>"},
                        {"title": "대상", "value": f"{alarm['name']}, {alarm['instance_id']}", "short": True},
                        {"title": "시간", "value": kst_time.strftime("%Y-%m-%d %H:%M:%S"), "short": True},
                        {"title": "Cloudwatch 바로가기", "value": f"<{cloudwatch_base}#{cloudwatch_query}|지표 기록 확인>", "short": True},
                        {"title": "Lambda 바로가기", "value": f"<{lambda_base}#/functions/alarm_base|Lambda 소스 보기>", "short": True},
                        {"title": "Scheduler 바로가기", "value": f"<{scheduler_base}#schedules/ec2-notification/{resource_name}|알람주기/비활성화 설정>", "short": True},
                        {"title": "LogGroup 바로가기", "value": f"<{loggroup_base}alarm_base/log-events$3Fstart$3D-1800000|Lambda 실행 로그 조회>", "short": True},
                    ],
                    "image_url": image_url
                }
            ]
        }
        slack_messages.append(slack_message)

    for missing in missing_list:
        slack_message = {
            "text": f"<{ec2_base}#Instances:tag:Name={missing['name']};v=3;|:rotating_light: {missing['name']}/{resource_name} 데이터 부족>",
            "attachments": [
                {
                    "color": "warning",
                    "fields": [
                        {"title": "설명", "value": f"{missing['name']} EC2 서버에서 최근 {str(condition['check_interval'])}분 동안 메트릭이 수집되지 않음\n<@U0587R99T2Q>"},
                        {"title": "대상", "value": f"{missing['name']}, {missing['instance_id']}", "short": True},
                        {"title": "시간", "value": kst_time.strftime("%Y-%m-%d %H:%M:%S"), "short": True},
                        {"title": "Cloudwatch 바로가기", "value": f"<{cloudwatch_base}#{cloudwatch_query}|지표 기록 확인>", "short": True},
                        {"title": "Lambda 바로가기", "value": f"<{lambda_base}#/functions/alarm_base|Lambda 소스 보기>", "short": True},
                        {"title": "Scheduler 바로가기", "value": f"<{scheduler_base}#schedules/ec2-notification/{resource_name}|알람주기/비활성화 설정>", "short": True},
                        {"title": "LogGroup 바로가기", "value": f"<{loggroup_base}alarm_base/log-events$3Fstart$3D-1800000|Lambda 실행 로그 조회>", "short": True},
                    ]
                }
            ]
        }
        slack_messages.append(slack_message)
    return slack_messages


# 슬랙 메세지 발송
def send_slack_message(slack, slack_messages):
    client = WebClient(slack["token"])
    for slack_message in slack_messages:
        result = client.chat_postMessage(
            channel=slack["channel"],
            username=slack["username"],
            icon_emoji=slack["icon"],
            text=slack_message['text'],
            attachments=slack_message['attachments']
        )

code 동작?

위 Lambda python코드는 json 형태의 event 값을 받아서 트리거된다.

최초에 def lambda_handler(event, context)가 트리거 되며, 전달받은 event를 파싱한 뒤 순차적으로 아래의 4개 함수가 트리거한다.

  • def get_alarm_list(condition, metric):
    • return alarm_list, missing_list
  • def capture_metric_image(metric):
    • return image_url
  • def build_slack_message(alarm_list, missing_list, slack, condition, metric, image_url):
    • return slack_messages
  • def send_slack_message(slack, slack_messages):

get_alarm_list

특정 태그 조건(운영환경, 작동중)을 갖는 EC2를 대상으로 최근 3번의 메트릭을 조회 후, Alarm/Data missing 판단

boto3의 cloudwatch 클라이언트 중 메트릭을 조회하는 방법으로는 get_metric_data 또는 get_metric_statistics 두가지 방법이 있다. 호출 비용상으로는 get_metric_statistics 이 조금 더 저렴하나. 크게 유의미한 차이가 있지 않고, data는 시간순서순으로 정렬이 되지만 statistics는 기간내 무작위로 response가 돌아오기 때문에 data로 선택하였다. 물론 stastistics 결과값을 정렬해서 사용해도 되긴함

capture_metric_image

alarm_list에 값이 있다면 get_metric_widget_image메소드를 호출하여 png 이미지 생성 후 S3 버킷의 특정경로에 업로드 및 URL 회수

결국 metric_sources로 전달하는 쿼리내용이 중요하지만, 참고자료처럼 메트릭 조회화면 > 소스 > 이미지 API를 선택하면 확인가능, false >False 등 사소한 문자열 변경 필요

build_slack_message

웹콘솔 AWS서비스 주소 경로 별거없더라. 가독성을 위해 변수화하여 템플릿화하였음

slack message에서 사용자/그룹을 멘션할때 단순히 @ID로는 호출이 되지않음. 사용자/그룹별 별도의 형식으로 호출하여야 함

https://api.slack.com/reference/surfaces/formatting#mentioning-users
https://api.slack.com/reference/surfaces/formatting#mentioning-groups

send_slack_message

발신자 표시명 변경, attachment 등은 webhook이 아닌 API호출시에 가능

Test message 발송

생성된 Lambda함수의 실행을 확인하기 위해, 테스트용 alarm_list와 missing_list 데이터가 있는 event json을 전달하여 실행을 확인할 것이다.


테스트로 이동하여, 다음의 필요값이 있는 event.json을 붙여넣어 테스트를 실행한다.

  • Slack App token
  • Slack Channel
  • Slack Mention target (멤버ID 또는 그룹ID)
  • S3 Bucket name

test_event.json

{
    "slack": {
        "token": "xoxb-[Bot User OAuth Token]",
        "channel": "C05[Channelid]",
        "username": "AWS Alarm",
        "icon": "warning",
        "mention": "<@U05[Userid]>"
    },
    "condition": {
        "threshold": 75,
        "check_interval": 15
    },
    "metric": {
            "alarm_list": [{"instance_id": "i-aaaaa", "name": "test", "metric": "72.04%, 76.91%, 83.29%"}],
            "missing_list": [{"instance_id": "i-aaaaa", "name": "test", "metric": "5.46%"}],
            "namespace": "AWS/EC2",
            "metric": "CPUUtilization",
            "metric_query": "metricsV2?graph=~(metrics~(~(~(expression~'SELECT*20MAX*28CPUUtilization*29*20FROM*20SCHEMA*28*22AWS*2fEC2*22*2c*20InstanceId*29*20GROUP*20BY*20InstanceId*20ORDER*20BY*20MAX*28*29*20DESC*20LIMIT*2010~id~'q1~region~'ap-northeast-2~period~300~stat~'Maximum)))~view~'timeSeries~stacked~false~region~'ap-northeast-2~stat~'Maximum~period~300~title~'liveData~true~yAxis~(left~(label~'*25~showUnits~false)~right~(showUnits~false))~start~'-PT3H~end~'P0D)",
            "image_s3_bucket": "[s3bucketname]",
            "image_query": [ { "expression": "SELECT MAX(CPUUtilization) FROM SCHEMA(\"AWS/EC2\", InstanceId) GROUP BY InstanceId ORDER BY MAX() DESC LIMIT 10", "id": "q1", "region": "ap-northeast-2", "period": 300, "stat": "Maximum" } ]
    }
}

테스트를 실행하면 다음과 같이 test,i-aaaaa 라는 가상의 EC2 대상으로 알람상황과 데이터 없음 상황을 가정한 테스트 Slack message를 수신할 수 있다.


EventBridge 설정 + 권한

Test를 통해 Lambda 함수가 정상적으로 작동하는것을 확인했으면 이제 Amazon EventBridge 를 사용해 주기적으로 해당 함수를 트리거 할것이다.

Amazon EventBridge 콘솔로 이동한 뒤, 좌측의 Scheduler에서 일정을 선택한다. 그 뒤 일정 생성을 클릭한다.

이후 일정이름을 슬랙 메세지 링크 연동을 위해 metric과 같은 값으로 설정하고, 5분마다 실행할 수 있도록 설정한다. 유연한 기간기능은 비활성화하고, 다른값은 기본값을 유지한다.


이후 대상 선택에서 AWS Lambda를 선택한뒤, 앞서 생성한 alarm_base Lambda 함수를 선택한다.

Payload에는 위의 test_event.json에서 alarm_list와 missing_list 리스트를 비운채로 추가한다.

CPU_Payload

{
    "slack": {
        "token": "xoxb-[Bot User OAuth Token]",
        "channel": "C05[Channelid]",
        "username": "AWS Alarm",
        "icon": "warning",
        "mention": "<@U05[Userid]>"
    },
    "condition": {
        "threshold": 75,
        "check_interval": 15
    },
    "metric": {
            "alarm_list": [],
            "missing_list": [],
            "namespace": "AWS/EC2",
            "metric": "CPUUtilization",
            "metric_query": "metricsV2?graph=~(metrics~(~(~(expression~'SELECT*20MAX*28CPUUtilization*29*20FROM*20SCHEMA*28*22AWS*2fEC2*22*2c*20InstanceId*29*20GROUP*20BY*20InstanceId*20ORDER*20BY*20MAX*28*29*20DESC*20LIMIT*2010~id~'q1~region~'ap-northeast-2~period~300~stat~'Maximum)))~view~'timeSeries~stacked~false~region~'ap-northeast-2~stat~'Maximum~period~300~title~'liveData~true~yAxis~(left~(label~'*25~showUnits~false)~right~(showUnits~false))~start~'-PT3H~end~'P0D)",
            "image_s3_bucket": "[s3bucketname]",
            "image_query": [ { "expression": "SELECT MAX(CPUUtilization) FROM SCHEMA(\"AWS/EC2\", InstanceId) GROUP BY InstanceId ORDER BY MAX() DESC LIMIT 10", "id": "q1", "region": "ap-northeast-2", "period": 300, "stat": "Maximum" } ]
    }
}

Alarm test

지난글과 유사하게, stress 명령어를 사용해 10분간 cpu에 100% 부하를 주겠다

stress -c 2 -t 600


약 15분 뒤, 다음과 같이 personal 이라는 이름의 실제 인스턴스 대상으로 알람 메세지를 수신할 수 있다.


또한 데이터 없음을 확인하기위하여, 해당 인스턴스를 중지해두고 일정시간 뒤 시작하면 다음과 같이 메트릭 없음에 대한 알람을 확인할 수 있다


Outro

일련의 과정을 통해 Lambda 함수를 통해 직접 metric을 조회하고 상태를 파악해 Slack 메세지를 발송하는 알람을 만들어 보았다.


앞서 언급하였듯, 이 방식은 기존의 AWS Chatbot 기반 알람 메시지의 아쉬웠던점을 개선하였으며, 한번 구축해두면 확장이 용이하다는 추가적인 장점이 있다.

다른 metric을 감시하는 추가적인 알람이 필요한 경우, 해당 metric name의 EventBridge Scheuler를 생성하고 필요한 event(payload)값들을 수정하여 생성해둔 alarm_base lambda 함수에 꽂아주면 새로운 알람 설정이 완료된다.

예를 들어 StatusCheckFailed 라는 지표를 감시하고자 한다면, 다음과 같은 payload를 전달하기만 하면 된다.

StatusCheckFailed_Payload.json

{
    "slack": {
        "token": "xoxb-[Bot User OAuth Token]",
        "channel": "C05[Channelid]",
        "username": "AWS Alarm",
        "icon": "warning",
        "mention": "<@U05[Userid]>"
    },
    "condition": {
        "threshold": 1,
        "check_interval": 15
    },
    "metric": {
            "alarm_list": [],
            "missing_list": [],
            "namespace": "AWS/EC2",
            "metric": "StatusCheckFailed",
            "metric_query": "metricsV2?graph=~(view~'timeSeries~stacked~false~metrics~(~(~(expression~'SELECT*20MAX*28StatusCheckFailed*29*20FROM*20SCHEMA*28*22AWS*2fEC2*22*2c*20InstanceId*29*20GROUP*20BY*20InstanceId*20ORDER*20BY*20MAX*28*29*20DESC*20LIMIT*2010~label~'**cffc**b9ac1~id~'q1)))~region~'ap-northeast-2~stat~'Average~period~300)&query=~'*7bAWS*2fEC2*2cInstanceId*7d*20status",
            "image_s3_bucket": "[s3bucketname]",
            "image_query": [ { "expression": "SELECT MAX(StatusCheckFailed) FROM SCHEMA(\"AWS/EC2\", InstanceId) GROUP BY InstanceId ORDER BY MAX() DESC LIMIT 10", "id": "q1", "region": "ap-northeast-2", "period": 300, "stat": "Maximum" } ]
    }
}

블로그 게시용으로 간소화하다보니 기본 metric 대상으로만 설명하였다. cloudwatch agent 를 구성하여 Mem, Disk 같은 추가적인 지표를 수집하거나, 운영중인 서비스 상태에 대해 Custom metric을 수집하고있다면 위와같이 간편하게 알람을 확장할 수 있다.

또한 payload에서 토큰과 멤버ID같이 기밀성 정보는 Payload로 전달하는것이 아닌, 별도의 Parameter Store에 저장해두고 Lambda 실행시 Parameter store에서 긁어오는식으로 개선할 수도 있을것이다.


참고 자료

https://github.com/korniichuk/cloudwatch-alarm?tab=readme-ov-file#create-amazon-s3-bucket
https://brunch.co.kr/@alden/53
https://hyunki1019.tistory.com/152
https://ssue-dev.tistory.com/entry/Python-Module%EC%9D%84-Lambda-Layer-%EC%83%9D%EC%84%B1-%EB%B0%A9%EB%B2%95

Leave a Reply