Published on

AWS Bedrock InvokeModel 403·Throttling 해결 - IAM·VPC·쿼터

Authors

서론에서부터 결론까지 한 번에 읽고 바로 적용할 수 있게, AWS Bedrock InvokeModel 호출 시 가장 흔한 장애인 **403(AccessDenied 계열)**과 **Throttling(TooManyRequests/LimitExceeded 계열)**을 IAM·VPC·쿼터 3축으로 정리합니다. 특히 “콘솔에서는 되는데 코드/파드/ECS에서만 실패”하는 케이스는 대개 자격 증명 체인(AssumeRole/IRSA), 리전/모델 접근, VPC 경로, 쿼터 중 하나에서 꼬입니다.

아래는 문제를 빨리 좁히기 위한 핵심 질문입니다.

  • 403인가, Throttling인가? (에러 코드/메시지 정확히 확인)
  • 호출 주체는 누구인가? (IAM User/Role, IRSA, Task Role, Lambda Execution Role)
  • 어떤 엔드포인트로 나가고 있는가? (Public, PrivateLink, NAT)
  • 모델/리전 접근이 열려 있는가? (Bedrock 모델 액세스, 리전 불일치)
  • 호출량/동시성이 쿼터를 넘는가? (Requests per minute, concurrency)

관련해서 STS/IRSA 권한 꼬임을 다루는 글도 함께 보면 진단 속도가 빨라집니다: EKS IRSA 설정했는데 STS AccessDenied 뜰 때

1) 먼저 에러를 분류하자: 403 vs Throttling

1-1. 403(AccessDenied/Forbidden)에서 흔한 메시지 패턴

  • AccessDeniedException: User is not authorized to perform: bedrock:InvokeModel
  • UnrecognizedClientException / The security token included in the request is invalid
  • ForbiddenException (SDK/런타임에 따라 표현 다름)
  • AccessDeniedException: ... because no identity-based policy allows ...

이 경우는 거의 항상 권한(정책/리소스/조건) 또는 자격 증명(토큰/AssumeRole) 문제입니다.

1-2. Throttling에서 흔한 메시지 패턴

  • ThrottlingException: Rate exceeded
  • TooManyRequestsException
  • LimitExceededException

이 경우는 요청 속도/동시성/쿼터 문제이며, 재시도(backoff)와 쿼터 증설, 호출 패턴 개선이 핵심입니다.

2) 403 해결 1순위: IAM 정책(InvokeModel/InvokeModelWithResponseStream)

Bedrock 런타임 호출은 보통 bedrock-runtime:InvokeModel 액션(표기/SDK에 따라 bedrock:InvokeModel로 보이기도 함)이 필요합니다. 스트리밍을 쓰면 InvokeModelWithResponseStream도 필요합니다.

2-1. 가장 흔한 실수: 리소스 ARN을 너무 좁게 잡음

Bedrock 모델 ARN은 리전/모델 ID에 따라 달라집니다. 우선은 진단을 위해 리소스를 넓게 허용하고, 정상화 후 좁히는 방식이 안전합니다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "BedrockInvoke",
      "Effect": "Allow",
      "Action": [
        "bedrock:InvokeModel",
        "bedrock:InvokeModelWithResponseStream"
      ],
      "Resource": "*"
    }
  ]
}

정상 동작 확인 후에는 Resource를 모델 ARN로 제한하거나, Condition으로 리전/태그/소스 VPCe 등을 제한합니다.

2-2. 콘솔은 되는데 코드에서만 403: 호출 주체가 다르다

콘솔에서 테스트는 내 사용자/SSO 세션으로 되고, 실제 코드는 EC2 Instance Profile / ECS Task Role / EKS IRSA Role / Lambda Role로 돌아갑니다. CloudTrail에서 eventName=InvokeModeluserIdentity를 확인해 “누가 호출했는지”부터 확정하세요.

EKS라면 IRSA의 신뢰 정책(OIDC) 꼬임도 403/AccessDenied로 나타납니다. 체크리스트는 이 글이 유용합니다: EKS IRSA인데 AccessDenied? OIDC·TrustPolicy·SA 점검

2-3. STS 토큰/자격 증명 체인 문제

다음 증상이 있으면 자격 증명 문제를 의심합니다.

  • 로컬에서는 되는데 배포 환경에서만 invalid security token
  • EKS에서 서비스어카운트 바꿨더니 갑자기 403
  • AssumeRole 체인(중간 role)에서 ExternalId/MFA 조건이 걸림

즉시 확인할 것

  • AWS_REGION/AWS_DEFAULT_REGION이 실제 Bedrock 리전과 일치하는지
  • SDK가 어떤 credential provider를 쓰는지(환경변수/웹아이덴티티/인스턴스 메타데이터)
  • EKS라면 AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN 설정 여부

3) 403 해결 2순위: Bedrock 모델 액세스/리전/모델 ID

