🔥 Observability 비용 함정, 관찰하는 데 돈이 새는 이유

#AWS#CloudWatch#Observability#비용#FinOps
1553자
17분

16:9 가로 표지. 흰 배경. 위쪽에 'CloudWatch Bill, 4 Pillars' 굵은 다크 슬레이트 산세리프 제목. 가운데에 네 개의 둥근 모서리 기둥이 나란히 서 있다. 왼쪽부터 AWS 오렌지 'Collection' 기둥에 PutMetricData / PutLogEvents / PutTraceSegments 라벨과 'per call · per MB' 단가 단위. 에메랄드 'Storage' 기둥에 Logs ingestion · storage · auto downsample 라벨과 'per GB · month' 단가. 슬레이트 블루 'Query' 기둥에 Logs Insights scan · GetMetricData 라벨과 'per scan GB · per call' 단가. 앰버 'Alarm + Event' 기둥에 Alarm · Composite · Anomaly · EventBridge 라벨과 'per alarm month · per million events' 단가. 기둥 위쪽은 회색 가는 'CloudWatch invoice' 가로선으로 묶여 있고, 아래쪽에는 'diagnose: which pillar grew?' 한 줄 화살표. 흰 배경, 둥근 모서리, 절제된 그림자, 깔끔한 산세리프 글꼴의 프로페셔널 IT 다이어그램

Synthetics 카나리 한 줄을 1분 주기로 한 달 켜면 청구서에 약 $84가 올라간다는 이야기를 두 편 앞서 풀어 두었다. 한 줄짜리 관찰 도구 한 대만으로도 그 정도다. 그러면 4축(Metric / Log / Alarm / Event)을 함께 켜면 한 달 청구서는 어디까지 자라는가.

청구서를 결정한 건 내가 켠 메트릭의 수가 아니라 데이터의 형태이었다. 같은 한 개 메트릭, 같은 한 줄 로그, 같은 한 개 알람도 cardinality, 해상도, retention 세 변수가 청구서를 10배까지 구분한다. 운영 첫 날에 이 셋을 잡지 않으면 기본값은 항상 비싼 쪽으로 기울어 있다. 한 가지 다행은, 잡아 두는 데 드는 코드 양은 다섯 줄이면 충분하다는 점이다.

청구서는 4개 기둥으로 나뉜다

CloudWatch 청구서를 처음 받아 들면 라인이 한눈에 들어오지 않는다. MetricMonitorUsage, Requests, DataProcessing-Bytes, TimedStorage-ByteHrs, DataScanned-Bytes, AlarmMonitorUsage, DashboardsUsageHour 같은 usage type이 리전 prefix와 함께 CUR(Cost and Usage Report)에 줄줄이 늘어선다. 라인 하나하나는 다른 곳에서 온다. 그래서 청구서 전체를 한눈에 읽으려면 라인 하나하나가 아니라 그 위 기둥을 봐야 한다.

기둥은 4개다. 첫째, 수집. PutMetricData / PutLogEvents / PutTraceSegments 같은 호출비가 들어가는 곳이다. 단가는 호출당 또는 MB당. 둘째, 저장. Logs ingestion·storage, 메트릭은 자동 다운샘플 retention timeline로 흘러간다. 단가는 GB·월당. 셋째, 쿼리. Logs Insights 스캔과 GetMetricData 호출이 들어간다. 단가는 스캔한 GB당 또는 호출당. 넷째, 알람·이벤트. Alarm/Composite/Anomaly Detection, EventBridge 인제스션이 묶이는 단계. 단가는 알람·월당 또는 백만 이벤트당이다.

내 청구서가 어디서 컸는지 진단할 때, 4 기둥 어디서 들어왔는지부터 보면 길이 빠르게 더 좁다. 같은 $50라도 수집 라인이 컸으면 cardinality 의심, 저장 라인이 컸으면 retention 의심, 쿼리 라인이 컸으면 Insights filter 누락 의심이다. 4축 개관이 무엇을 기록하는지를 본 글이었다면, 4 기둥은 어떻게 청구하는지를 짚는다.

