AWS에 Claude apps gateway 배포
기준일: 2026-10-23
공식 기준: Deploy Claude apps gateway on AWS
개요
AWS에서 Claude apps gateway를 돌리는 워크드 예제입니다. 지원되는 프로덕션 배포가 아니라 고객 관리 인프라용 작동 예제이므로 구성 요소가 어떻게 맞물리는지 본 뒤 환경에 맞게 조정하세요. 플랫폼 공통 요건은 배포 가이드에 있습니다. 모델 upstream은 Amazon Bedrock, 컴퓨트는 Amazon ECS(AWS Fargate) 또는 Amazon EKS, 예시 IdP는 Okta이며 OIDC 호환 IdP면 모두 동작합니다.
Bedrock만이 AWS의 Claude upstream은 아닙니다. gateway는 AWS 인증과 AWS Marketplace 과금을 쓰는 Anthropic 운영 Claude API인 Claude Platform on AWS를 Bedrock 대신 또는 함께 지원합니다. upstream 항목, 자격 증명, IAM 권한이 이 페이지의 Bedrock 범위와 다르며 Claude Platform on AWS upstream reference가 바뀌는 점을 다룹니다. 나머지는 그대로 적용됩니다.
핵심 개념
gateway는 개발자가 IdP로 로그인하는 네트워크 내부 private HTTPS 엔드포인트로 동작합니다. 개발자의 Claude Code 세션은 gateway의 IAM 역할로 Bedrock의 Claude 모델에 닿으므로 모델 자격 증명이 개발자 머신에 남지 않습니다. 참조 구성이 프로비저닝하는 것:
- Amazon ECS on AWS Fargate 서비스 또는 Amazon EKS Deployment (gateway 컨테이너)
- Amazon ECR 저장소 (gateway 이미지)
- private 서브넷의 Amazon RDS for PostgreSQL (공개 접근 불가, gateway store)
- AWS Secrets Manager secret: JWT 서명 키, OIDC client secret, Postgres URL
bedrock:InvokeModel,bedrock:InvokeModelWithResponseStream,bedrock:CountTokens를 가진 IAM 역할 (ECS task role 또는 EKS의 IRSA)- HTTPS용 internal Application Load Balancer
사전 요구:
- 위 리소스를 만들 권한이 있는 AWS 계정
- 설치·인증된 AWS CLI v2와 로컬 Docker
- 서로 다른 AZ의 private 서브넷 2개 이상이 있는 VPC와 NAT gateway 경유 아웃바운드(internal 로드 밸런서는 2개 AZ 서브넷이 필요하고 gateway는 Bedrock과 IdP로의 egress가 필요)
- redirect URI
https://<gateway-host>/oauth/callback을 가진 Okta OIDC 웹 애플리케이션 - gateway용 TLS 호스트명(보통 Route 53 private hosted zone의 내부 DNS 이름이 로드 밸런서를 가리킴)과 그 이름의 ACM 인증서(import 또는 AWS Private CA 발급)
상세
환경 변수
모든 명령은 셸에서 AWS_REGION, ACCOUNT_ID, VPC_ID, PRIVATE_SUBNETS 네 값을 읽습니다. Bedrock이 필요한 Claude 모델을 제공하는 US 리전을 고르세요. 이 예제는 gateway 내장 모델 카탈로그가 us.anthropic.* inference profile로 해석되는 것에 의존하며 IAM 정책이 그 ARN을 허용합니다. US 외 리전에서는 해당 geo의 inference-profile ID로 models: 블록을 추가하고 IAM 정책의 ARN 접두사를 맞게 바꿉니다. VPC ID를 모르면 aws ec2 describe-vpcs로 나열한 뒤 서브넷을 조회합니다.
aws ec2 describe-subnets --filters "Name=vpc-id,Values=<your-vpc-id>" \
--query 'Subnets[].{ID:SubnetId,AZ:AvailabilityZone,CIDR:CidrBlock}' --output table
export AWS_REGION=us-east-1 # a US region where Bedrock serves the Claude models you need
export ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"
export VPC_ID=<your-vpc-id>
export PRIVATE_SUBNETS="<subnet-id-a> <subnet-id-b>"
1. security group 생성
세 security group이 트래픽 경로를 잇습니다: 사내 네트워크 -> 로드 밸런서 443, 로드 밸런서 -> gateway 8080, gateway -> Postgres 5432. 그 외에는 도달 불가입니다. ECS Fargate에서는 배포 단계가 $ALB_SG를 로드 밸런서에, $GW_SG를 서비스에 붙입니다. EKS에서는 AWS Load Balancer Controller가 ALB용 frontend security group을 직접 만들므로 $ALB_SG와 $GW_SG는 쓰이지 않고, 배포 단계의 inbound-cidrs 어노테이션이 리스너를 사내 네트워크로 제한하며 DB security group이 클러스터의 security group을 허용합니다.
ALB_SG="$(aws ec2 create-security-group --group-name claude-gateway-alb \
--description "Claude gateway ALB" --vpc-id "$VPC_ID" \
--query GroupId --output text)"
GW_SG="$(aws ec2 create-security-group --group-name claude-gateway-svc \
--description "Claude gateway service" --vpc-id "$VPC_ID" \
--query GroupId --output text)"
DB_SG="$(aws ec2 create-security-group --group-name claude-gateway-db \
--description "Claude gateway Postgres" --vpc-id "$VPC_ID" \
--query GroupId --output text)"
aws ec2 authorize-security-group-ingress --group-id "$ALB_SG" \
--protocol tcp --port 443 --cidr <your-corporate-cidr>
aws ec2 authorize-security-group-ingress --group-id "$GW_SG" \
--protocol tcp --port 8080 --source-group "$ALB_SG"
aws ec2 authorize-security-group-ingress --group-id "$DB_SG" \
--protocol tcp --port 5432 --source-group "$GW_SG"
2. IAM 역할 생성과 use case 양식 제출
gateway는 Bedrock Claude 모델 호출만 허용하는 전용 task role로 실행됩니다. Bedrock upstream reference에 따라 정책은 cross-region inference-profile ARN과 기반 foundation-model ARN을 모두 포함해야 합니다.
cat > bedrock-invoke.json <<EOF
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream", "bedrock:CountTokens"],
"Resource": [
"arn:aws:bedrock:${AWS_REGION}:${ACCOUNT_ID}:inference-profile/us.anthropic.*",
"arn:aws:bedrock:*::foundation-model/anthropic.*"
]
}]
}
EOF
cat > ecs-trust.json <<'EOF'
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "ecs-tasks.amazonaws.com" },
"Action": "sts:AssumeRole"
}]
}
EOF
aws iam create-role --role-name claude-gateway-task \
--assume-role-policy-document file://ecs-trust.json
aws iam put-role-policy --role-name claude-gateway-task \
--policy-name bedrock-invoke --policy-document file://bedrock-invoke.json
ECS에는 실행 역할도 필요합니다. ECS agent가 ECR에서 이미지를 pull하고 나중에 만드는 Secrets Manager 값을 주입할 때 쓰며, gateway의 AWS SDK가 런타임에 쓰는 task role과 별개입니다.
aws iam create-role --role-name claude-gateway-execution \
--assume-role-policy-document file://ecs-trust.json
aws iam attach-role-policy --role-name claude-gateway-execution \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
cat > secrets-read.json <<EOF
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"],
"Resource": [
"arn:aws:secretsmanager:${AWS_REGION}:${ACCOUNT_ID}:secret:gateway-jwt-secret-??????",
"arn:aws:secretsmanager:${AWS_REGION}:${ACCOUNT_ID}:secret:gateway-oidc-client-secret-??????",
"arn:aws:secretsmanager:${AWS_REGION}:${ACCOUNT_ID}:secret:gateway-postgres-url-??????"
]
}]
}
EOF
aws iam put-role-policy --role-name claude-gateway-execution \
--policy-name read-gateway-secrets --policy-document file://secrets-read.json
정책은 공유 계정에서 무관한 secret까지 매치하는 bare gateway-* 와일드카드 대신 secret마다 ARN 하나를 지정합니다. 끝의 -??????는 Secrets Manager가 모든 secret ARN에 붙이는 임의의 6자 접미사에 정확히 매치하며, 끝의 -*는 단순 접두사 glob이라 gateway-postgres-url-prod 같은 더 긴 이름도 매치합니다. IAM 정책이 gateway의 Bedrock 호출을 허용하고 Bedrock은 상용 리전에서 기본으로 모델 접근을 켜지만, 남은 계정 수준 관문은 Anthropic의 일회성 use case 양식입니다. 계정에서 아무도 제출하지 않았다면 Amazon Bedrock 콘솔의 Model catalog에서 Anthropic 모델을 선택해 양식을 완료하세요(제출 즉시 접근 허용, AWS Organizations 양식과 제출자 IAM 권한은 Amazon Bedrock 문서 참고). EKS 트랙은 두 ECS 역할 대신 IRSA 역할에 두 정책 문서를 재사용합니다.
3. Amazon RDS for PostgreSQL 프로비저닝
인스턴스는 private 서브넷에서 Postgres 16, 공개 주소 없음, 스토리지 암호화로 실행합니다. 먼저 DB를 private 서브넷에 두는 subnet group과, 서버가 평문 연결을 거부하도록 rds.force_ssl=1을 켠 parameter group을 만듭니다(parameter group family가 인스턴스의 엔진 메이저 버전과 맞아야 하므로 엔진 버전을 한 번 고정).
aws rds create-db-subnet-group --db-subnet-group-name claude-gateway-db \
--db-subnet-group-description "Claude gateway" --subnet-ids $PRIVATE_SUBNETS
PG_VERSION=16
PG_FAMILY="postgres${PG_VERSION}"
aws rds create-db-parameter-group --db-parameter-group-name claude-gateway-db \
--db-parameter-group-family "$PG_FAMILY" \
--description "Claude gateway - require TLS on every connection"
aws rds modify-db-parameter-group --db-parameter-group-name claude-gateway-db \
--parameters "ParameterName=rds.force_ssl,ParameterValue=1,ApplyMethod=immediate"
생성한 마스터 비밀번호로 인스턴스를 만듭니다.
PGPASS="$(openssl rand -hex 24)"
aws rds create-db-instance --db-instance-identifier claude-gateway-db \
--engine postgres --engine-version "$PG_VERSION" \
--db-instance-class db.t4g.micro \
--allocated-storage 20 --db-name claude_gateway \
--master-username gateway --master-user-password "$PGPASS" \
--db-subnet-group-name claude-gateway-db \
--db-parameter-group-name claude-gateway-db \
--vpc-security-group-ids "$DB_SG" \
--no-publicly-accessible --storage-encrypted
--master-user-password 리터럴 인자는 명령이 실행되는 동안 프로세스 테이블과 감사/EDR 로그에 노출됩니다. 공유·모니터링 호스트에서는 번들의 setup.sh처럼 0600 파일의 --cli-input-json으로 비밀번호를 전달하세요. 인스턴스가 올라올 때까지 기다린 뒤(몇 분 소요) private 엔드포인트를 읽고 gateway가 쓸 연결 문자열을 조립합니다.
aws rds wait db-instance-available --db-instance-identifier claude-gateway-db
DB_HOST="$(aws rds describe-db-instances --db-instance-identifier claude-gateway-db \
--query 'DBInstances[0].Endpoint.Address' --output text)"
GATEWAY_POSTGRES_URL="postgres://gateway:${PGPASS}@${DB_HOST}:5432/claude_gateway?sslmode=verify-full"
sslmode=verify-full은 암호화만이 아니라 RDS 서버 인증서의 체인과 호스트명까지 gateway가 검증하게 합니다. trust anchor는 AWS RDS certificate bundle이며, 아래 이미지 빌드 단계가 /etc/claude/rds-global-bundle.pem으로 복사하고 NODE_EXTRA_CA_CERTS로 신뢰합니다. URL에 libpq 스타일 sslrootcert= 파라미터를 붙이지 마세요. gateway 드라이버는 쿼리 문자열에서 sslmode만 읽고 sslrootcert를 Postgres에 시작 파라미터로 전달해 서버가 거부합니다. ECS 서비스나 EKS 파드는 이 VPC에서 실행돼야 인스턴스의 private 엔드포인트에 닿고, claude-gateway-db security group은 gateway의 security group만 허용합니다.
4. gateway.yaml 작성
upstreams 블록이 auth: {}로 Bedrock을 가리키므로 gateway는 ECS의 task role 또는 EKS의 IRSA 역할에서 AWS 기본 자격 증명 체인으로 인증합니다. 모든 필드는 설정 레퍼런스를 보세요. 두 listen 필드가 gateway 앞단을 설명합니다.
public_url: 외부https://origin, loopback이 아닌 바인드에 필수. gateway는 IdPredirect_uri와 discovery 문서를 이 값으로만 만들며X-Forwarded-*헤더로는 만들지 않음trusted_proxies: 앞단의 소스 범위. TCP peer가 이 목록에 있을 때만X-Forwarded-For를 존중하고 신뢰하는 hop을 지나 체인을 걸어, IP별 로그인 rate limit과 감사 이벤트가 로드 밸런서가 아니라 개발자 IP를 기록
두 트랙 모두 앞단은 internal ALB이고 ALB 노드는 연결된 서브넷에서 주소를 받으므로 trusted_proxies를 그 서브넷의 CIDR로 설정합니다. 그 서브넷의 모든 호스트를 프록시로 신뢰하게 되므로 ALB의 ingress 소스인 사내 CIDR이 겹치지 않게 하고, X-Forwarded-For로 클라이언트 IP를 위조할 수 있는 신뢰할 수 없는 워크로드와 서브넷을 공유하지 마세요. ALB의 client port preservation 속성 routing.http.xff_client_port.enabled는 어느 쪽이어도 됩니다(켜면 203.0.113.7:54321, [2001:db8::1]:54321 형태로 쓰고 gateway는 포트를 떼고 읽음).
listen:
host: 0.0.0.0
port: 8080
public_url: https://claude-gateway.internal.example.com
trusted_proxies: [<your-alb-subnet-cidrs>]
oidc:
issuer: https://example.okta.com
client_id: 0oa1example2
client_secret: ${OIDC_CLIENT_SECRET} # EKS: ${file:/secrets/oidc-client-secret}
allowed_email_domains: [example.com]
# The Okta org authorization server returns a thin id_token that omits
# email and groups; the gateway fills them from /userinfo.
userinfo_fallback: true
# Okta emits groups only when the `groups` scope is requested and the
# app's groups claim filter allows them.
scopes: [openid, profile, email, offline_access, groups]
session:
jwt_secret: ${GATEWAY_JWT_SECRET} # EKS: ${file:/secrets/jwt-secret}
ttl_hours: 8 # bounds deprovision latency; lower
# toward 1 for tighter revocation
store:
postgres_url: ${GATEWAY_POSTGRES_URL} # EKS: ${file:/secrets/postgres-url}
# readiness_grace_seconds: 300 # keep passing the health check
# through an RDS failover
upstreams:
- provider: bedrock
region: <your-region> # match $AWS_REGION so the IAM
# policy's ARNs cover it
auth: {} # AWS default credential chain:
# ECS task role, or IRSA on EKS
Okta에만 해당하는 것은 oidc 블록뿐입니다. Microsoft Entra ID를 쓰려면 issuer를 https://login.microsoftonline.com/<tenant-id>/v2.0으로 설정하고 userinfo_fallback과 groups scope를 제거하세요. Entra는 그룹 이름이 아니라 그룹 Object ID를 내보내므로 managed.policies는 GUID에 매치하거나 oidc.groups_claim: roles로 App Roles에 매치해야 합니다.
5. Secrets Manager에 secret 저장
secret 3개를 만듭니다(IAM 단계의 실행 역할이 이미 읽을 수 있음).
aws secretsmanager create-secret --name gateway-jwt-secret \
--secret-string "$(openssl rand -base64 32)"
aws secretsmanager create-secret --name gateway-oidc-client-secret \
--secret-string '<your-okta-client-secret>'
aws secretsmanager create-secret --name gateway-postgres-url \
--secret-string "$GATEWAY_POSTGRES_URL"
각 호출이 출력하는 ARN을 메모하세요(ECS task definition이 secret을 ARN으로 참조). 리터럴 --secret-string은 명령이 실행되는 동안 프로세스 테이블과 감사/EDR 로그에 보이므로 공유·모니터링 호스트에서는 값을 0600 파일에 두고 --secret-string file://<path>로 전달하세요(번들의 setup.sh는 0600 임시 파일을 --cli-input-json에 넘겨 secret 값을 프로세스 argv에서 뺌). gateway.yaml 자체에는 secret 값이 없습니다. 모든 자격 증명이 부팅 시 ${VAR} 또는 ${file:...} 확장으로 해석되기 때문입니다.
- ECS: 다음 단계의 빌드가
gateway.yaml을 이미지의/etc/claude/gateway.yaml로 복사하고, task definition이secrets필드로 세 secret을 환경 변수로 주입하므로 YAML이${GATEWAY_JWT_SECRET},${OIDC_CLIENT_SECRET},${GATEWAY_POSTGRES_URL}을 참조 - EKS:
gateway.yaml을 ConfigMap에서, secret을/secrets아래 파일로 마운트하고${file:/secrets/...}로 참조. Kubernetes Secret은 External Secrets Operator나 Secrets Store CSI driver의 AWS provider로 Secrets Manager에서 가져오거나kubectl로 직접 생성
6. 이미지 빌드와 Amazon ECR push
컨테이너 이미지 요건에 따라 linux-x64 glibc 바이너리를 빌드 컨텍스트의 ./claude에 둡니다. 요건대로 직접 Dockerfile을 쓰거나 번들의 Dockerfile에서 시작하세요. 번들 Dockerfile은 앞 단계에서 채운 gateway.yaml을 이미지의 /etc/claude/gateway.yaml로 복사합니다. ECS에서는 그 내장 복사본이 구성이 컨테이너에 닿는 방식이므로 파일을 쓴 뒤 빌드합니다(EKS는 ConfigMap 마운트라 내장 복사본이 쓰이지 않음). 이미지는 연결 문자열의 sslmode=verify-full trust anchor로 AWS RDS 인증서 번들도 담으므로 빌드 컨텍스트에 먼저 내려받습니다. AWS가 번들을 회전하므로(새 리전 CA가 추가됨) 체크섬을 고정하거나 커밋하지 말고 빌드마다 다운로드하세요.
curl -fL --proto '=https' -o rds-global-bundle.pem \
https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem
컨테이너 이미지 요건에는 번들이 없으므로 직접 Dockerfile을 쓴다면 복사와 신뢰를 위한 두 줄을 추가하세요(번들 Dockerfile에는 이미 둘 다 있음).
COPY rds-global-bundle.pem /etc/claude/rds-global-bundle.pem
ENV NODE_EXTRA_CA_CERTS=/etc/claude/rds-global-bundle.pem
ECR 저장소를 만들고 Docker를 로그인합니다. immutable 태그라 배포 단계가 고정하는 <version> 태그를 나중에 다른 이미지로 조용히 재지정할 수 없습니다.
aws ecr create-repository --repository-name claude-gateway \
--image-tag-mutability IMMUTABLE \
--image-scanning-configuration scanOnPush=true
aws ecr get-login-password --region "$AWS_REGION" \
| docker login --username AWS --password-stdin \
"${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
이미지를 빌드하고 push합니다. 아래 task definition은 linux/amd64로 실행하므로 플랫폼이 맞아야 합니다. ARM64(Graviton) Fargate는 linux-arm64 바이너리로 linux/arm64를 빌드하고 cpuArchitecture를 ARM64로 설정하세요.
docker build --platform=linux/amd64 \
-t "${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/claude-gateway:<version>" .
docker push "${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/claude-gateway:<version>"
7. 배포
ECS Fargate: 클러스터와 gateway stderr(감사 이벤트와 운영 로그 모두 포함)용 로그 그룹을 만듭니다. 보존 기간은 별도 호출이고 없으면 CloudWatch가 로그를 영구 보관하므로 감사 보존 정책에 맞춰 90일로 둡니다.
aws ecs create-cluster --cluster-name claude-gateway
aws logs create-log-group --log-group-name /ecs/claude-gateway
aws logs put-retention-policy --log-group-name /ecs/claude-gateway \
--retention-in-days 90
task definition을 씁니다. task role이 Bedrock 권한을, 실행 역할이 secret 주입을 담당하며 Secrets Manager 단계의 secret ARN을 사용합니다(claude-gateway-task.json).
{
"family": "claude-gateway",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "1024",
"memory": "2048",
"runtimePlatform": { "cpuArchitecture": "X86_64", "operatingSystemFamily": "LINUX" },
"executionRoleArn": "arn:aws:iam::<account-id>:role/claude-gateway-execution",
"taskRoleArn": "arn:aws:iam::<account-id>:role/claude-gateway-task",
"containerDefinitions": [
{
"name": "gateway",
"image": "<account-id>.dkr.ecr.<region>.amazonaws.com/claude-gateway:<version>",
"portMappings": [{ "containerPort": 8080 }],
"secrets": [
{ "name": "GATEWAY_JWT_SECRET", "valueFrom": "<gateway-jwt-secret ARN>" },
{ "name": "OIDC_CLIENT_SECRET", "valueFrom": "<gateway-oidc-client-secret ARN>" },
{ "name": "GATEWAY_POSTGRES_URL", "valueFrom": "<gateway-postgres-url ARN>" }
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/claude-gateway",
"awslogs-region": "<region>",
"awslogs-stream-prefix": "gateway"
}
}
}
]
}
aws ecs register-task-definition --cli-input-json file://claude-gateway-task.json
gateway를 health check하는 target group이 있는 internal ALB를 앞에 둡니다. --ip-address-type ipv4가 중요합니다: internal dual-stack ALB는 public 범위 AAAA 레코드를 게시해 /login private-network 검사가 거부합니다.
ALB_ARN="$(aws elbv2 create-load-balancer --name claude-gateway \
--scheme internal --type application --ip-address-type ipv4 \
--subnets $PRIVATE_SUBNETS --security-groups "$ALB_SG" \
--query 'LoadBalancers[0].LoadBalancerArn' --output text)"
TG_ARN="$(aws elbv2 create-target-group --name claude-gateway \
--protocol HTTP --port 8080 --vpc-id "$VPC_ID" --target-type ip \
--health-check-path /readyz \
--query 'TargetGroups[0].TargetGroupArn' --output text)"
HTTPS 리스너를 추가합니다. --ssl-policy는 최신 TLS 하한을 고정합니다(생략하면 TLS 1.0/1.1을 여전히 받는 레거시 기본 ELBSecurityPolicy-2016-08로 되돌아감). ALB는 기본으로 60초 동안 데이터가 없으면 연결을 닫습니다. gateway의 keepalive ping이 스트림을 그 기본값 안에 두므로 timeout을 올리면 ping 주기 위로 여유가 생깁니다(메커니즘과 구버전 gateway는 트러블슈팅의 끊긴 스트림 행 참고). 아래 명령이 리스너를 추가하고 timeout을 올립니다.
aws elbv2 create-listener --load-balancer-arn "$ALB_ARN" \
--protocol HTTPS --port 443 \
--ssl-policy ELBSecurityPolicy-TLS13-1-2-2021-06 \
--certificates CertificateArn=<your-acm-certificate-arn> \
--default-actions Type=forward,TargetGroupArn="$TG_ARN"
aws elbv2 modify-load-balancer-attributes --load-balancer-arn "$ALB_ARN" \
--attributes Key=idle_timeout.timeout_seconds,Value=3600
서비스를 만듭니다. deployment circuit breaker는 잘못된 이미지나 부팅 불가능한 구성으로 task가 계속 실패하는 배포를 실패 task를 무한 재시작하는 대신 마지막 정상 상태로 되돌립니다.
aws ecs create-service --cluster claude-gateway --service-name claude-gateway \
--task-definition claude-gateway --desired-count 1 --launch-type FARGATE \
--deployment-configuration "deploymentCircuitBreaker={enable=true,rollback=true}" \
--health-check-grace-period-seconds 60 \
--network-configuration "awsvpcConfiguration={subnets=[$(echo $PRIVATE_SUBNETS | tr ' ' ',')],securityGroups=[$GW_SG],assignPublicIp=DISABLED}" \
--load-balancers "targetGroupArn=$TG_ARN,containerName=gateway,containerPort=8080"
60초 grace period는 콜드 task가 이미지를 pull하고 store에 연결하고 첫 health check에 답할 시간을 줍니다. target group의 GET /readyz health check는 store 도달 가능성을 확인하므로 Postgres에 닿지 못하는 task는 로테이션에 들어가지 않습니다. RDS failover 같은 짧은 DB 중단에도 check를 통과시키려면 Outage behavior에 따라 store.readiness_grace_seconds를 설정합니다(/healthz 대안도 거기 있음). task는 public IP 없는 private 서브넷에서 실행되므로 모든 egress(Bedrock, IdP, Secrets Manager, ECR, CloudWatch Logs)가 NAT gateway를 거칩니다. Bedrock 트래픽을 public 경로에서 빼려면 bedrock-runtime interface VPC endpoint를 만들고 upstream base_url을 그리로 지정합니다(IdP는 여전히 인터넷 egress 필요).
마지막으로 개발자에게 private하게 해석되는 호스트명을 줍니다. Route 53 private hosted zone에서 gateway의 내부 DNS 이름을 ALB에 alias하고 listen.public_url을 그 호스트명으로 설정합니다(internal ALB 자체의 *.elb.amazonaws.com 이름은 private 주소로 해석되지만 ACM 인증서를 실을 수 없으므로 자기 이름을 사용). 첫 로그인 전에 OAuth 클라이언트의 승인된 redirect URI를 <public_url>/oauth/callback으로 업데이트하세요. public_url을 바꾼 뒤에는 새 태그로 이미지를 재빌드·push하고, 새 task definition revision을 등록해 재배포합니다. ECS에서는 이 설정이 이미지의 내장 gateway.yaml에 있고 gateway는 public origin을 이 설정으로만 만들며 X-Forwarded-Host와 X-Forwarded-Proto는 무시합니다. X-Forwarded-For는 listen.trusted_proxies가 설정됐을 때만 클라이언트 IP에 사용됩니다.
EKS: kubectl과 eksctl이 로컬에 설치돼 있어야 하고, IAM OIDC provider와 AWS Load Balancer Controller가 설치된 기존 EKS 클러스터가 필요합니다. 파드가 RDS private 엔드포인트에 닿도록 클러스터가 $VPC_ID에 있어야 하고 claude-gateway-db security group이 $GW_SG 대신 클러스터의 파드/노드 security group을 허용해야 합니다. EKS에서 gateway는 ECS 역할이 아닌 IRSA로 Bedrock 자격 증명을 얻습니다. IAM 단계의 ecs-tasks.amazonaws.com trust 정책은 여기 적용되지 않고, IRSA에는 클러스터 OIDC provider에 federate하며 system:serviceaccount:claude-gateway:gateway로 범위가 제한된 trust 정책을 가진 역할이 필요합니다. eksctl create iamserviceaccount가 그 역할을 만들고 정책을 붙이고 Kubernetes service account에 역할 ARN을 어노테이션하는 일을 한 번에 합니다. IAM 단계의 두 정책 문서를 attach 가능한 managed policy로 만듭니다.
BEDROCK_POLICY_ARN="$(aws iam create-policy --policy-name claude-gateway-bedrock-invoke \
--policy-document file://bedrock-invoke.json --query Policy.Arn --output text)"
SECRETS_POLICY_ARN="$(aws iam create-policy --policy-name claude-gateway-secrets-read \
--policy-document file://secrets-read.json --query Policy.Arn --output text)"
kubectl create namespace claude-gateway
eksctl create iamserviceaccount --cluster <your-cluster> --region "$AWS_REGION" \
--namespace claude-gateway --name gateway --role-name claude-gateway \
--attach-policy-arn "$BEDROCK_POLICY_ARN" \
--attach-policy-arn "$SECRETS_POLICY_ARN" \
--approve
secrets 정책은 파드가 Secrets Manager를 직접 읽을 때만 필요합니다(Secrets Store CSI driver의 AWS provider가 마운트하는 파드의 service account로 읽는 경우). Kubernetes Secret을 다른 방식으로 만든다면 제외합니다. provider는 회전된 secret을 reconcile할 때 DescribeSecret을 호출하므로 GetSecretValue만 허용하면 첫 배포에서는 마운트되지만 회전을 반영하지 못하게 되어, 정책의 두 action이 모두 필요합니다. gateway는 Kubernetes deployment 설명대로 표준 Deployment + Service + Ingress로 배포하되 다음을 포함합니다.
serviceAccountName: gatewaygateway.yaml을 ConfigMap에서 마운트하고 secret을/secrets에 마운트- readiness probe를
GET /readyz로 지정
앞단은 AWS Load Balancer Controller가 관리하는 Ingress가 internal ALB를 프로비저닝합니다. 어노테이션:
alb.ingress.kubernetes.io/scheme: internal,alb.ingress.kubernetes.io/target-type: ipalb.ingress.kubernetes.io/ip-address-type: ipv4(/loginprivate-network 검사가 거부할 public 범위 AAAA 레코드가 게시되지 않도록)alb.ingress.kubernetes.io/inbound-cidrs: <your-corporate-cidr>(controller가 관리하는 frontend security group이 기본0.0.0.0/0대신 사내 네트워크만 허용)alb.ingress.kubernetes.io/certificate-arn(ACM 인증서)alb.ingress.kubernetes.io/ssl-policy: ELBSecurityPolicy-TLS13-1-2-2021-06(TLS 1.0/1.1을 받는 레거시 기본 정책으로 돌아가지 않도록)alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=3600(gateway의 스트리밍 keepalive 위 여유, 트러블슈팅 참고)
IRSA에서는 AWS SDK가 projected service-account 토큰을 읽어 AWS STS와 교환하므로 파드가 EC2 인스턴스 메타데이터 서비스를 필요로 하지 않고, egress NetworkPolicy가 gateway 파드에서 169.254.169.254를 막아도 됩니다. 트러블슈팅의 노드 hop-limit 문제는 IRSA를 쓰지 않고 노드 인스턴스 역할에 의존하는 클러스터에만 적용됩니다.
8. 개발자 머신에 gateway URL 배포
gateway는 실행 중이지만 gateway URL이 개발자 머신에 있기 전에는 /login에서 닿을 수 없습니다. forceLoginMethod, forceLoginGatewayUrl, parentSettingsBehavior: "merge" opt-in을 포함한 전체 managed settings 스니펫을 MDM으로 각 기기에 배포하세요. 개발자가 수동으로 고를 수 있는 gateway 옵션은 로그인 선택기에 없습니다(Managed settings 배포 참고).
Terraform 참조
동반 번들 examples/gateway/aws가 이 페이지를 코드로 패키징합니다.
setup.sh: ECS Fargate 트랙에서 위 프로비저닝 walkthrough를 같은aws명령으로 스크립트화. 멱등이라 기존 리소스를 감지해 건너뛰므로 재실행해도 안전하고 기본값은 환경 변수로 override. Okta OIDC client secret과 ACM 인증서는 직접 만들어야 합니다. 없이 실행하면 ECS/ALB 배포를 건너뛰고 빠진 입력을 알려create-secret명령을 출력하므로 둘을 만든 뒤 재실행. Bedrock use case 양식과 Route 53 alias는 자동 실행되지 않고 다음 단계로 출력되며 클라이언트 MDM push는 이 페이지에서 수동 단계로 남음gateway.yaml.example: gateway.yaml 단계의 구성 템플릿(선택 키는 주석 처리).gateway.yaml로 복사하고 빌드 전에 모든REPLACE_ME를 교체Dockerfile: 미리 빌드된linux-x64바이너리로 런타임 이미지를 빌드하고 채운gateway.yaml을/etc/claude/gateway.yaml로, store의sslmode=verify-full을 받쳐 주는 AWS RDS 인증서 번들을 복사.setup.sh는 번들이 빌드 컨텍스트에 없을 때만 내려받으므로 AWS CA 회전을 반영하려면 파일을 삭제하고 새 태그로 재빌드. 구성 파일에는 secret 값이 없으므로(자격 증명은 부팅 시${VAR}확장으로 해석) 구성 수정은 새 태그 재빌드를 뜻하며setup.sh가 파일 해시로 이미지에 태그를 달아 자동화terraform/: 같은 ECS Fargate 범위를 선언적으로 프로비저닝 — security group, IAM 역할, ECR 저장소, RDS 인스턴스, Secrets Manager secret, internal ALB 뒤의 ECS 서비스. VPC와 private 서브넷은 변수로 전달하는 사전 요건. Terraform이 ECR 저장소를 만들지만 이미지를 빌드하지 않고 서비스 정의가 이미지를 참조하므로 apply는 두 번입니다. 저장소에 대한 targeted apply, 이미지 빌드와 push, 전체 apply. 변수, 원격 상태, 정리는 번들의terraform/README.md
이 페이지처럼 번들도 지원되는 프로덕션 배포가 아니라 고객 관리 인프라용 작동 예제이므로 의존하기 전에 검토하고 환경에 맞게 조정하세요.
트러블슈팅
gateway 부팅·로그인 오류는 플랫폼 공통 troubleshooting 표를 보세요. 아래는 AWS 고유 항목입니다.
| 증상 | 원인 | 해결 |
|---|---|---|
CLI /login: Gateway hosts must be on your organization's private network; <host> resolves to the public (or unrecognized) address <ip> |
gateway 이름이 public 주소로 하나 이상 해석됨. dual-stack internal ALB가 public 범위 AAAA 레코드를 게시하고 private-network 검사는 해석된 모든 주소가 private이어야 함 | --ip-address-type ipv4로 ALB를 만들거나 public AAAA 레코드 없는 별도 내부 전용 DNS 이름 사용 |
모든 Bedrock 요청이 502, 로그에 Could not load credentials from any providers |
task가 task role 없는 ECS EC2 launch type에서 실행되거나, 파드가 IRSA 없는 EKS 노드에서 실행되어 자격 증명이 인스턴스 메타데이터에서 오는데 IMDSv2 기본 hop limit 1이 컨테이너 안에서 막음. 이 페이지의 두 트랙은 해당 없음(Fargate task role과 IRSA는 인스턴스 메타데이터를 쓰지 않음) | task role과 IRSA를 선호. 인스턴스 자격 증명이 불가피하면 aws ec2 modify-instance-metadata-options --instance-id <id> --http-put-response-hop-limit 2로 hop limit 상향(트레이드오프는 공통 표 참고) |
Bedrock 요청이 403 AccessDeniedException |
계정이 Anthropic의 일회성 use case 양식을 제출하지 않았거나, 계정 첫 호출에서 시작되는 자동 AWS Marketplace 구독이 아직 끝나지 않았거나, task role 정책에 inference-profile 또는 foundation-model ARN이 빠짐 | Bedrock 콘솔 Model catalog에서 use case 양식 제출. 방금 제출했거나 계정의 첫 호출이면 몇 분 후 재시도. 두 ARN 계열에 bedrock:InvokeModel과 bedrock:InvokeModelWithResponseStream 허용 |
Bedrock이 on-demand throughput 미지원이라는 ValidationException |
커스텀 models: 항목이 해당 리전에서 inference profile로만 제공되는 bare foundation-model ID로 매핑됨 |
cross-region inference profile ID(us.anthropic.*)로 매핑(내장 카탈로그는 이미 그렇게 함) |
gateway가 아무것도 로그하기 전에 ECS task가 ResourceInitializationError로 중지 |
실행 역할이 Secrets Manager secret을 읽지 못하거나 private 서브넷에 Secrets Manager/ECR로 가는 경로가 없음 | 세 gateway- secret의 ARN에 대해 실행 역할에 secretsmanager:GetSecretValue 부여, NAT gateway로 egress 제공(없으면 Secrets Manager, ECR, awslogs 드라이버가 같은 단계에서 필요로 하는 CloudWatch Logs의 interface endpoint와 S3 gateway endpoint) |
| gateway 부팅이 Postgres 연결 타임아웃 오류로 종료 | DB security group이 gateway의 security group에 5432를 허용하지 않거나 서비스가 DB의 VPC 밖에서 실행 | DB의 security group에서 gateway의 security group으로 5432 허용, DB subnet group과 같은 VPC에서 서비스 실행 |
| gateway 부팅이 Postgres TLS 인증서 검증 오류로 종료 | 연결 문자열이 sslmode=verify-full인데 이미지가 RDS CA 번들을 신뢰하지 않음(번들 미복사 또는 NODE_EXTRA_CA_CERTS가 가리키지 않음) |
빌드 단계의 두 Dockerfile 줄(번들 복사와 NODE_EXTRA_CA_CERTS 설정)을 추가하고 재빌드, 새 태그로 push, 재배포 |
| 조용한 구간 후 스트리밍 응답이 중간에 끊김 | v2.1.229 이전 gateway가 Bedrock 또는 Claude Platform on AWS upstream에서 upstream이 조용한 동안(예: 스트림 출력 없는 extended thinking) 아무것도 보내지 않음. ALB는 기본으로 60초 데이터가 없으면 연결을 닫아 그 공백에서 스트림을 자름. v2.1.229+ gateway는 조용한 스트림을 그 timeout 안에 유지: 해당 upstream에서는 스트림 데이터 없이 약 15초가 지나면 SSE ping 이벤트를 내보내고 Anthropic API upstream에서는 API 자체 ping을 중계 |
gateway를 v2.1.229+로 업데이트하거나 modify-load-balancer-attributes 또는 EKS의 load-balancer-attributes Ingress 어노테이션으로 idle_timeout.timeout_seconds를 3600으로 설정 |
텔레메트리
gateway는 머신별 OTEL 구성 없이 개발자별 사용량 메트릭을 제공합니다. Claude Code는 OpenTelemetry(OTLP) 메트릭, 로그, 선택적 trace를 내보내며 Monitoring usage가 CLI가 보고하는 모든 것을 다룹니다. /login으로 로그인한 세션에서 CLI는 각 export에 인증된 IdP 아이덴티티 속성 user.id, user.email, user.groups를 찍어 사용량이 개발자별로 집계됩니다.
gateway 자체는 인증된 OTLP relay입니다. telemetry.forward_to를 listen.public_url과 함께 설정하면 연결된 모든 클라이언트에 OTEL exporter 설정을 push하고 그들의 OTLP 트래픽을 나열한 각 목적지로 그대로 전달합니다. 각 목적지는 metrics, logs, traces를 독립적으로 opt-in하며 기본은 metrics만입니다(신호별 필드와 민감도 트레이드오프는 telemetry reference). gateway는 텔레메트리를 버퍼링·집계·저장하지 않으므로 데이터가 도착하는 곳은 전적으로 collector의 exporter 구성입니다. 클라이언트 텔레메트리는 기본 off이고 telemetry.forward_to 구성이 연결된 개발자에게 켭니다. 각 대화형 클라이언트는 push된 설정에 대한 보안 승인 대화상자를 표시합니다. AWS에서 각 신호의 목적지:
- 클라이언트 metrics, logs, traces:
telemetry.forward_to를 AWS Distro for OpenTelemetry (ADOT) collector 같은 OpenTelemetry collector에 지정하고 거기서 Amazon CloudWatch, Amazon Managed Service for Prometheus, 임의 OTLP 백엔드로 export합니다. collector는https://로 닿는 자체 내부 서비스로 실행하세요(loopback 예외와CLAUDE_GATEWAY_ALLOW_LOOPBACK는telemetryreference). - gateway 로그: ECS Fargate에서는 추가 설정이 없습니다.
awslogs드라이버가 감사 이벤트와 운영 로그를 담은 gateway의 stderr를 위에서 만든/ecs/claude-gateway로그 그룹으로 전달합니다. EKS는 파드 로그가 기본으로 CloudWatch에 도달하지 않아 로그 수집(컨테이너 로그 캡처를 켠 Amazon CloudWatch Observability add-on 또는 Fluent Bit DaemonSet)을 설치하기 전까지 감사 추적이 사라집니다. 어느 트랙이든 CloudWatch Logs Insights로 조회하고 metric filter로 알람을 구동합니다. - 컨테이너 메트릭: 태스크별 CPU, 메모리, 네트워크는
aws ecs update-cluster-settings --cluster claude-gateway --settings name=containerInsights,value=enabled로 클러스터에 Container Insights를 켭니다. EKS는 Amazon CloudWatch Observability add-on을 설치합니다. - 지출: 텔레메트리는 사후 사용량을 보여주고 spend limits는 gateway의 개발자별 실시간 보기와 강제입니다.
비용 귀속 (Cost attribution)
gateway는 모든 Bedrock 요청을 자기 principal(ECS task role 또는 EKS IRSA 역할)로 서명하므로 기본적으로 AWS는 그 지출 전체를 IAM principal 하나로 봅니다. AWS 자체 청구 데이터에서 나누는 두 방법이 있고 결합할 수 있습니다.
개발자별: assume_role: Bedrock 권한을 갖고 gateway의 principal을 신뢰하는 두 번째 IAM 역할을 만들고, 그 principal에 해당 역할에 대한 sts:AssumeRole을 허용하고, Bedrock upstream에 assume_role을 session_name: email로 설정합니다. gateway는 개발자당 시간당 한 번 그 역할을 assume하며 세션 이름을 그들의 이메일로 두고 결과로 요청에 서명합니다. Claude Code v2.1.281+ 에서 실행되는 gateway가 필요합니다. 역할은 다른 AWS 계정에 있어도 됩니다(Bedrock in another AWS account). Terraform 번들의 task role 옆에 두는 예:
resource "aws_iam_role" "bedrock_user" {
name = "claude-gateway-bedrock-user"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{ Effect = "Allow", Action = "sts:AssumeRole", Principal = { AWS = aws_iam_role.task.arn } }]
})
}
resource "aws_iam_role_policy" "bedrock_user_invoke" { # same Bedrock policy as the task role's
role = aws_iam_role.bedrock_user.id
policy = aws_iam_role_policy.bedrock_invoke.policy
}
resource "aws_iam_role_policy" "task_assume_bedrock_user" {
role = aws_iam_role.task.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{ Effect = "Allow", Action = "sts:AssumeRole", Resource = aws_iam_role.bedrock_user.arn }]
})
}
assume_role을 설정하면 gateway는 spend 계량용 무료 CountTokens 호출을 포함한 모든 Bedrock 호출을 assume한 역할의 자격 증명으로 서명하므로, gateway의 principal은 assume_role이 없는 upstream에 대해서만 자체 Bedrock 정책이 필요합니다. gateway가 요청 시점에 STS를 호출하므로 private 서브넷이 sts.<region>.amazonaws.com에 닿는 경로가 필요합니다(사전 요건의 NAT gateway가 제공하며, 그 호스트명에 응답하는 STS interface VPC endpoint도 가능). 활성 개발자 한 명당 gateway 레플리카당 시간당 STS 호출이 1회 발생합니다. 각 개발자의 요청은 AWS에 arn:aws:sts::<account>:assumed-role/<role>/<email> principal로 도달합니다. principal별 지출은 IAM principal 데이터를 포함하는 billing export로 봅니다(활성화와 청구 도구는 AWS IAM principal cost allocation 문서).
팀별: application inference profile: models와 managed 섹션만 사용합니다. 팀과 모델마다 Bedrock application inference profile을 하나씩 만들고 각 profile에 팀 태그를 달아 cost allocation tag로 활성화합니다. 그런 다음 gateway.yaml에서 팀마다 자체 모델 id를 주고 각 IdP 그룹을 그 팀의 id에 고정합니다.
models:
# Include the served model's id, such as claude-opus-4-8, in each id so the gateway's spend meter can price it.
- id: platform-claude-opus-4-8
upstream_model:
bedrock: arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abc123
- id: data-claude-opus-4-8
upstream_model:
bedrock: arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/def456
managed:
policies:
- match: {groups: [team-platform]}
cli: {availableModels: [platform-claude-opus-4-8], enforceAvailableModels: true}
- match: {groups: [team-data]}
cli: {availableModels: [data-claude-opus-4-8], enforceAvailableModels: true}
- match: {}
cli: {availableModels: [claude-opus-4-8, claude-sonnet-4-6], enforceAvailableModels: true}
고정된 팀의 개발자는 팀 id로 --model platform-claude-opus-4-8처럼 Claude Code를 시작해야 합니다. 이 옵션 없이 시작한 세션은 gateway가 그들에게 거부하는 기본 모델로 실행되기 때문입니다. gateway는 availableModels를 모델 선택기뿐 아니라 모든 요청에서 강제하고 AWS 청구는 활성화한 태그로 지출을 그룹화합니다. match: {} catch-all이 없으면 어떤 정책에도 매치되지 않는 개발자가 카탈로그의 모든 모델을 받아 어느 팀의 profile에든 청구할 수 있습니다. 비용: 구성이 팀 수 x 모델 수로 커지고, 이 upstream의 Bedrock 요청에 서명하는 역할(gateway의 principal, assume_role이면 assume하는 역할)이 application-inference-profile/* ARN도 호출할 수 있어야 합니다. gateway 자체 지출 계량이 이 id를 가격 책정하는 방식은 spend limits 문서의 How requests are priced를 보세요.
개발자가 청구할 팀을 고르게 하기: 두 팀에서 일하는 개발자가 세션마다 청구할 팀을 고르게 하려면 그 팀 쌍에 자체 IdP 그룹과 정책을 줍니다. (1) IdP에서 개발자를 team-platform-and-data 같은 새 그룹에 추가하고, 이미 로그인한 개발자는 /login을 다시 실행하거나 다음 silent refresh를 기다려 gateway가 새 그룹을 보게 합니다. (2) 그 그룹의 정책을 단일 팀 정책 위에 두고 두 팀의 id를 모두 허용하며, 두 팀 정책이 설정하는 다른 설정은 복사합니다.
managed:
policies:
- match: {groups: [team-platform-and-data]}
cli: {availableModels: [platform-claude-opus-4-8, data-claude-opus-4-8], enforceAvailableModels: true}
- match: {groups: [team-platform]}
cli: {availableModels: [platform-claude-opus-4-8], enforceAvailableModels: true}
- match: {groups: [team-data]}
cli: {availableModels: [data-claude-opus-4-8], enforceAvailableModels: true}
- match: {}
cli: {availableModels: [claude-opus-4-8, claude-sonnet-4-6], enforceAvailableModels: true}
(3) 개발자가 작업 대상 팀의 id를 claude --model data-claude-opus-4-8처럼 전달하게 합니다. gateway는 처음 매치되는 정책을 적용하므로 두 팀 그룹에만 속한 개발자는 team-platform 정책을 받고, data 팀 id로 하는 세션은 입력한 id가 아니라 Claude Code의 기본 모델을 지명하는 is not in your role's availableModels allowlist로 실패합니다. 두 팀 그룹을 한 정책의 groups에 함께 나열하면 어느 팀의 누구든 두 팀의 profile에 청구할 수 있습니다. gateway 자체 spend limits에서 개발자는 세션이 어떤 id를 쓰든 기간당 총계와 한도가 하나이며 기본은 그룹들의 한도 중 가장 낮은 것입니다. 한 팀의 낮은 한도가 다른 팀 작업을 막지 않게 하려면 그 개발자에게 사용자별 한도를 설정하세요.