모델로 가는 하나의 문, 5부. 아이덴티티, Quota, Chargeback
custom dimension은 다섯 개, time series는 곱으로 늘어난다. 5부는 cardinality를 버티는 테넌트 아이덴티티, quota, chargeback 모델을 만든다.

chargeback 설계를 결정하는 것은 회계상의 취향이 아니라 Azure Monitor의 한도다. custom metric은 dimension key를 10개까지 허용하고, API Management가 그중 5개를 기본값으로 이미 쓰며, 정책이 만들어 내는 활성 time series는 추가한 모든 dimension의 고유 값들의 곱이다. 값이 열 개씩인 dimension 세 개면 time series 천 개이고, 상한은 12시간 윈도 기준 리전당, subscription당 활성 time series 50,000개다. 그 metric에 사용자 ID나 요청 ID를 올리면 플랫폼 자체의 텔레메트리가 장애가 된다. 4부가 세 개의 요청 경로를 마무리했다. 이번 편은 이 시리즈 전체가 존재하는 이유인 질문에 답한다. 어느 팀이 무엇을 썼고, 게이트웨이는 한 팀이 다른 모두의 quota를 쓰는 것을 어떻게 막는가.
credential은 둘, 맡은 일은 서로 다르다
API Management는 두 가지 메커니즘을 주는데, 사람들은 이 둘이 보완재인데도 으레 대체재처럼 다룬다.
subscription key는 product, quota, 분석의 목적에서 소비자를 식별한다. Ocp-Apim-Subscription-Key 헤더로 도착하거나, 헤더가 없을 때만 검사되는 subscription-key 쿼리 파라미터로 도착하며, 두 이름 모두 API별로 설정할 수 있다. key는 쌍으로 발급되므로 애플리케이션이 key A에서 key B로 옮겨 가고 A를 재생성하는 일을 중단을 최소화하며 할 수 있다.
key에 대해서는 두 가지를 가정으로 넘기는 대신 설계로 다뤄야 한다. 첫째, API Management에는 subscription key의 수명 주기가 내장되어 있지 않다. 만료일도, 자동 rotation도 없다. rotation은 Azure PowerShell이나 SDK로 직접 만드는 워크플로이고, 만들지 않으면 key는 영원히 산다. 둘째, key는 기본적으로 backend로 전달되고, 거기서 backend 모니터링 로그와 그 로그가 실려 가는 모든 곳에 남을 수 있다. inbound 섹션 끝의 set-header로 그것을 제거하는 두 줄짜리 수정이, 그러지 않으면 감사인이 발견하게 될 credential-in-logs 소견을 막는다.
Microsoft Entra ID가 발급한 JWT는 다른 질문에 답한다. 이 호출자가 지금 권한이 있는가. JWT는 자체적으로 만료되고, 중앙에서 폐기할 수 있으며, claim을 나른다. 애플리케이션은 OAuth2 client credentials flow로 하나를 얻고, 게이트웨이는 다른 무엇보다 먼저 그것을 검증한다:
<validate-jwt header-name="Authorization" require-scheme="Bearer"
require-expiration-time="true" require-signed-tokens="true"
clock-skew="60"
output-token-variable-name="jwt"
failed-validation-httpcode="401"
failed-validation-error-message="Invalid or missing token.">
<openid-config url="https://login.microsoftonline.com/{{tenant-id}}/v2.0/.well-known/openid-configuration" />
<audiences>
<audience>api://genai-gateway</audience>
</audiences>
<required-claims>
<claim name="roles" match="any">
<value>model.invoke</value>
</claim>
</required-claims>
</validate-jwt>
output-token-variable-name이 제 몫을 하는 부분이다. 검증된 token이 정책 변수에 담기므로, 이후의 정책들은 호출자가 통제하는 헤더를 믿는 대신 거기서 claim을 읽을 수 있다. 이것이 곧바로 중요해지는데, 다음 결정이 어떤 값으로 quota의 key를 삼느냐이기 때문이다.
익명 접근의 함정
이를 만들어 내는 설정이 무해해 보이기 때문에 분명히 적어 둘 가치가 있다. API를 subscription key 없이 호출할 수 있는지는 두 개의 독립적인 설정에 달려 있다. API 자체가 subscription을 요구하는가, 그리고 API가 할당된 모든 product가 subscription을 요구하는가. API가 subscription을 요구하지 않으면, product 범위의 key가 통했을 상황에서도 key 없는 호출이 API 컨텍스트에서 허용된다. 게이트웨이는 시킨 대로 정확히 하고 있을 뿐이고, 의도를 말해 준 사람이 없었다.
유료 모델 용량 앞에 선 게이트웨이라면 규칙은 모든 API가 subscription을 요구하고 모든 product가 subscription을 요구한다이고, validate-jwt 정책은 어쨌든 실행되므로 인증되지 않은 호출은 두 번 실패한다. key의 범위도 중요하다. 서비스 범위나 전체 API 범위의 key는 인스턴스의 모든 API를 열어 주는데, 플랫폼 팀 자체 도구에는 맞고 소비자에게는 틀리다. 소비자는 product 범위의 key를 받는다.
quota, 그리고 무엇에 대고 셀 것인가
1부의 token limit 정책은 counter-key를 받고, 그 선택이 테넌시 모델의 전부다. subscription ID를 key로 삼으면 소비자당 버킷 하나가 생기는데, 1부의 인시던트에 필요했던 것이 바로 그것이다. 마케팅 batch job은 고객 어시스턴트의 quota를 비울 수 없다. 서로 다른 counter를 가진 서로 다른 subscription이기 때문이다.
<llm-token-limit counter-key="@(context.Subscription.Id)"
tokens-per-minute="{{tpm_tier}}"
estimate-prompt-tokens="true"
remaining-tokens-variable-name="remainingTokens"
remaining-tokens-header-name="x-remaining-tokens" />
남은 허용량을 응답 헤더로 돌려주는 것은 속성 두 개를 더 쓸 가치가 있다. 자신의 여유분을 볼 수 있는 클라이언트는 429를 받기 전에 물러설 수 있고, 스로틀링에 관한 지원 대화는 양쪽 모두가 볼 수 있는 숫자에서 시작한다.
product가 tier가 된다. 분당 한도는 높고 일일 quota는 소박한 interactive product, 그 반대인 batch product, 그리고 노트북 속 폭주 루프가 인시던트가 아니라 성가심에 그칠 만큼 한도가 낮은 sandbox product. 테넌트는 Entra ID 애플리케이션 하나에 product subscription 하나 이상을 더한 것이고, 2부의 30-tenants 레이어에 있는 Terraform이 프로비저닝하며, 티켓으로는 절대 만들지 않는다.
dimension 예산
이제 리드의 제약을, 그것이 만들어 내는 설계 규칙으로 다시 적는다. Azure Monitor는 custom metric의 dimension key를 10개로 제한하고, API Management는 그중 5개를 Region, Service ID, Service Name, Service Type을 포함한 기본값으로 쓴다. 남는 것은 정책당 최대 5개의 custom dimension이다. 더 큰 한도는 조합에서 온다. 활성 time series는 해당 기간 동안 각 dimension이 가진 고유 값들의 곱과 같으므로, 값이 열 개씩인 dimension 세 개는 천 개를 기여하고, 리전 상한은 12시간 기준 subscription당 활성 time series 50,000개다. 같은 리전의 여러 API Management 인스턴스는 같은 리전 합계에 기여한다.
그 결과는 플랫폼 자체 문서에 적어 둘 가치가 있는 깔끔한 분리다:
- metric은 cardinality가 낮은 dimension만 나른다. 테넌트, 모델 alias, 환경. dimension 세 개, 값 집합은 유한하고, 대시보드는 빨리 뜨고 알림은 맞는 대상에 울린다.
- 나머지는 전부 로그가 나른다. 요청 ID, 수집한다면 사용자 ID, 프롬프트와 completion 크기, 지연 시간, 어느 backend가 응답했는지. 요청 단위 귀속이 사는 곳은 Application Insights와 Log Analytics이고, 월간 chargeback job이 거기를 쿼리한다.
<llm-emit-token-metric namespace="llm-metrics">
<dimension name="Tenant" value="@(context.Subscription.Name)" />
<dimension name="ModelAlias" value="@(context.Request.MatchedParameters.GetValueOrDefault("model","unknown"))" />
<dimension name="Environment" value="{{env}}" />
</llm-emit-token-metric>
dimension 세 개, 의도한 숫자다. 누군가 요청할 네 번째는 사용자 ID이고, 답은 아니오다. 사용자 ID는 로그 레코드에 속한다. 거기서는 cardinality가 리전 전체의 metric 예산이 아니라 스토리지 비용으로 계산되기 때문이다.
재무와의 논쟁을 버티는 chargeback
월간 job은 평범한 데이터 엔지니어링이고, 그 신뢰성은 정밀함이 아니라 정확도를 명시하는 데서 온다. 3부와 4부가 세 가지 등급을 세웠고, chargeback 문서는 그것들에 이름을 붙인다:
- 스트리밍 없는 동기: 모델 자신의
usage로 계량한다. 정확하다. - 스트리밍된 동기: 프롬프트와 completion token을 게이트웨이에서 추정한다. 스트리밍은 정책 설정과 무관하게 추정을 강제하기 때문이다. 근사치이고, 그 편차는 별도의 metric으로 추적한다.
- batch: job 자체의 요청 수와 출력 파일 속 응답별 usage에서 온다. 정확하다. control plane이 제출했기 때문이다.
job은 테넌트별 수치와 대조 한 줄을 산출한다. 귀속된 지출의 합 대 해당 기간의 실제 리소스 비용이다. 몇 퍼센트의 잔차가 달마다 안정적이라면 그것은 추정의 정직한 비용이다. 잔차가 커진다면 버그이고, 대개는 게이트웨이를 우회하는 경로를 찾아낸 호출자다. 잔차를 숨기는 대신 공개하는 것이 나머지 숫자를 믿을 만하게 만들고, 1부가 경고한 우회 경로의 탐지기 역할도 겸한다.
팀에게 자기만의 게이트웨이가 필요할 때
가끔 어떤 소비자의 요구 사항은 공유 인프라에 맞지 않는다. 자체 네트워크 격리가 필요한 규제 워크로드나, 다른 누구의 것도 건드리지 않고 자기 API를 직접 관리해야 하는 팀이다. API Management workspace가 이를 위해 존재하고, 선택지는 둘이다. v2 tier에서 추가 게이트웨이 비용 없이 내장 기능에 접근할 수 있는 서비스의 기본 managed gateway, 아니면 Basic v2, Standard v2, Premium, Premium v2에서 쓸 수 있는 별도의 workspace gateway다. 후자는 강한 런타임 격리와 독립적인 스케일링, 호스트네임, 네트워크 구성을 사 주는 대신, 추가 비용과 더 긴 배포와 더 적은 지원 리전이라는 값을 치른다.
이것이 되돌릴 수 있는 결정인지는 디테일 하나가 정한다. workspace gateway의 가상 네트워크 구성은 게이트웨이 생성 시점에만 설정할 수 있고 이후에는 바꿀 수 없다. 그 네트워크 구성은 API Management 인스턴스의 것과도 독립적이다. 격리 없이 만들어진 workspace gateway를 나중에 격리가 필요해진 팀이 갖고 있다면, 그것은 재구축이다.
지켜봐야 할 실패 모드
- 정책에 들어간 높은 cardinality의 dimension. 오류를 내지 않는다. 조용히 리전의 활성 time series 예산을 소비하고, 첫 증상은 다른 팀들의 custom metric이 나타나지 않는 것이다.
- 만료되지 않는 key. 내장된 만료도 rotation도 없다. rotation 워크플로가 없으면 폐기된 애플리케이션에 발급된 credential이 여전히 동작하고, 그것을 보고하는 것은 아무것도 없다.
- backend 로그 속의 subscription key. 기본 동작은 그것을 전달한다. inbound에서 제거하고, 가정하는 대신 backend의 로그를 한 번 확인해서 검증하라.
- subscription을 요구하지 않는 API. API 컨텍스트의 익명 접근이고, 열려 있다기보다 관대해 보이는 설정이 만들어 낸 결과다.
- 잔차 없이 공개된 chargeback. 공개하지 않은 불일치를 product owner가 처음 발견하는 순간, 이후의 모든 숫자는 협상 대상이 된다.
6부가 물려받는 것
실제 아이덴티티를 가진 테넌트, 버티는 quota, 그리고 정확도가 명시된 팀별 월간 수치. 이것이 다음 질문을 정확히 세팅한다. 가장 싼 token은 아예 보내지 않은 token이고, semantic caching은 이 시리즈에서 청구서를 귀속하는 데 그치지 않고 낮추는 첫 메커니즘이다. 또한 위 숫자들의 의미도 조용히 바꾼다. cache hit는 아무 비용도 들지 않았지만 여전히 누군가의 보고서에 나타나야 하는 요청이기 때문이다.
다음으로 읽을 글
- 6부, Semantic Caching과 그 실패 모드, 가장 싼 token이 아예 보내지 않은 token이 되고, 그것을 해내는 메커니즘이 정확성의 표면으로 변하는 곳이다.
- 4부, 요청 경로 밖의 비동기 작업, 이 chargeback 모델이 의존하는 batch 회계가 만들어진 곳이다.
- Your LLM Bill Is an Observability Problem, 특정 게이트웨이 이전의, 귀속 논증의 일반 버전이다.
이것을 대규모로 운영할 때의 인프라와 플랫폼 쪽 이야기는 ercan.cloud에, 허브는 ercanermis.com에 있다.
참고 자료
Ercan의 다른 글
같은 저자, 다른 영역의 사이트 두 개.