1. 도입: 방문자 14명에 $31, 뭔가 단단히 잘못됐다

사이드 프로젝트 하나쯤 굴려본 사람이라면 이 시나리오가 남 일 같지 않을 거다. Dev.to에 올라온 "My app didn't go viral. My AWS bill did." 글이 딱 그 케이스다. Umami 애널리틱스는 한 달에 방문자 14명을 찍었는데, AWS는 $31을 청구했다. 방문자 한 명당 $2.21. 무료 도구치고는 캘리포니아에서 제일 비싼 학습 도구가 된 셈이다.

여기서 핵심은 결말이다. 그 14명 방문자는 청구서와 아무 상관이 없었다. 진짜 범인은 같은 주말에 글쓴이가 작업하던 전혀 다른 프로젝트(vigil-crest)였고, Bedrock의 과금 구조 때문에 그 비용이 엉뚱한 앱 이름표를 달고 있었던 것뿐이다.

이 글이 인프라 엔지니어한테 와닿는 이유는, "비용이 늘었다"는 단순한 사건이 아니라 대시보드가 거짓말을 하고, AI 비용 추적 도구가 잘못된 모델명을 들이밀고, 과금이 엉뚱한 프로젝트에 귀속되는 복합적인 함정을 정확히 짚었기 때문이다. 실무에서 비용 폭탄 원인을 추적해본 사람은 안다. 진짜 어려운 건 "얼마 나왔냐"가 아니라 "누가, 어디서, 왜 썼냐"를 증거로 못 박는 일이다.

2. 핵심: 왜 작은 트래픽에서도 청구서가 터지는가 — 과금 구조의 비결정성

원문이 잘 짚은 포인트 두 개를 인프라 관점으로 풀어보자.

(1) 트래픽 수와 비용은 직결되지 않는다

Umami 같은 클라이언트 사이드 애널리틱스는 "내 자바스크립트를 실행한 브라우저"만 센다. 봇도, 백엔드 API 호출도, Bedrock 모델 호출도 못 본다. 즉 방문자 수와 실제 과금 사이엔 인과관계가 없는데, 사람은 자꾸 이 둘을 묶어서 본다. 글쓴이 표현대로 "서로 무관한 두 숫자를 엮어 스스로를 겁줬다."

실무에서도 똑같다. CloudWatch 대시보드의 요청 수가 평소랑 같은데 비용만 튀는 경우, 십중팔구 범인은 트래픽이 아니라 데이터 전송, NAT Gateway, 로그 보존, 항상 켜진 인스턴스 같은 "조용한 비용"이다. 댓글에 달린 사례처럼 Cloud Logging 로그 보존만으로 이틀에 $50이 나오기도 한다.

(2) 모델/리소스 과금은 "프로젝트"를 모른다

원문에서 제일 중요한 기술적 통찰이 이거다. Bedrock 과금은 계정 레벨이다. 어떤 함수가 호출했든, 그 모델 비용은 호출한 리소스의 태그를 물려받지 않는다. Application Inference Profile을 따로 설정하고 그걸 호출하지 않는 한, 모든 모델 비용은 "프로젝트 없음" 버킷에 떨어지고, 뭔가가 그걸 떠안게 된다. 글쓴이의 경우 비용 추적 도구가 그걸 멋대로 Clew Directive 탓으로 돌렸다.

비유하자면 회사 공용 법인카드 명세서 같은 거다. 카드 한 장으로 여러 팀이 긁었는데, 명세서엔 "누가 긁었는지"가 안 찍히면 회계가 추정으로 갖다 붙인다. 그 추정이 틀리면 엉뚱한 팀이 누명을 쓴다.

(3) 비용의 "모양"을 읽는 법

원문에서 제일 실무적으로 배울 만한 건 토큰 사용량 분해다. 28M 토큰, 8일 활동, 그중 이틀이 70%를 차지. 그리고 그 모양이:

Cache writes: 4.1M tokens, $15.33 (55%)
Cache reads:  23.8M tokens, $7.14 (26%)
Output:       346K tokens, $5.20
Input:        120K tokens, $0.36

