跳到正文
FunCoding

搜索

搜索文档、智能体、博客、Skill 和 MCP

Claude apps gateway:在 AWS 上部署

在 AWS 上部署 Claude apps gateway 的完整示例:ECS Fargate 或 EKS、RDS for PostgreSQL、Secrets Manager、Bedrock 的 IAM 角色认证,含安全组、IAM、数据库、gateway.yaml、镜像、负载均衡、Terraform 参考、排障、遥测与成本归属。

本页逐步演示在 AWS 上运行 Claude apps gateway 的一种方式。这套配置是客户自管基础设施的工作示例,不是受支持的生产部署:先用它了解各部分如何组合,再按你自己的环境调整(与平台无关的要求见部署指南)。

示例以 Amazon Bedrock 作为模型上游,计算用 Amazon ECS on AWS Fargate 或 Amazon EKS,Okta 是示例身份提供商(IdP),但任何兼容 OpenID Connect(OIDC)的 IdP 都可以。Bedrock 不是 AWS 上唯一的 Claude 上游:网关也支持 Claude Platform on AWS(Anthropic 运营的、带 AWS 认证和 AWS Marketplace 计费的 Claude API),可以代替或与 Bedrock 并用;它的上游条目、凭据和 IAM 权限与本页限定到 Bedrock 的不同,其余部分原样适用。

架构

网关作为你网络上的私有 HTTPS 端点运行,开发者通过你的 IdP 登录。他们的 Claude Code 会话经网关的 IAM 角色访问 Amazon Bedrock 上的 Claude 模型,所以没有模型凭据落在开发者机器上。参考配置创建:

  • 运行网关容器的 Amazon ECS on AWS Fargate 服务或 Amazon EKS Deployment
  • 存放网关镜像的 Amazon ECR 仓库
  • 网关存储用的 Amazon RDS for PostgreSQL 实例,在私有子网里,不可公开访问
  • 存放 JWT 签名密钥、OIDC 客户端密钥和 Postgres URL 的 AWS Secrets Manager 密钥
  • 带 bedrock:InvokeModel、bedrock:InvokeModelWithResponseStream 和 bedrock:CountTokens 的 IAM 角色,作为 ECS 任务角色挂载,或在 EKS 上通过 IAM Roles for Service Accounts(IRSA)绑定
  • 提供 HTTPS 的内部 Application Load Balancer

前提

演练会创建网关自己的资源,但它建立在你已有的网络和身份基础设施之上。开始之前你需要:

  • 有权创建上述资源的 AWS 账号
  • 已安装并认证的 AWS CLI v2,以及本地安装的 Docker
  • 至少有两个位于不同可用区的私有子网、并通过 NAT 网关有出站互联网访问的 VPC:内部负载均衡器需要两个可用区的子网,网关需要访问 Bedrock 和你的 IdP
  • 一个重定向 URI 为 https://<gateway-host>/oauth/callback 的 Okta OIDC web 应用(见 IdP 设置)
  • 网关的 TLS 主机名,通常是 Route 53 私有托管区里指向负载均衡器的内部 DNS 名,并有该名字的 ACM 证书(导入的,或由 AWS Private CA 签发的)

设置环境变量

本页每个命令都从你的 shell 读取四个值:AWS_REGION、ACCOUNT_ID、VPC_ID 和 PRIVATE_SUBNETS。选一个 Bedrock 提供你所需 Claude 模型的美国区域:演练依赖网关内置的模型目录,它解析到 us.anthropic.* 推理配置文件,IAM 策略授权的也是这些 ARN。在非美国区域,要加一个带该地理范围推理配置文件 ID 的 models: 块,并把 IAM 策略的 ARN 前缀改成匹配的。如果手头没有 VPC ID,用 aws ec2 describe-vpcs 列出你的 VPC,再列出该 VPC 的子网,找两个位于不同可用区的私有子网:

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   # Bedrock 提供你所需 Claude 模型的美国区域
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>"

部署网关

下面的步骤用 aws 命令创建完整部署。

第 1 步:创建安全组