같은 데이터, 10배 청구서, cardinality · 해상도 · retention

16:9 가로 기술 다이어그램. 흰 배경. 왼쪽에 작은 원형 노드 'one metric / one log / one alarm'이 있고, 오른쪽으로 세 갈래 화살표가 부채꼴로 나뉜다. 위쪽 화살표는 앰버 'Cardinality' 박스로 이어지고 'dimension explosion 10K metrics ~3000 dollar' 부제. 가운데 화살표는 에메랄드 'Resolution' 박스로 이어지고 '1s vs 60s, 60x calls' 부제. 아래쪽 화살표는 슬레이트 블루 'Retention' 박스로 이어지고 'unset = Never expire' 부제. 박스마다 오른쪽에 점점 커지는 AWS 오렌지 달러 기호가 그려져 있고, 아래쪽에는 '0'에서 '3000+'까지 가는 가로 비용 스케일 바. 흰 배경, 둥근 모서리, 절제된 그림자, 깔끔한 산세리프 글꼴의 프로페셔널 IT 기술 다이어그램

같은 한 개 메트릭, 같은 한 줄 로그가 청구서를 10배까지 구분하는 변수가 셋이다.

먼저 cardinality. 한 PutMetricData 호출에 Dimensions=[{Name: 'user_id', Value: req.user.id}] 한 줄을 넣으면 사용자 1만 명일 때 메트릭이 1만 개로 나뉜다. 좌표 키는 namespace × MetricName × Dimensions 조합인데 사용자 ID 한 글자만 달라도 다른 메트릭으로 친다 (Metric: 숫자 하나가 찍히는 과정에서 본 cardinality 폭발). Custom metric 무료 한도 10개를 빼면 약 9,990개가 첫 1만 개 tier 단가로 들어가, 한 달 청구서가 약 $3,000이 되는 흐름이다. 같은 코드 한 줄, 차이는 dimension 한 개. 운영에서는 user_id를 dimension이 아니라 logs metadata로 옮기는 결정이 청구서를 두 군데 단위로 구분한다.

해상도는 그 다음 변수다. StorageResolution이 60(standard 1분)인 메트릭과 1(high-res 1초)인 메트릭은 단가가 같지만, 1초 해상도는 데이터를 60배 더 자주 보내야 정렬한다. 한 메트릭에 1초 해상도를 켜면 한 달 약 $16~$26 호출비가 추가한다는 Metric 워크드 예제가 그 계산이다. 1초 해상도가 필요한 운영은 흔하지 않다. 거래 latency p99 같은 SLO가 1초 단위로 변동하는 서비스만 해당하고, 대부분의 운영은 60초로도 답이 나온다.

세 번째는 retention. CloudWatch Logs의 retentionInDays는 명시하지 않으면 Never expire 상태다. Lambda가 자동 생성하는 LogGroup도 같은 default를 받는데, 이 지점가 storage 비용이 자라는 가장 흔한 원인이다. 작년 어느 Lambda function의 LogGroup을 처음 열었더니 5년치 로그가 그대로 남아 있었던 적이 있다. 함수는 한 번도 만진 적 없는데 청구서의 storage 라인은 천천히 부풀어 있었다. retentionInDays 22개 enum에서 30이나 90 한 값만 명시해 두면 끝나는 일이었다.

세 변수는 독립이지만 서로를 부른다. cardinality가 폭발한 곳은 보통 retention도 무한이고, retention 무한인 LogGroup은 보통 Insights 스캔 비용도 같이 큰다 (Insights는 스캔 단위로 과금되는 엔진이라, 2024-11에 추가된 field indexes·filterIndex가 있어도 기본 흐름은 데이터 양이 단가를 결정한다). 한 변수만 잡아도 다른 둘이 같이 줄어드는 일이 종종 일어난다.

작은 백엔드 한 대로 풀어 보자

작은 백엔드 한 대를 떠올려 보자. API Gateway → Lambda → DynamoDB 한 묶음, 일평균 5만 요청, 평균 응답 200ms. 무지성 디폴트로 두면 청구서가 어디까지 자라는지 한 번 풀어 보자 된다.