웹 앱이 14명한테 응답하는 비용은 절대 이렇게 안 생겼다. 앞단에 캐시 쓰기가 무겁고, 뒤로 캐시 읽기가 무겁고, 실제 입출력은 거의 없는 모양은 "큰 고정 컨텍스트를 캐싱해두고 매 턴마다 다시 읽는 에이전트"의 지문이다. 댓글에서 다른 엔지니어가 정확히 짚었듯, 이 패턴은 깜짝 놀랄 모델 청구서를 만났을 때 CloudTrail을 한 줄씩 읽기 전에 던질 수 있는 좋은 첫 가설이다.

3. 실무 관점: 조기 감지와 흔한 함정

(1) AWS Budgets로 비용 경보부터 걸어라

가장 먼저 할 일은 청구서가 터지기 전에 알림을 받는 거다. 콘솔에서도 되지만, CLI로 예산을 거는 게 재현 가능해서 좋다. 아래는 월 $10 임계치에 80% 도달 시 이메일 알림을 거는 예시다.

# budget.json
{
  "BudgetName": "monthly-cost-guard",
  "BudgetLimit": { "Amount": "10", "Unit": "USD" },
  "TimeUnit": "MONTHLY",
  "BudgetType": "COST"
}

# notifications.json
[
  {
    "Notification": {
      "NotificationType": "ACTUAL",
      "ComparisonOperator": "GREATER_THAN",
      "Threshold": 80,
      "ThresholdType": "PERCENTAGE"
    },
    "Subscribers": [
      { "SubscriptionType": "EMAIL", "Address": "you@example.com" }
    ]
  }
]
$ aws budgets create-budget \
    --account-id 123456789012 \
    --budget file://budget.json \
    --notifications-with-subscribers file://notifications.json

# (성공 시 출력 없음 — exit code 0)
$ echo $?
0

여기서 흔한 함정 하나. 권한이 부족하면 이런 에러를 만난다:

An error occurred (AccessDeniedException) when calling the CreateBudget operation:
User: arn:aws:iam::123456789012:user/devops is not authorized to perform:
budgets:CreateBudget on resource: arn:aws:budgets::123456789012:budget/monthly-cost-guard

이 경우 IAM 정책에 budgets:CreateBudget, budgets:ModifyBudget 액션이 필요하다. 그리고 Budgets는 글로벌 서비스라 리전을 us-east-1 기준으로 다루는 게 안전하다(공식 문서 확인 권장).

(2) Cost Explorer로 "무엇이" 쓰는지 분해하기

경보가 울렸으면 다음은 분해다. 글쓴이가 Amazon Q로 했던 일을 CLI로 직접 하면 이렇다. 서비스별 일자별 비용을 뽑아보자.

$ aws ce get-cost-and-usage \
    --time-period Start=2024-05-01,End=2024-06-01 \
    --granularity DAILY \
    --metrics "UnblendedCost" \
    --group-by Type=DIMENSION,Key=SERVICE \
    --query 'ResultsByTime[].{date:TimePeriod.Start, groups:Groups[?Metrics.UnblendedCost.Amount > `0.5`].[Keys[0],Metrics.UnblendedCost.Amount]}'

출력 예시(특정 이틀에 비용이 몰린 모양을 보고 싶을 때):

[
  {
    "date": "2024-05-24",
    "groups": [
      ["Amazon Bedrock", "11.20"],
      ["Amazon Elastic Compute Cloud - Compute", "0.21"]
    ]
  },
  {
    "date": "2024-05-25",
    "groups": [
      ["Amazon Bedrock", "8.40"]
    ]
  }
]

여기서 핵심 교훈. Cost Explorer가 깨끗한 데이터를 줘도, 그 위에 얹는 "이야기"는 틀릴 수 있다. 원문에서 Amazon Q는 정확한 증거(역할명, 인스턴스, 타임스탬프, 모델)를 가져왔지만 세 번이나 잘못된 범인을 지목했다. "앱이 비싸다 → 앱 로그를 봐라" 같은 그럴듯한 서사를 붙인 거다. 데이터는 문제가 아니었고, 데이터 위의 내러티브가 문제였다.

그래서 글쓴이의 룰: "Trust retrieval, verify recall." 도구가 가져온 것(retrieve)은 믿되, 도구가 기억하는 것(recall)은 검증하라. 같은 Q가 Haiku 가격을 실제의 1/4로 잘못 인용하기도 했다. 모델명, 단가 같은 "외운 값"은 항상 코드와 IAM 정책으로 대조해야 한다.

