AWS/AWS 알아두면 좋은 지식

[Lambda] 이벤트 처리 중복 알람 이슈 해결

[앙금빵] 2024. 11. 10.

1. Issue

(1) AWS 계정 내 Auto Scaling Group의 활동을 탐지하기 위해 EventBridge 서비스를 이용하여 슬랙으로 전송을 받는 Lambda를 구성하였다.

Workflow


(2) 여기서 지나칠 수 없는 이슈가 발생하게 되어지는데, 그것은 바로 동일한 이벤트에 대하여 중복된 알람이 발생되는 부분이다.

 

(3) ASG 활동을 탐지하기 위해 EventBridge 설정을 아래와 같이 설정하였는데, 이 설정 경우, 인스턴스별 이벤트를 탐지하기에, 하나의 스케일링 활동에서 각 인스턴스마다 Lambda 함수가 호출이 되어지게 된다. 예를 들어, ASG가 10개에서 20개 인스턴스로 스케일 아웃되어지는 경우, 함수가 10번 호출되어 동일한 스케일링 이벤트에 대해 여러 알림이 전송된다.

{
  "source": ["aws.autoscaling"],
  "detail-type": [
    "EC2 Instance Launch Successful",
    "EC2 Instance Terminate Successful",
    "EC2 Instance Launch Unsuccessful",
    "EC2 Instance Terminate Unsuccessful"
  ]
}

 

(4) 정리하자면, 단일 스케일링 액션에서 여러 번 Lambda 트리거되어 하나의 이벤트에 대해 중복된 알림을 받게 되어진다.

 

(5) 이와 관련하여 Case Open을 진행해보았으나, ASG나 EventBridge 설정을 통해 해결할 수 없는 부분은 없으며 코드레벨에서 해결해야한다는 답변을 받았다.

Case Open 답변

 

2. 문제 해결과정

목적은 단일 스케일링 액션에서 EventBridge에서 발생되는 트리거를 통합하여 스케일링 액션당 하나의 알림만 보내는 것을 목표로 둔다.

 

이를 해결하기 위해서 Stateless 특성을 지닌 Lambda 함수에서 어떻게 중복방지를 할 것인지에 대한 아이디어가 중요하였다.

 

아이디어 1. SNS Topic을 통한 Slack 전송

AWS 측에서는 SNS Topic을 통해 중복알람을 방지할 수 있다는 부분을 공유해주었다.

 

그러나 이 방법 경우 제한된 Template으로 단순 Scale Event 정보가 아닌 Scale 활동 당시 CPU 사용량, 변화 후 ASG 수량과 같은 Customize 한 정보를 담아서 원하는 포맷의 알람을 받는데 있어서 원하는 방향이 아니었다.

ASG > Activity > Create notification

 

(★) 아이디어 2. 별도 저장소에 Scale 활동을 정보를 저장하고 참조하여 알람 발송하기

다른 방법은 별도 저장소에 scale 활동정보를 저장하고 참조하는 로직을 구현하는 것이다. 구현만 가능하다면, 원하는 내용을 담아 전달할 수 있는 찾고 있던 방법이다.

 

Lambda 함수가 이벤트를 보내기 전, 해당 알람을 보낸 기록을 확인하고 없을 경우 알람 전송, 있을 경우에는 보내지 않으면 된다. 여기서 나는 Scale 활동 기록을 S3 bucket 저장소로 채택하였으며, (1) Scale 활동을 기록을 통한 중복 알람 방지 (2) Event에 대한 필요정보를 JSON으로 기록하여 Athena로 분석을 가능하도록 로직을 추가하였다.

 

여기서 핵심적으로 중요한 부분은 어떻게 Scale 활동에 대한 중복을 판단하는 방법이다. 여기서 한 Scale 활동에서 발생하는 다수의 이벤트에서 모두 공통적으로 가지는 값을 찾는 것이 핵심이다.

 

이를 위해 Scale Activity에 대한 이벤트 로그중 하나를 살펴보면 다음과 같다. (일부 정보는 마스킹처리를 진행하였다.)

{
  "version": "0",
  "id": "30b292c8-2f00-60bc-2f11-31f4b5cdcf14",
  "detail-type": "EC2 Instance Launch Successful",
  "source": "aws.autoscaling",
  "account": "abc123456789",
  "time": "2024-11-08T05:28:47Z",
  "region": "ap-northeast-1",
  "resources": [
    "arn:aws:autoscaling:ap-northeast-1:abc123456789:autoScalingGroup:xxxxxxxxxxxxxxx:autoScalingGroupName/jonghyun-test",
    "arn:aws:ec2:ap-northeast-1:abc123456789:instance/i-xxxxxxxxxxxxxxx"
  ],
  "detail": {
    "Origin": "EC2",
    "Destination": "AutoScalingGroup",
    "Description": "Launching a new EC2 instance: i-xxxxxxxxxxxxxxx",
    "EndTime": "2024-11-08T05:28:47.160Z",
    "RequestId": "73164c28-fbfd-7053-600e-a3091c6d90fe",
    "ActivityId": "73164c28-fbfd-7053-600e-a3091c6d90fe",
    "StartTime": "2024-11-08T05:28:41.052Z",
    "EC2InstanceId": "i-xxxxxxxxxxxxxxx",
    "StatusCode": "InProgress",
    "StatusMessage": "",
    "Details": {
      "Subnet ID": "subnet-xxxxxxxxxxxxxxx",
      "Availability Zone": "ap-northeast-1a",
      "InvokingAlarms": [
        {
          "AlarmArn": "arn:aws:cloudwatch:ap-northeast-1:abc123456789:alarm/jonghyun-test",
          "Trigger": {
            "MetricName": "CPUUtilization",
            "ComparisonOperator": "GreaterThanOrEqualToThreshold",
            "Statistic": "AVERAGE",
            "Period": 60,
            "EvaluationPeriods": 2,
            "Threshold": 50
          },
          "AlarmName": "jonghyun-test",
          "NewStateValue": "ALARM"
        }
      ]
    },
    "Cause": "At 2024-11-08T05:28:33Z a monitor alarm jonghyun-test in state ALARM triggered policy jonghyun-test-scaleout changing the desired capacity from 2 to 4. At 2024-11-08T05:28:38Z an instance was started in response to a difference between desired and actual capacity, increasing the capacity from 2 to 4.",
    "AutoScalingGroupName": "jonghyun-test"
  }
}

 

