프로젝트에 이벤트 기반 아키텍처를 도입한 이유

OhJiwoo·2024년 4월 13일
0

프로젝트에 이벤트 기반 아키텍처를 도입하게 된 이유에 대해서 정리해보고자 합니다.

프로젝트 개요

먼저 어떤 프로젝트인지에 대해서 짧게 언급하고 넘어가겠습니다. 대학교 4학년 때 수강했었던 클라우드 컴퓨팅이라는 교과목에서 진행했던 프로젝트입니다. AWS에서 제공하는 SDK(Software Development Kit)를 이용하여 AWS의 리소스를 다루는 코드를 작성하는 것입니다. 최종으로 구현한 기능은 다음과 같습니다.

  • EC2 인스턴스 2개로 HTCondor Cluster를 구축할 것
  • 인스턴스 리스트 출력, 인스턴스 시작, 인스턴스 중지, AMI로부터 인스턴스 생성, AMI 이미지 정보 출력, 가용영역과 리전 출력, 인스턴스 재부팅, 인스턴스 종료 구현
  • Systems Manager를 이용하여 HTCondor Cluster에 condor_status 명령어 결과 반환
  • SNS(Simple Notification Service)를 활용한 이메일 알림 서비스

프로젝트에 대해 더 궁금하다면, Github를 참고해주세요!

기존 아키텍처

저는 SNS를 활용한 이메일 알림 서비스에 이벤트 기반 아키텍처를 도입하였습니다. 해당 서비스는 사용자가 알림을 설정하면 이메일을 전송하는 기능을 가지고 있습니다. 알림은 인스턴스 시작, 인스턴스 중지, 인스턴스 생성, 인스턴스 종료에 대해 설정할 수 있고, 만약 인스턴스 시작에 대한 알림을 설정했다면 인스턴스를 시작하는 작업을 할 때마다 이메일로 알림을 받을 수 있습니다.

기능을 구현하기 위해 AWS의 SNS(Simple Notification Service)를 사용했습니다. 사용자가 알림을 생성하면 그에 해당하는 SNS Topic을 생성합니다.

또한 알림을 생성할 때 이메일을 입력받는데, 20초 안에 해당 이메일을 통해 확인을 하지 않으면 알림을 생성할 수 없도록 구현하였습니다.

이는 SNS에서 Subscribe로 이메일을 등록했을 때 확인해야만 정상적으로 구독이 이루어지기 때문입니다.

메일을 통해 확인을 하면 SNS Topic과 구독이 정상적으로 생성됩니다.

이제 인스턴스 시작에 대한 알림이 생성된 것입니다. 알림의 존재 여부를 판단하기 위해 알림의 후보가 될 수 있는 4가지 API 작업이 이루어질 때마다 아래의 코드를 실행하였습니다.

# 알람 확인 후 알람 전송
def send(self, action):
    try:
        topics = self.sns.list_topics()
        for topic in topics['Topics']:
            if topic['TopicArn'].split(':')[-1] == action:
                self.sns.publish(
                    TopicArn=topic['TopicArn'],
                    Message=f"당신의 AWS 계정으로 {action} 작업이 이루어졌습니다. 본인의 활동이 맞는지 확인해보세요."
                    )
                break

    except ClientError as err:
        print("Cannot send the email")
        print(err.response["Error"]["Code"], end=" ")
        print(err.response["Error"]["Message"])

어떤 알림이 설정되었는지 알지 못하기 때문에 알림의 후보인 인스턴스 시작, 인스턴스 중지, 인스턴스 생성, 인스턴스 종료 작업을 프로그램으로 수행할 때마다 SNS Topic이 있는지를 확인하고, 있다면 직접 메시지를 전송하는 방식인 것입니다.

아키텍처로 그려보면 다음과 같습니다.

해당 아키텍처의 문제점은 2가지가 있습니다.

  1. 만약 프로그램이 확장되어 알림의 개수가 많아진다면 너무 비효율적이다.

    알림의 개수가 10개만 되어도 만약 존재할지도 모르는 알림을 발견하기 위해 10번의 for문을 돌아야 하는 셈입니다. 이는 너무 시작 복잡도 측면에서 비효율적입니다.

  2. 프로그램이 아닌 AWS 콘솔을 통해 작업을 수행하면 알림을 받지 못한다.

    SNS Topic에 메시지를 보내는 send 함수는 프로그램 내부에서만 호출됩니다. 그래서 프로그램을 통해서가 아닌 AWS 콘솔에서 작업을 수행하면 알림이 설정되어 있어도 이메일을 받지 못하게 됩니다.

만약 해당 프로젝트를 대학교 전공 수업 수준에서 마무리한다면 이런 식으로 아키텍처를 구성해도 문제가 되지 않을 것입니다. 하지만 실제로 운영되는 서비스라고 한다면, 보다 효율적으로 서비스를 개선할 필요가 있을 것입니다. 그래서 이벤트 기반 아키텍처를 생각하게 되었습니다.

