---
title: AWS에서 WIF 사용하기
url: https://platform.claude.com/docs/ko/manage-claude/wif-providers/aws
description: Workload Identity Federation과 STS 발급 ID 토큰을 사용하여 Lambda, EC2, ECS 또는 EKS의 AWS 워크로드를 Claude API에 인증합니다.
---

AWS 워크로드는 AWS가 서명한 OIDC ID 토큰을 교환함으로써 정적 API 키 없이 Claude API에 인증할 수 있습니다. 권장 경로는 AWS STS [`GetWebIdentityToken`](https://docs.aws.amazon.com/STS/latest/APIReference/API_GetWebIdentityToken.html) API를 호출하는 것으로, 워크로드가 AWS 자격 증명을 가지고 있는 곳이라면 어디서든 작동합니다: Lambda, EC2, ECS, EKS. EKS 워크로드는 대안으로 [Kubernetes 프로젝션 토큰 경로](https://platform.claude.com/docs/ko/manage-claude/wif-providers/aws#use-eks-projected-service-account-tokens)를 사용할 수 있으며, 이 경로는 구성 단계가 더 적지만 파드 내부에서만 작동합니다.

이 가이드는 두 경로를 모두 보여줍니다. 기본 개념(서비스 계정, 페더레이션 발급자, 페더레이션 규칙)에 대해서는 [Workload Identity Federation](https://platform.claude.com/docs/ko/manage-claude/workload-identity-federation)을 참조하세요.

## 사전 요구 사항

* [WIF 개념](https://platform.claude.com/docs/ko/manage-claude/workload-identity-federation#concepts)에 대한 이해: 서비스 계정, 페더레이션 발급자, 페더레이션 규칙.
* IAM 역할이 연결된 AWS 워크로드(EKS 파드, ECS 태스크, Lambda 함수 또는 EC2 인스턴스).
* 워크로드에서 사용 가능한 `aws` CLI 또는 AWS SDK.
* Anthropic 조직에 대해 Claude Console에서 서비스 계정, 페더레이션 발급자, 페더레이션 규칙을 생성할 수 있는 권한.

## STS 웹 ID 토큰 사용(권장)

AWS STS `GetWebIdentityToken` API는 호출자의 IAM ID를 증명하는, AWS가 서명한 OIDC 토큰을 반환합니다. 워크로드의 주변(ambient) AWS 자격 증명을 사용하기 때문에 동일한 통합으로 Lambda, EC2, ECS, EKS를 모두 지원합니다.

### AWS 구성

<Steps>
  <Step title="계정에 대해 아웃바운드 웹 ID 페더레이션 활성화">
    이는 계정 수준 플래그이며 기본적으로 꺼져 있습니다. AWS 콘솔에서 **IAM**을 열고 **Account settings**를 선택한 다음 **Outbound web identity federation**을 활성화하세요. 프로그래밍 방식으로 활성화하려면:

    ```bash
    python3 -c "import boto3; boto3.client('iam').enable_outbound_web_identity_federation()"
    ```

    이 기능이 활성화되어 있지 않으면 `GetWebIdentityToken` 호출이 `OutboundWebIdentityFederationDisabledException`으로 실패합니다.
  </Step>

  <Step title="워크로드의 IAM 역할에 API 호출 권한 부여">
    Lambda 함수, EC2 인스턴스 또는 ECS 태스크가 실행되는 IAM 역할에 다음 정책을 연결하세요:

    ```json
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": ["sts:GetWebIdentityToken"],
          "Resource": "*"
        }
      ]
    }
    ```
  </Step>

  <Step title="계정의 STS 발급자 URL 찾기">
    아웃바운드 페더레이션을 활성화하면 **IAM > Account settings** 페이지에 `https://<uuid>.tokens.sts.global.api.aws` 형식의 값을 가진 **Get Token Issuer URL** 필드가 표시됩니다. 이 URL은 AWS 계정마다 고유하므로 다음 단계를 위해 복사해 두세요. 프로그래밍 방식으로 가져오려면:

    ```bash
    python3 -c "import boto3; print(boto3.client('iam').get_outbound_web_identity_federation_info())"
    ```
  </Step>
</Steps>

### Anthropic 구성

Claude Console에서 [Workload identity](https://platform.claude.com/settings/workload-identity-federation) 페이지(`platform.claude.com/settings/workload-identity-federation`)로 이동하여 **Connect workload**를 클릭하고 **AWS** 타일을 선택하세요. 마법사가 발급자 등록, 서비스 계정 생성, 페더레이션 규칙 생성 과정을 안내합니다.

마법사가 이러한 리소스를 대신 생성해 줍니다. 마법사에 입력하든 [Admin API](https://platform.claude.com/docs/ko/manage-claude/wif-admin-api)로 전송하든 다음 값을 사용하세요:

**페더레이션 발급자:** 이전 단계에서 복사한 계정별 STS 발급자 URL을 등록하세요. 공개 JWKS 엔드포인트를 노출하므로 디스커버리 모드를 사용하세요.

```json
{
  "name": "aws-sts",
  "issuer_url": "https://<uuid>.tokens.sts.global.api.aws",
  "jwks": { "type": "discovery" }
}
```

**페더레이션 규칙:** `GetWebIdentityToken`에 전달하는 audience와 `sub` 클레임의 호출 역할 IAM 역할 ARN을 매칭하세요. `sub` 값은 API를 호출한 워크로드의 IAM 역할 ARN이며 `arn:aws:iam::<account>:role/<role-name>` 형식입니다. 토큰에는 `aws_account`, `org_id`, `principal_id` 및 전달한 모든 `request_tags`를 포함하는 `https://sts.amazonaws.com/` 클레임도 담겨 있습니다. 더 세밀한 제어를 위해 규칙의 `claims` 맵이나 CEL `condition`으로 이들을 매칭할 수 있습니다.

워크로드가 허용하는 한 최대한 구체적으로 지정하세요. 정확한 역할 ARN을 매칭하고, 여러 IAM 역할이 동일한 Anthropic 서비스 계정에 매핑되어야 하는 경우에만 `subject_prefix`를 넓히세요(예: `arn:aws:iam::123456789012:role/*`).

```json
{
  "name": "prod-inference",
  "issuer_id": "fdis_...",
  "match": {
    "subject_prefix": "arn:aws:iam::123456789012:role/inference-worker",
    "audience": "https://api.anthropic.com"
  },
  "target": { "type": "service_account", "service_account_id": "svac_..." },
  "workspace_id": "wrkspc_...",
  "oauth_scope": "workspace:developer",
  "token_lifetime_seconds": 600
}
```

### 토큰 획득 및 사용

`https://api.anthropic.com`을 audience로 지정하여 `GetWebIdentityToken`을 호출한 다음, 그 결과를 `identity_token_provider` (typescript: `identityTokenProvider`; php: `identityProvider`; csharp: `IdentityTokenProvider`; go: `option.WithFederationTokenProvider`; java: `IdentityTokenConfig`)를 통해 SDK의 페더레이션 자격 증명에 전달하세요. Java를 제외하고(해당 예제의 주석 참조), 이 옵션은 토큰 제공자를 받으므로, SDK는 갱신할 때마다 STS를 다시 호출합니다.

<Note>
  `GetWebIdentityToken`은 리전별 STS 엔드포인트에서만 사용할 수 있습니다. `'STS' object has no attribute 'get_web_identity_token'` 또는 유사한 오류가 발생하면 STS 클라이언트를 특정 리전에 고정하고(예: `boto3.client("sts", region_name="us-east-1")`) AWS SDK가 해당 API를 포함할 만큼 최신 버전인지 확인하세요.
</Note>

<CodeGroup>
  ```bash cURL
  JWT=$(aws sts get-web-identity-token \
    --region us-east-1 \
    --audience "https://api.anthropic.com" \
    --signing-algorithm RS256 \
    --duration-seconds 900 \
    --query WebIdentityToken --output text)

  RESPONSE=$(curl -sS https://api.anthropic.com/v1/oauth/token \
    -H "content-type: application/json" \
    --data @- <<JSON
  {
    "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
    "assertion": "$JWT",
    "federation_rule_id": "$ANTHROPIC_FEDERATION_RULE_ID",
    "organization_id": "$ANTHROPIC_ORGANIZATION_ID",
    "service_account_id": "$ANTHROPIC_SERVICE_ACCOUNT_ID",
    "workspace_id": "$ANTHROPIC_WORKSPACE_ID"
  }
  JSON
  )

  ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r .access_token)

  curl https://api.anthropic.com/v1/messages \
    -H "authorization: Bearer $ACCESS_TOKEN" \
    -H "anthropic-version: 2023-06-01" \
    -H "content-type: application/json" \
    -d '{
      "model": "claude-opus-5-5",
      "max_tokens": 1024,
      "messages": [{"role": "user", "content": "Hello from AWS"}]
    }' | jq -r '.content[] | select(.type == "text") | .text'
  ```

  ```python Python
  import os

  import anthropic
  import boto3
  from anthropic import WorkloadIdentityCredentials


  def get_sts_web_identity_token() -> str:
      sts = boto3.client("sts", region_name="us-east-1")
      resp = sts.get_web_identity_token(
          Audience=["https://api.anthropic.com"],
          SigningAlgorithm="RS256",
          DurationSeconds=900,
      )
      return resp["WebIdentityToken"]


  client = anthropic.Anthropic(
      credentials=WorkloadIdentityCredentials(
          identity_token_provider=get_sts_web_identity_token,
          federation_rule_id=os.environ["ANTHROPIC_FEDERATION_RULE_ID"],
          organization_id=os.environ["ANTHROPIC_ORGANIZATION_ID"],
          service_account_id=os.environ["ANTHROPIC_SERVICE_ACCOUNT_ID"],
          workspace_id=os.environ.get("ANTHROPIC_WORKSPACE_ID"),
      ),
  )

  message = client.messages.create(
      model="claude-opus-5-5",
      max_tokens=1024,
      messages=[{"role": "user", "content": "Hello from AWS"}],
  )
  print(next(block.text for block in message.content if block.type == "text"))
  ```

  ```typescript TypeScript
  import Anthropic from "@anthropic-ai/sdk";
  import { oidcFederationProvider } from "@anthropic-ai/sdk/lib/credentials/oidc-federation";
  import { STSClient, GetWebIdentityTokenCommand } from "@aws-sdk/client-sts";

  const sts = new STSClient({ region: "us-east-1" });

  async function getStsWebIdentityToken(): Promise<string> {
    const out = await sts.send(
      new GetWebIdentityTokenCommand({
        Audience: ["https://api.anthropic.com"],
        SigningAlgorithm: "RS256",
        DurationSeconds: 900
      })
    );
    return out.WebIdentityToken!;
  }

  const client = new Anthropic({
    credentials: oidcFederationProvider({
      identityTokenProvider: getStsWebIdentityToken,
      federationRuleId: process.env.ANTHROPIC_FEDERATION_RULE_ID!,
      organizationId: process.env.ANTHROPIC_ORGANIZATION_ID!,
      serviceAccountId: process.env.ANTHROPIC_SERVICE_ACCOUNT_ID,
      workspaceId: process.env.ANTHROPIC_WORKSPACE_ID,
      baseURL: "https://api.anthropic.com",
      fetch
    })
  });

  const message = await client.messages.create({
    model: "claude-opus-5-5",
    max_tokens: 1024,
    messages: [{ role: "user", content: "Hello from AWS" }]
  });
  for (const block of message.content) {
    if (block.type === "text") {
      console.log(block.text);
    }
  }
  ```

  ```go Go
  ctx := context.TODO()
  cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion("us-east-1"))
  if err != nil {
  	panic(err)
  }
  stsClient := sts.NewFromConfig(cfg)

  getStsToken := option.IdentityTokenFunc(func(ctx context.Context) (string, error) {
  	out, err := stsClient.GetWebIdentityToken(ctx, &sts.GetWebIdentityTokenInput{
  		Audience:         []string{"https://api.anthropic.com"},
  		SigningAlgorithm: "RS256",
  		DurationSeconds:  aws.Int32(900),
  	})
  	if err != nil {
  		return "", err
  	}
  	return *out.WebIdentityToken, nil
  })

  client := anthropic.NewClient(
  	option.WithFederationTokenProvider(getStsToken, option.FederationOptions{
  		FederationRuleID: os.Getenv("ANTHROPIC_FEDERATION_RULE_ID"),
  		OrganizationID:   os.Getenv("ANTHROPIC_ORGANIZATION_ID"),
  		ServiceAccountID: os.Getenv("ANTHROPIC_SERVICE_ACCOUNT_ID"),
  		WorkspaceID:      os.Getenv("ANTHROPIC_WORKSPACE_ID"),
  	}),
  )

  message, err := client.Messages.New(ctx, anthropic.MessageNewParams{
  	Model:     anthropic.ModelClaudeOpus5_5,
  	MaxTokens: 1024,
  	Messages: []anthropic.MessageParam{
  		anthropic.NewUserMessage(anthropic.NewTextBlock("Hello from AWS")),
  	},
  })
  if err != nil {
  	panic(err)
  }
  for _, block := range message.Content {
  	if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok {
  		fmt.Println(textBlock.Text)
  		break
  	}
  }
  ```

  ```java Java
  import com.anthropic.config.AuthenticationConfig;
  import com.anthropic.config.AuthenticationType;
  import com.anthropic.config.IdentityTokenConfig;
  import com.anthropic.config.InMemoryProfileConfigProvider;
  import com.anthropic.config.ProfileConfig;
  // ...
  import software.amazon.awssdk.regions.Region;
  import software.amazon.awssdk.services.sts.StsClient;
  import software.amazon.awssdk.services.sts.model.GetWebIdentityTokenRequest;

  void main() {
      StsClient sts = StsClient.builder().region(Region.US_EAST_1).build();

      // Java SDK는 함수가 아니라 토큰 자체 또는 파일 경로를 받습니다.
      // source("value")를 사용하면 path(...)에 토큰이 담깁니다. SDK는 Anthropic 액세스 토큰을
      // 갱신할 때마다 동일한 토큰을 전송하므로, 토큰이 만료되었거나
      // jti 클레임이 있는 경우 이미 사용되었다면 실패합니다. 장시간 실행되는
      // 프로세스에서는 source("file")을 전달하고 매 갱신 전에
      // 파일에 새 토큰을 기록하세요.
      String stsToken = sts.getWebIdentityToken(
                      GetWebIdentityTokenRequest.builder()
                              .audience("https://api.anthropic.com")
                              .signingAlgorithm("RS256")
                              .durationSeconds(900)
                              .build())
              .webIdentityToken();

      AnthropicClient client = AnthropicOkHttpClient.builder()
              .configurationProvider(InMemoryProfileConfigProvider.of(ProfileConfig.builder()
                      .organizationId(System.getenv("ANTHROPIC_ORGANIZATION_ID"))
                      .workspaceId(System.getenv("ANTHROPIC_WORKSPACE_ID"))
                      .authentication(AuthenticationConfig.builder()
                              .type(AuthenticationType.OIDC_FEDERATION)
                              .federationRuleId(System.getenv("ANTHROPIC_FEDERATION_RULE_ID"))
                              .serviceAccountId(System.getenv("ANTHROPIC_SERVICE_ACCOUNT_ID"))
                              .identityToken(IdentityTokenConfig.builder()
                                      .source("value")
                                      .path(stsToken)
                                      .build())
                              .build())
                      .build()))
              .build();

      var message = client.messages().create(MessageCreateParams.builder()
              .model(Model.CLAUDE_OPUS_5_5)
              .maxTokens(1024)
              .addUserMessage("Hello from AWS")
              .build());

      IO.println(message.content());
  }
  ```

  ```csharp C#
  using Amazon.SecurityToken;
  using Amazon.SecurityToken.Model;
  // ...
  using Anthropic.Credentials;
  // ...

  var credentials = new WorkloadIdentityCredentials(new WorkloadIdentityOptions
  {
      FederationRuleId = Environment.GetEnvironmentVariable("ANTHROPIC_FEDERATION_RULE_ID")!,
      OrganizationId = Environment.GetEnvironmentVariable("ANTHROPIC_ORGANIZATION_ID"),
      ServiceAccountId = Environment.GetEnvironmentVariable("ANTHROPIC_SERVICE_ACCOUNT_ID"),
      WorkspaceId = Environment.GetEnvironmentVariable("ANTHROPIC_WORKSPACE_ID"),
      IdentityTokenProvider = new StsTokenProvider(),
  });
  using var client = new AnthropicClient(new ClientOptions { Credentials = credentials });

  var message = await client.Messages.Create(new()
  {
      Model = Model.ClaudeOpus5_5,
      MaxTokens = 1024,
      Messages = [new() { Role = Role.User, Content = "Hello from AWS" }],
  });
  foreach (var block in message.Content)
  {
      if (block.Value is TextBlock textBlock)
      {
          Console.WriteLine(textBlock.Text);
      }
  }

  class StsTokenProvider : IIdentityTokenProvider
  {
      private readonly AmazonSecurityTokenServiceClient _sts = new(Amazon.RegionEndpoint.USEast1);

      public async Task<string> GetIdentityTokenAsync(CancellationToken ct = default)
      {
          var resp = await _sts.GetWebIdentityTokenAsync(new GetWebIdentityTokenRequest
          {
              Audience = ["https://api.anthropic.com"],
              SigningAlgorithm = "RS256",
              DurationSeconds = 900,
          }, ct);
          return resp.WebIdentityToken;
      }
  }
  ```

  ```bash CLI
  TOKEN_FILE=$(mktemp)
  aws sts get-web-identity-token \
    --region us-east-1 \
    --audience "https://api.anthropic.com" \
    --signing-algorithm RS256 \
    --duration-seconds 900 \
    --query WebIdentityToken --output text > "$TOKEN_FILE"

  export ANTHROPIC_IDENTITY_TOKEN_FILE="$TOKEN_FILE"
  # ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID,
  # ANTHROPIC_SERVICE_ACCOUNT_ID, ANTHROPIC_WORKSPACE_ID는 환경 변수에서 읽어옵니다
  ant messages create \
    --model claude-opus-5-5 \
    --max-tokens 1024 \
    --message '{role: user, content: "Hello from AWS"}'
  ```

  ```php PHP
  use Anthropic\Client;
  use Anthropic\Lib\Credentials\CredentialResult;
  use Anthropic\Lib\Credentials\IdentityTokenProvider;
  use Anthropic\Lib\Credentials\TokenCache;
  use Anthropic\Lib\Credentials\WorkloadIdentityCredentials;
  use Aws\Sts\StsClient;

  $sts = new StsClient(['region' => 'us-east-1', 'version' => 'latest']);

  class StsIdentityToken implements IdentityTokenProvider
  {
      public function __construct(private StsClient $sts) {}

      public function getIdentityToken(): string
      {
          return $this->sts->getWebIdentityToken([
              'Audience' => ['https://api.anthropic.com'],
              'SigningAlgorithm' => 'RS256',
              'DurationSeconds' => 900,
          ])['WebIdentityToken'];
      }
  }

  $client = new Client(credentials: new CredentialResult(
      provider: new TokenCache(new WorkloadIdentityCredentials(
          identityProvider: new StsIdentityToken($sts),
          federationRuleId: getenv('ANTHROPIC_FEDERATION_RULE_ID'),
          organizationId: getenv('ANTHROPIC_ORGANIZATION_ID'),
          serviceAccountId: getenv('ANTHROPIC_SERVICE_ACCOUNT_ID'),
          workspaceId: getenv('ANTHROPIC_WORKSPACE_ID') ?: null,
      )),
  ));

  $message = $client->messages->create(
      model: 'claude-opus-5-5',
      maxTokens: 1024,
      messages: [['role' => 'user', 'content' => 'Hello from AWS']],
  );
  $textBlock = array_find($message->content, static fn ($block): bool => $block->type === 'text');
  echo $textBlock->text, PHP_EOL;
  ```

  ```ruby Ruby
  require "anthropic"
  require "aws-sdk-sts"

  sts = Aws::STS::Client.new(region: "us-east-1")
  client = Anthropic::Client.new(
    credentials: Anthropic::WorkloadIdentityCredentials.new(
      identity_token_provider: -> {
        sts.get_web_identity_token(
          audience: ["https://api.anthropic.com"],
          signing_algorithm: "RS256",
          duration_seconds: 900,
        ).web_identity_token
      },
      federation_rule_id: ENV.fetch("ANTHROPIC_FEDERATION_RULE_ID"),
      organization_id: ENV.fetch("ANTHROPIC_ORGANIZATION_ID"),
      service_account_id: ENV.fetch("ANTHROPIC_SERVICE_ACCOUNT_ID"),
      workspace_id: ENV["ANTHROPIC_WORKSPACE_ID"],
    ),
  )

  message = client.messages.create(
    model: "claude-opus-5-5",
    max_tokens: 1024,
    messages: [{role: "user", content: "Hello from AWS"}]
  )
  puts message.content.find { it.type == :text }.text
  ```
</CodeGroup>

### 설정 확인

워크로드 내부에서 STS 발급 토큰을 직접 교환하고 응답을 확인하세요:

```bash cURL
JWT=$(aws sts get-web-identity-token \
  --region us-east-1 \
  --audience "https://api.anthropic.com" \
  --signing-algorithm RS256 \
  --duration-seconds 900 \
  --query WebIdentityToken --output text)

curl -sS https://api.anthropic.com/v1/oauth/token \
  -H "content-type: application/json" \
  -d "{
    \"grant_type\": \"urn:ietf:params:oauth:grant-type:jwt-bearer\",
    \"assertion\": \"$JWT\",
    \"federation_rule_id\": \"fdrl_...\",
    \"organization_id\": \"00000000-0000-0000-0000-000000000000\",
    \"service_account_id\": \"svac_...\",
    \"workspace_id\": \"wrkspc_...\"
  }" | jq
```

교환에 성공하면 `sk-ant-oat01-`로 시작하는 `access_token`과 초 단위의 `expires_in` 값이 반환됩니다. 교환이 불투명한 `401` `authentication_error` 응답(메시지 `Authentication failed`)과 함께 실패하면, Claude Console의 [인증 기록 페이지](https://platform.claude.com/settings/workload-identity-federation?tab=history)에서 거부 사유를 확인하고 [실패한 교환 문제 해결](https://platform.claude.com/docs/ko/manage-claude/wif-reference#troubleshoot-a-failed-exchange)을 참조하세요. AWS 측에서 가장 흔한 원인은 `iss` 불일치입니다(계정별 STS 발급자 URL이 등록된 `issuer_url`과 정확히 일치해야 합니다).

## EKS 프로젝션 서비스 계정 토큰 사용

워크로드가 EKS 파드에서 실행되는 경우 STS 호출을 건너뛰고 Kubernetes가 프로젝션한 서비스 계정 토큰을 디스크에서 직접 읽을 수 있습니다. Kubernetes는 기본적으로 OIDC 호환 토큰을 파드에 프로젝션하며, SDK는 파일 경로에서 이를 읽을 수 있으므로 토큰 제공자 callable이 필요하지 않습니다. 이 경로는 STS 경로보다 AWS 구성 단계가 두 개 적지만 파드 내부에서만 작동합니다. 기본 메커니즘은 [일반 Kubernetes 통합](https://platform.claude.com/docs/ko/manage-claude/wif-providers/kubernetes)과 동일합니다.

이 경로에는 추가로 [IAM OIDC 제공자가 활성화된](https://docs.aws.amazon.com/eks/latest/userguide/enable-iam-roles-for-service-accounts.html) EKS 클러스터와 클러스터에 대한 `kubectl` 액세스가 필요합니다.

### EKS 클러스터 구성

<Steps>
  <Step title="클러스터의 OIDC 발급자 URL 찾기">
    각 EKS 클러스터에는 고유한 OIDC 발급자가 있습니다. AWS CLI로 가져오세요:

    ```bash CLI
    aws eks describe-cluster \
      --name <cluster-name> \
      --query "cluster.identity.oidc.issuer" \
      --output text
    ```

    출력은 `https://oidc.eks.us-west-2.amazonaws.com/id/6FA42E7BFDE8549CB...`와 같은 형태입니다. 다음 섹션에서 이 URL을 페더레이션 발급자로 등록합니다.
  </Step>

  <Step title="서비스 계정 생성 및 Anthropic audience 토큰 프로젝션">
    EKS 파드 ID 웹훅은 `eks.amazonaws.com/role-arn` 어노테이션을 감지하고 `aud: sts.amazonaws.com`을 가진 토큰을 자동으로 프로젝션하며, 그 경로를 `AWS_WEB_IDENTITY_TOKEN_FILE`로 노출합니다. 해당 토큰은 AWS 역할 수임용입니다. Anthropic 교환을 위해서는 `audience: https://api.anthropic.com`으로 두 번째 토큰을 프로젝션하고 전용 경로에 마운트하세요.

    ```yaml
    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: inference-worker
      namespace: inference
      annotations:
        eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/inference-worker
    ```

    ```yaml
    apiVersion: v1
    kind: Pod
    metadata:
      name: inference-worker
      namespace: inference
    spec:
      serviceAccountName: inference-worker
      volumes:
        - name: anthropic-token
          projected:
            sources:
              - serviceAccountToken:
                  audience: https://api.anthropic.com
                  expirationSeconds: 3600
                  path: token
      containers:
        - name: app
          image: your-registry/inference-worker:latest
          env:
            - name: ANTHROPIC_IDENTITY_TOKEN_FILE
              value: /var/run/secrets/anthropic.com/token
            - name: ANTHROPIC_FEDERATION_RULE_ID
              value: fdrl_...
            - name: ANTHROPIC_ORGANIZATION_ID
              value: 00000000-0000-0000-0000-000000000000
            - name: ANTHROPIC_SERVICE_ACCOUNT_ID
              value: svac_...
            - name: ANTHROPIC_WORKSPACE_ID  # required when the rule covers multiple workspaces
              value: wrkspc_...
          volumeMounts:
            - name: anthropic-token
              mountPath: /var/run/secrets/anthropic.com
              readOnly: true
    ```
  </Step>

  <Step title="토큰의 클레임 형태 확인">
    프로젝션된 토큰은 클러스터의 OIDC 발급자가 서명한 "JSON Web Token"(JSON 웹 토큰), 즉 JWT입니다. `sub` 클레임은 Kubernetes 규칙인 `system:serviceaccount:<namespace>:<service-account-name>`을 따릅니다:

    ```json
    {
      "iss": "https://oidc.eks.us-west-2.amazonaws.com/id/6FA42E7BFDE8549CB...",
      "sub": "system:serviceaccount:inference:inference-worker",
      "aud": ["https://api.anthropic.com"],
      "kubernetes.io": {
        "namespace": "inference",
        "serviceaccount": { "name": "inference-worker", "uid": "..." }
      },
      "exp": 1775527120,
      "iat": 1775523520
    }
    ```

    `serviceAccountToken` 프로젝션은 `aud`를 `https://api.anthropic.com`으로 설정합니다. `AWS_WEB_IDENTITY_TOKEN_FILE`에 있는 별도의 IRSA 주입 토큰은 `aud: sts.amazonaws.com`을 가지며 이 교환이 아닌 AWS API 호출용입니다.
  </Step>
</Steps>

### Anthropic 구성

Claude Console에서 [Workload identity](https://platform.claude.com/settings/workload-identity-federation) 페이지(`platform.claude.com/settings/workload-identity-federation`)로 이동하여 **Connect workload**를 클릭하고 **AWS** 타일을 선택하세요. 마법사가 발급자 등록, 서비스 계정 생성, 페더레이션 규칙 생성 과정을 안내합니다.

마법사가 이러한 리소스를 대신 생성해 줍니다. 마법사에 입력하든 [Admin API](https://platform.claude.com/docs/ko/manage-claude/wif-admin-api)로 전송하든 다음 값을 사용하세요:

**페더레이션 발급자:** EKS 발급자는 공개 JWKS 엔드포인트를 노출하므로 디스커버리 모드를 사용하세요. 발급자 URL은 토큰의 `iss` 클레임과 정확히 일치해야 합니다. 클러스터당 하나의 발급자를 등록하세요.

```json
{
  "name": "prod-eks-uswest2",
  "issuer_url": "https://oidc.eks.us-west-2.amazonaws.com/id/6FA42E7BFDE8549CB...",
  "jwks": { "type": "discovery" }
}
```

**페더레이션 규칙:** Kubernetes `sub` 클레임과 Anthropic audience `https://api.anthropic.com`을 매칭하세요. (해당 audience로 전용 서비스 계정 토큰을 프로젝션하세요. IRSA 기본값인 `sts.amazonaws.com` 토큰을 재사용하지 마세요.)

```json
{
  "name": "prod-inference",
  "issuer_id": "fdis_...",
  "match": {
    "subject_prefix": "system:serviceaccount:inference:inference-worker",
    "audience": "https://api.anthropic.com"
  },
  "target": { "type": "service_account", "service_account_id": "svac_..." },
  "workspace_id": "wrkspc_...",
  "oauth_scope": "workspace:developer",
  "token_lifetime_seconds": 600
}
```

워크로드가 허용하는 한 최대한 구체적으로 지정하세요. 네임스페이스의 모든 서비스 계정이 동일한 Anthropic 서비스 계정에 매핑되어야 하는 경우에만 `subject_prefix`를 `system:serviceaccount:inference:*`로 완화하세요(끝의 `*`는 접두사 매칭을 의미합니다).

### 토큰 획득 및 사용

파드 내부에서 프로젝션된 토큰은 `/var/run/secrets/anthropic.com/token`에 있습니다(Pod 스펙에서 `ANTHROPIC_IDENTITY_TOKEN_FILE`로 노출됨). 해당 파일을 SDK의 페더레이션 자격 증명에 전달하면 SDK가 교환과 갱신을 처리합니다.

<CodeGroup>
  ```bash cURL
  JWT=$(cat "$ANTHROPIC_IDENTITY_TOKEN_FILE")

  RESPONSE=$(curl -sS https://api.anthropic.com/v1/oauth/token \
    -H "content-type: application/json" \
    --data @- <<JSON
  {
    "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
    "assertion": "$JWT",
    "federation_rule_id": "$ANTHROPIC_FEDERATION_RULE_ID",
    "organization_id": "$ANTHROPIC_ORGANIZATION_ID",
    "service_account_id": "$ANTHROPIC_SERVICE_ACCOUNT_ID",
    "workspace_id": "$ANTHROPIC_WORKSPACE_ID"
  }
  JSON
  )

  ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r .access_token)

  curl https://api.anthropic.com/v1/messages \
    -H "authorization: Bearer $ACCESS_TOKEN" \
    -H "anthropic-version: 2023-06-01" \
    -H "content-type: application/json" \
    -d '{
      "model": "claude-opus-5-5",
      "max_tokens": 1024,
      "messages": [{"role": "user", "content": "Hello from EKS"}]
    }' | jq -r '.content[] | select(.type == "text") | .text'
  ```

  ```python Python
  import os

  import anthropic
  from anthropic import IdentityTokenFile, WorkloadIdentityCredentials

  client = anthropic.Anthropic(
      credentials=WorkloadIdentityCredentials(
          identity_token_provider=IdentityTokenFile(
              os.environ["ANTHROPIC_IDENTITY_TOKEN_FILE"]
          ),
          federation_rule_id=os.environ["ANTHROPIC_FEDERATION_RULE_ID"],
          organization_id=os.environ["ANTHROPIC_ORGANIZATION_ID"],
          service_account_id=os.environ["ANTHROPIC_SERVICE_ACCOUNT_ID"],
          workspace_id=os.environ.get("ANTHROPIC_WORKSPACE_ID"),
      ),
  )

  message = client.messages.create(
      model="claude-opus-5-5",
      max_tokens=1024,
      messages=[{"role": "user", "content": "Hello from EKS"}],
  )
  print(next(block.text for block in message.content if block.type == "text"))
  ```

  ```typescript TypeScript
  import Anthropic from "@anthropic-ai/sdk";
  import { oidcFederationProvider } from "@anthropic-ai/sdk/lib/credentials/oidc-federation";
  import { identityTokenFromFile } from "@anthropic-ai/sdk/lib/credentials/identity-token";

  const client = new Anthropic({
    credentials: oidcFederationProvider({
      identityTokenProvider: identityTokenFromFile(process.env.ANTHROPIC_IDENTITY_TOKEN_FILE!),
      federationRuleId: process.env.ANTHROPIC_FEDERATION_RULE_ID!,
      organizationId: process.env.ANTHROPIC_ORGANIZATION_ID!,
      serviceAccountId: process.env.ANTHROPIC_SERVICE_ACCOUNT_ID,
      workspaceId: process.env.ANTHROPIC_WORKSPACE_ID,
      baseURL: "https://api.anthropic.com",
      fetch
    })
  });

  const message = await client.messages.create({
    model: "claude-opus-5-5",
    max_tokens: 1024,
    messages: [{ role: "user", content: "Hello from EKS" }]
  });
  for (const block of message.content) {
    if (block.type === "text") {
      console.log(block.text);
    }
  }
  ```

  ```go Go
  tokenPath := os.Getenv("ANTHROPIC_IDENTITY_TOKEN_FILE")

  readToken := option.IdentityTokenFunc(func(ctx context.Context) (string, error) {
  	raw, err := os.ReadFile(tokenPath)
  	if err != nil {
  		return "", fmt.Errorf("read identity token: %w", err)
  	}
  	return string(raw), nil
  })

  client := anthropic.NewClient(
  	option.WithFederationTokenProvider(readToken, option.FederationOptions{
  		FederationRuleID: os.Getenv("ANTHROPIC_FEDERATION_RULE_ID"),
  		OrganizationID:   os.Getenv("ANTHROPIC_ORGANIZATION_ID"),
  		ServiceAccountID: os.Getenv("ANTHROPIC_SERVICE_ACCOUNT_ID"),
  		WorkspaceID:      os.Getenv("ANTHROPIC_WORKSPACE_ID"),
  	}),
  )

  message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
  	Model:     anthropic.ModelClaudeOpus5_5,
  	MaxTokens: 1024,
  	Messages: []anthropic.MessageParam{
  		anthropic.NewUserMessage(anthropic.NewTextBlock("Hello from EKS")),
  	},
  })
  if err != nil {
  	panic(err)
  }
  for _, block := range message.Content {
  	if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok {
  		fmt.Println(textBlock.Text)
  		break
  	}
  }
  ```

  ```java Java
  AnthropicClient client = AnthropicOkHttpClient.fromEnv();

  var message = client.messages().create(MessageCreateParams.builder()
          .model(Model.CLAUDE_OPUS_5_5)
          .maxTokens(1024)
          .addUserMessage("Hello from EKS")
          .build());

  IO.println(message.content());
  ```

  ```csharp C#
  // ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID,
  // ANTHROPIC_SERVICE_ACCOUNT_ID, ANTHROPIC_WORKSPACE_ID, ANTHROPIC_IDENTITY_TOKEN_FILE 값을
  // pod의 환경 변수에서 읽습니다.
  using var client = new AnthropicClient();

  var message = await client.Messages.Create(new()
  {
      Model = Model.ClaudeOpus5_5,
      MaxTokens = 1024,
      Messages = [new() { Role = Role.User, Content = "Hello from EKS" }],
  });
  foreach (var block in message.Content)
  {
      if (block.Value is TextBlock textBlock)
      {
          Console.WriteLine(textBlock.Text);
      }
  }
  ```

  ```bash CLI
  # ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID,
  # ANTHROPIC_SERVICE_ACCOUNT_ID, ANTHROPIC_WORKSPACE_ID, ANTHROPIC_IDENTITY_TOKEN_FILE을 읽습니다
  ant messages create \
    --model claude-opus-5-5 \
    --max-tokens 1024 \
    --message '{role: user, content: "Hello from EKS"}'
  ```

  ```php PHP
  use Anthropic\Client;

  // ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID,
  // ANTHROPIC_SERVICE_ACCOUNT_ID, ANTHROPIC_WORKSPACE_ID, ANTHROPIC_IDENTITY_TOKEN_FILE을 읽습니다
  $client = new Client();

  $message = $client->messages->create(
      model: 'claude-opus-5-5',
      maxTokens: 1024,
      messages: [['role' => 'user', 'content' => 'Hello from EKS']],
  );
  $textBlock = array_find($message->content, static fn ($block): bool => $block->type === 'text');
  echo $textBlock->text, PHP_EOL;
  ```

  ```ruby Ruby
  require "anthropic"

  # ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID,
  # ANTHROPIC_SERVICE_ACCOUNT_ID, ANTHROPIC_WORKSPACE_ID, ANTHROPIC_IDENTITY_TOKEN_FILE을 읽습니다
  client = Anthropic::Client.new

  message = client.messages.create(
    model: "claude-opus-5-5",
    max_tokens: 1024,
    messages: [{role: "user", content: "Hello from EKS"}]
  )
  puts message.content.find { it.type == :text }.text
  ```
</CodeGroup>

<Tip>
  Pod 스펙이 이미 `ANTHROPIC_IDENTITY_TOKEN_FILE`, `ANTHROPIC_FEDERATION_RULE_ID`, `ANTHROPIC_ORGANIZATION_ID`, `ANTHROPIC_SERVICE_ACCOUNT_ID`, `ANTHROPIC_WORKSPACE_ID`를 설정하므로, 인수 없이 클라이언트를 생성하면 SDK가 페더레이션 환경 변수를 자동으로 읽습니다.
</Tip>

### 설정 확인

파드 내부에서 프로젝션된 토큰을 직접 교환하고 응답을 확인하세요:

```bash cURL
JWT=$(cat "$ANTHROPIC_IDENTITY_TOKEN_FILE")

curl -sS https://api.anthropic.com/v1/oauth/token \
  -H "content-type: application/json" \
  -d "{
    \"grant_type\": \"urn:ietf:params:oauth:grant-type:jwt-bearer\",
    \"assertion\": \"$JWT\",
    \"federation_rule_id\": \"$ANTHROPIC_FEDERATION_RULE_ID\",
    \"organization_id\": \"$ANTHROPIC_ORGANIZATION_ID\",
    \"service_account_id\": \"$ANTHROPIC_SERVICE_ACCOUNT_ID\",
    \"workspace_id\": \"$ANTHROPIC_WORKSPACE_ID\"
  }" | jq
```

교환에 성공하면 `sk-ant-oat01-`로 시작하는 `access_token`과 초 단위의 `expires_in` 값이 반환됩니다. 교환이 불투명한 `401` `authentication_error` 응답(메시지 `Authentication failed`)과 함께 실패하면, Claude Console의 [인증 기록 페이지](https://platform.claude.com/settings/workload-identity-federation?tab=history)에서 거부 사유를 확인하고 [실패한 교환 문제 해결](https://platform.claude.com/docs/ko/manage-claude/wif-reference#troubleshoot-a-failed-exchange)을 참조하세요. EKS 측에서 가장 흔한 원인은 프로젝션된 토큰의 `aud`가 규칙과 일치하지 않는 것입니다(IRSA 기본값인 `sts.amazonaws.com`이 아니라 `audience: https://api.anthropic.com`으로 토큰을 프로젝션하세요).

## 규칙 범위 지정

<Warning>
  `arn:aws:iam::123456789012:role/*`의 `subject_prefix`는 계정의 모든 IAM 역할과 매칭됩니다. 매칭되는 역할 중 하나라도 수임할 수 있는 모든 주체는 페더레이션된 Anthropic 토큰을 얻을 수 있습니다.
</Warning>

규칙의 `match` 블록을 사용 사례에 맞는 가장 좁은 범위로 제한하세요:

* **전체 역할 ARN 고정:** 계정의 다른 역할이 매칭되지 않도록 끝에 `*` 없이 `subject_prefix: "arn:aws:iam::<account>:role/<role-name>"`을 사용하세요.
* **계정 ID 고정:** 잘못 구성된 접두사에 대한 심층 방어 검사로서, `claims` 맵이나 CEL `condition`을 사용하여 토큰의 `https://sts.amazonaws.com/` 클레임의 `aws_account` 필드를 매칭하세요.
* **EKS에서 네임스페이스와 서비스 계정 고정:** `system:serviceaccount:` 접두사 뒤에 `*` 없이 정확한 `system:serviceaccount:<namespace>:<name>` 값을 사용하세요.
* **환경별로 별도의 규칙 사용:** 하나의 접두사를 넓혀 모두를 포괄하기보다는 프로덕션, 스테이징, 개발 워크로드에 대해 각각 별도의 규칙을 생성하세요.

## 다음 단계

* 전체 자격 증명 우선순위, 프로필 구성, 규칙 매칭 레퍼런스는 [WIF 레퍼런스](https://platform.claude.com/docs/ko/manage-claude/wif-reference)를 검토하세요.
* EKS가 아닌 자체 관리형 Kubernetes 클러스터의 경우 [Kubernetes에서 WIF 사용하기](https://platform.claude.com/docs/ko/manage-claude/wif-providers/kubernetes)를 참조하세요.