여기서 공통적으로 가지는 필드값은 "Cause" 에 대한 정보였다.

"Cause": "At 2024-11-08T05:28:33Z a monitor alarm jonghyun-test in state ALARM triggered policy jonghyun-test-scaleout changing the desired capacity from 2 to 4. At 2024-11-08T05:28:38Z an instance was started in response to a difference between desired and actual capacity, increasing the capacity from 2 to 4.",

 

여기서 "At 2024-11-08T05:28:33Z" 부분이 (1) 동일한 Scaling 활동의 Event에서는 동일한 값을 (2) 다른 Scaling 활동과 비교를 하였을때 Unique한 특성을 지녔기에 이 부분을 추출하여 중복알람을 판단할 수 있는 핵심 근거로 채택하였다.

cause = event['detail'].get('Cause')

timestamp_pattern = re.compile(r"At ([0-9-T:Z]+)")
timestamp_match = timestamp_pattern.search(cause)
if timestamp_match:
    cause_timestamp = timestamp_match.group(1)
else:
    cause_timestamp = None

 

이렇게 추출한 정보를 바탕으로 scaling_action_id를 다음과 같이 정의한다면 동일한 스케일링 활동에서 발생하는 모든 이벤트는 동일한 scaling_action_id를 갖게 되므로, 이 값을 기준으로 중복 여부를 판단할 수 있게 된다.

#cause 문자추출
capacity_pattern = re.compile(r"capacity from (\d+) to (\d+)")
capacity_match = capacity_pattern.search(cause)

from_capacity = int(capacity_match.group(1))
to_capacity = int(capacity_match.group(2))
    
scaling_action_id = f"{asg_name}-{from_capacity}-{to_capacity}-{cause_timestamp}"
object_key = f"asg-status/scaling_actions/{scaling_action_id}"

# scaling action 실행여부 판단
try:
    s3.head_object(Bucket=bucket_name, Key=object_key)
    print(f"{scaling_action_id} 이미 실행된 이벤트입니다.")
    return
except ClientError as e:
    if e.response['Error']['Code'] == '404':
        pass  # scaling_id 오브젝트가 존재하지 않는 경우 신규생성
    else:
        print(f"Error checking S3 object: {e}")
        return

try:
    s3.put_object(
        Bucket=bucket_name,
        Key=object_key,
        Body='',
        ContentType='text/plain'
    )
    print(f"S3 {object_key}에 {scaling_action_id} 저장")
except Exception as e:
    print(f"S3 저장실패: {e}")
    return

 

 
여기서, from_capacity, to_capacity 정보를 넣은 이유는 동일한 시간에 다른 scaling 활동을 의도치 않게 진행하여 발생할 수 있을 가능성을 고려하여 추가하였다. (가능성은 매우 희박하다.)
 
 
워크 플로으를 그려보자면 아래와 같이 그려진다.
 

추가 이슈사항

코드 수정후 문제가 해결되는 것으로 보였으나, 간헐적으로 중복알람이 발생되어지는 추가이슈가 확인되어졌다.

발생 원인으로는 Lambda 함수에서 하나의 Scaling Event에 대하여 동시적으로 실행되어지는 특성 때문이었다. 한 이벤트를 처리하는데 평균 Duration 타임은 200 ~ 400ms의 소요시간이 걸린다. S3에 scaling_id가 저장되어지는 시간에 다른 이벤트에 대한 처리가 이루어져 올바르게 중복판단이 되지 않았다.

 

그렇기에, 여러 이벤트에 대한 동시적 실행이 아닌 순차적으로 실행이 되어져야 하는 부분이 보장되어져야 했으며 이를 위해 Lambda concurrency 설정을 통해 순차적 실행이 되어지도록 수정하였다.

Lambda concurrency 설정

 

Configuration > Concurrency and recursion detection > Use reserved concurrency, Reserved concurrency = 1 로 설정하면 된다. 이 항목을 수정한 이후에 더이상 중복 알람은 발생되지 않았다.


3. 결론

정답을 알면 단순하지만, 이 방법은 검색엔진을 아무리 활용하더라도 찾을 수 없었기에 더욱 어려웠다. 찾는 과정이 어려웠던 만큼 해결한 보람이 더 컸다. Lambda 외 서비스 의존을 최소화 그리고 복잡한 구성 없이 이슈를 해결하였다는 부분에 강점을 두고 싶다.

 

 

 

 

댓글