왜 이 모니터링 아키텍처를 선택했을까: Hybrid LGTM, HA, 외부 API 헬스체크 설계 기록
이번 결정의 핵심은 도구 선호가 아니라 장애 중에도 관측이 끊기지 않는 구조를 만드는 일이었다.
초기 서비스에서 모니터링을 어떻게 시작할 것인가
서비스를 처음 운영하기 시작하면 모니터링에 대한 첫 번째 질문이 생긴다. 외부 SaaS 솔루션을 도입할 것인가, 아니면 오픈소스 기반으로 직접 구축할 것인가.
이 질문은 “어느 쪽이 더 좋은가”로 답할 수 없다. 팀 규모, 운영 역량, 비용 구조, 데이터 보관 요구사항에 따라 정답이 달라진다.
| 기준 | 외부 SaaS | 자체 구축 |
|---|---|---|
| 초기 도입 속도 | 빠름 (1~2시간) | 느림 (수일~수주) |
| 운영 부담 | 낮음 | 높음 |
| 비용 예측성 | 트래픽 비례 증가 | 인프라 고정 비용 |
| 데이터 보관 기간 | 솔루션 제한에 의존 | 자유 설정 가능 |
| 장애 시 관측 경로 | SaaS 장애 시 통째로 끊김 | 자체 HA 설계 가능 |
| 커스터마이징 | 제한적 | 자유도 높음 |
우리 상황은 이랬다. 소규모 팀에서 운영 전담 인력 없이 개발과 운영을 병행하고 있었다.
초기에는 CloudWatch로 시작했다.
이유는 단순했다. AWS 위에서 서비스를 운영하고 있었고, CloudWatch는 추가 인프라 없이 바로 쓸 수 있었다.
ECS 컨테이너에 awslogs 드라이버를 지정하면 로그가 CloudWatch Logs로 즉시 흘러갔고, 비용은 수집량 비례였지만 초기 트래픽에서는 사실상 0원에 가까웠다.
모니터링 시스템을 설계하기 전에, 서비스가 정상적으로 동작하는지부터 확인하는 게 먼저였다.
하지만 운영 기간이 늘면서 한계가 드러났다. 메트릭은 APM, 로그는 CloudWatch Logs, 트레이스는 별도 경로로 분산되어 있어서 장애가 터지면 “어디서부터 봐야 하는지”부터 정리해야 했다. 로그에서 문제 구간을 찾아도 해당 트레이스를 보려면 다른 도구로 이동해야 했고, 메트릭과 로그의 시간 축을 수동으로 맞추는 작업이 반복됐다.
여기에 비용 제약이 동시에 있었다. 트래픽 증가 시 모니터링 비용의 상한을 예측할 수 있어야 했다. 동일 조건으로 비교했을 때 SaaS 기반은 월 약 $1,257, 자체 구축은 월 약 $303으로 4배 이상 차이가 났다.
이 상황에서 요구사항을 두 가지로 좁혔다.
- 장애 분석 리드타임을 줄일 것
- 월간 운영비를 예측 가능한 범위로 유지할 것
제약 조건
아키텍처를 고르기 전에 먼저 못 바꾸는 조건을 고정했다.
| 제약 | 설명 |
|---|---|
| 관측 연속성 | 일부 컴포넌트 장애 시에도 최소 관측 경로 유지 필요 |
| 외부 의존성 | 결제/메시징 등 외부 API 장애를 내부 지표만으로는 조기 탐지 어려움 |
| 비용 통제 | 보관 기간/데이터량 증가에 따른 비용 상한 필요 |
| 운영 인력 | 고급 운영 전담팀 없이도 유지 가능한 복잡도여야 함 |
이 제약을 기준으로 대안을 비교했다.
대안 비교
후보 1: 상용 SaaS 중심
장점은 초기 진입 장벽이 낮다는 점이다. 구성 속도가 빠르고 운영 포인트가 상대적으로 적다.
단점은 장기 비용과 운영 제어다. 트래픽과 보관 기간이 늘면 월 비용 변동폭이 커질 수 있다. 보관 기간이나 데이터 해상도를 원하는 대로 설정하지 못하거나, 세밀한 제어에 추가 비용이 붙는 경우도 있다. 또한 SaaS 자체에 장애가 나면 관측 경로가 통째로 끊기는 위험이 있다. 모니터링 시스템의 가용성을 외부 서비스에 의존하게 되는 셈이다.
후보 2: Hybrid LGTM
장점은 제어권이다. 메트릭/로그/트레이스 저장 정책, 보관 기간, 경로 설계를 서비스 특성에 맞출 수 있다.
단점은 운영 복잡도 증가다. 배포, 스토리지 정책, 알림 규칙, 런북 관리가 필요해진다.
비용 비교(동일 기준)
| 항목 | Hybrid LGTM | 비교안 |
|---|---|---|
| 월 비용(동일 가정) | 약 $303/월 |
약 $1,257/월 |
비용만으로 결정을 내리지는 않았다. 다만 장기 운영에서 4배 이상 차이가 나는 구조는 무시하기 어려웠다.
더 중요한 건 스케일링 시 비용 곡선이 다르다는 점이다.
| 시나리오 | Hybrid LGTM | SaaS 비교안 | 배수 |
|---|---|---|---|
| 현재 (14 vCPU) | $303/월 | $1,257/월 | 4.2배 |
| 2배 스케일 (26 vCPU) | $303/월 | ~$2,100/월 | 6.9배 |
| 4배 스케일 (50 vCPU) | $353/월 | ~$3,800/월 | 10.8배 |
LGTM은 인프라 고정 비용이므로 트래픽이 늘어도 비용이 거의 변하지 않는다. SaaS는 vCPU/로그량 비례 과금이므로 스케일아웃 시 비용이 선형 증가한다.
산정 조건
이 비용 비교는 아래 기준으로 산출했다.
| 항목 | 값 |
|---|---|
| ECS 서비스 수 | App(2태스크) + Backoffice(1) + Seller(1) = 4태스크 |
| 로그 수집량 (일) | Prod ~10 GB, Staging ~3 GB |
| 트레이스 수집 | 100% (OTEL agent 전수 수집) |
| 보관 기간 | 메트릭 15일(Prometheus), 로그 7일(Loki)+365일(S3), 트레이스 31일(Tempo)+365일(S3) |
최종 선택: Hybrid LGTM + HA + 외부 API 헬스체크 분리
세 가지를 묶어서 설계했다.
LGTM은 Grafana Labs의 관측 스택을 구성하는 네 컴포넌트의 약자다.
- Loki: 로그 저장과 조회
- Grafana: 시각화, 대시보드, 알림 라우팅
- Tempo: 분산 트레이스 저장과 조회
- Mimir: 메트릭 장기 저장 (Prometheus 호환)
“Hybrid”라는 이름을 붙인 이유는, 이 컴포넌트 중 일부는 관리형(SaaS/Cloud)으로, 일부는 직접 운영(self-hosted)으로 구성했기 때문이다.
- Hybrid LGTM
- HA 구성(memberlist 기반)
- 외부 API 전용 헬스체크 경로
이 조합을 선택한 이유는 “탐지 속도”와 “관측 연속성”을 동시에 충족했기 때문이다.
아키텍처 구조
1) Control Plane과 Data Plane 분리
아래처럼 경로를 분리했다.
flowchart LR
A[Application / Infrastructure] --> B[Telemetry Ingest]
B --> C[Tempo Trace Store]
B --> D[Loki Log Store]
E[Prometheus] --> F[Grafana]
C --> F
D --> F
G[Alert Rules] --> H[AlertManager]
H --> I[Slack]
이 구조의 목적은 한 지점 장애가 전체 관측 불능으로 확산되는 걸 줄이는 데 있다.
2) HA 적용
단일 인스턴스 의존 구조에서는 저장소/쿼리 컴포넌트 장애가 곧 관측 단절로 이어진다. 그래서 Tempo/Loki를 memberlist 기반으로 분산 구성했다.
memberlist는 gossip 프로토콜 기반의 클러스터 멤버십 라이브러리다. 각 노드가 주기적으로 다른 노드에 상태 정보를 전파하는 방식으로 동작한다. 노드 하나가 장애를 일으키면 gossip으로 다른 노드들이 이를 감지하고, 복제 팩터 설정에 따라 해당 노드의 데이터를 다른 노드가 대신 서빙한다. 복제 팩터가 2면 한 노드가 죽어도 다른 노드에서 같은 데이터를 조회할 수 있다. 이것이 “장애 중에도 관측이 끊기지 않는” 구조의 핵심 메커니즘이다.
| 환경 | HA 적용 | 복제 전략 |
|---|---|---|
| Prod | 활성화 | 복제 강화, 관측 연속성 우선 |
| Staging | 활성화(검증 중심) | 최소 복제로 비용 통제 |
핵심은 “평시 성능 최고치”보다 “장애 중 최소 관측 경로 유지”다.
3) 외부 API 헬스체크 분리
내부 애플리케이션 헬스만으로는 외부 API 이상을 빠르게 감지하기 어렵다.
내부 /health가 정상이어도 외부 결제 API가 실패할 수 있기 때문이다.
그래서 외부 API 전용 탐지 경로를 분리했다.
sequenceDiagram
participant Scheduler as EventBridge Scheduler
participant Lambda as Health Check Lambda
participant External as External API
participant CloudWatch as CloudWatch Alarm
participant Slack as Slack
Scheduler->>Lambda: 주기 실행
Lambda->>External: Synthetic request
External-->>Lambda: 성공/실패 응답
Lambda->>CloudWatch: metric publish
CloudWatch->>Slack: 임계치 초과 시 알림
이 분리를 통해 “고객 문의 이후 인지”에서 “사전 인지”로 탐지 시점을 앞당겼다.
Write Path 설계: 쓰기 경로에 집중한 이유
LGTM 스택을 도입할 때 가장 먼저 고민한 건 “어떻게 보여줄 것인가”가 아니라 “어떻게 안전하게 쓸 것인가”였다.
이유는 간단하다. 쓰기가 실패하면 관측 데이터가 유실되고, 장애 중에 원인을 추적할 수 없게 된다. 읽기 성능은 나중에 튜닝할 수 있지만, 유실된 데이터는 복구할 수 없다.
세 컴포넌트의 쓰기 경로
Loki, Tempo, Prometheus는 모두 같은 패턴으로 데이터를 쓴다.
flowchart LR
A[데이터 수신] --> B[Distributor]
B --> C[Ingester]
C --> D[WAL · 로컬 디스크]
C --> E[메모리 버퍼]
E --> F[원격 스토리지 · S3]
| 컴포넌트 | 수신 | Ingester 동작 | WAL 위치 | 최종 저장소 |
|---|---|---|---|---|
| Loki | Log Entry → Distributor | 메모리 버퍼 + WAL 동시 기록 | 로컬 디스크 /loki/wal/ |
S3 (365일) |
| Tempo | Span → Distributor | 메모리 버퍼 + WAL 동시 기록 | 로컬 디스크 /tempo/wal/ |
S3 (365일) |
| Prometheus | Scrape → 수집기 | 메모리 → WAL → TSDB 블록 | 로컬 디스크 /prometheus/wal/ |
로컬 TSDB (15일) |
세 컴포넌트 모두 Ingester가 WAL(Write-Ahead Log)을 로컬 디스크에 먼저 기록한다.
WAL은 데이터가 메모리에만 있을 때 프로세스가 죽더라도 디스크에서 복구할 수 있게 하는 안전장치다. 데이터베이스에서 트랜잭션 안전성을 위해 사용하는 것과 같은 원리다. 메모리 버퍼에 있는 데이터가 S3로 flush되기 전에 Ingester가 크래시하면, WAL이 없으면 그 데이터는 사라진다. WAL이 있으면 프로세스 재시작 후 디스크에서 복구하여 S3로 재전송한다.
이 구간이 쓰기 경로의 핵심이다.
복제 전략과 쓰기의 관계
WAL만으로는 부족하다. 디스크 자체가 손상되거나 노드가 완전히 유실되면 WAL도 함께 사라진다.
그래서 복제 팩터를 설정했다. 복제 팩터가 2면, Distributor가 같은 데이터를 2개의 Ingester에 동시에 전달한다.
flowchart LR
D[Distributor] --> IA[Ingester A · WAL + Memory]
D --> IB[Ingester B · WAL + Memory]
IA --> S3[S3 Storage]
IB --> S3
Ingester A가 다운되어도 Ingester B에 같은 데이터가 남아 있어서 쿼리가 가능하다.
| 장애 시나리오 | WAL만 있을 때 | WAL + 복제 팩터 2 |
|---|---|---|
| Ingester 프로세스 크래시 | WAL에서 복구 가능 | 다른 Ingester에서 즉시 쿼리 가능 |
| 디스크 손상 | 데이터 유실 | 다른 Ingester에서 복구 가능 |
| 노드 완전 유실 | 데이터 유실 | 다른 노드에서 계속 서빙 |
복제 팩터를 높이면 쓰기 부하도 비례해서 증가한다. 팩터 2면 같은 데이터를 2번 써야 하므로 Ingester 리소스가 2배 필요하다.
현재 트래픽에서는 Loki ingestion이 한도의 0.1% 수준(0.01 MB/s, 한도 10 MB/s)이므로, 복제 팩터 2에서도 쓰기 부하가 문제되지 않았다. 트래픽이 10배 증가해도 한도의 1% 수준이라 당분간 복제 비용을 감당할 여유가 있다.
이 복제 전략이 “장애 중에도 관측이 끊기지 않는” 구조의 실제 메커니즘이다.
컴퓨팅 선택: 왜 Fargate와 ECS EC2를 나눴는가
모든 컴포넌트를 같은 방식으로 배포하지 않았다. 각 컴포넌트가 로컬 상태를 유지해야 하는지 여부에 따라 배포 대상을 나눴다.
구분 기준: 로컬 디스크에 상태를 유지하는가
| 특성 | 컴포넌트 | 배포 대상 |
|---|---|---|
| Stateless · Scale-out | App, Backoffice, Seller | Fargate |
| Stateful · WAL 필수 | Loki Ingester, Tempo Ingester, Prometheus TSDB | ECS EC2 + EBS |
애플리케이션(App, Backoffice, Seller)은 요청을 받아 처리하고 응답하면 끝이다. 모든 상태는 데이터베이스에 위임하므로 컨테이너가 죽어도 다른 컨테이너가 즉시 대체할 수 있다. 이런 워크로드는 Fargate가 적합하다. 인프라 관리 없이 태스크 수만 조절하면 된다.
반면 Loki/Tempo/Prometheus의 Ingester는 다르다. 이들은 WAL과 메모리 버퍼를 로컬에 유지하면서 데이터를 처리한다. 컨테이너가 재시작되면 WAL에서 데이터를 복구해야 하는데, 이 WAL이 영구 디스크에 있어야만 복구가 가능하다.
Fargate를 선택할 수 없었던 이유
Fargate에도 태스크 스토리지가 있다. 하지만 세 가지 제약이 LGTM 배포에 맞지 않았다.
| 제약 | 설명 | 영향 |
|---|---|---|
| Ephemeral Storage 소멸 | Fargate 태스크가 종료되면 로컬 스토리지가 함께 사라짐 | WAL 유실 → 아직 S3에 도달하지 못한 데이터 손실 |
| 태스크 IP 변경 | 재시작 시 새 IP가 할당됨 | memberlist gossip이 기존 노드를 인식하지 못해 클러스터 불안정 |
| EBS 직접 마운트 제한 | Fargate EBS(2024 출시)는 단일 태스크 연결만 지원하고 태스크 종료 시 detach | Ingester 재시작 시 볼륨 재연결 과정에서 WAL 복구 경로가 복잡해짐 |
Fargate EFS는 마운트 가능하지만, EFS는 네트워크 기반 파일시스템이라 WAL 쓰기에 필요한 순차 I/O 지연시간이 로컬 디스크 대비 높다. WAL은 모든 쓰기 연산 전에 디스크에 동기적으로 기록되므로, 쓰기 지연이 곧 전체 ingestion 처리량을 제한한다.
ECS EC2 + EBS를 선택한 이유
| 이유 | 설명 |
|---|---|
| EBS 영구 볼륨 | 컨테이너 재시작, EC2 재부팅에도 데이터 보존. WAL이 항상 디스크에 남아 있어 크래시 복구 가능 |
| 고정 네트워크 주소 | EC2 인스턴스에 고정 ENI를 할당하면 memberlist가 안정적으로 동작. 노드 장애 감지와 복구가 예측 가능해짐 |
| 비용 효율 | 모니터링 스택은 24/7 상시 실행이므로 Reserved Instance 적용 시 Fargate 대비 저렴 |
EBS 볼륨 타입은 gp3을 선택했다. gp3은 기본 3,000 IOPS와 125 MB/s 처리량을 제공하며, Loki/Tempo WAL의 순차 쓰기 패턴에 충분하다. Prometheus TSDB의 랜덤 읽기가 많은 경우에도 IOPS를 별도로 프로비저닝할 수 있어 유연하다.
이 선택에서 감수한 것
EC2를 직접 사용하면 Fargate에 비해 운영 포인트가 늘어난다.
- AMI 업데이트와 보안 패치를 직접 관리해야 한다
- EC2 인스턴스의 용량 계획(디스크, CPU, 메모리)을 주기적으로 점검해야 한다
- Auto Scaling Group 설정과 Capacity Provider 관리가 추가된다
이 부담은 모니터링 스택의 관측 연속성을 확보하기 위해 의도적으로 감수한 것이다. 애플리케이션은 Fargate로 운영 부담을 줄이고, 상태를 유지해야 하는 모니터링 인프라에만 EC2를 집중시켰다.
왜 이렇게 동작해야 했는가
원인 1. 장애 상황에서는 단일 관측 경로가 먼저 끊긴다
장애가 발생하면 보통 애플리케이션만 흔들리는 게 아니다. 로그 파이프라인, 쿼리 저장소, 알림 경로가 같이 영향을 받는다.
그래서 모니터링 시스템 자체도 부분 장애를 전제로 설계해야 한다.
원인 2. 내부 지표만 보면 외부 실패를 놓친다
5xx, CPU, 메모리만 보면 내부 서비스는 정상처럼 보일 수 있다.
하지만 결제/메시징 API가 부분 장애면 실제 사용자 경험은 이미 나빠져 있다.
외부 API는 별도 synthetic check가 필요하다.
원인 3. 비용은 순간값이 아니라 성장곡선으로 봐야 한다
모니터링 비용은 트래픽과 보관 기간이 같이 증가할 때 급격히 커질 수 있다. 초기 월비용만 비교하면 장기 의사결정이 틀어지기 쉽다.
그래서 이번 결정에서는 “현재 비용”보다 “증가 시 상한 예측 가능성”을 우선했다.
운영 관점에서 얻은 것
- 로그/트레이스 상호 추적 경로가 하나로 합쳐져 RCA 시작까지의 시간이 줄었다. 기존에는 CloudWatch Logs에서 로그를 찾고 별도 APM에서 트레이스를 연결하는 과정이 필요했지만, Grafana 단일 화면에서 로그 → 트레이스 → 메트릭을 바로 넘나들 수 있게 됐다.
- 보관 정책을 서비스 특성에 맞게 조정했다. 로그는 Loki에서 7일간 핫 데이터로 유지하고, S3에 365일 아카이브한다. 메트릭은 Prometheus에서 15일, 트레이스는 Tempo에서 31일 + S3 365일이다.
- 외부 API 헬스체크 분리로 탐지 시점을 앞당겼다. Lambda Log Forwarder 에러율이 89%에서 0%로 안정화됐고, Loki 쿼리 성공률은 55%에서 100%로, 트레이스 조회 성공률은 35%에서 100%로 개선됐다.
- 현재 로그 수집량은 한도의 0.1% 수준(Loki ingestion 0.01 MB/s, 한도 10 MB/s)으로 트래픽 10배 증가까지 현재 인프라로 대응 가능하다.
운영 관점에서 감수한 것
- IaC/배포/알림 규칙 관리 포인트가 늘었다.
- 팀 내 구조 이해 비용이 증가했다.
- 런북이 없으면 알림 대응 편차가 크게 벌어진다.
- 장애 훈련을 하지 않으면 HA 설계 의도가 운영에서 사라진다.
그래서 구조 도입과 동시에 아래를 고정했다.
- 알림 라우팅 표준
- 장애 분류 기준(내부/외부/관측 경로)
- 온콜 대응 런북
도입 순서(실무형)
- 먼저 “무엇을 포기할지” 결정한다.
- 관측 연속성이 우선이면 HA를 먼저 넣고, 초기 단순성이 우선이면 단일 구성으로 시작한다.
- 내부 헬스와 외부 의존성 헬스를 분리한다.
- 비용 비교는 월 총액만 보지 말고 보관 기간/데이터 증가율까지 같이 본다.
- 마지막에 도구를 선택한다.
구조를 먼저 정하고 도구를 고르면, 운영 중 방향 전환 비용이 훨씬 작다.
실패하기 쉬운 지점
- 대시보드를 많이 만드는 데 집중하고 알림 설계를 늦추는 경우
- 메트릭만 보고 외부 API synthetic check를 생략하는 경우
- HA를 켰지만 장애 연습이 없어 실제 대응이 단일 경로처럼 동작하는 경우
- 비용 비교를 초기 구간만 보고 보관 증가 시나리오를 빼는 경우
정리
이번 선택은 “최신 도구 도입”이 목적이 아니었다. 장애 중에도 관측이 유지되는 운영 구조를 먼저 고른 결과가 Hybrid LGTM + HA + 외부 API 헬스체크 분리였다.
운영 복잡도는 분명히 올라간다. 하지만 관측 연속성, 탐지 속도, 비용 예측 가능성을 동시에 만족하려면 구조적 분리가 필요했다.
참고 자료
- Grafana Loki: https://grafana.com/docs/loki/latest/
- Grafana Tempo: https://grafana.com/docs/tempo/latest/
- Prometheus: https://prometheus.io/docs/introduction/overview/
- Grafana Alerting: https://grafana.com/docs/grafana/latest/alerting/
- AWS EventBridge Scheduler: https://docs.aws.amazon.com/scheduler/latest/UserGuide/what-is-scheduler.html
- AWS Lambda: https://docs.aws.amazon.com/lambda/latest/dg/welcome.html
- AWS CloudWatch Alarms: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html
Comments