들어가며
안녕하세요. AB180에서 Airbridge 서비스의 DevOps 업무를 맡고 있는 정동호입니다.
Airbridge 서비스가 커지면서 운영 중에 확인해야 할 신호도 함께 늘어났습니다. 장애를 더 빨리 발견하고, 담당자가 더 빠르게 대응을 시작할 수 있도록 Alert을 만드는 방식과 전달되는 모습을 정리하고 싶었습니다.
처음 목표는 단순했습니다. Alert을 더 쉽게 만들고, 더 보기 좋게 보내고, 누가 봐야 하는지 명확히 하는 것이었습니다. 하지만 운영을 이어가다 보니 처음 개선한 구조만으로는 부족했습니다. Alert이 늘어나면서 작성 방식, 묶어 보여주는 방식, 대응 흐름, 시스템 안정성까지 함께 다듬어야 할 지점이 보였습니다.
이 글에서는 2025년 4월부터 시작한 Alert 개선 작업, 그 이후 1년 가까이 운영하며 느낀 문제, 그리고 2026년 6월까지 이어진 두 번째 개선 작업을 차례로 소개합니다.
동기
서비스의 가용성을 높이고, 장애가 발생하더라도 그 임팩트를 낮추는 방법은 여러 가지가 있습니다. 1년 전 저는 그중에서도 Alert 시스템을 개선하는 방향으로 Airbridge 서비스의 가용성을 높이고자 했습니다.
위 그림은 서비스가 정상 상태에서 장애를 겪고, 다시 정상 상태로 돌아오기까지의 흐름을 나타냅니다. 저는 Alert 시스템을 개선함으로써 이 흐름에서 다음 목표를 달성하고자 했습니다.
•
장애가 발생하기까지 걸리는 평균 시간인 MTTF(Mean Time To Failure) 늘리기
•
장애가 발생한 뒤 탐지되기까지 걸리는 평균 시간인 MTTD(Mean Time To Detect) 줄이기
•
장애가 탐지된 뒤 사람이 인지하기까지 걸리는 평균 시간인 MTTA(Mean Time To Acknowledge) 줄이기
•
사람이 장애를 인지한 뒤 사용자 임팩트를 줄이기까지 걸리는 평균 시간인 MTTM(Mean Time To Mitigate) 줄이기
결과적으로 장애는 더 적게 발생하게 하고, 발생하더라도 더 빠르게 탐지하고 대응할 수 있는 구조를 만들고 싶었습니다.
그렇다면 Alert으로 구체적으로 무엇을 할 수 있을까요?
Alert의 역할
안정적인 서비스를 위해서는 무엇보다 장애가 발생하지 않도록 예방하는 것이 중요합니다. 코드 리뷰, E2E 테스트, SDLC 준수처럼 변경 사항이 서비스에 반영되기 전에 문제를 줄이는 방법도 있지만, 운영 중인 서비스에서 발생하는 이상 징후를 빠르게 감지하는 것도 그만큼 중요합니다.
적절한 Alert을 구성하면 문제가 실제 장애로 이어지기 전에 위험 신호를 인지하고 미리 조치할 수 있습니다. 즉, Alert은 장애가 발생했을 때 담당자에게 알리는 도구이면서, 장애를 사전에 예방하기 위한 운영 장치이기도 합니다.
물론 모든 장애를 사전에 막을 수는 없습니다. 하지만 장애가 발생한 이후에도 Alert은 중요한 역할을 합니다. 잘 설계된 Alert은 "문제가 발생했다"는 사실과 함께, 담당자가 상황을 빠르게 파악하고 조치할 수 있도록 필요한 컨텍스트와 기능을 제공합니다. 이를 통해 장애를 더 빠르게 인지하고, 더 빠르게 완화하고, 결과적으로 복구에 걸리는 시간을 줄일 수 있습니다.
그렇다면 기존 Alert에는 어떤 아쉬움이 있었고, 왜 개선이 필요했을까요?
기존 Alert들의 아쉬웠던 점
Alert을 만들기가 어렵다
당시 Alert을 만들고 전달하는 데 관여하는 시스템은 꽤 다양했습니다. Grafana, Slack, PagerDuty, AWS CloudWatch, AWS EventBridge, AWS Lambda처럼 Alert 생성과 전달에 직접 관여하는 도구들이 있었고, NewRelic, VictoriaMetrics, Steampipe, OpenSearch, Druid, MySQL처럼 Alert의 기준이 되는 지표나 상태를 제공하는 시스템들도 함께 얽혀 있었습니다.
어떤 Alert을 어디서 만들고, 어떤 데이터를 기준으로 판단하고, 어떤 채널로 보내고, 누가 받게 할 것인지에 따라 작업 방식도 제각각이었습니다. 예를 들어 어떤 Alert은 Grafana에서 만들어 Slack으로 보내야 했고, 어떤 Alert은 CloudWatch Alarm으로 만든 뒤 별도 Lambda를 거쳐 가공해야 했습니다. 또 어떤 Alert은 Steampipe로 AWS 리소스 상태를 조회한 결과를 기준으로 판단해야 했습니다. 어떤 경우에는 PagerDuty 연동까지 고려해야 했습니다.
Alert을 만들기 위해 알아야 하는 시스템과 연결 구조가 많다 보니, 이미 전체 맥락을 알고 있는 사람에게는 크게 어렵지 않은 작업도 처음 작업하는 사람에게는 꽤 큰 시행착오가 되곤 했습니다.
조직 차원의 Best Practice나 일관된 컨벤션이 부족하다는 점도 아쉬웠습니다. 어떤 Alert을 어디에서 관리할지, 어떤 기준으로 Slack에만 전달할지 또는 PagerDuty Incident까지 생성할지, Alert 메시지에는 어떤 수준의 설명과 대응 링크를 포함할지, 담당 팀과 전달 경로는 어디에서 관리할지 등이 명확히 정리되어 있지 않았습니다. 결과적으로 Alert은 필요할 때마다 만들어졌지만, 시간이 지날수록 만드는 방식과 관리 방식이 점점 파편화되었습니다.
Alert을 보기가 어렵다
Alert을 만들었다고 해도, 실제로 Slack에서 받은 Alert이 항상 보기 좋은 형태였던 것은 아닙니다. 누가, 어떤 시스템에서, 어떤 방식으로 만들었는지에 따라 Alert 메시지의 품질이 꽤 들쭉날쭉했습니다.
위 이미지들은 예전에 Slack으로 전달되던 Alert의 예시입니다. Grafana Alert은 기본 Slack 연동 포맷에 가까운 형태였고, Amazon Q와 CloudWatch 기반 Alert은 또 다른 포맷이었습니다. PagerDuty로 전달되던 incident도 마찬가지로 형식이 제각각이었습니다.
물론 Alert 메시지의 "보기 좋음"이나 "품질"은 어느 정도 주관적인 영역입니다. 하지만 운영하는 입장에서 아쉬웠던 점은 분명했습니다. Alert 제목은 길고 복잡했고, Value나 Labels 같은 내부 값이 그대로 노출되어 핵심 정보를 한눈에 파악하기 어려웠습니다. Source, Silence, Dashboard, Panel 같은 링크는 제공되었지만, 장애 상황에서 담당자가 가장 먼저 무엇을 봐야 하는지는 명확하지 않았습니다. 일부 Alert에는 Dashboard 조회나 Log 조회처럼 대응에 도움이 될 것 같은 버튼이 포함되어 있었지만, 실제 연동이 제대로 되어 있지 않은 경우도 있었습니다.
버튼을 눌러도 바로 필요한 대시보드나 로그로 이동하지 못하거나, Alert의 컨텍스트가 충분히 전달되지 않아 결국 담당자가 직접 서비스, 클러스터, 리소스, 시간 범위를 다시 찾아야 하는 경우가 있었습니다. Alert은 왔지만, Alert에서 바로 다음 행동으로 이어지지 못했습니다.
Alert마다 담긴 정보의 수준도 달랐습니다. 어떤 Alert은 설명과 대응 힌트가 있었지만, 어떤 Alert은 단순히 임계치를 넘었다는 사실만 알려주었습니다. 어떤 서비스에서 발생한 문제인지, 영향 범위는 어디까지인지, 바로 확인해야 할 대시보드는 무엇인지, 임시로 조치할 방법은 있는지 등이 일관되게 제공되지 않았습니다.
결국 Alert은 오고 있었지만, 담당자가 그것을 보고 바로 상황을 이해하고 대응하기에는 부족한 경우가 많았습니다. 장애 상황에서는 몇 분의 차이도 크게 느껴지기 때문에, Alert을 받는 순간 필요한 정보를 최대한 빠르게 파악하고 다음 행동으로 이어질 수 있는 구조가 필요했습니다.
Alert의 대응 책임 관리가 어렵다
Alert이 울렸을 때, 이것을 누가 확인하고 대응해야 하는지 애매한 경우도 있었습니다. Alert이 특정 서비스나 리소스의 이상 징후를 알려주고 있더라도, 그것이 어떤 팀의 책임인지, 실제로 누가 먼저 확인해야 하는지, 필요하다면 어디로 에스컬레이션해야 하는지가 명확하지 않은 경우가 있었습니다.
이런 상황에서는 Alert을 인지하더라도 실제 조치로 이어지기까지 시간이 길어질 수밖에 없습니다. 담당자가 정해져 있지 않으면 서로 눈치를 보거나, 일단 아는 사람이 확인하거나, 뒤늦게 관련 팀을 찾아 전달하는 식으로 대응이 흘러가기 쉽습니다.
문제는 Alert이 울린 뒤의 대응 책임만이 아니었습니다. Alert 자체를 누가 소유하고 관리하는지도 명확하지 않은 경우가 있었습니다. 어떤 팀의 서비스와 관련된 Alert인지, 누가 조건을 변경할 수 있는지, 임계치나 메시지를 수정할 때 어떤 기준으로 검토해야 하는지에 대한 기준이 부족했습니다.
Alert은 한 번 만들고 끝나는 설정이라기보다, 서비스 구조와 트래픽, 운영 방식이 바뀌면 함께 관리되어야 하는 운영 규칙에 가깝습니다. 그런데 오너십이 명확하지 않으면 오래된 Alert이 그대로 남거나, 실제 담당 팀과 맞지 않는 전달 경로가 유지되거나, 조건이 바뀌었는데도 제대로 리뷰되지 않는 문제가 생기기 쉽습니다.
그래서 Alert은 "어떤 문제인지, 얼마나 중요한지, 어디를 봐야 하는지"와 함께, "누가 책임지고 확인해야 하는지, 이 Alert 자체를 누가 관리해야 하는지"도 드러나야 했습니다.
정리하면, 제가 개선하고 싶었던 상황은 다음과 같았습니다.
•
Alert을 만들고 관리하는 방식이 파편화되어 있어, 새 Alert을 추가하거나 수정할 때마다 시행착오가 크다.
•
Alert을 받아도 무슨 의미인지 한눈에 파악하기 어렵고, 어디를 먼저 확인해야 하는지 바로 알기 어렵다.
•
Alert이 울렸을 때 누가 확인하고 대응해야 하는지 애매하고, Alert 자체를 어떤 팀이 소유하고 관리해야 하는지도 명확하지 않다.
이 문제를 해결하기 위해, Alert 시스템을 단순히 알림을 보내는 기능에 그치지 않고, Alert을 더 쉽게 만들고, 한눈에 이해할 수 있고, 책임 있게 관리할 수 있는 운영 시스템으로 개선하고자 했습니다.
무엇을 어떻게 개선했나
앞서 정리한 문제는 크게 세 가지였습니다.
•
Alert을 만들고 관리하는 방식이 파편화되어 있다.
•
Alert을 보고 바로 상황을 이해하고 다음 행동으로 이어가기 어렵다.
•
Alert의 책임 구조와 관리 흐름이 명확하지 않다.
그래서 개선 방향도 이 세 가지에 맞췄습니다. Alert을 만드는 방식을 표준화하고, Slack 메시지에서 필요한 정보를 일관된 구조로 보여주고, Alert마다 담당자와 전달 경로가 드러나도록 구조를 정리했습니다.
Alert 생성과 관리 방식 표준화하기
기존에는 Alert을 만드는 방식이 출처와 전달 경로에 따라 제각각이었습니다. 어떤 Alert은 Grafana에서, 어떤 Alert은 CloudWatch Alarm으로, 어떤 Alert은 EventBridge와 Lambda를 거쳐 Slack으로 전달되었습니다. PagerDuty 연동이 필요한 경우에는 또 다른 설정을 알아야 했습니다.
이런 구조에서는 Alert을 하나 추가하려고 해도 먼저 알아야 할 것이 많았습니다. 어떤 도구에서 만들어야 하는지, 어떤 채널로 보내야 하는지, PagerDuty까지 연결해야 하는지, Slack 메시지에는 어떤 정보를 넣어야 하는지, 담당자는 어디에 정의해야 하는지를 매번 확인해야 했습니다.
그래서 먼저 Alert 생성과 관리 방식을 하나로 모았습니다.
Alert rule을 평가하고 실행하는 주체는 Grafana로 통일하고, Grafana, Slack, PagerDuty 사이의 연동은 Terraform Module로 추상화했습니다. 그리고 모든 Alert 정의는 사내 alerts 레포의 alerts/ 디렉터리 아래에서 IaC로 관리하도록 했습니다.
이제 Alert을 추가하거나 수정하는 사람은 전체 Alert 파이프라인을 모두 이해할 필요가 없어졌습니다. alerts/ 디렉터리에 있는 기존 Alert들을 참고해 필요한 설정을 추가하면 되고, 공통 모듈이 Slack 채널 연결, PagerDuty 연동, 메시지 포맷, 공통 버튼 생성처럼 반복되는 부분을 처리합니다.
Alert 작성자가 집중해야 할 부분은 "어떤 조건을 감지할 것인가", "얼마나 중요한 Alert인가", "누가 확인해야 하는가", "대응에 어떤 정보가 필요한가"가 되도록 만들고 싶었습니다. 레포 안에는 README.md와 SYNTAX.md 문서를 두어 Alert 작성 방식, 디렉터리 구조, 필요한 필드, 권장 컨벤션을 함께 관리했습니다. Alert을 코드로 관리하면서 리뷰와 변경 이력도 PR과 커밋 단위로 남게 되었고, 누가 어떤 Alert을 왜 추가했는지도 확인할 수 있게 되었습니다.
결과적으로 Alert 생성 작업은 여러 시스템을 돌아다니며 설정을 맞추는 일에서, 정해진 레포에 정해진 형식으로 Alert을 추가하는 일로 바뀌었습니다. 진입 장벽이 낮아졌고, 동시에 조직 차원의 컨벤션과 리뷰 흐름도 함께 만들 수 있었습니다.
모든 Alert은 {Main Category}/{Sub Category}/{Severity}/{Name}.yml 형태를 따르도록 했습니다. 예를 들어 인프라, 데이터 파이프라인, 비용, 리포트처럼 큰 카테고리를 먼저 나누고, 그 아래에 세부 컴포넌트와 심각도별 Alert을 배치했습니다.
이렇게 디렉터리 구조를 정리하니 Alert 파일의 위치만 봐도 어떤 영역의 Alert인지, 어느 정도의 심각도로 다루고 있는지 파악하기 쉬워졌습니다. 또한 특정 영역의 Alert을 한곳에서 모아 볼 수 있어, 중복 Alert이나 오래된 Alert을 점검하기도 쉬워졌습니다.
이 분류 작업은 비교적 수월했습니다. 이미 비용 추적을 위해 컴포넌트마다 태그와 레이블을 정리해둔 상태였기 때문에, 기존 분류 체계를 Alert 디렉터리 구조에도 그대로 반영할 수 있었습니다.
IaC로 관리되는 Alert의 예시는 위와 같습니다. 각 Alert 파일에는 datasource, query, threshold, condition, message, Runbook, dashboard 링크처럼 Alert을 정의하고 대응하는 데 필요한 정보가 함께 들어갑니다.
여기서 rules필드는 별도의 DSL(Domain-Specific Language)을 새로 만든 게 아닙니다. Grafana Alert이 JSON으로 직렬화된 내용을 YAML로 표현한 것입니다. 덕분에 Grafana에서 정의할 수 있는 Alert이라면 대부분 같은 구조로 IaC화할 수 있었습니다. 새 문법을 처음부터 설계하기보다, Grafana의 기존 표현 방식을 최대한 그대로 가져와 관리 방식을 코드 중심으로 바꾼 셈입니다.
물론 초반에는 이런 rules를 직접 작성하는 일이 쉽지만은 않았습니다. datasource마다 query 방식이 다르고, Grafana Alert의 내부 구조도 익숙해지기 전까지는 진입 장벽이 있었습니다. 그래서 주요 datasource별로 자주 쓰는 케이스의 예시를 몇 가지 만들어두고, 이후에는 그 예시를 참고해 Alert을 추가할 수 있도록 했습니다.
이후에는 LLM을 활용하면서 Alert 작성 방식이 더 쉬워졌습니다. 현재는 사람이 자연어로 "어떤 조건에서 어떤 메시지로 Alert을 받고 싶다"는 요구사항을 설명하면, LLM이 기존 예시와 컨벤션을 참고해 YAML 형태의 Alert 정의 초안을 만들어주는 방식으로도 활용하고 있습니다. 덕분에 Alert 작성자는 복잡한 연동 구조나 Grafana Alert의 세부 직렬화 형식을 모두 외울 필요 없이, "어떤 상황을 감지하고 싶은지"와 "그 Alert이 왜 필요한지"에 더 집중할 수 있게 되었습니다.
Alert 메시지를 바로 이해하고 대응할 수 있게 만들기
저는 Alert 메시지도 하나의 인터페이스라고 생각했습니다.
우리는 처음 보는 프로그램을 사용하더라도 창을 닫거나 최대화하는 버튼을 오래 찾지 않습니다. 화면마다 생김새는 조금씩 달라도, 익숙한 기능은 대체로 익숙한 위치에 있기 때문입니다. Alert도 마찬가지입니다. 장애 상황에서는 메시지를 천천히 읽고 해석할 여유가 많지 않기 때문에, 어떤 Alert이 오더라도 같은 위치에서 같은 종류의 정보를 확인할 수 있어야 했습니다.
그래서 Slack으로 전달되는 Alert 메시지의 구조를 최대한 일관되게 만들었습니다. 제목에는 Alert 이름, 상태, 심각도를 표시하고, 본문에는 사람이 바로 이해할 수 있는 설명을 넣었습니다. 그 아래에는 담당자, 팀, 서비스, 리전, 리소스 이름, 주요 라벨처럼 대응에 필요한 컨텍스트를 정해진 형태로 보여주도록 했습니다.
또한 Alert마다 공통적으로 사용할 수 있는 버튼도 같은 위치에 배치했습니다. IaC, PagerDuty, Silence 같은 버튼은 모든 Alert에서 동일한 의미로 동작하도록 만들었습니다. Runbook, Dashboard, Log처럼 Alert별로 별도 연동이 필요한 버튼은 설정된 경우에만 표시하고, 공통 버튼은 시스템에서 자동으로 생성해 연결되도록 했습니다.
이렇게 하면서 사람이 직접 링크를 잘못 넣거나, 버튼은 있는데 실제로는 동작하지 않는 문제를 줄일 수 있었습니다. Alert을 받은 사람은 메시지 안에서 PagerDuty Incident를 열어 보거나, 필요하면 Silence를 걸거나, 이 Alert이 정확히 어떻게 정의되어 있는지 확인하기 위해 IaC가 정의된 곳으로 이동할 수 있게 되었습니다.
Alert의 상태 변화도 Slack 스레드에 남기도록 했습니다. 누가 Acknowledge 했는지, Slack에서 처리했는지 PagerDuty에서 처리했는지, 언제 Resolve 되었는지, 대응 중 어떤 메모가 남았는지를 같은 스레드 안에서 확인할 수 있게 했습니다. 덕분에 Alert 하나를 중심으로 대응 흐름이 하나의 스레드에 기록되고, 나중에 다시 봤을 때도 무슨 일이 있었는지 파악하기 쉬워졌습니다.
무엇보다 중요한 점은, 누가 어떤 Alert을 어떻게 만들더라도 Slack에서 보이는 형태가 비슷해졌다는 것입니다. 포맷이 제각각이면 매번 메시지를 해석하는 데 시간이 들지만, 포맷이 일관되면 구성원들이 점점 익숙해지고, Alert을 받았을 때 어디를 봐야 하는지, 무엇을 눌러야 하는지 빠르게 판단할 수 있습니다.
물론 Alert 메시지만으로 모든 장애 대응이 해결되지는 않습니다. 하지만 최소한 Alert을 받는 순간 "이게 무슨 문제인지", "얼마나 중요한지", "어디를 먼저 확인해야 하는지"를 더 빠르게 판단할 수 있는 형태에 가까워졌습니다.
Alert 책임 구조를 명확히 하기
Alert을 보고 바로 상황을 이해할 수 있게 만드는 것만큼이나 중요했던 것은, 그 Alert을 누가 책임지고 확인해야 하는지, 그리고 Alert 자체를 누가 관리해야 하는지를 명확히 하는 일이었습니다.
Alert은 결국 사람이 대응해야 하는 신호입니다. 그런데 Alert이 울렸을 때 담당 팀이나 담당자가 드러나지 않으면, Alert을 본 사람이 먼저 "이건 내가 봐야 하나?", "이건 누구에게 물어봐야 하지?"부터 생각하게 됩니다. 장애 상황에서는 이 짧은 판단 과정도 대응 지연으로 이어질 수 있습니다.
그래서 먼저 Alert이 울렸을 때 리소스의 태그와 레이블 정보를 대응 흐름에 적극적으로 활용하도록 했습니다. Alert마다 담당 팀이나 담당자를 하나하나 직접 지정하는 방식 대신, Alert에 포함된 서비스·팀·리소스·환경 등의 메타데이터를 바탕으로 적절한 담당 팀과 담당자가 Slack 메시지에서 자동으로 멘션되도록 했습니다.
이 부분은 기존에 쌓아둔 태그·레이블 체계 덕분에 비교적 수월하게 연결할 수 있었습니다. 저희는 이미 비용 추적과 리소스 관리를 위해 서비스·팀·환경·컴포넌트 정보를 태그와 레이블로 관리하고 있었고, Alert에서도 이 정보를 그대로 활용할 수 있었습니다. 덕분에 Alert을 만들 때마다 담당자를 별도로 지정하기보다, 기존 메타데이터를 기준으로 책임 대상을 식별하고 메시지에 반영할 수 있었습니다.
전달 경로도 같은 규칙 안에서 정리했습니다. Alert 작성자가 Slack 채널이나 PagerDuty 연동 여부를 매번 직접 설정하는 대신, Alert의 분류와 Severity를 기준으로 적절한 Slack 채널, PagerDuty Service, Escalation Policy가 자동으로 연결되도록 했습니다. 예를 들어 Warning 수준의 Alert은 Slack 채널로만 전달하고, 실제 사용자 임팩트나 장애 가능성이 큰 Critical Alert은 PagerDuty Incident까지 생성되도록 했습니다.
이를 위해 Slack 채널, PagerDuty Service, Escalation Policy 같은 연동 리소스도 Terraform으로 함께 관리했습니다. Alert 정의와 전달 경로, 온콜 연결이 같은 규칙 아래에서 IaC로 관리되도록 만든 것입니다.
이때 앞서 설명한 Alert 디렉터리의 계층 구조가 중요한 기준이 되었습니다. Alert이 어느 경로에 위치하는지에 따라 어떤 서비스나 컴포넌트의 Alert인지, 어느 정도의 심각도로 다뤄야 하는지, 어떤 Slack 채널과 PagerDuty Service로 연결되어야 하는지, 어떤 팀이 소유하고 관리해야 하는지를 판단할 수 있게 했습니다. 덕분에 Alert 작성자는 복잡한 연동 설정을 매번 직접 맞추기보다, 정해진 구조에 맞춰 Alert을 추가하는 것만으로 적절한 담당자와 대응 경로로 이어지게 할 수 있었습니다.
또한 alerts 레포에서는 CODEOWNERS를 통해 Alert의 소유권과 수정 범위도 함께 관리했습니다. Alert 파일은 카테고리와 서비스 영역에 따라 디렉터리가 나뉘어 있었기 때문에, 각 경로별로 담당 팀을 CODEOWNERS에 지정할 수 있었습니다. 이를 통해 어떤 팀이 어떤 Alert 영역을 소유하는지, 어떤 Alert을 누가 변경할 수 있는지에 대한 기준을 레포 안에서 명확히 드러낼 수 있었습니다.
이렇게 Alert의 책임은 두 지점에서 관리되도록 만들었습니다. Alert이 실제로 울렸을 때는 태그·레이블을 기반으로 적절한 담당 팀과 담당자가 멘션되고, Alert 정의를 변경할 때는 디렉터리 구조와 CODEOWNERS를 기준으로 어느 팀의 영역인지 확인할 수 있게 했습니다.
결과적으로 Alert의 계층 구조는 단순히 파일을 정리하는 방식에 그치지 않고, Alert의 조건·전달 경로·온콜 연결·담당자 멘션·소유권을 함께 묶는 기준이 되었습니다. 이를 통해 Alert이 울렸을 때의 대응 책임과 Alert을 변경할 때의 관리 책임이 같은 분류 체계 안에서 이어지도록 만들 수 있었습니다.
Alert proxy의 역할
그림에는 다음장 부터 소개할 기능들도 일부 포함되어 있습니다
이런 구조가 실제로 동작할 수 있었던 데에는, 중간에서 Alert을 해석하고 전달하는 proxy가 있었습니다. 이 proxy는 Alert 개선 작업을 하면서 개발한 AWS Lambda 기반 컴포넌트입니다. 이후 개선 내용에서도 proxy가 계속 등장하기 때문에, 여기서 먼저 역할을 간단히 짚고 넘어가겠습니다. 이 글에서 proxy라고 부르는 것은 Grafana와 Slack, PagerDuty 사이에 있는 중간 계층입니다.
Grafana는 Alert rule을 평가하고, 조건이 충족되면 webhook을 보냅니다. proxy는 이 webhook을 받아 Alert의 category, severity, label, annotation, fingerprint 같은 정보를 해석합니다. 그리고 이 정보를 바탕으로 어느 Slack 채널에 보낼지, 어떤 PagerDuty Incident를 만들지, 누구를 멘션할지, 어떤 버튼을 붙일지, 기존 Slack 스레드를 업데이트해야 하는지를 결정합니다.
즉, Terraform module과 디렉터리 구조가 "Alert을 어떻게 정의할 것인가"를 표준화했다면, proxy는 그 정의가 실제 운영 흐름에서 같은 방식으로 보이고 동작하도록 연결하는 역할을 했습니다. Alert 작성자는 복잡한 Slack/PagerDuty API 연동을 직접 다루지 않아도 되었고, 운영자는 어느 팀의 어떤 Alert이든 비슷한 Slack 메시지와 lifecycle로 받아볼 수 있었습니다.
이 proxy 덕분에 Alert 메시지 포맷, 담당자 멘션, PagerDuty 연동, Slack 스레드 업데이트, Ack/Resolve 같은 상호작용을 한곳에서 일관되게 관리할 수 있었습니다. 특히 Slack 메시지를 Block Kit 기반의 일관된 인터페이스로 만들 수 있었던 것도 proxy가 있었기 때문입니다. Grafana의 기본 Slack notification에만 의존했다면 버튼, 섹션, 링크, 상태 표시, 스레드 업데이트 같은 표현을 원하는 방식으로 확장하기 어려웠을 것입니다.
별도의 proxy 계층이 있었기 때문에 이후 개선도 수월해졌습니다. 같은 Alert rule에서 여러 대상이 울릴 때 하나의 grouped Alert로 묶어 보여주는 일, Slack 버튼을 눌러 조사 명령이나 제한적인 조치 명령을 실행하는 일, AI 에이전트 분석 버튼처럼 새로운 자동화 인터페이스를 붙이는 일 모두 proxy에서 Alert context를 해석하고 Slack/PagerDuty API를 직접 제어할 수 있었기 때문에 가능했습니다.
또한 provider별 차이를 흡수할 수 있다는 장점도 있었습니다. Grafana, NewRelic, 외부 시스템이 보내는 payload는 서로 다를 수 있지만, proxy 안에서 공통 Alert model로 정리하면 Slack과 PagerDuty에서는 같은 경험으로 보여줄 수 있습니다. Alert fingerprint와 Slack 스레드 정보를 DynamoDB에 저장해 lifecycle을 이어갈 수 있었고, 실패 처리·재시도·에러 알림·자체 metric 같은 운영 기능도 proxy 한곳에 모아 개선할 수 있었습니다.
…이렇게만 끝났다면 이 주제로 글을 더 쓰진 않았을 것입니다. 1년 전 위와 같은 작업을 하고 Alert 레포 관리와 시스템 운영을 이어가면서, 여전히 아쉬웠던 점들과 필요한 기능이 있었고, 한 번 더 개선 작업을 하게 되었습니다.
뭐가 또 아쉬웠나
첫 번째 개선 이후 Alert을 만드는 방식과 전달 구조는 훨씬 나아졌습니다. Alert 정의는 IaC로 관리되었고, Slack 채널과 메시지 포맷, PagerDuty Service, 전달 경로도 모두 일관된 규칙 아래 자동으로 생성되고 동작했습니다. 예전처럼 "이 Alert은 어디서 어떻게 만들어야 하지?", "배포는 어디서 하지?", "이 버튼은 왜 눌러도 동작하지 않지?", "이 Alert은 누가 언제 왜 이렇게 만든 거지?" 같은 문제는 많이 줄었습니다.
하지만 Alert 시스템은 한 번 만들고 끝나는 도구가 아니었습니다. 실제로 1년 가까이 운영해 보니, 처음에는 보이지 않던 문제가 다시 보이기 시작했습니다. Alert이 많아질수록 단순히 잘 보내는 것만으로는 부족했습니다. 이제는 같은 Alert 안에서 생긴 여러 instance를 어떻게 보여줄지, 반복되는 Alert 정의를 어떻게 관리할지, 사람이 Alert을 본 뒤 실제로 무엇을 할 수 있을지, 그리고 Alert 시스템 자체의 안정성을 어떻게 확보하고 검증할지가 중요해졌습니다.
같은 Alert이 여러 대상으로 동시에 울리면 보기 어려웠다
Alert을 만들기 쉬워진 것은 분명 좋은 변화였습니다. 하지만 만들기 쉬워지면 자연스럽게 Alert의 수도 늘어납니다.
실제로 레포 안에는 점점 더 많은 팀과 서비스의 Alert이 들어오기 시작했습니다. Report, Platform, Integration, Network, WAS, FinOps처럼 다양한 도메인의 Alert이 추가되었고, SQS 지연, DLQ 메시지, Kafka lag, Airflow task 실패, JVM OOM, Druid query delay, Kubernetes pod 상태, CloudWatch error log 같은 운영 신호가 계속 늘어났습니다.
이때 특히 불편했던 것은 하나의 Alert rule이 여러 대상에 대해 동시에 울리는 경우였습니다. Grafana에서는 같은 rule이라도 region, name, node, pod, app처럼 label 값이 다르면 각각을 별도의 Alert instance로 봅니다. 여기서 instance는 "같은 Alert 조건에 걸린 개별 대상" 정도로 이해하면 됩니다.
예를 들어 "Pod unhealthy 상태가 일정 시간 이상 지속되면 알린다"는 Alert이 있을 때, 여러 pod가 동시에 unhealthy 상태가 되면 pod마다 Alert이 하나씩 생깁니다. 같은 rule에서 나왔어도 실제로 문제가 된 대상은 제각각인 셈입니다.
Grafana에는 이미 Alert Grouping 기능이 있었습니다. 어떤 label을 기준으로 Alert을 묶을지 정하면, 같은 rule에서 발생한 여러 instance를 하나의 group으로 다룰 수 있었습니다. 남은 과제는 그 묶인 결과를 Slack과 PagerDuty에서 어떻게 보여주고, 어떤 lifecycle로 관리할 것인가였습니다.
단순히 group으로 묶었다고 해서 운영자가 보기 좋은 메시지가 자동으로 만들어지는 것은 아니었습니다. group 안에 어떤 대상들이 들어 있는지, 어떤 대상이 아직 firing인지, 어떤 대상이 방금 resolve되었는지, 새로 추가된 대상이 있는지 보여줘야 했습니다. 또한 같은 group의 상태 변화가 여러 Slack 메시지로 흩어지지 않고 하나의 흐름 안에서 이어져야 했습니다.
그래서 첫 번째 개선에서 "Alert 메시지를 보기 좋게 만드는 것"까지는 했다면, 이후에는 "Grafana가 묶어준 Alert group을 운영자가 이해하기 쉬운 Slack 메시지와 PagerDuty lifecycle로 어떻게 표현할 것인가"가 중요한 문제가 되었습니다.
반복되는 Alert 정의가 늘어났다
같은 Alert을 EKS 클러스터별로 중복 정의하던 시절
Alert 정의가 많아질수록 YAML을 직접 복사해 조금씩 바꾸는 방식도 한계가 보였습니다. 같은 종류의 SQS lag Alert을 queue마다 만들거나, 같은 CloudWatch error log Alert을 서비스와 리전마다 만들거나, Pod OOM Alert을 여러 클러스터마다 반복해서 만들 때가 많았습니다.
처음에는 기존 파일을 복사해서 이름, query, threshold, label만 바꾸면 됐습니다. 하지만 이런 파일이 많아지면 공통 동작을 고치기 어려워집니다. 버튼 이름을 바꾸거나, Runbook 링크를 추가하거나, region 값을 변경하거나, description 표현을 고치려면 비슷한 파일을 여러 개 찾아 수정해야 했습니다.
더 큰 문제는 일관성이었습니다. 같은 의도의 Alert인데 어떤 파일에는 dashboard 링크가 있고 어떤 파일에는 없거나, 어떤 파일은 labels 구성이 조금 다르거나, 어떤 파일은 threshold 표현이 달라지는 식의 차이가 생기기 쉬웠습니다.
리뷰할 때도 비슷한 어려움이 있었습니다. 두 Alert 정의가 거의 같은데 일부 필드만 다르면, 그 차이가 의도된 것인지 복사 과정에서 빠진 실수인지 바로 알기 어려웠습니다. 새 Alert을 만들 때 어떤 파일을 기준으로 복사해야 하는지도 애매해지기 시작했습니다.
그래서 Alert 작성 방식에는 개별 YAML 파일 정리와 함께, 반복되는 패턴을 재사용할 수 있는 구조가 필요했습니다.
Alert을 보고 나서도 다음 행동까지 거리가 있었다
Slack 메시지에 Runbook, Dashboard, IaC, PagerDuty 같은 버튼을 붙인 것은 큰 도움이 되었습니다. 하지만 실제 장애 대응을 하다 보면, 링크를 누르는 것만으로는 충분하지 않은 경우가 많았습니다.
특히 Runbook은 효과가 분명했습니다. 잘 작성된 Runbook이 연결된 Alert은 담당자가 바로 상황을 이해하고, 어떤 순서로 확인해야 하는지 따라가며, 필요한 dashboard나 명령으로 빠르게 이동할 수 있었습니다. 실제로 구성원들이 적극적으로 Runbook을 작성해주었고, 이 시점에는 97개의 Runbook이 작성되어 있었습니다.
하지만 전체 Alert 수에 비하면 Runbook 수는 여전히 부족했습니다. 모든 Alert에 좋은 Runbook을 붙이는 것은 쉽지 않았고, 한번 작성한 Runbook을 계속 최신 상태로 유지하는 것도 만만치 않았습니다. 서비스 구조가 바뀌고, dashboard가 바뀌고, 확인해야 하는 명령이나 운영 절차가 바뀌면 Runbook도 함께 바뀌어야 했습니다.
또 Runbook이 있다고 해도, 실제 장애 대응에서는 Alert의 label과 value를 보고 그때그때 판단해야 하는 부분이 많았습니다. 예를 들어 Kubernetes 관련 Alert을 받으면 담당자는 결국 현재 문제가 된 namespace, pod, deployment를 기준으로 로그를 보고, pod 상태를 확인하고, rollout history를 봐야 합니다. SQS나 Lambda Alert이라면 해당 queue와 function의 최근 지표와 error log를 확인해야 합니다.
결국 Runbook은 잘 작성되어 있을 때 매우 강력했지만, 모든 Alert에 충분한 Runbook을 작성하고 계속 최신으로 관리하는 데에는 한계가 있었습니다. 문제는 Alert을 본 뒤의 다음 행동이 여전히 사람의 기억과 경험에 많이 의존했다는 점입니다.
장애 대응 중에는 매번 비슷하게 반복하는 조사 작업도 많았습니다. 로그를 조회하거나, 현재 rollout 상태를 확인하거나, 특정 namespace와 pod의 상태를 보거나, SQS queue와 Lambda function의 최근 지표를 확인하는 식입니다. 하지만 이런 작업은 Slack 메시지 밖에서 따로 이루어졌습니다. 담당자는 Alert에서 label과 value를 읽고, 다른 도구로 이동해 값을 옮기고, 결과를 다시 Slack 스레드에 공유해야 했습니다.
완화 작업은 더 조심스러웠습니다. 재시작이나 스케일링처럼 반복되는 작업이라도 프로덕션 시스템 상태를 직접 바꾸는 일이라, 실행 권한·확인 절차·결과 기록이 하나의 흐름으로 묶여 있지 않으면 선뜻 자동화하기 어려웠습니다.
즉 첫 번째 개선으로 Alert을 더 잘 읽을 수 있게는 되었지만, Alert을 보고 조사와 대응을 시작하는 과정은 여전히 Alert 메시지 바깥에 많이 남아 있었습니다.
모니터링 시스템에 SPOF가 늘어났다
Alert 시스템을 정리하면서 모니터링 시스템 안에 SPOF(Single Point of Failure)가 될 수 있는 지점도 늘어났습니다. Alert rule의 정의와 배포는 alerts 레포와 Terraform에 모였고, Alert rule의 평가는 Grafana가 맡게 되었습니다. Alert이 실제로 전달된 뒤의 Slack 메시지, PagerDuty Incident, Ack/Resolve 같은 lifecycle은 proxy가 관리했습니다.
역할이 명확해진 것은 좋은 변화였습니다. 어디에서 Alert을 정의하고, 어디에서 평가하고, 어디에서 전달 상태를 관리하는지 분명해졌기 때문입니다. 하지만 역할이 한곳으로 모인 만큼, 그 지점이 실패했을 때 전체 Alert 흐름이 영향을 받을 가능성도 커졌습니다.
예를 들어 Grafana가 실패하면 Alert rule을 평가하지 못할 수 있고, proxy가 실패하면 평가된 Alert을 Slack이나 PagerDuty로 전달하지 못할 수 있습니다. Slack API 호출 실패, PagerDuty API 호출 실패, DynamoDB에 저장해야 할 Slack 스레드 정보 누락, Lambda timeout, 인증 실패처럼 proxy 내부에서만 보이는 문제도 있었습니다.
더 어려운 점은 이런 실패가 겉으로 잘 드러나지 않을 수 있다는 것이었습니다. 다른 시스템의 이상을 알려주는 경로가 조용히 멈추면, 실제 장애가 발생해도 아무도 모를 수 있습니다. Alert을 정의하고, 평가하고, Slack과 PagerDuty로 전달하고, 이후 상태 변화를 관리하는 흐름 자체도 계속 확인할 수 있어야 했습니다.
Who watches the Watchmen? - Watchmen by Alan Moore & Dave Gibbons
결국 "모니터링 시스템은 누가 모니터링할 것인가"라는 질문으로 이어졌습니다. 서비스 장애를 발견하기 위해 만든 시스템이지만, 그 시스템도 Grafana, proxy, Slack, PagerDuty 같은 여러 구성 요소 위에서 동작합니다. 이 중 하나가 멈추거나 느려지거나 실패가 겉으로 드러나지 않으면, 모니터링 시스템은 조용해 보여도 실제로는 가장 중요한 역할을 하지 못하고 있을 수 있습니다.
두 번째 개선
첫 번째 개선으로 Alert 시스템의 기본 골격은 잡혔습니다. Alert 정의는 코드로 관리됐고, Slack과 PagerDuty로 이어지는 흐름도 일관된 규칙 안에서 동작하게 됐습니다.
하지만 Alert을 만들고 보내는 일이 쉬워지자, 운영하면서 봐야 할 문제가 조금 달라졌습니다. 같은 Alert rule에서 여러 대상이 동시에 울릴 때는 그 상태 변화를 한눈에 볼 수 있어야 했고, 반복되는 Alert 정의는 계속 복사해서 늘리는 대신 재사용할 수 있어야 했습니다. Alert을 받은 뒤 조사·완화로 이어지는 거리도 줄여야 했고, Alert 전달 경로 자체가 실패했을 때 이를 알아차릴 수 있어야 했습니다.
그래서 두 번째 개선에서는 다음 네 가지를 중심으로 기존 구조를 다듬었습니다.
•
같은 Alert rule에서 여러 대상이 동시에 울릴 때, 그 상태 변화를 하나로 묶어 한눈에 보여준다.
•
반복되는 Alert 정의를 공통 패턴으로 재사용할 수 있는 구조로 정리한다.
•
Runbook을 보완하고, 반복되는 조사와 제한적인 완화 작업을 Slack 버튼으로 연결한다.
•
SPOF가 될 수 있는 Alert 정의·평가·전달 경로의 안정성을 측정하고 검증한다.
이 밖에도 Alert 작성 문법을 다시 정리하거나, Grafana 외에 NewRelic, synthetic monitoring(자체 개발·운영 중인 모니터링 시스템) 같은 provider를 proxy에 연동하는 등의 작업도 함께 진행했습니다. 다만 이 글에서는 핵심적인 변화에 집중하기 위해 그 내용까지는 따로 다루지 않겠습니다.
Grouped Alert을 제대로 다루기
먼저 손본 것은 Grouped Alert의 표현 방식이었습니다. Grafana에는 이미 Alert Grouping 기능이 있었지만, Grafana가 Alert을 묶어준다는 사실만으로 Slack과 PagerDuty에서 보기 좋은 대응 흐름이 만들어지지는 않았습니다.
같은 Alert rule에서 여러 instance가 동시에 발화할 때, 각각을 독립 메시지로 보내면 Slack 채널이 금방 복잡해집니다. 반대로 group 하나로만 뭉뚱그려 보여주면 실제로 어떤 리소스가 문제인지 놓치기 쉽습니다. 핵심은 "묶되, 묶인 안쪽의 상태를 잃지 않는 것"이었습니다.
위 예시는 Kubernetes Pod 관련 Alert을 owner별로 group해서 보내는 모습입니다.
Slack에서는 grouped Alert을 대표 메시지 하나로 보여주되, 그 안에 현재 영향을 받는 대상들을 함께 표시하도록 했습니다. 새 대상이 추가되거나 기존 대상이 해결되면, 그 변화는 같은 스레드에 남겼습니다. 여러 메시지가 채널에 흩어지는 대신, 하나의 장애 묶음을 하나의 대화 흐름으로 볼 수 있게 한 것입니다.
상태 변화가 한 번에 많이 생길 때도 따로 다뤘습니다. 대상이 여러 개 추가되거나 해결될 때마다 댓글을 하나씩 남기면 스레드가 금방 길어지고, Slack rate limit에도 영향을 받을 수 있습니다. 그래서 여러 상태 변화를 한 번에 묶어(batch) 보여주도록 했습니다.
PagerDuty 쪽도 Slack에서 보는 grouped Alert과 같은 문제를 가리키도록 맞췄습니다. Incident 제목에는 어떤 group의 문제인지 드러나게 하고, 담당자 정보도 같은 기준으로 이어지게 했습니다. Slack에서는 하나의 묶음으로 보이는데 PagerDuty에서는 다른 문제처럼 보이면 대응 흐름이 다시 갈라지기 때문입니다.
결과적으로 같은 원인에서 나온 여러 Alert을 Slack에서 하나의 흐름으로 볼 수 있게 되었습니다.
(참고로 위 그림을 보면 Alert마다 붙은 emoji가 조금씩 다릅니다. 정적인
같은 emoji도 있고, 정지된 이미지라 잘 보이진 않지만 동적인 emoji도 있는데, 이러한 차이는 의도된 것입니다. OOM이나 Error 발생처럼 이미 일어난 사실을 알리는 Alert에는 정적인 emoji를, CPU starving이나 Pod unhealthy처럼 지금도 이어지고 있는 상태를 알리는 Alert에는 움직이는 emoji를 쓰도록 했습니다)
반복되는 Alert 정의 줄이기
반복되는 Alert 정의도 다시 정리했습니다. 복사해서 조금씩 바꾸는 방식은 처음에는 빠르지만, Alert 수가 늘어날수록 유지보수 비용과 실수 가능성을 같이 키웁니다.
이를 줄이기 위해 global/templates와 matrix를 추가했습니다. global/templates는 반복되는 Alert 구조를 공통 template으로 정의해두는 기능, matrix는 같은 Alert을 여러 리전·queue·datasource·service처럼 서로 다른 조합으로 펼쳐 여러 Alert을 만드는 기능입니다.
예를 들어 SQS queue lag, CloudWatch error log, 여러 클러스터의 Pod OOM, ALB 5xx, Lambda error/throttle, ECS memory/CPU/max-capacity처럼 반복되는 패턴은 template으로 만들었습니다. 그리고 queue 이름, region, cluster, threshold, dashboard 변수처럼 Alert마다 달라지는 부분만 matrix에 입력하도록 했습니다.
이렇게 하니 같은 종류의 Alert을 만들 때 기존 파일을 복사해 조금씩 수정하는 일이 줄었습니다. 공통 메시지 구조, 버튼, Runbook/dashboard 링크 처리, datasource 처리 방식을 한곳에서 고칠 수 있었고, 새 Alert을 추가할 때도 기존 template과 matrix를 따라가면 됐습니다.
물론 모든 Alert을 template으로 만들 수는 없습니다. 서비스마다 고유한 쿼리와 대응 방식이 있는 Alert도 많습니다. 하지만 반복 패턴이 분명한 Alert을 template과 matrix로 옮긴 것만으로도, 관리하는 Alert이 많아질수록 생기는 불일치와 유지보수 비용을 줄일 수 있었습니다.
Slack 메시지에서 바로 대응 시작하기
다음으로는 Slack 메시지 안에서 할 수 있는 일을 늘렸습니다. Runbook과 dashboard 링크는 여전히 중요했지만, 실제 대응에서는 링크가 있는 것만으로 충분하지 않을 때도 있었습니다.
담당자가 Alert을 확인한 뒤 랩탑을 열고, VPN을 연결하고, 필요한 계정이나 클러스터에 접근한 다음, Alert의 label 값을 옮겨 로그와 상태를 확인하기까지 시간이 걸리는 경우가 있었습니다. 특히 매번 비슷하게 반복하는 조회나 제한적인 완화 작업이라면, 이 과정을 Slack 메시지 안에서 조금이라도 줄일 수 있으면 좋겠다 싶었습니다.
그래서 기존의 Runbook, Dashboard, IaC, PagerDuty, Silence 버튼에 custom action button을 추가했습니다.
Alert YAML의 message.actions에 명령을 정의하면, Slack 메시지에 버튼으로 표시됩니다. 버튼을 누르면 proxy가 별도 Lambda invocation으로 명령을 실행하고, 누가 어떤 버튼을 눌렀는지와 실행 결과를 같은 Slack 스레드에 댓글로 남깁니다.
예를 들어 특정 Alert에서 로그 조회 명령을 버튼으로 제공하거나, Kubernetes rollout 상태를 확인하거나, 제한된 상황에서 rollout restart 같은 완화 작업을 제공할 수 있습니다. 단일 명령뿐 아니라 여러 명령의 순차 실행도 지원했습니다. 앞 명령은 환경 준비에 쓰고, 마지막 명령의 결과만 스레드에 보여주는 식으로 사용할 수 있게 했습니다.
이 기능에서 가장 신경 쓴 부분은 안전성이었습니다.
먼저 버튼 이름이 !로 끝나면 실행 전에 Slack confirm dialog가 뜨도록 했습니다. 실수로 위험한 작업을 실행하는 일을 줄이기 위한 장치입니다.
명령 안에서도 Alert의 label과 value를 사용할 수 있게 했습니다. 예를 들어 ${labels.namespace}나 ${labels.pod} 같은 값을 명령에 넣을 수 있습니다. 이 값들은 Alert 발화 시점에 proxy가 실제 label 값으로 치환합니다. 다만 shell 명령으로 실행되는 값이므로, command injection을 막기 위해 치환 값에는 shell quoting을 적용했습니다.
권한이 더 필요한 작업을 위해서는 actionRole도 추가했습니다. 기본적으로 action command는 proxy Lambda의 기본 권한으로 실행되지만, Alert 파일에 actionRole을 지정하면 해당 Alert에 허용된 IAM role을 assume해서 실행할 수 있습니다.
이 YAML은 중앙에서 관리중인 AWS IAM 정의 파일이고
왼쪽과 같은 yaml이 실제 IAM Role, Trust relationship, IRSA(IAM Roles for Service Accounts) 로 만들어지게 됩니다.
이때 중요한 점은 권한 허용 여부를 proxy 코드가 임의로 판단하지 않는다는 것입니다. proxy는 AssumeRole을 호출할 때 Alert 경로를 session tag로 함께 전달합니다. 예를 들어 alerts/{main}/{sub}/{severity}/{name} 형태의 경로를 alertFullPath, alertMain, alertSub, alertSeverity 같은 tag로 나누어 보냅니다.
실제로 "이 Alert이 이 role을 사용할 수 있는가"는 대상 IAM role의 trust relationship policy가 판단합니다. Principal은 proxy Lambda의 execution role로 제한하고, Condition에서는 session tag를 매칭합니다. 즉, 어떤 Alert에 어떤 role을 허용할지는 IAM trust policy에 남고, 그 변경은 Terraform PR 리뷰를 거치게 됩니다.
허용되지 않은 Alert이 role ARN만 적어둔 경우에는 AssumeRole이 거부되고, 명령은 실행되지 않습니다. Alert 경로가 없거나 형식이 잘못되어 session tag를 만들 수 없는 경우도 마찬가지로 fail-closed로 처리했습니다. 실패 사유는 Slack 스레드에 남겨, 버튼을 누른 사람이 왜 실행되지 않았는지 바로 알 수 있게 했습니다.
이 구조 덕분에 누군가 실수든 고의든 Alert에 과한 권한을 붙여 실행하는 일을 막으면서도, 필요한 자동화는 적절한 리뷰와 승인을 거쳐 추가 권한을 허용할 수 있게 되었습니다.
한편, 버튼으로 명령까지 실행하게 된 만큼 proxy로 들어오는 입력 자체의 신뢰성도 따져야 했습니다. 그동안은 webhook이 어디서 왔는지 따로 검증하지 않았습니다. proxy가 하는 일이 Slack에 메시지를 보내거나 PagerDuty incident를 만드는 정도라, 위조된 요청이 들어와도 가짜 Alert이 하나 뜨는 정도여서 큰 위험이 아니었기 때문입니다. 하지만 버튼이 명령 실행으로 이어지면서, 위조된 요청 하나가 실제 작업을 일으킬 수 있게 됐습니다. 그래서 이제는 들어오는 요청이 신뢰할 수 있는 곳에서 왔는지 확인합니다. Grafana, PagerDuty, NewRelic webhook은 Bearer token으로, Slack 버튼 클릭과 상호작용은 HMAC-SHA256 서명과 replay 보호로 검증합니다.
실패 처리도 성격에 따라 나눴습니다. 핵심 Alert 전달 실패는 fail-fast로 처리해 재시도를 유도하고, Slack 채널 누락처럼 on-call 전달과 직접 무관한 문제는 warn 수준으로 낮춰 invocation 전체가 죽지 않도록 했습니다.
AI 에이전트와 연동하기
Alert을 받은 뒤 필요한 정보를 모으는 과정도 줄이고 싶었습니다. 담당자는 보통 Alert 제목과 설명을 읽고, label과 value를 확인한 뒤, IaC 정의와 대시보드, 관련 로그, 최근 변경 사항을 차례로 봅니다. 익숙한 Alert이라면 빠르게 넘어갈 수 있지만, 오랜만에 보는 서비스나 복잡한 문제에서는 이 초기 확인에 시간이 꽤 걸립니다.
그래서 AB180에서 이미 운영하고 있던 내부 AI 에이전트인 abot을 Alert 흐름에 연결했습니다. Slack 메시지의 abot 버튼을 누르면 proxy가 Alert 이름, 설명, labels, values, IaC 링크 같은 정보를 모아 abot에 분석을 요청합니다. 사용자는 필요하면 modal에서 추가 context를 입력할 수도 있습니다.
abot은 버튼을 누른 사람의 OAuth 기반 identity로 동작하도록 했습니다. Grafana, AWS, Kubernetes 등 트러블슈팅에 필요한 시스템의 정보를 조회하더라도, 사용자가 실제로 볼 수 있는 범위 안에서만 정보를 가져와야 했기 때문입니다.
분석 결과는 같은 Slack 스레드에 댓글로 남겼습니다. 어떤 지표와 로그를 확인했는지, 어떤 가능성을 우선 의심할 수 있는지, RCA(Root Cause Analysis)에 정리할 만한 단서가 무엇인지가 대응 흐름 안에 함께 남도록 한 것입니다.
초기에는 단순히 Alert 정보를 넘기는 수준이었지만, 운영하면서 prompt도 계속 다듬었습니다. EKS 로그와 포렌식 관점에서 어떤 순서로 확인하면 좋을지, 어떤 명령을 조심해야 하는지, RCA 형식으로 무엇을 정리해야 하는지를 prompt에 포함했습니다.
또한 이 prompt는 Alert 정보에 맞게 동적으로 생성하고, 분석 결과와 함께 스레드에 남겼습니다. 한 번의 분석으로 끝나지 않더라도, 다음 분석이 앞선 prompt와 결과를 이어받아 같은 맥락에서 계속 진행될 수 있게 하기 위해서입니다.
분석 모드도 둘로 나누었습니다. 장애가 막 터져 당장 1차 대응 방향부터 빠르게 잡아야 할 때는 fast mode를 쓰고, 급한 불은 어느 정도 끈 뒤 시간이 조금 더 걸리더라도 근본 원인을 차근차근 확실하게 짚고 싶을 때는 deep mode를 쓰도록 했습니다. 같은 Alert이라도 지금 필요한 것이 속도인지 깊이인지에 따라 골라 쓸 수 있게 한 것입니다.
이 기능이 장애 대응을 자동으로 대신해주는 것은 아닙니다. 다만 Alert을 받은 뒤 여러 시스템을 열어 같은 정보를 다시 모으는 시간을 줄이고, 조사 내용과 RCA에 필요한 단서를 Slack 스레드에 남기는 데 도움이 되었습니다.
모니터링 시스템 모니터링하기
앞서 "모니터링 시스템은 누가 모니터링할 것인가"라는 질문을 남겼습니다. 두 번째 개선에서는 Alert을 정의하고 평가하고 전달하는 과정 자체를 관측 대상에 포함했습니다.
가장 먼저 proxy의 운영 지표를 모았습니다. proxy는 Alert마다 Ack·Resolve까지 걸린 시간, 지금 떠 있는 Alert이 몇 건이고 얼마나 오래됐는지, 같은 Alert이 다시 울린 횟수 같은 값을 metric으로 VictoriaMetrics에 push합니다. 인지까지 걸리는 시간(MTTA)이 늘어지지는 않는지, 처리되지 않고 방치되는 Alert은 없는지, 너무 자주 반복해서 울리는 Alert은 없는지를 보기 위한 것입니다.
Lambda가 응답 없이 멈추는 경우도 대비했습니다. 실행이 timeout에 가까워지면 그 직전을 감지해 알림을 남기는 timeout watchdog을 두어, webhook은 들어왔는데 아무 처리도 되지 않는 경우를 잡도록 했습니다. proxy가 처리 도중 실패하면 그 내용을 Slack으로 알리고, 원인을 빠르게 확인할 수 있도록 full stack trace와 원본 event payload를 함께 담았습니다.
다만 proxy가 직접 알리는 방식에는 한계가 있습니다. proxy가 그 알림조차 보내기 전에 죽으면, 보내졌어야 할 Alert이 그대로 누락될 수 있습니다. 그래서 proxy 바깥에, 서로 다른 시스템에 기대는 감지 장치를 두 개 뒀습니다.
하나는 Grafana입니다. proxy가 보낸 metric을 monitoring 도메인의 Alert으로 평가해, proxy가 신호를 멈추거나 비정상 값을 내면 바로 드러나도록 했습니다.
다만 여기에는 빈틈이 있습니다. 이 metric은 결국 EKS 위에서 도는 VictoriaMetrics에 쌓이고, 그것을 평가해 Alert을 띄우는 Grafana도 같은 EKS 위에 있습니다. 부분적인 실패는 잡을 수 있지만, EKS나 Grafana가 통째로 죽으면 그것을 감지할 Grafana도 함께 죽은 상태입니다. 모니터링 시스템이 자기 자신을 감시하는 구조라, 그 자신이 죽으면 아무 신호도 나오지 않습니다.
다른 하나는 모니터링 시스템 바깥의 deadman switch입니다. Grafana는 정상일 때 주기적으로 자신의 health(/api/health)를 heartbeat로 내보내고, 이 heartbeat는 EKS와 독립된 CloudWatch가 받습니다. CloudWatch alarm은 heartbeat 값이 떨어지거나 아예 들어오지 않으면(데이터 없음을 장애로 간주) 발동해, Grafana나 proxy를 거치지 않고 PagerDuty와 Slack으로 바로 알립니다. 감지하는 쪽과 감지되는 쪽을 서로 다른 인프라에 둔 것이라, EKS와 CloudWatch가 동시에 죽지 않는 한 모니터링 시스템의 다운을 알 수 있습니다.
두 번째 개선의 초점은 Alert을 만들고 보내는 방식보다, 이미 늘어난 Alert을 어떻게 다루느냐에 있었습니다. 같은 rule에서 여러 대상이 동시에 울릴 때는 하나의 흐름으로 묶어 보여주고, 반복되는 정의는 template과 matrix로 줄였습니다. Slack 메시지 안에서는 custom action button과 abot으로 조사와 완화를 바로 시작할 수 있게 했고, Alert을 정의하고 평가하고 전달하는 경로 자체도 관측 대상에 넣었습니다.
첫 번째 개선이 Alert 시스템의 골격을 세우는 작업이었다면, 두 번째 개선은 그 위에서 운영하며 드러난 문제를 메우는 작업이었습니다. 무엇보다 모니터링 시스템이 조용할 때, 그것이 정상이라 조용한 것인지 따로 확인할 수 있게 됐습니다.
이후 개선 과제
두 번째 개선은 마쳤지만, 아직 더 개선해야 할 부분이 남아 있다고 생각합니다. 앞으로의 과제를 짧게 적어 둡니다.
수집한 운영 지표를 제대로 활용하기
앞서 "모니터링 시스템 모니터링하기"에서 proxy가 Alert 운영 지표를 VictoriaMetrics로 push한다고 했습니다. 덕분에 다음과 같은 질문에는 지금도 데이터로 답할 수 있습니다.
•
어느 채널에 어떤 Alert이 얼마나 자주 울리는가
•
특정 담당자나 팀에게 Alert이 과하게 몰리지는 않는가
•
아무런 상호작용(Ack·조사·대응) 없이 Firing과 Auto Resolve만 반복하는, 사실상 의미 없는 Alert은 없는가
다만 이렇게 보이는 것과, 이를 근거로 실제 Alert을 다듬고 조정하는 것은 별개의 일입니다. 임계치를 조정하거나, 묶거나, 아예 없애는 가지치기는 아직 본격적으로 하지 못하고 있습니다. Alert이 떠서 인지되기까지(MTTA)와 해결되기까지(MTTR) 걸린 시간도 지표로 쌓고 있지만, 이 수집 자체가 최근에야 자리를 잡은 터라 수치를 보고 있을 뿐 실제로 줄이려는 노력으로는 아직 적극적으로 이어가지 못하고 있습니다.
Alert IaC 개선
Alert 정의는 alerts 레포에서 CI/CD로 배포되지만, 지금은 Grafana Terraform provider의 grafana_rule_group 리소스에 의존하고 있고 여기서 오는 제약이 있습니다.
grafana_rule_group은 하나의 rule group 안에 여러 Alert rule을 블록으로 담는 구조입니다. 그런데 이 rule들이 Grafana 내부에서 group에 묶인 채 순서를 갖고 저장되다 보니, 개별 rule을 group과 떼어 따로 정의할 수 없습니다. 그래서 두 가지가 불편합니다.
•
rule 하나만 고쳐도 PR에서는 rule group 전체가 바뀐 것처럼 보여서, 무엇이 바뀌었는지 한눈에 드러나는 diff를 보기 어렵습니다.
•
평가 주기(interval_seconds)가 rule group 단위라, alert마다 다른 평가 주기를 주려면 group을 잘게 쪼개야 합니다. 그러면 Grafana UI에도 evaluation group이 잔뜩 생겨 오히려 보기 불편해집니다.
다행히 최근 Grafana가 Alert rule을 Kubernetes 리소스처럼 다루는 새 alerting API를 내놓았습니다. Grafana가 대시보드나 alert 같은 자기 리소스를 Kubernetes API 스타일로 관리하는 App Platform 위에 올라간 것으로, Alert rule을 rule group과 분리된 개별 리소스로 정의하고 rule마다 평가 주기를 따로 지정할 수 있습니다. Terraform provider에도 이를 감싼 grafana_apps_rules_alertrule_v0alpha1 리소스가 추가됐습니다.
다만 이름에 드러나듯 아직 alpha라 당장 도입은 보류하고 있습니다. 이 API가 stable해지면, 지금의 grafana_rule_group 대신 grafana_apps_rules_alertrule로 옮겨 rule group과 rule을 분리해 정의하는 방향으로 가려 합니다. 이렇게 하면 rule 하나만 바꿔도 그 변경만 깔끔하게 드러나는 diff를 볼 수 있고, rule마다 평가 주기를 세밀하게 조정해 모니터링 자원도 더 효율적으로 쓸 수 있을 것으로 기대합니다.
마치며
처음 목표는 단순했습니다. Alert을 더 쉽게 만들고, 받았을 때 한눈에 이해할 수 있고, 누가 책임지는지 분명하게 만드는 것이었습니다. 1차 개선으로 Alert 정의를 IaC로 모으고 Slack 메시지와 전달 경로를 표준화하면서 그 토대를 잡았습니다.
운영을 이어가며 드러난 문제는 2차 개선에서 손봤습니다. 같은 rule에서 쏟아지는 Alert을 하나로 묶어 보여주고, 반복되는 정의를 template과 matrix로 줄이고, Slack 메시지 안에서 조사와 완화를 시작할 수 있게 했습니다. 모니터링 시스템 자신이 멈췄을 때 그것을 알아차릴 장치도 마련했습니다.
덕분에 Alert을 만들고, 보내고, 대응하는 일이 전보다 수월해졌습니다. 다만 처음 동기에서 개선하려 했던 MTTF·MTTD·MTTA·MTTM을 지금 다 정량적으로 평가할 수 있는 것은 아닙니다. 아쉽게도 당시에는 각 지표의 AS IS를 측정할 여건이 되지 않아, 얼마나 개선했는지 수치로 확인하기는 어렵습니다.
그럼에도 각 지표를 개선하는 방향으로는 꾸준히 움직이고 있다고 생각합니다. Alert을 더 쉽게 만들 수 있게 되면서도 grouped Alert과 운영 지표로 경보 피로를 줄여, 위험 신호를 더 넓게 잡고(MTTF) 정작 탐지해야 할 신호를 놓치지 않도록 했습니다(MTTD). 또 태그·레이블을 기반으로 적절한 담당자에게 적절한 경로로 Alert을 보내 인지까지 걸리는 시간을 줄였고(MTTA), Runbook과 custom action button, abot 같은 AI 에이전트로 조사와 완화를 Slack 안에서 시작할 수 있게 해 완화까지 걸리는 시간을 줄여가고 있습니다(MTTM).
정량적으로는 이제 인지까지 걸리는 MTTA와 복구까지 걸리는 MTTR을 데이터로 볼 수 있게 됐습니다. 전에는 감으로만 짐작하던 시간입니다. 반면 MTTF·MTTD·MTTM은 아직 측정 체계를 갖추지 못해, 개선 방향은 잡되 그 효과를 수치로 확인하지는 못하고 있습니다. 측정을 시작한 MTTA와 MTTR도 아직 그 시간을 본격적으로 줄이는 단계까지는 가지 못했습니다. 그래도 무엇을 손봐야 할지 데이터로 짚을 수 있게 된 것만으로도 의미가 있다고 생각합니다. 그래서 다음에는 이 수치를 근거로 Alert을 실제로 줄이고 다듬는 일까지 이어가려 합니다. 지금까지가 지표를 측정할 수 있게 만드는 일이었다면, 이제부터는 그 수치를 보며 실제로 줄여가는 일이 남았습니다.
이 글이 비슷한 고민을 하는 분께 조금이나마 참고가 되면 좋겠습니다. 긴 글 읽어주셔서 감사합니다.
ᴡʀɪᴛᴇʀ
Dongho Jung @dongho-jung
DevOps Engineer @AB180

.png&blockId=38ea69a8-2507-80df-abc3-e7433d4757dc&width=3600)








.png&blockId=38ea69a8-2507-802f-8016-f6293a25bed9)























