들어가며 — 이거 우리 인증 환경에서 돌아가는거 맞나?
AWS 기술 블로그에 Amazon Bedrock 기반 Claude Code, 조직에서 안전하게 운영하기: LLM Gateway 구축 가이드라는 훌륭한 글이 있습니다. LiteLLM Proxy를 게이트웨이로 두고, IAM Identity Center(IdC) SSO로 일반 사용자를 인증하고, Virtual Key로 팀별 예산을 통제하는 구성입니다.
그런데 실제로 팀 내에서 사용하는 테스트 계정에서 적용하려고 하니, 가장 근본적인 문제가 발생했습니다. 배포 대상 계정이 AWS Organizations의 멤버 계정이었고, 이 계정이 가진 IdC는 조직 인스턴스(Organizational Instance) 가 아니라 계정 인스턴스(account instance) 였습니다.
계정 인스턴스는 permission set을 지원하지 않고, 고객 관리형 애플리케이션도 OIDC 기반만 지원합니다. SAML 2.0 customer managed application을 호스팅할 수 없다는 뜻이고, 결과적으로 org-sso 페더레이션이 AWS 레벨에서 구조적으로 불가능했습니다. 권한 문제라 티켓을 열어 풀 수 있는 종류가 아니라, IAM Identity Center 인스턴스 유형 자체의 제약이었습니다.
고민하던 끝에, 완성한 것은 바로 ‘신원 소스를 Amazon Cognito User Pool 단독으로 가져가는 cognito-native 구성’ 입니다. 이 글은 배포를 완료 하기 까지 경험했던 삽질을 포함한 구축기입니다. 아키텍처와 배포 절차뿐 아니라, 공식 문서에는 안 나오는 실패 케이스를 예시로 들어 원인과 대응 방법까지 그대로 올렸습니다. 부디 여러분은 같은 삽질을 반복하지 않으시길…
이 글에서 다루는 것 — IdC 계정 인스턴스 판별법, Cognito 단독 인증 체인 설계, 원본 AWS 샘플의 cognito-native 개조 지점, 실패 케이스 8건(인프라 6 + Windows 온보딩 2), 운영·비용 실측치
다루지 않는 것 — LiteLLM 자체의 상세 설정 레퍼런스, Bedrock 모델 프롬프트 엔지니어링, 웹검색 등 게이트웨이 본체 밖의 확장 기능
1. 왜 LLM 게이트웨이를 두는가 — 통제 없는 확산의 비용
먼저 왜 게이트웨이를 구축하는지 이유를 짧게 정리하고 넘어가겠습니다. 우선 개발자가 Bedrock을 직접 호출해도 동작은 합니다. 문제는 팀·회사 단위로 확산됐을 때입니다.
| 구분 | 직접 연결 (문제점) | LLM Gateway (해결책) |
|---|---|---|
| 사용량 추적 | IAM 역할 단위만 — 개발자별 구분 불가 | 사용자 / 팀 / 프로젝트 단위 세분화 |
| 예산 관리 | IAM으로 월 예산 차단 불가 | 팀·사용자별 예산 캡 자동 차단 |
| 인증 | 개발자별 자격증명 직접 관리 | 로그인 → 가상키 자동 발급·회전 |
| 콘텐츠 안전 | 개별 적용 어려움 | Bedrock Guardrail + 시크릿 탐지 기본 |
| 감사 로그 | CloudTrail (제한적) | 요청/토큰/비용을 키·사용자별 기록 |
| 접근 제어 | IAM 정책만 | 모델 allowlist · 팀별 RPM/TPM · SG CIDR |
핵심은 단일 제어 지점(Single Control Plane) 입니다. 회사 건물에 출입문을 100개 뚫어두는 대신 로비에 게이트 하나를 두고 사원증을 찍게 한다면? 키·비용·모델·로그를 한 곳에서 통제하면, 흩어진 API Key·Shadow AI·예측 불가 비용이라는 세 가지 리스크를 같은 지점에서 해소 할 수 있게 됩니다.
또한, 개발자 입장에서 전환 비용은 거의 없습니다. OpenAI 호환 SDK를 쓰는 코드라면 바뀌는 건 base_url 한 줄입니다.
# BEFORE — 각 PC에 API Key가 흩어져 있음
from openai import OpenAI
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
# AFTER — base_url만 게이트웨이로. 키는 팀별 임시 가상키
from openai import OpenAI
client = OpenAI(
base_url="https://<gateway-domain>/openai",
api_key=get_gateway_token(),
)
Claude Code의 경우도 같은 원리입니다. 사용자 스코프 ~/.claude/settings.json에 ANTHROPIC_BASE_URL과 apiKeyHelper만 넣으면 됩니다.
2. 막힌 지점 — 내 계정의 IdC는 어떤 인스턴스인가
본격적인 설계 전에, 여러분의 계정이 org-sso로 갈 수 있는지부터 판별해야 합니다. 명령 두 줄이면 끝납니다.
aws sts get-caller-identity
# ── 출력 예시 ──
{
"UserId": "AABBCC....",
"Account": "123456789012",
"Arn": "arn:aws:iam::123456789012:user/aws_user"
}
aws sso-admin list-instances --region us-east-1
# ── 출력 예시 ──
{
"Instances": [
{
"InstanceArn": "arn:aws:sso:::instance/ssoins-1234567890abcd",
"IdentityStoreId": "d-12345abcd",
"OwnerAccountId": "123456789012",
"Name": "aws-user-mzc",
"CreatedDate": "2026-01-01T00:00:00+09:00",
"Status": "ACTIVE"
}
]
}
# OwnerAccountId 가 조직 관리 계정이 아니면 ⇒ 계정 인스턴스 = cognito-native 로 가야 함
OwnerAccountId가 조직 관리 계정이면 조직 인스턴스이므로 AWS 공식 가이드의 SSO 버전을 그대로 쓰시면 됩니다. 하지만 위와 동일한 자기 계정 ID가 나오면 계정 인스턴스이고, 이 글의 구성대로 Cognito 구축을 권장합니다.
AWS 공식 문서 의 표현을 빌리자면, 계정 인스턴스는 단일 AWS 계정에 바인딩되며 permission set을 지원하지 않습니다. 고객 관리형 애플리케이션은 OIDC 기반만 지원 대상이고, 다중 계정에 걸친 사용이나 더 넓은 기능이 필요하면 조직 인스턴스를 쓰라고 권고합니다. 즉 우회가 아니라 설계를 바꿔야 하는 제약입니다.
3. cognito-native 아키텍처
3.1 요청 1건이 거치는 7개 통제 지점
| 단계 | 구성요소 | 역할 | 프로토콜 |
|---|---|---|---|
| 1 | Cognito Hosted UI | 로그인 (PKCE) | OAuth 2.0 |
| 2 | llmgw-login / 토큰 헬퍼 | access token 획득·캐시 | HTTPS |
| 3 | API Gateway (Cognito authorizer) | JWT 검증 | access token |
| 4 | Token Service Lambda | cognito:groups → 팀 매핑 | AWS SDK |
| 5 | DynamoDB / LiteLLM /key/generate | 가상키 캐시·발급 | REST |
| 6 | ALB → LiteLLM (ECS Fargate) | 가상키 인증·라우팅 | HTTPS Bearer |
| 7 | Bedrock VPC Endpoint + Guardrail | Claude 모델 호출 | PrivateLink |
위 로직에서 가장 중요한 포인트를 짚자면 단계 4 입니다. 팀 정보를 Identity Store에 조회하러 가지 않고, 토큰 안의 cognito:groups 클레임을 그대로 씁니다. Cognito 그룹명(llmgw-<팀>)과 LiteLLM의 team_alias를 1:1로 맞춰두었기 때문에, 팀을 추가할 때 Lambda 코드 수정도 재배포도 필요 없습니다. Cognito에서 그룹을 만들고 사용자를 넣으면, Token Service가 첫 요청 때 LiteLLM에 같은 이름의 팀이 있는지 조회하고 없으면 만들어 줍니다(lookup-or-create). 관리자는 그 뒤에 Admin UI에서 예산과 모델 allowlist만 조정하면 됩니다.
3.2 5-Layer 인증 체인
| Layer | 메커니즘 | 검증 주체 | 실패 시 |
|---|---|---|---|
| 1 — 로그인 | Cognito Hosted UI (PKCE) | Cognito User Pool | 토큰 미발급 |
| 2 — API GW | Cognito User Pools authorizer | API Gateway | 401 Unauthorized |
| 3 — 팀 매핑 | cognito:groups (llmgw- 접두어) | Token Lambda | 403 (0개 또는 다중 그룹) |
| 4 — LLM 요청 | 가상키 (Bearer Token) | LiteLLM Proxy | 401 Unauthorized |
| 5 — Bedrock | ECS Task Role (SigV4) | Amazon Bedrock | AccessDenied |
⚠ Layer 2에서 빈번하게 발생하는 실수 — Cognito authorizer는 access token(
token_use=access)만 허용합니다.id_token을 보내면 401입니다. 두 토큰 모두cognito:groups를 담고 있어서 “그룹 정보가 들어있으니 문제 없겠지?” 하고id_token을 보내는 경우가 있는데, 메서드에 authorization scope를 지정하면 authorizer는 access token만 받습니다. 토큰 헬퍼가 access token을 전송하도록 고정해 두세요.
각 계층이 독립적으로 검증하므로 한 계층이 우회 되어도 다음에서 차단되는 방어 심층화 구조입니다. 또한 개발자 PC에 AWS 자격증명을 필요로 하지 않습니다. (aws configure를 설정 한 적이 없는 노트북에서도 동작합니다) 런타임에 쓰이는 마스터키와 DB 자격증명은 전부 AWS Secrets Manager에서만 읽게 되어 있습니다. 최초 배포 시 시드값을 담는 config 파일은 커밋 대상에서 제외해 저장소에 남지 않도록 합니다.
3.3 org-sso 버전과 무엇이 다른가
| 항목 | cognito-native (이 게시글) | org-sso (공식 가이드) |
|---|---|---|
| 로그인 | llmgw-login (Cognito Hosted UI) | aws sso login |
| 신뢰 앵커 | Cognito User Pools authorizer | API GW IAM (SigV4) |
| 팀 원시값 | Cognito Group 이름 | 권한세트(Permission Set) 이름 |
| 신원원 | Cognito User Pool 단독 | IAM Identity Center |
| 사용자 등록 | admin-create-user + add-to-group | IdC 사용자 + 권한세트 할당 |
| 적용 상황 | IdC 계정 인스턴스 / usable IdC 없음 | IdC 조직 인스턴스 |
| 개발자 PC 요건 | AWS 자격증명 불필요 | AWS CLI + SSO 프로파일 필요 |
중요한 포인트는, 같은 코드베이스에서 신원 방식만 변경한다 라는 점입니다. 게이트웨이 본체(VPC·ECS·LiteLLM·Bedrock 경로)는 동일하고, AuthStack만 다릅니다.
여기에서는 이것을 런타임 스위치로 만들어 두었습니다. Token Service Lambda 파일 하나에 _resolve_org_sso_principal과 _resolve_cognito_native_principal이 함께 있고, AUTH_MODE 환경변수가 둘 중 하나를 고릅니다. 코드를 포크하지 않고 설정 한 줄로 신원 방식을 바꿀 수 있으니, 조직 SSO가 나중에 확보되면 그때 org-sso로 되돌리면 됩니다.
4. 배포 — AWS 원본 샘플을 cognito-native로 개조하기
4.1 AWS 원본 샘플의 어디를 고쳐야 하는가
먼저 확인해야 할 것이 하나 있습니다. AWS 원본 샘플(Claude Code on Bedrock — Enterprise Blueprint)은 README의 사전 요구사항부터 “AWS Organization + IAM Identity Center 활성화” 를 명시하고 있습니다. clone해서 그대로 배포하면 org-sso 버전이 나옵니다. 그러므로 계정 인스턴스 환경이라면 코드를 고쳐야 합니다.
다행히 고칠 범위는 넓지 않습니다. 원본 소스는 루트 스택 아래 5개의 Nested Stack으로 되어 있는데, 그중 하나만 손대면 됩니다.
| 스택 | 내용 | cognito-native 개조 |
|---|---|---|
| Network | VPC · 보안 그룹 · VPC Endpoints | 그대로 |
| Database | Aurora Serverless v2 (PostgreSQL) | 그대로 |
| Auth | Token Service Lambda + API Gateway | 전면 교체 |
| Gateway | ECS Fargate + ALB + LiteLLM Proxy | 그대로 |
| Monitoring | DynamoDB · CloudWatch Dashboard | 그대로 |
게이트웨이 본체는 건드리지 않습니다. 3.3절에서 “같은 코드베이스에서 신원 방식만 갈아끼운다”고 했던 말의 실체가 이 표입니다. 개조 지점은 세 군데입니다.
개조 ① — API Gateway authorizer 교체
원본 lib/stacks/auth-stack.ts는 IAM 인증입니다. Cognito 리소스는 단 하나도 없습니다.
// BEFORE (원본) — SSO 자격증명의 SigV4 서명을 검증
tokenResource.addMethod('POST', integration, {
authorizationType: apigateway.AuthorizationType.IAM,
});
cognito-native에서는 User Pool을 새로 만들고 authorizer를 붙입니다. 브라우저 로그인을 위한 Hosted UI 도메인과, 시크릿 없는 PKCE용 퍼블릭 App Client가 함께 필요합니다.
// AFTER (cognito-native)
const pool = new cognito.UserPool(this, 'GatewayUserPool', { /* 비밀번호 정책 등 */ });
pool.addDomain('HostedUI', { cognitoDomain: { domainPrefix: '<prefix>' } });
pool.addClient('CliClient', {
generateSecret: false, // PKCE 퍼블릭 클라이언트
oAuth: { flows: { authorizationCodeGrant: true }, scopes: [ /* ... */ ] },
});
const authorizer = new apigateway.CognitoUserPoolsAuthorizer(this, 'Authz', {
cognitoUserPools: [pool],
});
tokenResource.addMethod('POST', integration, {
authorizer,
authorizationType: apigateway.AuthorizationType.COGNITO,
authorizationScopes: ['openid', 'email', 'profile'], // ← 이 줄이 access token을 강제
});
마지막 authorizationScopes가 3.2절에서 언급한 지점입니다. AWS 공식 문서가 동작을 명확히 규정하고 있습니다.
COGNITO_USER_POOLSauthorizer에서 OAuth Scopes 옵션을 지정하지 않으면 API Gateway는 전달된 토큰을 identity token으로 간주해 사용자 풀의 신원과 대조합니다. 지정하면 전달된 토큰을 access token으로 간주해, 토큰에 담긴 access scope를 메서드에 선언된 authorization scope와 대조합니다. — API Gateway 개발자 안내서
즉 이 한 줄의 유무가 id_token 허용 여부를 판단합니다. 여기서 별도의 리소스 서버나 커스텀 스코프를 만들 필요는 없으며, Hosted UI의 PKCE 플로우가 openid email profile을 요청하므로 발급된 access token의 scope 클레임에 그 값이 그대로 실리고, 메서드에 같은 스코프를 선언해 두면 통과합니다. 그리고 access token에도 cognito:groups가 함께 실리기 때문에, 뒤의 Lambda가 팀을 읽는 데 아무 문제가 없습니다. 팀 그룹(llmgw-*)도 이 스택에서 함께 생성합니다.
개조 ② — Token Service Lambda의 신원 판별 로직
원본은 SigV4로 서명된 호출자의 ARN을 파싱해 사용자명을 얻습니다. cognito-native에서는 authorizer가 이미 검증해서 넣어준 클레임을 읽으면 됩니다.
# BEFORE (원본) — assumed-role ARN에서 SSO 사용자명 추출
arn = event["requestContext"]["identity"]["userArn"]
username = arn.split("/")[-1]
# AFTER (cognito-native) — authorizer가 검증한 클레임에서 사용자·팀 추출
def _extract_groups(raw):
"""cognito:groups는 통합 방식에 따라 네이티브 리스트, JSON 문자열
('["llmgw-dev"]'), 쉼표 구분 문자열 중 하나로 도착한다. 세 경우를 모두 받는다."""
if raw is None:
return []
if isinstance(raw, list):
return [str(g) for g in raw]
s = str(raw).strip()
if not s:
return []
try:
parsed = json.loads(s)
if isinstance(parsed, list):
return [str(g) for g in parsed]
except json.JSONDecodeError:
pass
return [g.strip() for g in s.strip("[]").replace('"', "").split(",") if g.strip()]
claims = event["requestContext"]["authorizer"]["claims"]
user_key = claims.get("sub") or claims.get("cognito:username") or claims.get("email")
prefix = os.environ.get("COGNITO_TEAM_GROUP_PREFIX", "") # 예: "llmgw-"
candidates = [g for g in _extract_groups(claims.get("cognito:groups"))
if g.startswith(prefix)]
if len(candidates) != 1:
return {"statusCode": 403} # 0개 또는 다중 배정은 거부
team_alias = candidates[0]
여기에서는 세 가지를 확인 해 보겠습니다.
첫째, cognito:groups를 배열로 가정하면 안 됩니다. JWT 페이로드 안에서는 JSON 배열이지만, requestContext.authorizer.claims로 넘어올 때는 통합 방식에 따라 네이티브 리스트, '["llmgw-dev"]' 같은 JSON 문자열, 쉼표 구분 문자열 중 하나로 도착합니다. 몇번의 시도 끝에, 결국 위처럼 세 경우를 모두 받아내는 헬퍼를 두는 것으로 정리했습니다. 한 형태만 가정하고 짜면 언젠가 조용히 403이 발생합니다.
둘째, 사용자 식별자는 email이 아니라 sub를 씁니다. 이메일은 변경될 수 있지만 sub는 불변이라, 가상키 캐시의 키로 쓰기에 안전합니다. 이메일은 사람이 읽을 표시용으로만 씁니다.
셋째, 팀 접두사(llmgw-)와 다중 그룹 정책은 상수가 아니라 환경변수로 주입합니다. 조직마다 접두사가 다를 테니 코드를 고치지 않고 바꿀 수 있게 해두는 편이 낫습니다.
개조 ③ — 클라이언트 헬퍼
aws sso login이 사라졌으므로 개발자 PC 쪽 스크립트도 함께 바뀝니다. 이건 뒤의 4.5절에서 따로 다루겠습니다.
이 세 가지를 제외한 나머지는 원본 코드를 그대로 씁니다.
4.2 사전 요구사항
로컬 도구:
| 도구 | 최소 버전 | 확인 |
|---|---|---|
| AWS CLI | v2 | aws --version |
| Node.js | 18 이상 (LTS 20 권장) | node --version |
| AWS CDK | v2 | cdk --version |
| Docker | 데몬 실행 필수 | docker info |
| Python | 3.12 이상 (Lambda 런타임 기준) | python --version |
샘플 코드:
AWS 계정 준비:
- Amazon Cognito User Pool 생성 권한 (IdC·외부 IdP 불필요)
- Amazon Bedrock 모델 액세스 승인 — 게이트웨이 리전(ap-northeast-2)에서 Claude 모델
- ACM 인증서 또는 Route 53 공개 호스티드존 (ALB HTTPS용)
- CDK Bootstrap — 게이트웨이 리전(ap-northeast-2)
docker info && node -v && cdk --version && aws --version
# Bedrock 추론 프로파일 확인 — 모델 ID를 추측하지 말고 반드시 조회할 것
aws bedrock list-inference-profiles --region ap-northeast-2 \
--query "inferenceProfileSummaries[?contains(inferenceProfileId,'anthropic')].inferenceProfileId"
aws acm list-certificates --region ap-northeast-2 --certificate-statuses ISSUED
curl -s https://checkip.amazonaws.com # SG allowlist 후보 IP
4.3 배포
개조가 끝났으면 배포 방식은 원본과 같습니다.
cdk bootstrap aws://<ACCOUNT_ID>/ap-northeast-2
npm install && npx cdk synth
cdk deploy LlmGatewayStack -c certificateArn=<ACM_ARN> --outputs-file outputs.json
완료되면 루트 스택 아래 5개의 Nested Stack(Network / Database / Auth / Gateway / Monitoring)이 생성됩니다. 소요 시간은 약 15~25분입니다.
⚠ CDK Bootstrap에서 “no changes”는 정상이 아닙니다. Qualifier 불일치일 수 있으니 확인하세요.
aws cloudformation describe-stacks --stack-name CDKToolkit --region ap-northeast-2 \ --query "Stacks[0].Parameters[?ParameterKey=='Qualifier'].ParameterValue" --output text
4.4 초기 사용자 등록
AuthStack은 사용자 0명으로 배포됩니다. 사용자를 만들고 팀 그룹에 배정합니다. 그룹명이 곧 LiteLLM 팀(team_alias) 입니다.
POOL_ID=<POOL_ID> # outputs.json 참조
aws cognito-idp admin-create-user --region ap-northeast-2 --user-pool-id $POOL_ID \
--username dev@example.com --user-attributes Name=email,Value=dev@example.com \
--desired-delivery-mediums EMAIL
aws cognito-idp admin-add-user-to-group --region ap-northeast-2 --user-pool-id $POOL_ID \
--username dev@example.com --group-name llmgw-dev
4.5 개조 ③ — 개발자 클라이언트 설정
앞서 미뤄둔 개조 ③입니다. AWS 원본 샘플 저장소의 scripts/에는 스크립트가 두 개 있고, 둘 다 org-sso를 전제로 합니다.
| 원본 저장소 스크립트 | 역할 | 전제 |
|---|---|---|
get-gateway-token.sh | apiKeyHelper — SSO 자격증명으로 Token Service를 SigV4 호출해 가상키 획득 | aws sso login |
setup-developer.sh | 개발자 온보딩 안내 — AWS CLI·jq 확인, SSO 프로파일 구성 안내 | aws sso login |
cognito-native에는 aws sso login이라는 단계 자체가 없습니다. 신원 계층을 바꿨으니 클라이언트 헬퍼도 함께 갈아끼워야 합니다. 원본 스크립트를 기준으로 삼되, 아래 네 가지 기능을 다시 구현했습니다.
| 기능 | 원본 대응 | cognito-native에서 바뀌는 부분 |
|---|---|---|
| ① 클라이언트 설정 | setup-developer.sh | SSO 프로파일 구성 대신, Hosted UI 엔드포인트와 client_id를 로컬 config에 기록하고 settings.json을 병합 |
| ② 로그인 | (없음 — aws sso login이 그 자리) | 브라우저 PKCE 인증으로 access token 획득·캐시 |
| ③ 토큰 헬퍼 (apiKeyHelper) | get-gateway-token.sh | SigV4 서명 대신 Bearer access token으로 Token Service 호출 |
| ④ 헬스체크 | (없음) | 토큰 → 가상키 → /v1/models까지 전체 경로 자동 검증 |
즉 ③만 원본 스크립트의 인증 방식을 바꿔 이식한 것이고, ①②④는 cognito-native를 위해 새로 만든 것입니다. 파일명은 조직마다 달라질 수 있으니 이 글에서는 기능 이름으로 부르겠습니다. 개발자 입장에서 실제 절차는 세 줄입니다 — ① 설정 → ② 로그인 → ④ 확인. 편집할 파일은 없습니다.
setup-developer에 해당하는 ①이 ~/.claude/settings.json에 병합하는 내용은 이렇습니다.
{
"env": {
"ANTHROPIC_BASE_URL": "https://<gateway-domain>",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "<opus-alias>",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "<sonnet-alias>",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "<haiku-alias>",
"AWS_REGION": "ap-northeast-2"
},
"apiKeyHelper": "powershell -ExecutionPolicy Bypass -File \"<repo>\\scripts\\get-gateway-token.ps1\""
}
ANTHROPIC_BASE_URL이 빠지면 Claude Code가 가상키를 들고 api.anthropic.com으로 요청해 401이 납니다. 온보딩에서 가장 흔한 실수 중 하나였습니다.
Windows Powershell 주의 — git bash(MINGW)는 PTY를 제공하지 않아 Claude Code 대화형 실행이 실패합니다. PowerShell을 쓰거나
winpty claude로 실행하세요.
5. 실전에서 막혔던 지점 8건
여기부터가 이 글의 키 포인트입니다. 공식 문서만 읽어서는 드러나지 않고, 실제로 구축해 본 환경에서 경험해 본 것들입니다.
인프라 구축 단계 (6건)
① 모델 ID에 버전 suffix를 붙여서 인식 실패
- 2026년 모델들은
global.추론 프로파일 전용입니다.us.프리픽스를 가정하지 마세요. 반드시aws bedrock list-inference-profiles로 실제 값을 조회해서 쓰고, LiteLLM 설정에는bedrock/global.anthropic.<model-id>형태로 넣습니다. Bedrock의 글로벌 교차 리전 추론은 정식 기능이며, 리전별로 지원 모델이 다릅니다.
② Bedrock Marketplace 403
- ECS Task Role 권한 부족이었습니다. Task Role에 Marketplace 액세스 인라인 정책을 추가해 해결했습니다.
③ Claude Code의 beta 파라미터를 LiteLLM이 거부
- Claude Code가 보내는 실험적 beta 파라미터를 LiteLLM이 아직 처리하지 못했습니다. 클라이언트 측에서
CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1로 꺼주면 통과합니다.
④ Permission Boundary 강제 환경에서 CDK 배포 실패
- 사내 정책상 모든 IAM 역할에 Permission Boundary가 강제되는 계정이었습니다. CDK가 생성하는 역할에 자동으로 붙지 않아 배포가 실패했고, CDK Aspect로 Boundary를 일괄 주입해서 해결했습니다. 규제 환경에 배포하실 분들은 미리 확인하세요.
⑤ ARM64 / x64 아키텍처 불일치 — exec format error
- 모든 Fargate·Lambda를 Graviton(arm64)으로 잡았는데, 로컬 x64 머신에서 빌드한 이미지를 올리니 태스크가 즉시 종료됐습니다.
fromAsset에platform: LINUX_ARM64를 지정하거나 buildx+QEMU 크로스빌드가 필요합니다. 에러 메시지가 불친절해서 원인 찾는 데 시간이 걸립니다.
⑥ 모델을 추가했는데 DB에 반영되지 않음
STORE_MODEL_IN_DB=True환경변수가 빠져 있었습니다. 한 줄입니다.
Windows 개발자 온보딩 (2건)
- 검증 환경: Windows 11 · Windows PowerShell 5.1 · Python 3.10 · 시스템 로캘 cp949(한국어). 두 건 모두 한글 Windows에서만 재현됩니다.
⑦ cp949 / UTF-8 BOM 인코딩 충돌
UnicodeDecodeError: 'cp949' codec can't decode byte 0xbf in position 2: illegal multibyte sequence
- PowerShell 5.1이
config.json을 UTF-8 with BOM으로 저장하는데(BOM은EF BB BF3바이트), Python의Path.read_text()는 인코딩 미지정 시 시스템 로케일을 씁니다. 한글 Windows에서는 cp949이므로 BOM 세 번째 바이트0xBF에서 디코딩이 깨집니다. 영문 Windows(cp1252)에서는0xBF가 유효 문자로 매핑돼 그냥 통과합니다 — 로케일 의존 버그라 재현 환경을 맞추지 않으면 못 찾습니다.
근본 해결은 읽는 쪽에 인코딩을 명시하는 것입니다. utf-8-sig는 BOM 유무 양쪽 모두 안전합니다.
return json.loads(p.read_text(encoding="utf-8-sig")) # ← 인코딩 명시
쓰는 쪽도 마찬가지로 수정 했습니다. PowerShell에서 Set-Content/Out-File은 BOM을 붙이므로 아래 패턴을 씁니다.
[System.IO.File]::WriteAllText($path, $content, (New-Object System.Text.UTF8Encoding($false)))
⑧ forceLoginMethod: "gateway" 의미 충돌로 로그인 완전 차단
- 이게 가장 시간이 많이 소요되었던 건입니다. Claude Code 기동 시 이런 에러가 났습니다.
This machine's managed settings require a first-party login, but an
Anthropic-issued credential (ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN,
or apiKeyHelper) is configured. ...
claude auth login으로 우회하면 다시 forceLoginMethod is 'gateway' in managed settings로 막히는 순환 상태가 됩니다.
원인은 C:\Program Files\ClaudeCode\managed-settings.json의 { "forceLoginMethod": "gateway" } 였습니다. 여기서 "gateway"는 Anthropic이 운영하는 조직 관리형 게이트웨이를 뜻합니다. LiteLLM 같은 서드파티 프록시가 아닙니다. 즉 “1st-party OAuth로 로그인하라”는 강제 옵션이므로, apiKeyHelper로 주입되는 비-OAuth 가상키와는 정의상 양립할 수 없습니다.
서드파티 게이트웨이 연결에는 managed-settings.json이 아예 필요 없습니다. 필요한 건 사용자 스코프 settings.json의 ANTHROPIC_BASE_URL + apiKeyHelper뿐입니다.
진단할 때 헷갈리기 쉬운 지점도 함께 적어둡니다.
- 에러 본문이
forceLoginOrgUUID를 언급하지만 이건 org pin 계열 실패에 공통으로 붙는 힌트 문구입니다. 그 키가 없다고 해서 이 원인을 배제하면 안 됩니다. managed-settings.json의 JSON이 깨지면 파일 전체가 무시됩니다. 절단 테스트로 쓸 수 있습니다 — 이 상태에서 에러가 사라지면 이 파일이 원인입니다.- 기동 시 Settings Error 다이얼로그의 “Continue without these settings”를 고르면 그 실행에 한해 무시합니다. 영구 수정 전 검증용으로 유용합니다.
- 레거시 경로
C:\ProgramData\ClaudeCode\managed-settings.json은 claude code v2.1.75부터 미지원입니다. 두 경로 모두 확인 해 주세요.
조직 관리 단말 주의 —
managed-settings.json은 IT가 배포하는 정책 파일입니다. 회사 정책으로 org pin이 걸린 단말이라면 임의로 해제하지 말고 정책 소유 부서와 협의하십시오. 검증 목적이라면 Windows managed settings가 적용되지 않는 WSL2에서 진행하는 것이 안전합니다.
그 밖에 자주 마주치는 증상
| 증상 | 원인 | 해결책 |
|---|---|---|
| 로그인은 됐는데 401 | id_token을 전송 | access token(token_use=access) 사용 |
| 403 (팀 매핑 실패) | llmgw- 그룹이 0개이거나 다중 배정 | 정확히 1개만 배정 |
| 게이트웨이 접속 타임아웃 | SG allowlist에 현재 egress IP 없음(유동 IP) | checkip로 확인 후 ingress CIDR 갱신·재배포 |
| 빈 Hosted UI 화면 | 로컬 config에 남은 예전 client_id | 로컬 설정 캐시 삭제 후 setup 재실행 |
| Bedrock AccessDenied | 모델 미승인 / Task Role 권한 | Model access 확인 · global 프로파일 ARN 확인 |
6. 운영 — 팀 추가는 재배포 없이
새 팀 추가
- Cognito에서
llmgw-<팀>그룹 생성 + 사용자 배정 - LiteLLM Admin UI(Teams)에서 같은 이름의 팀에 Models / Max Budget 설정
Lambda 수정도 재배포도 없습니다. 앞서 말한 “팀 정보는 토큰이 알려준다” 설계의 장점입니다.
예산·모델 제어
Admin UI에서 GUI로 하거나 API로도 가능합니다.
curl -X POST "https://<gateway-domain>/key/update" \
-H "Authorization: Bearer <MASTER_KEY>" -H "Content-Type: application/json" \
-d '{ "key":"sk-...", "max_budget":100.0, "budget_duration":"30d", "rpm_limit":100 }'
마스터키 회전
aws secretsmanager put-secret-value --region ap-northeast-2 \
--secret-id <secret-name> --secret-string "sk-<new-master-key>"
aws ecs update-service --region ap-northeast-2 \
--cluster <cluster> --service <service> --force-new-deployment
개발자 가상키는 별도 DB 레코드라 마스터키 회전에 영향받지 않습니다.
오프보딩 — 실수 할 수 있는 포인트가 하나 있습니다
aws cognito-idp admin-disable-user --region ap-northeast-2 \
--user-pool-id <POOL_ID> --username dev@example.com
Cognito 비활성화만으로는 부족합니다. 이미 발급된 가상키가 DynamoDB 캐시 TTL(기본 30일)까지 살아 있을 수 있습니다. 즉시 차단이 필요하면 LiteLLM Admin UI에서 해당 가상키를 revoke하는 작업을 반드시 병행하세요. 비밀번호 변경·재설정도 마찬가지로 기존 토큰·가상키를 즉시 무효화하지 않습니다.
관측
- 사용량 대시보드 — CloudWatch (모델/팀별 토큰·비용·지연, 사용자 Top-N)
- 요청 로그 — LiteLLM Admin UI + CloudWatch Logs
/ecs/<service-name> - “이번 달 비용이 왜 늘었지”에 답할 수 있는 상태를 만드는 것, 이게 게이트웨이를 두는 이유입니다
7. 비용 — 인프라 실측과 TCO 시나리오
인프라 월 비용 (ap-northeast-2, PoC 테스트 환경 구성 실측 기준)
| 서비스 | 구성 | 예상 월 비용 (USD) |
|---|---|---|
| ECS Fargate (Graviton) | 2 vCPU / 4GB · 24×7 · 1 태스크 | 약 $35~50 |
| Aurora Serverless v2 | 0.5~4 ACU | 약 $15~40 |
| ALB (공개+내부) | 트래픽 기반 | 약 $25~35 |
| NAT Gateway | 1개 (dev) | 약 $35~45 |
| VPC 인터페이스 엔드포인트 | bedrock/secrets/ssm/ecr/logs 등 | 약 $50~80 |
| DynamoDB / API GW / Secrets | 온디맨드 소량 | 약 $3~8 |
| 합계 (인프라) | — | 약 $165~260 / 월 |
Bedrock 모델 호출 비용은 별도이며 사용자·팀 예산으로 제어합니다. PoC/테스트가 끝나면 cdk destroy LlmGatewayStack으로 반드시 환경을 정리하시길 바랍니다. 상시 과금되는 항목 중에 큰 비중을 차지하는 것은 VPC 인터페이스 엔드포인트와 NAT Gateway입니다.
100명 규모 12개월 TCO 시나리오
아래는 사내에서 테스트한 PoC 환경에서 발생한 실측치와 공개 자료를 조합한 자체 산정 시나리오입니다. 조직마다 사용 패턴이 다르므로 그대로 인용하기보다 계산식에 각자 값을 넣어보시길 권합니다.
예시) 게이트웨이 경유 최적화 시 인당 월 $200 / 비최적화 직접 사용 시 $350, 월 인프라 $450, 일회성 구축비 $25K, 시니어 1 FTE 월 $7K.
TCO = (개발자수 × 월토큰비용 × 12) + (월인프라 × 12) + 구축비 + (운영FTE × $7,000 × 12)
| 시나리오 | 계산 | 12개월 TCO |
|---|---|---|
| LLM Gateway (MSP 구축) | $240K + 인프라 $5.4K + 구축 $25K + 운영 $0 | $270K |
| Direct Bedrock (통제 없음) | $420K + 운영 0.5 FTE $42K | $462K |
| 자체 구축 | $240K + 인프라 $5.4K + 구축 $35K + 운영 1.0 FTE $84K | $364K |
Direct 대비 약 41% 절감, 투자 회수는 1.4개월로 계산됩니다. 여기서 절감의 대부분은 인프라비가 아니라 모델 라우팅과 예산 캡으로 토큰 낭비를 막는 데서 나옵니다. 단순 질문이 Opus로 새는 것을 게이트웨이가 막아주는 효과입니다.
마치며
정리하면 세 가지입니다.
base_url한 줄이면 충분합니다. 기존 .NET/Python OpenAI 코드를 거의 수정 없이 Bedrock으로 전환할 수 있습니다.- 보안과 비용을 동시에 잡습니다. 개인 API Key를 제거한 5중 방어와 팀별 예산 캡이 같은 지점에서 걸립니다.
- CDK로 반복 배포됩니다. AWS 원본 샘플에서 AuthStack 하나만 갈아끼우면 되므로, 개조본도 그대로 팀 안에서 재사용 가능한 IaC 자산이 됩니다.
그리고 이 글의 출발점이었던 이야기 — 조직 SSO가 없어서 LLM Gateway를 구축하지 못할 이유는 없습니다. IdC 계정 인스턴스 제약은 우회가 아니라 설계 교체로 푸는 문제였고, Cognito User Pool을 유일 신원 베이스로 두면 개발자 PC에 AWS 자격증명 없이도 같은 수준의 통제가 성립합니다. 조직 SSO가 있으면 org-sso 버전으로, 없으면 이 글의 cognito-native 버전으로 — 같은 코드베이스에서 신원 방식만 갈아끼우면 됩니다.
프로덕션 배포 전에는 반드시 자체 보안 검토를 수행하시기 바랍니다.
참고 자료
- Amazon Bedrock 기반 Claude Code, 조직에서 안전하게 운영하기: LLM Gateway 구축 가이드 — AWS 기술 블로그
- Account instances of IAM Identity Center — AWS 공식 문서
- Control access to REST APIs using Amazon Cognito user pools as an authorizer — AWS 공식 문서
- Understanding the access token — Amazon Cognito 공식 문서
- Integrate a REST API with an Amazon Cognito user pool (OAuth Scopes 동작) — AWS 공식 문서
- Claude Code settings (managed settings, forceLoginMethod)
- LiteLLM 공식 문서
- Claude Code on Bedrock — Enterprise Blueprint (aws-samples/sample-aws-kr-enterprise) — 이 글의 기준이 된 AWS 원본 샘플. 인증은 org-sso 기준입니다.