三个安全组串起流量路径:你的企业网络在 443 上访问负载均衡器,负载均衡器在 8080 上访问网关,网关在 5432 上访问 Postgres,其他都不可达。怎么挂载取决于计算轨道:在 ECS Fargate 上,部署步骤把 $ALB_SG 挂到负载均衡器、把 $GW_SG 挂到服务;在 EKS 上,AWS Load Balancer Controller 为 ALB 创建自己的前端安全组,所以 $ALB_SG 和 $GW_SG 用不到:部署步骤的 inbound-cidrs 注解把监听器限制到你的企业网络,数据库安全组改为放行集群的安全组。

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 角色并提交用例表单

网关用一个专用任务角色运行,它唯一的权限是在 Bedrock 上调用 Claude 模型。按 Bedrock 上游参考,策略必须同时覆盖跨区域推理配置文件 ARN 和底层基础模型 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 拉取镜像并注入稍后创建的 Secrets Manager 值;它与网关的 AWS SDK 在运行时使用的任务角色是分开的:

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

策略对每个密钥点名一个 ARN,而不是用裸的 gateway-* 通配符(在共享账号里它还会匹配无关的密钥);结尾的 -?????? 恰好匹配 Secrets Manager 附加在每个密钥 ARN 上的随机六字符后缀;结尾用 -* 只是普通前缀 glob,还会匹配 gateway-postgres-url-prod 这类更长的名字。IAM 策略授予网关调用 Bedrock 的权限,而 Bedrock 在商业区域默认启用模型访问;剩下的账号级关卡是 Anthropic 的一次性用例表单:如果账号里没人提交过,打开 Amazon Bedrock 控制台,在 Model catalog 选一个 Anthropic 模型并完成表单,提交后立即获得访问(AWS Organizations 表单以及提交者需要的 IAM 权限见「Claude Code on Amazon Bedrock」)。EKS 轨道在 IRSA 角色上复用这两份策略文档,而不是用两个 ECS 角色,见部署步骤。

第 3 步:创建 Amazon RDS for PostgreSQL

实例运行在私有子网里,没有公网地址,并启用存储加密。引擎版本固定为 Postgres 16,满足网关支持的 PostgreSQL 14 下限,并保证下面的参数组系列与实例匹配。先创建把数据库放进私有子网的子网组,以及带 rds.force_ssl=1 的参数组,使服务器拒绝明文连接;引擎版本只固定一次,因为参数组的系列必须与实例运行的引擎主版本匹配:

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 日志里(密钥步骤的说明也涉及同样的暴露);在共享或受监控的主机上,改为从 0600 文件经 --cli-input-json 传密码,就像随附包的 setup.sh 那样。等待实例就绪(可能需要几分钟),然后读取它的私有端点并拼出网关要用的连接串:

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 服务器证书的链和主机名,而不只是加密。信任锚是 AWS RDS 证书包,下面的镜像构建步骤把它复制到 /etc/claude/rds-global-bundle.pem 并通过 NODE_EXTRA_CA_CERTS 信任。不要在 URL 后面追加 libpq 风格的 sslrootcert= 参数:网关的驱动只从查询字符串读取 sslmode,会把 sslrootcert 作为启动参数转发给 Postgres,而服务器会拒绝它。ECS 服务或 EKS pod 必须在这个 VPC 里运行才能到达实例的私有端点,claude-gateway-db 安全组只放行网关的安全组。

第 4 步:编写 gateway.yaml

upstreams 块以 auth: {} 指向 Bedrock,所以网关经 AWS 默认凭据链从 ECS 上的任务角色或 EKS 上的 IRSA 角色认证(每个字段见配置参考)。两个 listen 字段描述网关前面是什么:public_url 是外部的 https:// 源,任何非环回绑定都必须设置,网关只用这个值构造 IdP 的 redirect_uri 和它的发现文档,从不用 X-Forwarded-* 头;trusted_proxies 是前端的源范围,网关只在 TCP 对端在该列表里时才采信 X-Forwarded-For,并沿链越过受信任的跳数,使每 IP 登录速率限制和审计事件记录开发者 IP 而不是负载均衡器的。