Lambda는 console.log() 한 줄마다 자동으로 LogGroup에 쌓는다. 한 요청이 START·REPORT 자동 행과 사용자 로그 네다섯 줄을 합쳐 평균 5KB쯤 만든다고 가정하면, 5KB × 5만 요청/일 × 30일은 약 7.5GB다. Logs ingestion $0.50/GB baseline에서 약 $4/월. Storage는 retention 디폴트가 Never라 매달 누적되며 자라는 라인이다. 6개월만 흘러도 storage가 ingest와 비슷한 무게로 따라온다.

여기에 custom metric 두세 개를 dimension에 request_id 포함해 켜면 cardinality가 폭발한다. 위 단계에서 본 흐름과 똑같다. 1만 unique request_id에서 약 $3,000으로 나뉜다. Insights 스캔을 일 4번 5GB씩 돌리면 약 $3, alarm 5개와 dashboard 한 장이 합쳐 약 $4.

청구서를 다 합치면 cardinality 한 줄이 거의 전부를 견인한다. 로그·알람·대시보드가 합쳐 두 자릿수 달러인데 dimension 폭발 라인 하나가 네 자릿수인 일이 흔히 일어나는 곳이다. 무지성 디폴트의 진짜 함정은 어떤 한 라인이 압도한다는 사실이지, 모든 라인이 조금씩 자라는 게 아니다.

같은 서비스를 첫 날에 잡아 두면 그림이 다르다. aws logs put-retention-policy 한 줄로 retention을 30일에 묶고, dimension에는 환경/리전/서비스 세 가지만 두고 request_id는 logs metadata로 보낸다. Live Tail이 필요 없는 LogGroup은 IA log class로 단가가 약 절반. Insights 쿼리는 filter를 가장 먼저 두는 다섯 줄 규칙으로 스캔 GB를 적게 만든다. 같은 트래픽, dimension 한 줄을 빼는 것만으로 청구서의 99%가 없다. 매달 약 두 자릿수 달러 라인으로.

차이를 만든 코드는 다섯 줄이다. 트래픽도 같고 기능도 같다. 변한 건 데이터의 형태뿐이다.

운영 첫 날의 5줄 체크리스트

첫 날에 잡아 두는 명령은 길지 않다. 5개 항목으로 디폴트가 만드는 함정은 거의 다 차단한다.

1. retentionInDays 명시: 새 LogGroup은 만들 때부터 retention을 잡는다. Lambda가 자동 생성한 LogGroup도 사후에 잡을 수 있다.

bash
aws logs put-retention-policy \
  --log-group-name "/aws/lambda/my-function" \
  --retention-in-days 30
bash
aws logs put-retention-policy \
  --log-group-name "/aws/lambda/my-function" \
  --retention-in-days 30

값은 22개 enum 중에서, 1, 7, 30, 90, 365 다섯 가지면 거의 다 커버한다. CDK·Terraform 운영이면 logRetention: 30을 LogGroup 정의에 함께 적어 두는 한 줄이면 끝난다.

2. Dimension allow-list: cardinality 폭발은 dimension 입력 시점에 막는 게 가장 싸다. PutMetricData 호출 코드에서 dimension에 들어갈 수 있는 키를 화이트리스트로 잡아 두고, 그 외(특히 user_id, request_id, trace_id)는 logs metadata로 보낸다. dimension은 그래프의 축에만 두고, 조사가 필요한 식별자는 로그에 둔다. 두 단계의 단가 단위가 다르기 때문이다.

3. IA log class: Live Tail·metric filters·EMF가 필요 없는 LogGroup은 IA로 만든다. ingestion 단가가 약 절반이고, storage와 Insights 쿼리 단가는 Standard와 같다 (IA 미지원 기능 표 먼저 확인). Standard에서 IA로 변경은 불가능하니 LogGroup 만들 때 결정해야 한다.