(3) 흔한 함정 모음

  • 비용 추적 도구의 모델명/단가를 믿어버린다. 원문처럼 "Sonnet이 비싸니까 범인이다" 하고 종결하면 틀린다. IAM 정책이 Nova ARN으로만 스코프돼 있으면 Sonnet 호출은 AccessDenied로 막히니, 그 함수는 물리적으로 Sonnet을 청구할 수 없다. 코드와 정책이 최종 증거다.
  • 애널리틱스 방문자 수와 인프라 비용을 엮는다. 둘은 별개 시스템이다.
  • 조용한 비용을 놓친다. 토큰 폭주는 시끄럽고 자기 한정적이다(노트북 닫으면 끝남). 진짜 무서운 건 항상 켜진 t3.micro처럼 시간당 과금되는 리소스다. 싸고 잊혀진 게 표준 비용으로 슬금슬금 쌓인다. 댓글의 로그 보존 비용($50/2일)도 같은 부류다.
  • 리전을 잘못 골라서 데이터 전송/빌드 비용을 더 낸다. 댓글 사례처럼 US Central이 아닌 곳에서 빌드하다 추가 과금이 붙기도 한다.

(4) 대안과 트레이드오프

모델 비용을 프로젝트별로 추적하고 싶다면 Application Inference Profile을 설정해서 태그가 붙는 프로파일을 호출하는 방식이 있다. 다만 설정 부담이 늘고, 호출 코드를 프로파일 ARN으로 바꿔야 한다. 작은 사이드 프로젝트라면 차라리 프로젝트별로 AWS 계정을 분리(또는 Organizations + 계정 분리)하는 게 비용 귀속을 가장 깔끔하게 만든다. 비용이 계정 경계에서 자연스럽게 갈리기 때문이다.

4. 사이드 프로젝트용 비용 방어 — 배포 전 체크리스트

원문 교훈을 인프라 체크리스트로 압축하면 이렇다.

  • AWS Budgets 경보부터 건다. 임계치 50%/80%/100% 세 단계. 청구서보다 알림이 먼저 와야 한다.
  • "항상 켜진" 리소스 목록을 안다. EC2, NAT Gateway, RDS, ElastiCache는 트래픽 0이어도 돈을 먹는다. 안 쓰면 끈다.
  • 로그 보존 기간과 verbosity를 점검한다. 디버깅용으로 켠 verbose 로그가 청구서의 주범이 되는 게 흔하다.
  • IAM 정책으로 모델/리소스를 스코프한다. 비싼 모델 ARN 접근을 아예 막아두면, 사고로라도 비싼 호출이 안 나간다(원문의 Nova-only 스코핑).
  • 비용이 어느 프로젝트에 귀속되는지 미리 정한다. 계정 분리 또는 Inference Profile/태깅 전략. "프로젝트 없음" 버킷을 줄여라.
  • 비용 도구의 결론을 코드/정책/CloudTrail로 교차 검증한다. 첫 번째 확신에 찬 문장을 받아들이지 마라.

5. 정리: 한 줄 요약과 적용 대상

한 줄 요약: AWS 비용 폭탄의 진짜 어려움은 "얼마"가 아니라 "누가 왜 썼는지를 증거로 못 박는 것"이다. 대시보드도, 비용 추적 AI도 깨끗한 데이터 위에 틀린 이야기를 얹을 수 있으니, 가져온 것은 믿되 기억한 것은 검증하라.

누가 언제 써야 하나:

  • 사이드 프로젝트/스타트업을 AWS에 굴리는 개인 개발자 — 배포 전에 Budgets 경보와 항상 켜진 리소스 목록부터 챙겨라.
  • Bedrock 등 모델 API를 여러 프로젝트에서 공유하는 팀 — 지금 비용 귀속 전략(계정 분리 or Inference Profile)을 정해라. "프로젝트 없음" 버킷은 사고의 온상이다.
  • 이미 청구서가 튄 사람 — Cost Explorer로 비용의 모양(캐시 쓰기/읽기/입출력 비율)부터 보고, 그게 앱 트래픽 모양인지 에이전트 모양인지 판단해라.

마지막으로 원문 글쓴이의 결론이 인상적이다. 범인은 바이럴이 아니라 "EC2로 만든 트렌치코트를 입고 다음 프로젝트를 만들던 나 자신"이었다고. 비용 추적의 끝은 대개 거울 속에 있다.

참고 자료

728x90

+ Recent posts