IAM이 맞아도, 다음 케이스에서 403/AccessDenied처럼 보일 수 있습니다.

  • Bedrock에서 해당 파운데이션 모델 접근이 아직 승인되지 않음(Model access)
  • 리전 불일치: 예) us-east-1에서만 가능한 모델을 ap-northeast-2로 호출
  • 모델 ID 오타: SDK는 404 대신 403/Validation으로 뭉개서 주는 경우가 있음

3-1. AWS CLI로 최소 재현(가장 중요)

애플리케이션에서 복잡하게 재현하지 말고, 동일한 IAM 주체로 CLI 호출을 먼저 성공시키세요.

aws sts get-caller-identity

aws bedrock-runtime invoke-model \
  --region us-east-1 \
  --model-id anthropic.claude-3-5-sonnet-20240620-v1:0 \
  --content-type application/json \
  --accept application/json \
  --body '{"anthropic_version":"bedrock-2023-05-31","max_tokens":128,"messages":[{"role":"user","content":[{"type":"text","text":"hello"}]}]}' \
  /tmp/out.json

cat /tmp/out.json
  • get-caller-identity로 “누구로 호출하는지” 고정
  • --region을 명시해서 리전 혼선을 제거
  • 모델 ID는 콘솔/문서에서 정확히 복사

4) 403 해결 3순위: VPC(PrivateLink)·엔드포인트·DNS·NACL

Bedrock를 프라이빗 서브넷에서 호출할 때 흔한 함정은 네트워크 경로가 막혀서 SDK 레벨에서 애매한 실패(때로는 403처럼 보이는)를 만드는 것입니다. 특히 다음 구성이 섞이면 복잡해집니다.

  • NAT 없이 인터넷 불가
  • Interface VPC Endpoint(PrivateLink)로만 AWS API 접근
  • VPC DNS 비활성화/커스텀 DNS
  • 엔드포인트 SG/NACL이 443을 막음

4-1. Bedrock Runtime용 VPC Endpoint를 제대로 만들었는지

Bedrock는 제어 플레인과 런타임 엔드포인트가 분리되는 경우가 있어, 보통 런타임 호출에는 bedrock-runtime 엔드포인트가 중요합니다.

체크 포인트

  • VPC Endpoint 타입: Interface
  • 서브넷: 호출 워크로드가 있는 서브넷에 ENI가 붙는지
  • Security Group: 워크로드 SG → VPCe SG로 TCP 443 egress/ingress 허용
  • Private DNS: 켰는지(일반적으로 켜는 편이 편함)

4-2. Private DNS를 끄면 생기는 문제

Private DNS를 끄면 bedrock-runtime.<region>.amazonaws.com이 퍼블릭으로 해석되어 NAT/인터넷이 필요합니다. NAT가 없다면 타임아웃/연결 실패가 나고, 재시도 중 엉뚱한 에러로 보일 수 있습니다.

EKS 환경에서 네트워크/IRSA/코어DNS가 섞여 장애가 나는 패턴은 아래 글의 접근 방식이 유사합니다: EKS TLS handshake timeout 해결 - IRSA·VPC·CoreDNS

4-3. 빠른 네트워크 진단 명령

컨테이너/노드에서 DNS와 443 연결만 확인해도 절반은 끝납니다.

# DNS 확인
nslookup bedrock-runtime.us-east-1.amazonaws.com

# TLS 연결 확인
curl -Iv https://bedrock-runtime.us-east-1.amazonaws.com
  • 사설 IP로 해석되면 PrivateLink 경로 가능성이 큼
  • 퍼블릭 IP로 해석되면 NAT/인터넷 경로가 필요

5) Throttling 해결: 쿼터·동시성·재시도 전략

Bedrock Throttling은 “내 코드가 느려서”가 아니라 “서비스가 보호를 위해 막는” 상황입니다. 해결은 크게 세 가지 축입니다.

  1. 쿼터 확인/증설
  2. 클라이언트 재시도(backoff/jitter)
  3. 호출 패턴 개선(배치/큐/동시성 제한)

5-1. Service Quotas에서 확인할 항목

계정/리전별로 제한이 다르고, 모델/온디맨드/프로비저닝 여부에 따라 달라질 수 있습니다. 먼저 다음을 확인합니다.

  • Bedrock 관련 Requests per minute(RPM) 또는 Transactions per second(TPS)
  • 동시 요청 제한(Concurrency)
  • 특정 모델에 대한 제한(모델별 할당)

증설 요청 전에는 “현재 피크 RPM/동시성”을 CloudWatch/애플리케이션 로그로 수치화해 두는 게 좋습니다.

5-2. SDK 레벨 재시도: 지수 백오프 + 지터

Throttling은 재시도로 대부분 완화되지만, 동시 재시도 폭주가 2차 장애를 만들 수 있어 지터가 중요합니다.

아래는 Python(boto3)에서 간단히 적용 가능한 패턴입니다.

import json
import random
import time
import boto3
from botocore.config import Config
from botocore.exceptions import ClientError