개선된 아키텍처

결과적으로 개선된 아키텍처는 다음과 같습니다.

  1. CloudTrail 추적이 모든 API 호출을 기록합니다.
  2. 사용자가 알림을 생성하면 그에 맞는 EventBridge 규칙과 SNS Topic을 생성합니다.
  3. 알림으로 설정한 API 작업이 일어난다면 EventBridge에 의해 자동으로 SNS에 메시지가 게시됩니다.
  4. SNS에 의해 사용자에게 이메일 알림이 전송됩니다.

이렇게 아키텍처를 구성하면 콘솔에서 작업을 해도 알림을 받을 수 있으며, EventBridge에 의해 알아서 API 호출을 인식할 수 있습니다.

코드 살펴보기

AWS 자습서를 참고했습니다.

CloudTrail 추적 생성

콘솔을 통해 CloudTrail 추적을 생성합니다.

그러면 S3 버킷이 자동으로 생성되고, 버킷으로 API 호출 이력이 저장됩니다.

EventBridge 규칙 생성

event_pattern = '{\n' \
+ '  "source": ["aws.ec2"],\n' \
+ '  "detail-type": ["AWS API Call via CloudTrail"],\n' \
+ '  "detail": {\n' \
+ '    "eventSource": ["ec2.amazonaws.com"],\n' \
+ '    "eventName": ["' + name + '"]\n' \
+ '  }\n' \
+ '}'

# EventBridge 규칙 생성
self.event.put_rule(
    Name = name,
    EventPattern=event_pattern,
    State='ENABLED'
)

EventBridge 규칙 생성을 자동화하기 위해 API 이름을 name에 저장하여 사용했습니다.

EventBridge 규칙에 대상 연결

# EventBridge가 SNS에 메시지를 게시할 권한 부여 (리소스 기반 정책)
attribute = self.client.get_topic_attributes(  # 기존 권한 가져오기
    TopicArn=topic
)

json_policy = json.loads(attribute['Attributes']['Policy'])   # 문자열을 json으로 변환
                        
# 추가할 권한 정의
policy = {
    "Sid": "AWSEvents_" + name + "_id",
    "Effect": "Allow",
    "Principal": {
         "Service": "events.amazonaws.com"
    },
    "Action": "sns:Publish",
    "Resource": topic
}
json_policy['Statement'].append(policy)   # 권한 추가

# 새로운 권한 생성 (json을 문자열로 바꾸기 위해 json.dumps 사용)
new_policy = '{"Version":"' + json_policy['Version'] + '",' + '"Id":"' + json_policy['Id'] + '",' + '"Statement":' + json.dumps(json_policy['Statement']) + '}'

# 속성 업데이트
response = self.client.set_topic_attributes(
    TopicArn = topic,
    AttributeName = 'Policy',
    AttributeValue = new_policy
)

# 대상(sns) 연결
self.event.put_targets(
    Rule=name,
    Targets=[
    {
        "Id": name + "_SNS",
        "Arn": topic,
        "Input": json.dumps("귀하의 계정으로 " + name + " 작업이 이루어졌습니다. 본인이 수행한 활동이 아니라면 계정의 보안을 체크해보세요.", ensure_ascii = False)
    }]
)

이 부분이 가장 어려웠습니다. EventBridge 규칙에 SNS Topic을 대상으로 연결하기 위해서는 SNS에 리소스 기반 정책을 부여해야 했기 때문입니다. 그래서 생성한 SNS Topic의 속성을 가져와서 Policy 부분을 수정하는 작업을 수행했습니다.

  1. Topic의 속성에서 Policy 부분 가져와서 Json으로 변환
  2. EventBridge가 SNS에 메시지를 Publish 할 수 있도록 권한을 Json으로 작성
  3. 새로운 권한을 기존 권한 뒤에 붙이기
  4. 새로운 정책을 문자열로 변환하여 속성 업데이트
  5. 권한이 부여된 SNS Topic을 EventBridge 대상으로 설정

결론!

이렇게 불필요한 API 사용을 줄이고 코드 효율을 증가할 수 있었습니다. 아키텍처에 사용된 EventBridge, S3, SNS 모두 서버리스 서비스이기 때문에 비용 측면에서도 아주 효율적입니다. 다만 CloudTrail 추적으로 인해 S3에 과도한 API 이력이 저장되면서 비용이 발생할 우려가 있어서, 알림이 존재할 때만 추적 로깅을 활성화하도록 코드를 구현하였습니다. 즉 알림을 생성할 때 기존에 설정된 알림이 없을 때에 로깅을 활성화했고, 알림을 삭제할 때 삭제하려는 알림이 마지막 남은 것이라면 로깅을 비활성화했습니다.

이보다 더 좋은 아키텍처가 있을지를 고민해보고 계속 프로젝트를 업데이트해나갈 예정입니다.

profile
클라우드에 대해 공부하고 있습니다👩‍🎓

0개의 댓글