올해 초 우리는 전용 서버 호스팅 환경에서 AWS로 인프라를 이전했는데, 지금 와서 돌아보면 정말 잘한 선택이었다는 생각이 듭니다. 자동화를 폭넓게 구축해 두었기 때문에 서버를 직접 만지작거리는 데 쓰는 시간이 크게 줄었죠. :) 이 글에서는 에러 알림(error notification) 처리량에 변화가 생길 때 우리 인프라에서 어떤 일이 일어나는지 소개합니다.
스케일링 방식
우리의 처리 파이프라인은 Sidekiq에 크게 의존하고 있으며, Sidekiq은 그동안 놀랄 만큼 안정적으로 동작해 왔습니다. 파이프라인 작업의 대부분은 오토스케일링 그룹(autoscaling group)에 속한 인스턴스들이 처리합니다. Sidekiq 백로그(backlog)가 증가하면 CloudWatch 알람이 트리거되어 해당 그룹에 새 인스턴스가 시작되고, 트래픽이 잦아들어 백로그가 사라지면 인스턴스는 다시 종료됩니다.
더 세련되게 스케일링하기
이 설정의 유일한 걸림돌은, 인스턴스가 종료되기 전에 Sidekiq 워커가 하던 작업을 모두 마친 상태여야 한다는 점입니다. Sidekiq은 완료되기 전에 종료된 작업을 다시 스케줄링하는 기능을 지원하지만, 가능하면 그런 상황은 피하는 것이 좋습니다. 작업을 재스케줄링해 처음부터 다시 시작하는 것보다 몇 초 더 들여 끝까지 완료하는 편이 낫기 때문입니다. 다행히 Sidekiq에는 현재 작업을 마친 후 새로운 작업 수락을 중단하도록 워커 프로세스에 지시하는 기능이 있어서, 종료를 앞두고 워커를 우아하게 드레인(drain)하기가 아주 쉽습니다. 핵심은 워커들에게 드레인을 시작해야 한다고 알려주는 것인데, 여기서 우리는 Lifecycle Hook, CloudWatch Events + Lambda, EC2 Simple Systems Manager(SSM), 그리고 약간의 코드를 조합해 사용합니다.
이 글의 나머지 부분에서는 여러분도 우리와 같은 구성을 따라 해서, 조용히 그리고 자연스럽게 종료되는 인스턴스를 만드는 방법을 보여드리겠습니다.
사전 준비 사항
먼저, 사용자가 정의한 규칙에 따라 인스턴스를 종료하는 스케일링 정책이 적용된 오토스케일링 그룹이 필요합니다. 그다음, 인스턴스가 SSM Agent를 실행하도록 구성되어 있고 SSM 접근 권한이 있는 IAM 역할로 실행 중이어야 합니다. 또한 이 인스턴스들은 오토스케일링 API와 상호작용할 수 있는 권한도 갖춰야 합니다. 다음은 인스턴스 역할(instance role)을 위한 정책 문서 예시입니다.
코드 살펴보기
준비가 끝났다면 각 조각들을 연결할 차례입니다. 프로세스의 맨 끝, 즉 인스턴스 위에서 동작하며 Sidekiq 드레인을 처리하는 스크립트부터 시작해 거꾸로 거슬러 올라가 보겠습니다. 먼저 워커들을 조용히 시키고 작업이 끝날 때까지 기다리는 Ruby 코드입니다.
Sidekiq API를 통해 현재 인스턴스에서 실행 중인 Sidekiq 프로세스 목록을 가져오고, 새로운 작업 수락을 중단하도록 지시한 뒤, 모든 프로세스에서 활성 작업(active job)이 없어질 때까지 기다립니다.
다음은 인스턴스 종료 시점에 이 스크립트를 실행시키는 셸 스크립트입니다.
설정할 부분이 꽤 있지만, 이 스크립트의 핵심은 8번째 줄부터 시작됩니다. 먼저 라이프사이클 훅을 처리 중이라는 신호를 오토스케일링 그룹에 보냅니다. 이 호출에는 가드(guard) 역할이라는 부수적인 효과도 있습니다. 만약 이 인스턴스가 오토스케일링 그룹에 의해 종료 대상으로 지정되지 않았다면 이 명령은 실패하고, 워커 셧다운 스크립트도 실행되지 않습니다. 원래 이 스크립트는 인스턴스가 종료 예정일 때만 호출되어야 하지만, 조금 방어적으로 작성해 두면 해가 될 것은 없습니다. :) 문제없이 진행된다고 가정하면, 워커들을 종료한 뒤 오토스케일링 그룹에게 종료를 계속 진행하라고 알립니다. 이 API 호출을 하지 않으면, 훅 타임아웃이 만료될 때까지 인스턴스는 계속 실행 상태로 남아 있게 됩니다(타임아웃에 대해서는 뒤에서 설명합니다).
SSM 문서
이제 이 셸 스크립트를 실행시켜 줄 무언가가 필요합니다. 바로 여기서 SSM Agent가 등장합니다. SSM Agent는 백그라운드에서 조용히 대기하면서, SendCommand를 통해 Document가 전달되기를 기다렸다가 지시받은 작업을 수행합니다. 우리의 Document는 에이전트에게 종료 스크립트를 실행하도록 지시합니다.
Document는 EC2 콘솔의 Systems Manager 공유 리소스(Shared Resources) 섹션에 있는 Documents 링크(사이드바 하단 부근)를 클릭하고, Create Document 버튼을 누른 뒤, Content 입력란에 JSON을 붙여 넣으면 생성할 수 있습니다.
Lambda 함수
Document가 준비되었다면, 인스턴스 종료 시점에 대상 인스턴스에서 이 Document를 실행할 Lambda 함수를 만듭니다. 참고할 코드는 다음과 같습니다.
PrepInstanceForTermination은 여러분이 Document에 지정한 이름으로 바꿔야 하지만, 그 외에는 이 코드를 그대로 복사해서 붙여 넣으면 됩니다.
이 Lambda 함수에는 (기본적인 Lambda 권한 외에) 몇 가지 추가 권한이 필요하며, 다음은 그를 위한 IAM 정책 문서입니다.
마찬가지로 Document 이름은 여러분의 환경에 맞게 교체해야 할 수 있습니다.
훅과 이벤트 규칙
조금만 더 힘내세요. 거의 다 왔습니다! 남은 작업은 두 가지, CloudWatch Event와 Lifecycle Hook을 만드는 것입니다. 먼저 Event부터:
CloudWatch 관리 콘솔에서 사이드바의 Events를 클릭한 다음 Create rule 버튼을 클릭합니다. 이 규칙이 방금 만든 Lambda 함수를 트리거하게 됩니다.