config = Config(
    region_name="us-east-1",
    retries={"max_attempts": 10, "mode": "adaptive"},
)
client = boto3.client("bedrock-runtime", config=config)

MODEL_ID = "anthropic.claude-3-5-sonnet-20240620-v1:0"

body = {
    "anthropic_version": "bedrock-2023-05-31",
    "max_tokens": 128,
    "messages": [{"role": "user", "content": [{"type": "text", "text": "summarize this"}]}],
}


def invoke_with_backoff(max_tries=8):
    for i in range(max_tries):
        try:
            resp = client.invoke_model(
                modelId=MODEL_ID,
                contentType="application/json",
                accept="application/json",
                body=json.dumps(body),
            )
            return json.loads(resp["body"].read())
        except ClientError as e:
            code = e.response.get("Error", {}).get("Code", "")
            if code in {"ThrottlingException", "TooManyRequestsException", "LimitExceededException"}:
                base = min(2 ** i, 20)
                sleep = base + random.random()  # jitter
                time.sleep(sleep)
                continue
            raise


print(invoke_with_backoff())
  • mode="adaptive"는 AWS SDK 재시도에 도움이 되지만, 애플리케이션 레벨에서 동시성 제한까지 같이 해야 안정적입니다.

5-3. 호출 패턴 개선: 동시성 제한 + 큐잉

Throttling을 근본적으로 줄이려면 “재시도”만으로는 부족합니다.

  • API 서버: 사용자 요청을 바로 Bedrock에 쏘지 말고 내부 큐(SQS/Kafka)로 흡수
  • 워커: 동시성(예: 5~20)을 고정하고, 초당 처리량을 일정하게 유지
  • 배치 가능하면 배치로 합치기(요청 수 자체를 줄임)

Node.js라면 p-limit 같은 라이브러리로 동시성을 제한하는 방식이 간단합니다.

import pLimit from "p-limit";
import { BedrockRuntimeClient, InvokeModelCommand } from "@aws-sdk/client-bedrock-runtime";

const client = new BedrockRuntimeClient({ region: "us-east-1" });
const limit = pLimit(10); // 동시성 10으로 제한

const modelId = "anthropic.claude-3-5-sonnet-20240620-v1:0";

async function invokeOnce(text) {
  const body = JSON.stringify({
    anthropic_version: "bedrock-2023-05-31",
    max_tokens: 128,
    messages: [{ role: "user", content: [{ type: "text", text }] }],
  });

  const cmd = new InvokeModelCommand({
    modelId,
    contentType: "application/json",
    accept: "application/json",
    body,
  });

  const res = await client.send(cmd);
  return JSON.parse(new TextDecoder().decode(res.body));
}

const inputs = Array.from({ length: 200 }, (_, i) => `hello ${i}`);
const results = await Promise.all(inputs.map((t) => limit(() => invokeOnce(t))));
console.log(results.length);

6) 운영 관점 체크리스트: 30분 내 원인 좁히기

6-1. CloudTrail/로그로 “누가/어디서/무엇을” 확정

  • CloudTrail에서 InvokeModel 이벤트 확인
  • userIdentity.arn으로 실제 호출 Role 확인
  • awsRegion이 의도한 리전인지 확인

6-2. IAM Policy Simulator로 최소 권한 검증

  • 호출 Role에 bedrock:InvokeModel이 Allow인지
  • Condition에 의해 Deny 되는지(특히 aws:SourceVpce, aws:RequestedRegion)

6-3. VPC 경로는 DNS→443 순서로 확인

  • DNS가 VPCe로 가는지
  • 443이 SG/NACL에서 열려 있는지
  • NAT 없이 퍼블릭으로 나가려는지

6-4. 쿼터는 수치로 말하기

  • 현재 피크 RPM/동시성
  • 재시도 횟수/지연
  • 95/99p latency

이 수치가 있어야 쿼터 증설도 승인 가능성이 올라가고, 설령 증설이 안 돼도 “동시성 제한 + 큐잉”으로 설계를 바꿀 근거가 생깁니다.

결론: 403은 ‘정체(Identity)’, Throttling은 ‘속도(Speed)’ 문제다

  • 403은 대부분 “누가 호출하는지(실제 Role) + 어떤 권한이 있는지(IAM) + 모델/리전 접근이 열렸는지 + VPC 경로가 맞는지”를 확정하면 풀립니다.
  • Throttling은 “쿼터 수치 확인 → 동시성 제한 → 지수 백오프/지터 → 필요 시 쿼터 증설” 순서로 접근하면 안정화됩니다.

InvokeModel 장애는 한 번 꼬이면 IAM/VPC/쿼터가 서로 영향을 주는 것처럼 보이지만, 위 순서대로 에러 분류 → 최소 재현(CLI) → 호출 주체 확정(CloudTrail) → 네트워크 경로 → 쿼터/동시성으로 분해하면 대부분 30~60분 내에 원인을 특정할 수 있습니다.