4. Sampling: 트레이스와 카나리는 sampling을 default로 켠다. X-Ray는 reservoir 1 + fixed rate 5%가 default. Synthetics는 1분 주기 대신 5분 주기로 시작해 필요한 곳만 1분으로 적는다 (1분 주기 한 대 = 약 $84).

5. Cost Anomaly Detection + Budgets: 위 4줄이 디폴트 함정만 차단한다면, 5번째는 예상하지 못한 spike를 잡는 단계다. AWS Cost Anomaly Detection(2020-12-16 GA)이 머신러닝으로 일별 사용량 변동을 보고, AWS Budgets는 임계값(예: 월 $200) 위에서 SNS 알림을 보낸다. 관찰 도구를 관찰하는 도구가 운영 첫 날의 마지막 줄이다. Budgets 모니터링·알림은 무료. 자동 액션을 다는 budget만 첫 2개 무료, 그 이후는 하루 $0.10/budget이다.

코드 양으로는 5줄, IAM Role 한 개, 콘솔 클릭 몇 번. 매달 청구서가 자라지 않는 운영의 출발점은 이 다섯이다.

5줄이 닿지 않는 곳

이 5줄로 잡히지 않는 운영도 있다. 데이터 양이 진짜로 많아지는 서비스, 그러니까 수십 TB 로그·수만 unique 메트릭·밀리초 단위 분산 트레이스가 핵심인 곳이다. CloudWatch 가격 곡선이 다른 도구와 만나는 교차점에서 결정이 다르다.

수십 TB 로그를 자주 검색하는 운영은 OpenSearch Service가 싸지는 영역이다. Insights는 per-scan, OpenSearch는 매달 클러스터 고정 비용이라, 쿼리가 자주면 클러스터 단가가 여러 곳으로 나뉘어 GB당 단가가 한 자릿수 더 작다. 단, 운영 부담이 늘어난다. 셀프 호스팅 클러스터의 노드·shard·스냅샷을 직접 관리해야 한다.

분산 트레이스의 깊이가 운영의 중심이면 APM 영역, Datadog, New Relic, Dynatrace다. p99·p999 latency, 코드 라인별 flame graph, 자동 anomaly correlation이 거기서부터 시작한다. AWS 안의 서비스만 봐도 되는 운영이라면 ADOT + Application Signals가 그 영역을 채워 가는 중이다 (X-Ray SDK·Daemon은 2026-02-25 maintenance, 2027-02-25 EOL 예정).

EC2/EKS 위에 Grafana Loki + Prometheus 셀프 호스팅을 띄우는 운영도 있다. ingestion 비용이 EBS·S3 단가로 떨어져 청구서가 평탄하다. 단, 관찰 도구를 운영하는 일이 추가로 늘어난다. 클러스터 업그레이드·dashboard 정의·alert routing을 직접 책임을 맡는다.

운영 첫 날에 retentionInDays를 30으로 잡고 dimension에 user_id를 넣지 않으면 한 달 뒤 청구서가 자라지 않는다. 이 한 줄이 섹션 3 열 편을 통틀어 가장 자주 새는 곳에 대한 답이다. 다음 섹션부터는 컴퓨트, 청구서의 가장 큰 항목이 시작한다.

참고 자료

YouTube 영상

채널 보기
인공지능은 세상을 어떻게 숫자로 읽는가? - 이미지, 소리 그리고 텍스트가 행렬이 되는 원리 | 선형대수학
우리가 매일 쓰는 맞춤법 검사기와 라우터 속에 숨겨진 알고리즘은? | Trie 자료구조 이야기
트라이(Trie)에서 단어를 삭제하는 방법 | Trie 자료구조 이야기
스칼라 곱셈과 내적의 기하학적 의미 | 선형대수학
마지막편, 트라이 노드를 50% 이상 줄이는 방법? 압축 트라이 성능 분석 | Trie 자료구조 이야기
AI는 데이터를 어떻게 분류할까? 벡터의 거리와 KNN 알고리즘 | 선형대수학
AI는 왜 수백 차원의 벡터를 사용할까? 고차원 공간과 행렬 | 선형대수학
AI를 위한 선형대수학 - 소개 | 선형대수학