왼쪽에서는 감시할 이벤트와 추가 조건(예: 특정 오토스케일링 그룹에서 발생한 이벤트만 트리거하도록 제한)을 선택하고, 오른쪽에서는 규칙의 대상(target)을 지정합니다. 우리의 경우 앞서 만든 Lambda 함수가 대상입니다. Lambda 함수 대상에 대해서는 기본값을 그대로 두어도 됩니다.
여기까지 마쳤다면 EC2 관리 콘솔로 돌아가 관리하려는 인스턴스가 속한 오토스케일링 그룹으로 이동합니다. Lifecycle Hooks 탭을 클릭하면 훅을 생성할 수 있습니다. 우리의 설정은 다음과 같습니다.

우리가 주목하는 전환(transition)은 Terminate이며, 어떤 이유로든 셸 스크립트의 API 호출이 600초(Heartbeat Timeout) 안에 이루어지지 않더라도 종료가 계속 진행되기를 바랍니다(Default Result: CONTINUE). 덕분에 설령 Sidekiq 워커가 작업을 끝내지 못했더라도, 10분이 지나면 인스턴스는 어느 쪽이든 반드시 종료됩니다.
마무리
여기까지 오느라 고생하셨습니다! 이제 여러분은 Sidekiq 인스턴스를 인간적이고 우아한 방식으로 종료할 수 있는 오토스케일링 그룹을 갖추게 되었습니다. 즐겁게 활용하세요!
P.S.: 이 구성 요소들을 조합하는 방법을 다룬 awslabs의 아주 유용한 튜토리얼에 감사를 전합니다.