两条轨道的前端都是内部 ALB(不论是直接创建还是由 AWS Load Balancer Controller 创建),而 ALB 的节点从它所挂接的子网取地址,所以把 trusted_proxies 设为这些子网的 CIDR。这会把这些子网里的每个主机都当作代理来信任:要让 ALB 的入站来源(你的企业 CIDR)不与它们重叠,也不要与可能通过 X-Forwarded-For 伪造客户端 IP 的不受信任工作负载共享这些子网。ALB 的客户端端口保留属性 routing.http.xff_client_port.enabled 开或关都可以:开启时,ALB 把客户端写成 203.0.113.7:54321 或 [2001:db8::1]:54321,网关两种都能读并丢弃端口。

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]
  # Okta 组织授权服务器返回的 id_token 很精简,不含邮箱和组;
  # 网关从 /userinfo 补全它们。
  userinfo_fallback: true
  # Okta 只在请求 `groups` scope 且应用的组声明过滤器允许时才发出组。
  scopes: [openid, profile, email, offline_access, groups]

session:
  jwt_secret: ${GATEWAY_JWT_SECRET}              # EKS:${file:/secrets/jwt-secret}
  ttl_hours: 8                                   # 限定取消预配的延迟;
                                                 # 要更紧的撤销就调低到 1

store:
  postgres_url: ${GATEWAY_POSTGRES_URL}          # EKS:${file:/secrets/postgres-url}
  # readiness_grace_seconds: 300                 # 在 RDS 故障转移期间
                                                 # 继续通过健康检查

upstreams:
  - provider: bedrock
    region: <your-region>                        # 与 $AWS_REGION 一致,
                                                 # 使 IAM 策略的 ARN 覆盖它
    auth: {}                                     # AWS 默认凭据链:
                                                 # ECS 任务角色,或 EKS 上的 IRSA

只有 oidc 块是 Okta 专有的。改用 Microsoft Entra ID 时,把 issuer 设为 https://login.microsoftonline.com/<tenant-id>/v2.0,去掉 userinfo_fallback 和 groups scope,并注意 Entra 发出的是组对象 ID 而不是名字,所以 managed.policies 必须匹配 GUID,或匹配 App Roles 并设 oidc.groups_claim: roles(见 IdP 设置)。

第 5 步:把密钥存进 AWS Secrets Manager

创建三个密钥;第 2 步的执行角色已经能读取它们:

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 任务定义按 ARN 引用密钥。字面的 --secret-string 参数在每条命令运行期间会出现在进程表和审计/EDR 日志里;在共享或受监控的主机上,把值放进 0600 文件,改传 --secret-string file://<path>(随附包的 setup.sh 同样让密钥值不出现在进程 argv 里,把 0600 临时文件传给 --cli-input-json)。与密钥不同,gateway.yaml 本身不含密钥值,因为每个凭据都在启动时经 ${VAR} 或 ${file:...} 展开解析。一切如何到达容器取决于轨道:在 ECS 上,下一步的构建把 gateway.yaml 复制进镜像的 /etc/claude/gateway.yaml,任务定义通过其 secrets 字段把三个密钥注入为环境变量,所以 YAML 引用 ${GATEWAY_JWT_SECRET}、${OIDC_CLIENT_SECRET} 和 ${GATEWAY_POSTGRES_URL};在 EKS 上,从 ConfigMap 挂载 gateway.yaml,并把密钥作为文件挂载到 /secrets,以 ${file:/secrets/...} 引用,可以用 External Secrets Operator 或 Secrets Store CSI driver 的 AWS 提供商从 Secrets Manager 获取 Kubernetes Secret,也可以直接用 kubectl 创建。

第 6 步:构建镜像并推送到 Amazon ECR

按容器镜像要求构建镜像,把 linux-x64 glibc 二进制放在构建上下文的 ./claude。可以按这些要求自己写 Dockerfile,也可以从随附包的 Dockerfile 开始,它把前面步骤填好的 gateway.yaml 复制进镜像的 /etc/claude/gateway.yaml;在 ECS 上这份嵌入的副本就是配置到达容器的方式,所以构建要在文件写好之后。EKS 轨道改为在部署时从 ConfigMap 挂载 gateway.yaml,所以那里嵌入的副本不用。镜像还带有 AWS RDS 证书包,作为连接串 sslmode=verify-full 的信任锚,所以先把它下载到构建上下文。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 登录。不可变标签意味着部署步骤固定的 <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"

构建并推送镜像。下面的任务定义运行 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 轨道。 创建集群和网关 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

写任务定义(保存为 claude-gateway-task.json)。任务角色携带 Bedrock 权限,执行角色注入密钥;用 Secrets Manager 步骤里的密钥 ARN:

{
  "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

在前面放一个对网关做健康检查的目标组和内部 ALB。--ip-address-type ipv4 很重要:内部双栈 ALB 会发布公网范围的 AAAA 记录,而 /login 的私有网络检查会拒绝它们:

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 秒没有数据后关闭连接;网关的 keepalive ping 让流保持在这个默认值之内,所以调高超时是在 ping 节奏之上增加余量(关于连接断流的排障行说明了机制和较旧的网关)。下面的命令添加监听器并调高超时:

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

创建服务。部署断路器会把任务持续失败(因坏镜像或无法启动的配置)的部署回滚到上一个稳定状态,而不是永远重启失败的任务:

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 秒的宽限期给冷任务时间拉取镜像、连接存储并响应它的第一次健康检查,之后 ECS 才开始把失败计入部署。目标组对 GET /readyz 的健康检查会验证存储可达,所以无法到达 Postgres 的任务永远不会进入轮转;要在短暂的数据库中断(如 RDS 故障转移)期间让任务继续通过检查,按「中断行为」设 store.readiness_grace_seconds(那里也涵盖 /healthz 的替代方案)。任务运行在没有公网 IP 的私有子网里,所以所有出口(到 Bedrock、你的 IdP、Secrets Manager、ECR 和 CloudWatch Logs)都经 NAT 网关;要让 Bedrock 流量不走公网路径,创建 bedrock-runtime 接口 VPC 端点并把上游的 base_url 指向它(见 Bedrock 上游参考),IdP 仍需要互联网出口。最后给开发者一个能私有解析的主机名:在 Route 53 私有托管区里把网关的内部 DNS 名别名到 ALB,并把 listen.public_url 设为该主机名(内部 ALB 自己的 *.elb.amazonaws.com 名字解析到私有地址,但它无法携带你的 ACM 证书,所以要用你自己的名字)。第一次登录之前,把 OAuth 客户端的授权重定向 URI 更新为 <public_url>/oauth/callback。更改 public_url 之后,要用新标签重新构建并推送镜像、注册新的任务定义修订并重新部署:在 ECS 上该设置在镜像嵌入的 gateway.yaml 里,网关只用该设置构造公开源,忽略 X-Forwarded-Host 和 X-Forwarded-Proto;只有设了 listen.trusted_proxies 时才采信 X-Forwarded-For 作为客户端 IP。

EKS 轨道。 这个轨道需要在本地安装 kubectl 和 eksctl,以及一个已有的、带 IAM OIDC 提供商并安装了 AWS Load Balancer Controller 的 EKS 集群。集群必须在 $VPC_ID 上,pod 才能到达 RDS 私有端点,且 claude-gateway-db 安全组必须用集群的 pod 或节点安全组代替 $GW_SG 来放行。在 EKS 上,网关通过 IRSA 而不是 ECS 角色获得 Bedrock 凭据:第 2 步里 ecs-tasks.amazonaws.com 的信任策略不适用,IRSA 需要信任策略联合集群 OIDC 提供商、限定到 system:serviceaccount:claude-gateway:gateway 的角色。eksctl create iamserviceaccount 一步创建该角色、附加策略并给 Kubernetes 服务账号加上角色 ARN 注解。把第 2 步的两份策略文档变成它能附加的托管策略:

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

只有当 pod 自己读取 Secrets Manager 时才需要密钥策略(如 Secrets Store CSI driver 的 AWS 提供商使用挂载 pod 的服务账号时);如果用别的方式创建 Kubernetes Secret,就去掉它。该提供商需要策略里的两个动作:它在协调轮换后的密钥时调用 DescribeSecret,所以只授予 GetSecretValue 的话,首次部署能挂载,但之后不再获取轮换。按「Kubernetes 部署」把网关部署为标准的 Deployment 加 Service 和 Ingress,带有:serviceAccountName: gateway;gateway.yaml 从 ConfigMap 挂载、密钥挂载在 /secrets;就绪探针指向 GET /readyz。前端用 AWS Load Balancer Controller 管理的 Ingress 来创建内部 ALB,加上这些注解:alb.ingress.kubernetes.io/scheme: internal 和 alb.ingress.kubernetes.io/target-type: ip;alb.ingress.kubernetes.io/ip-address-type: ipv4(这样不会发布供 /login 私有网络检查拒绝的公网范围 AAAA 记录);alb.ingress.kubernetes.io/inbound-cidrs: <your-corporate-cidr>(这样控制器管理的前端安全组只放行你的企业网络,而不是它默认的 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(在网关流式 keepalive 之上留出余量)。用了 IRSA 时,AWS SDK 读取投射的服务账号令牌并与 AWS STS 交换,所以 pod 从不需要 EC2 实例元数据服务;出口 NetworkPolicy 可以对网关 pod 阻止 169.254.169.254;下面排障里的节点 hop-limit 问题只适用于跳过 IRSA、依赖节点实例角色的集群。

第 8 步:把网关 URL 推送到开发者机器

网关现在运行了,但在网关 URL 到达开发者机器之前,他们无法从 /login 到达它。在你经 MDM 部署到每台设备的托管设置文件里设 forceLoginMethod 和 forceLoginGatewayUrl;登录选择器里没有供开发者手动选择的网关选项。

Terraform 参考

位于 examples/gateway/aws 的配套包把本页打包成代码:

  • setup.sh:用同样的 aws 命令在 ECS Fargate 轨道上把上面的创建演练写成脚本。它是幂等的:已有资源会被检测到并跳过,所以重新运行是安全的,任何默认值都可以通过环境变量覆盖。Okta OIDC 客户端密钥和 ACM 证书仍要你自己创建:没有它们的运行会跳过 ECS/ALB 部署、点名缺失的输入并打印 create-secret 命令,创建好两者再重新运行。Bedrock 用例表单和 Route 53 别名作为后续步骤打印出来而不会自动运行。
  • gateway.yaml.example:第 4 步 gateway.yaml 的配置模板,包含被注释掉的可选键。复制为 gateway.yaml 并在构建前替换每个 REPLACE_ME。
  • Dockerfile:用预构建的 linux-x64 二进制构建运行时镜像,复制你填好的 gateway.yaml 到 /etc/claude/gateway.yaml,外加锚定存储 sslmode=verify-full 的 AWS RDS 证书包。setup.sh 只在构建上下文里没有该证书包时才下载它;要获取 AWS CA 轮换,删除该文件并用新标签重建。配置文件不含密钥值,因为每个凭据都在启动时经 ${VAR} 展开解析,所以改配置就意味着用新标签重建。
  • terraform/:以声明方式创建同样的 ECS Fargate 范围:安全组、IAM 角色、ECR 仓库、RDS 实例、Secrets Manager 密钥,以及内部 ALB 后面的 ECS 服务。VPC 和私有子网仍是前提,作为变量传入。Terraform 创建 ECR 仓库但不构建镜像,服务定义引用镜像,所以 apply 分两遍:先对仓库做有目标的 apply,再构建并推送,然后做完整 apply;包里的 terraform/README.md 涵盖变量、远程状态和拆除。

与本页一样,这个包是客户自管基础设施的工作示例,不是受支持的生产部署:依赖它之前要审查并按你的环境调整。

排障

网关启动和登录错误见与平台无关的排障表;下面的条目是 AWS 特有的。

症状原因修复
CLI /login:Gateway hosts must be on your organization's private network; <host> resolves to the public (or unrecognized) address <ip>网关名解析到至少一个公网地址:双栈内部 ALB 会发布公网范围的 AAAA 记录,而私有网络检查要求每个解析出的地址都是私有的用 --ip-address-type ipv4 创建 ALB,或提供一个没有公网 AAAA 记录的单独内部 DNS 名
每个 Bedrock 请求都返回 502;日志显示 Could not load credentials from any providers任务运行在没有任务角色的 ECS EC2 启动类型上,或 pod 运行在没有 IRSA 的 EKS 节点上,所以凭据来自实例元数据,而 IMDSv2 默认的跳数限制 1 把它拦在容器里;本页的两条轨道都不受影响:Fargate 任务角色和 IRSA 不使用实例元数据优先用任务角色和 IRSA;实例凭据无法避免时,用 aws ec2 modify-instance-metadata-options --instance-id <id> --http-put-response-hop-limit 2 提高跳数限制
Bedrock 请求返回 403 AccessDeniedException账号没有提交 Anthropic 的一次性用例表单、账号首次调用时自动开始的 AWS Marketplace 订阅尚未完成,或任务角色的策略缺少推理配置文件或基础模型 ARN从 Bedrock 控制台的 Model catalog 提交用例表单;刚提交或这是账号的首次调用时,等几分钟再重试;在两个 ARN 系列上都授予 bedrock:InvokeModel 和 bedrock:InvokeModelWithResponseStream
Bedrock 返回 ValidationException,说不支持按需吞吐自定义 models: 条目映射到该区域只经推理配置文件提供的裸基础模型 ID改为把模型映射到它的跨区域推理配置文件 ID(us.anthropic.*);内置目录已经这样做
ECS 任务在网关记录任何东西之前以 ResourceInitializationError 停止执行角色无法读取 Secrets Manager 密钥,或私有子网没有到 Secrets Manager 或 ECR 的路径在三个 gateway- 密钥的 ARN 上给执行角色 secretsmanager:GetSecretValue,并经 NAT 网关提供出口;没有 NAT 时,提供 Secrets Manager、ECR 和 CloudWatch Logs(awslogs 驱动在同一阶段需要)的接口端点以及 S3 网关端点
网关启动因 Postgres 连接超时错误退出数据库安全组没有在 5432 上放行网关的安全组,或服务运行在数据库所在 VPC 之外在数据库的安全组上允许来自网关安全组的 5432,并让服务与数据库子网组在同一个 VPC 里运行
网关启动因 Postgres TLS 证书验证错误退出连接串设了 sslmode=verify-full,但镜像不信任 RDS CA 证书包:证书包没有复制进镜像,或 NODE_EXTRA_CA_CERTS 没指向它加上构建步骤里复制证书包并设置 NODE_EXTRA_CA_CERTS 的两行 Dockerfile,然后重建、用新标签推送并重新部署
流式响应在一段安静期之后中途断开v2.1.229 之前的网关在 Bedrock 或 Claude Platform on AWS 上游上,上游安静期间(如没有流式输出的扩展思考)什么都不发;ALB 默认在 60 秒没有数据后关闭连接,所以在这个间隙把流切断。v2.1.229 及以上的网关让安静的流保持在该超时之内:在这些上游上,约 15 秒没有流数据时网关发出一个 SSE ping 事件,在 Anthropic API 上游上则转发 API 自己的 ping更新网关,并按上面所示把 ALB 空闲超时调高到 3600 秒

遥测

网关无需任何每机器的 OTEL 配置就给你按开发者的用量指标。Claude Code 发出 OpenTelemetry(OTLP)指标、日志和选择加入的追踪(CLI 报告的一切见「监控用量」)。在经 /login 登录的会话里,CLI 用已认证的 IdP 身份属性 user.id、user.email 和 user.groups 给每次导出打标,所以用量按开发者汇总。网关自己是经认证的 OTLP 中继:同时设置 telemetry.forward_to 和 listen.public_url,它就把 OTEL 导出器设置推送给每个已连接的客户端,并把它们的 OTLP 流量原样转发到你列出的每个目的地。每个目的地独立选择是否启用指标、日志和追踪,默认只有指标(每信号的字段和敏感度权衡见 telemetry 参考);网关不缓冲、聚合或存储遥测,数据落在哪里完全取决于收集器的导出器配置。客户端遥测默认关闭;配置 telemetry.forward_to 才会为已连接的开发者开启它,每个交互式客户端会为推送的设置显示安全审批对话框。在 AWS 上,每个信号映射到目的地的方式:

  • 客户端指标、日志和追踪:把 telemetry.forward_to 指向 OpenTelemetry 收集器(如 AWS Distro for OpenTelemetry(ADOT)收集器),再从那里导出到 Amazon CloudWatch、Amazon Managed Service for Prometheus 或任何 OTLP 后端。要把收集器作为自己的内部服务运行,经 https:// 可达(环回例外和 CLAUDE_GATEWAY_ALLOW_LOOPBACK 见 telemetry 参考)。
  • 网关日志:在 ECS Fargate 上无需额外设置:awslogs 驱动把携带审计事件和运维日志的网关 stderr 交付到上面创建的 /ecs/claude-gateway 日志组。在 EKS 上,pod 日志默认不会到达 CloudWatch,所以在你安装日志收集(启用容器日志捕获的 Amazon CloudWatch Observability 附加组件,或 Fluent Bit DaemonSet)之前审计轨迹会丢失。两条轨道上都可以用 CloudWatch Logs Insights 查询日志,并用指标过滤器驱动告警。
  • 容器指标:用 aws ecs update-cluster-settings --cluster claude-gateway --settings name=containerInsights,value=enabled 在集群上启用 Container Insights,得到每任务的 CPU、内存和网络;在 EKS 上安装 Amazon CloudWatch Observability 附加组件。
  • 花费:遥测显示事后的用量;支出限额是网关实时的按开发者视图和强制执行。

成本归属

网关用它自己的主体(ECS 任务角色或 EKS IRSA 角色)给每个 Bedrock 请求签名,所以默认 AWS 看到的是该花费都在一个 IAM 主体之下。有两种办法在 AWS 自己的计费数据里拆分它,并且可以组合。

用 assume_role 按开发者拆分

创建持有 Bedrock 权限并信任网关主体的第二个 IAM 角色,给该主体对它的 sts:AssumeRole,并在 Bedrock 上游上设带 session_name: email 的 assume_role。网关随后每个开发者每小时承担一次该角色,会话名设为他们的邮箱,并用结果给他们的请求签名(需要网关运行 Claude Code v2.1.281 及以上)。该角色也可以在另一个 AWS 账号里(见「另一个 AWS 账号里的 Bedrock」)。在 Terraform 里,放在 Terraform 包里的任务角色旁边:

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" {   # 与任务角色相同的 Bedrock 策略
  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 时,网关用被承担角色的凭据给每个 Bedrock 调用签名(包括用于支出计量的免费 CountTokens 调用),所以只有对没有 assume_role 的上游,网关的主体才需要自己的 Bedrock 策略。因为网关在请求时调用 STS,私有子网需要有到 sts.<region>.amazonaws.com 的路径:前提里的 NAT 网关提供它,应答该主机名的 STS 接口 VPC 端点也行;每个活跃开发者每个网关副本每小时一次 STS 调用。每个开发者的请求以主体 arn:aws:sts::<account>:assumed-role/<role>/<email> 到达 AWS;要按主体查看花费,用含 IAM 主体数据的账单导出(AWS 的 IAM 主体成本分配页面说明如何启用以及哪些账单工具显示它)。

用应用推理配置文件按团队拆分

这条路线只用 models 和 managed 两个部分。给每个团队和模型创建一个 Bedrock 应用推理配置文件,给每个配置文件打上团队标签,并把该标签激活为成本分配标签。然后在 gateway.yaml 里给每个团队自己的模型 id,并把每个 IdP 组固定到其团队的 id:

models:
  - 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}

要告诉被固定团队里的开发者用 --model platform-claude-opus-4-8(他们团队的 id)启动 Claude Code,因为不带它启动的会话运行默认模型,而网关对他们拒绝该模型。网关在每个请求上(不只是模型选择器里)强制 availableModels,AWS 账单按你激活的标签对花费分组。没有 match: {} 兜底时,不匹配任何策略的开发者会得到目录里的每个模型,并能给任一团队的配置文件计费。代价:配置随团队数乘以模型数增长,并且为这个上游的 Bedrock 请求签名的角色还必须被允许调用 application-inference-profile/* ARN(该角色是网关的主体,用了 assume_role 时则是它承担的角色);网关自己的支出计量如何给这些 id 定价,见 pricing。

下一步

  • 配置参考:每个 gateway.yaml 选项,包括 managed.policies 和 telemetry
  • 部署与运维:IdP 设置、健康检查、JWT 密钥轮换、升级和安全模型
  • Claude apps gateway 概览:快速开始和连接开发者
  • AWS 的 Claude apps gateway 示例:AWS 维护的、涵盖一系列客户环境的部署示例