13
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AgentCore Gateway L2 Constructを実装する時の注意点とポイントまとめ

13
Last updated at Posted at 2025-12-01

2026/01/20 更新

@mazyu36 さんがコントリビュートしてくださいました🙏
本記事のように面倒なことをする必要はなくなりました!ありがとうございます;;

こんにちは、ふくちです。
先日、AgentCore GatewayのL2 Constructがalpha版として登場しました。

今回はRuntimeとGatewayをともにL2 Constructで実装してエージェントを動かし、その際の注意点・現状ならこうするべき(かも)というプラクティスをご紹介します。

以下を参考にしました。

現状のAgentCore Gateway L2 Constructの制限

alpha版だからなのか、いくつか制限事項があります。特に2点目については大きな問題なので、注意が必要です。

  • ターゲットにおいて統合プロバイダーをCDK上で設定できない
  • デフォルト作成されるCognitoだとM2M認証になっていない

ターゲットにおいて統合プロバイダーをCDK上で設定できない

AgentCore Gatewayの構成要素は大きく「Gateway本体」と「そのGatewayからの接続先(ターゲット)」の2つです。

CDKで書く時も、上記を意識して書く必要があります。例えば以下の感じです。
Gatewayを作成しておいて、そこにターゲットを追加して紐づけていく形です。

// Gateway本体を作成する
const gateway = new agentcore.Gateway(this, 'LambdaGateway', {
    gatewayName,
    description: 'Agents can access to Lambda function to get current time.',
    protocolConfiguration: new agentcore.McpProtocolConfiguration({
        instructions: 'Agents can access to Lambda function to get current time.',
        searchType: agentcore.McpGatewaySearchType.SEMANTIC, // ツールのセマンティック検索有効化
        supportedVersions: [
            agentcore.MCPProtocolVersion.MCP_2025_03_26,
        ],
    }),
    // authorizerConfigurationの設定を省略するとInbound Auth用Cognitoが自動で作成される
    // 実装例(JWT): authorizerConfiguration: agentcore.GatewayAuthorizer.usingCustomJwt()
    // 実装例(IAM): authorizerConfiguration: agentcore.GatewayAuthorizer.usingAwsIam()
});

// AgentCore Gatewayのターゲット登録(Lambda関数の場合)
// 最終的なツール名は「<gatewayTargetName>__<toolSchema.name>」の形
// ここで言うと「lambda-function-target__get-current-time」
const lambdaTarget = gateway.addLambdaTarget('LambdaTarget', {
    gatewayTargetName: 'lambda-function-target',
    description: 'Agents can get current time.',
    lambdaFunction: toolFunction,
    toolSchema: toolSchema
});

// 他のターゲットは以下の形で追加可能
gateway.addMcpServerTarget('id', {props})
gateway.addOpenApiTarget('id', {props})
gateway.addSmithyTarget('id', {props})

ここで重要なのが、ターゲットで設定可能なのが以下の3つのカテゴリだけということです。

  • Lambda ARM
  • MCP Server
  • REST API(OpenAPI, Smithy)

コンソール上で確認できる統合プロバイダーの設定は、CDKでは不可能(というかAPI経由での設定自体不可能)です。
マネジメントコンソール上からしか設定不可とドキュメントに明記されています。
image.png

You can only add an integration provider template as a target through the AWS Management Console and not through the API.
統合プロバイダーテンプレートをターゲットとして追加できるのは、AWS マネジメントコンソール経由のみであり、API 経由ではありません。

したがって、統合プロバイダーを用いる場合はGatewayだけをCDKで作成し、ターゲットはあとからマネジメントコンソールで設定という形になります。

デフォルト作成されるCognitoだとM2M認証になっていない

これがちょっと問題です。

前提として、AgentCore RuntimeからGatewayを用いる際、Gateway側のインバウンド認証でJSON Web Tokens(JWT)が必要です。Cognitoなどを用いるやつです。
(IAM認証でもできるのかもしれませんが、まだ試せていません。)

コンソール上からだとこのCognitoのクイック作成が可能です。これを選択してGatewayをデプロイすると、自動でM2M認証のCognitoユーザープールができあがります。
image.png

image.png
作成されたユーザープールにおいてM2M認証かどうかを確認するには、「アプリケーションクライアント」→「ログインページ」タブ→OAuth付与タイプが「クライアント認証情報の付与」になっているかを確認すれば良さそうです。参考は以下。

また、カスタムスコープは「{Gatewayの名前}/genesis-gateway/invoke」が自動で設定されます。


CDKの話に戻りましょう。
Gateway L2 Constructでも、Gateway作成時に認証情報の設定をスキップすると、自動でCognitoが作成されます。

const gateway = new agentcore.Gateway(this, 'LambdaGateway', {
    gatewayName,
    description: 'Agents can access to Lambda function to get current time.',
    protocolConfiguration: new agentcore.McpProtocolConfiguration({
        instructions: 'Agents can access to Lambda function to get current time.',
        searchType: agentcore.McpGatewaySearchType.SEMANTIC, // ツールのセマンティック検索有効化
        supportedVersions: [
            agentcore.MCPProtocolVersion.MCP_2025_03_26,
        ],
    }),
    // authorizerConfigurationの設定を省略するとInbound Auth用Cognitoが自動で作成される
    // 実装例(JWT): authorizerConfiguration: agentcore.GatewayAuthorizer.usingCustomJwt()
    // 実装例(IAM): authorizerConfiguration: agentcore.GatewayAuthorizer.usingAwsIam()
});

By default, if not provided, the construct will create and configure Cognito as the default identity provider (inbound Auth setup).
デフォルトでは、指定されていない場合、コンストラクトはCognitoをデフォルトのIDプロバイダとして作成および設定します(インバウンド認証の設定)。

私はてっきり、マネジメントコンソールから作成した時と同じようなM2M認証のCognitoユーザープールが作成されると思っていたのですが、実態を確認すると違うものになっていました。
image.png

OAuth付与タイプが「クライアント認証情報の付与」になっておらず、またカスタムスコープも設定されていません。もっと言うとクライアントシークレットも設定されていないです(画面には写っていませんが)。

これだとAgentCore IdentityでResource Credential Providerを設定することができないため、エージェントからGatewayを呼び出すことができません。

したがって、Gateway L2 Constructを用いてエージェントが用いるGatewayを作成したい場合、Cognitoの自前実装+Gateway作成時のcustomJWT設定が必要です。

厳密に言うと、自前実装したCognitoを用いてResource Credential Providerを作成する必要があります。
このProviderはAgentCore Identityで作成するのですが、現状IdentityはCDKで作ることができません。

したがって、

  • Resource Credential Provider→コンソールからの手動作成(もしくはCLI/SDKでの作成)
  • それを見越した設定をCDK側で入れ込んであげる必要がある

という形になります。

できるだけCDKを使ってリソース構築してエージェントを動かしてみる

ここまでの制約を踏まえて、現状のCDK実装における最適解の一案を以下に示しておきます。
例えば、AgentCore Runtime + Gatewayを作成し、ターゲットにLambda関数と統合プロバイダーのSlackを設定してみましょう。

CDKでは以下のリソースを作成します。

  • AgentCore Gateway
    • Gateway本体
    • 自前Cognito
    • 現在時刻を取得するLambdaターゲットとスキーマ定義
    • Gatewayに紐づくIAMロール(L2 Construct使用)
  • AgentCore Runtime
    • エージェントのソースコードをビルドするCodeBuildプロジェクト(L3 Construct使用)
    • Runtime本体
    • Runtimeに紐づくIAMロールやECR、CloudWatch Logs(L2 Construct使用)

その後、コンソール上で以下を設定します。

  • AgentCore Identity
    • Slack Bot TokenをAPI Keyとして設定
    • 自前Cognitoを元にしたResource Credential Providerを設定

AgentCore Gateway L2 Constructの実装例

AgentCore GatewayとRuntimeをそれぞれL2 Constructで作成しています。
また、上記注意点のとおり、Cognitoを自前実装してGatewayのインバウンド認証でM2M認証ができるようにしてあります。

Runtimeにデプロイするエージェントをビルドする際は、以下ブログを参考にdeploy-time-buildというL3 Constructを用いて簡略化しています。

lib/gateway-with-self-cognito.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as cognito from 'aws-cdk-lib/aws-cognito';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as agentcore from '@aws-cdk/aws-bedrock-agentcore-alpha';
import { Platform } from 'aws-cdk-lib/aws-ecr-assets';
import { ContainerImageBuild } from 'deploy-time-build';

export interface GatewayWithSelfCognitoStackProps extends cdk.StackProps {
  providerName: string;
  gatewayName: string;
  apiKeyName: string;
}

export class GatewayWithSelfCognitoStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: GatewayWithSelfCognitoStackProps) {
    super(scope, id, props);

	const { providerName, gatewayName, apiKeyName } = props;

    // ============Cognito自前構築(M2M認証用)============

    // 1. User Pool
    const userPool = new cognito.UserPool(this, 'GatewayUserPool', {
      userPoolName: `${gatewayName}-userpool`,
      selfSignUpEnabled: false,
    });

    // Cognitoドメイン(client_credentials フローのトークンエンドポイントに必要)
    userPool.addDomain('GatewayUserPoolDomain', {
      cognitoDomain: {
        domainPrefix: `${gatewayName}-${this.account}`,
      },
    });

    // 2. Resource Server + カスタムスコープ
    const invokeOAuthScope = new cognito.ResourceServerScope({
      scopeName: 'genesis-gateway:invoke',
      scopeDescription: 'Invoke AgentCore Gateway',
	});

	const resourceServer = userPool.addResourceServer('GatewayResourceServer', {
      identifier: gatewayName,
	  scopes: [invokeOAuthScope],
	});

    const invokeScope = cognito.OAuthScope.resourceServer(
      resourceServer,
      invokeOAuthScope,
    );

    // 3. M2M用クライアント(client_credentials + secret)
    const m2mClient = userPool.addClient('GatewayM2MClient', {
      generateSecret: true,
      oAuth: {
        flows: { clientCredentials: true },
        scopes: [invokeScope],
      },
    });

    // 4. discoveryUrlの構築
    const discoveryUrl = `https://cognito-idp.${this.region}.amazonaws.com/${userPool.userPoolId}/.well-known/openid-configuration`;

    // カスタムスコープ(Identity設定で使用)
    // 実態としては'<gatewayName>/genesis-gateway:invoke'な形
    const cognitoScope = `${resourceServer.userPoolResourceServerId}/${invokeOAuthScope.scopeName}`;

    // ============Cognito自前構築============

    // ============AgentCore Gateway============

    const gateway = new agentcore.Gateway(this, 'AgentCoreGateway', {
      gatewayName,
      description: 'Agents can access to Lambda functions and Slack workspace.',
      protocolConfiguration: new agentcore.McpProtocolConfiguration({
        instructions: 'Agents can access to Lambda functions and Slack workspace.',
        searchType: agentcore.McpGatewaySearchType.SEMANTIC,
        supportedVersions: [
          agentcore.MCPProtocolVersion.MCP_2025_06_18,
        ],
      }),
      // 自前CognitoをCustom JWTとして使用
	  authorizerConfiguration: agentcore.GatewayAuthorizer.usingCustomJwt({
		discoveryUrl,
		allowedClients: [m2mClient.userPoolClientId],
      }),
    });

    // GatewayサービスロールにOutbound認証権限を追加
    gateway.role.addToPrincipalPolicy(new iam.PolicyStatement({
      actions: [
        'bedrock-agentcore:GetWorkloadAccessToken',
        'bedrock-agentcore:GetResourceApiKey',
      ],
      resources: [
        `arn:aws:bedrock-agentcore:${this.region}:${this.account}:workload-identity-directory/default`,
        `arn:aws:bedrock-agentcore:${this.region}:${this.account}:workload-identity-directory/default/workload-identity/${gatewayName}-*`,
        `arn:aws:bedrock-agentcore:${this.region}:${this.account}:token-vault/default`,
        `arn:aws:bedrock-agentcore:${this.region}:${this.account}:token-vault/default/apikeycredentialprovider/*`,
      ],
    }));

    // GatewayサービスロールにSecrets Manager権限を追加(API Key取得用)
    gateway.role.addToPrincipalPolicy(new iam.PolicyStatement({
      actions: ['secretsmanager:GetSecretValue'],
      resources: [
        `arn:aws:secretsmanager:${this.region}:${this.account}:secret:bedrock-agentcore-identity!default/apikey/${apiKeyName}-*`,
      ],
    }));

    // AgentCore Gatewayのターゲット登録のため、Lambda関数も作成する
    const toolFunction = new lambda.Function(this, 'ToolFunction', {
      runtime: lambda.Runtime.PYTHON_3_14,
      handler: 'gateway_target.lambda_handler',
      code: lambda.Code.fromAsset('lambda')
    });

    // ToolSchemaはinline / S3 / localAsset のいずれか
    const toolSchema = agentcore.ToolSchema.fromInline([
      {
        name: 'get-current-time',
        description: '指定されたタイムゾーンの現在時刻をISO 8601形式で返すツール',
        inputSchema: {
          type: agentcore.SchemaDefinitionType.OBJECT,
          properties: {
            timezone: {
              type: agentcore.SchemaDefinitionType.STRING,
              description: 'タイムゾーン(例: Asia/Tokyo, UTC, America/New_York)。デフォルトはAsia/Tokyo'
            }
          }
          // required: ['timezone'] とすれば必須パラメータにできるが今回は省略
        },
        outputSchema: {
          type: agentcore.SchemaDefinitionType.OBJECT,
          properties: {
            current_time: {
            type: agentcore.SchemaDefinitionType.STRING,
              description: 'ISO 8601形式の現在時刻'
            },
            timezone: {
              type: agentcore.SchemaDefinitionType.STRING,
              description: '使用したタイムゾーン'
            }
          }
        }
      }
    ]);

    // AgentCore Gatewayのターゲット登録
    // 3rd Partyツールとの統合はCDK上では不可能。Lambda/MCP/OpenAPI/Smithyのみ対応
    // 最終的なツール名は「<gatewayTargetName>__<toolSchema.name>」の形
    // ここで言うと「lambda-function-target__get-current-time」
    const lambdaTarget = gateway.addLambdaTarget('LambdaTarget', {
      gatewayTargetName: 'lambda-function-target',
      description: 'Agents can get current time.',
      lambdaFunction: toolFunction,
      toolSchema: toolSchema
    });

    // GatewayのURLはAgentCore Runtimeの環境変数に設定する
    if (!gateway.gatewayUrl) {
      throw new Error('GatewayのURLが発行されていません')
    };
    const gatewayUrl: string = gateway.gatewayUrl;

    // ============AgentCore Gateway============

    // ============AgentCore Runtime============

    // 「deploy-time-build」というL3 Constructを使ってCodeBuildプロジェクト構築~buildキックまで自動的に実施
    const agentcoreRuntimeImage = new ContainerImageBuild(this, 'AgentWithGatewayImage', {
      directory: './agent',
      platform: Platform.LINUX_ARM64,
    });

    // AgentCore Runtime(L2 Construct)
    const runtime = new agentcore.Runtime(this, 'AgentCoreRuntime', {
      runtimeName: 'StrandsAgentWithGateway',
      agentRuntimeArtifact: agentcore.AgentRuntimeArtifact.fromEcrRepository(
        agentcoreRuntimeImage.repository,
        agentcoreRuntimeImage.imageTag
      ),
      description: 'Gateway経由でツールを使えるStrands Agent',
      environmentVariables: {
        GATEWAY_URL: gatewayUrl,
        PROVIDER_NAME: providerName,
        COGNITO_SCOPE: cognitoScope,
      }
    });

    // RuntimeのIAMロールにIdentity Token Vault へのアクセス権限を追加
    // GetResourceOauth2Token: Identity経由でOAuth2トークンを取得するために必要
    runtime.role.addToPrincipalPolicy(new iam.PolicyStatement({
      actions: ['bedrock-agentcore:GetResourceOauth2Token'],
      resources: [
        `arn:aws:bedrock-agentcore:${this.region}:${this.account}:token-vault/default`,
        `arn:aws:bedrock-agentcore:${this.region}:${this.account}:token-vault/default/oauth2credentialprovider/${providerName}`,
        `arn:aws:bedrock-agentcore:${this.region}:${this.account}:workload-identity-directory/default`,
        `arn:aws:bedrock-agentcore:${this.region}:${this.account}:workload-identity-directory/default/workload-identity/*`,
      ],
    }));

    // Secrets Manager へのアクセス権限(Cognito クライアントシークレット取得用)
    // シークレット名: bedrock-agentcore-identity!default/oauth2/<providerName>
    runtime.role.addToPrincipalPolicy(new iam.PolicyStatement({
      actions: ['secretsmanager:GetSecretValue'],
      resources: [
        `arn:aws:secretsmanager:${this.region}:${this.account}:secret:bedrock-agentcore-identity!default/oauth2/${providerName}-*`,
      ],
    }));

    // Bedrock 基盤モデル・Inference Profile へのアクセス権限
    runtime.role.addToPrincipalPolicy(new iam.PolicyStatement({
      actions: [
        'bedrock:InvokeModel',
        'bedrock:InvokeModelWithResponseStream',
      ],
      resources: [
        // 基盤モデル(東京・大阪)
        'arn:aws:bedrock:ap-northeast-1::foundation-model/*',
        'arn:aws:bedrock:ap-northeast-3::foundation-model/*',
        // Inference Profile(東京・大阪)
        `arn:aws:bedrock:ap-northeast-1:${this.account}:inference-profile/*`,
        `arn:aws:bedrock:ap-northeast-3:${this.account}:inference-profile/*`,
      ],
    }));

    // ============AgentCore Runtime============

    new cdk.CfnOutput(this, 'AgentCoreRuntimeArn', {
      value: runtime.agentRuntimeArn,
    });

    new cdk.CfnOutput(this, 'ResourceCredentialProviderName', {
      value: providerName,
      description: 'Resource Credential Provider名(Identityコンソールで同名のProviderを作成する)',
    });

    // Cognito関連の出力(Identity設定時に必要)
    new cdk.CfnOutput(this, 'CognitoUserPoolId', {
      value: userPool.userPoolId,
      description: 'Cognito User Pool ID',
    });

    new cdk.CfnOutput(this, 'CognitoClientId', {
      value: m2mClient.userPoolClientId,
      description: 'M2MクライアントID(トークン取得時に使用)',
    });

    new cdk.CfnOutput(this, 'CognitoScope', {
      value: cognitoScope,
      description: 'カスタムスコープ(トークン取得時に使用)',
    });
  }
}

Cognito自前実装の理由とポイント

ここから、簡単にコードの解説をしていきます。

CDKドキュメントを調査した結果、以下のことがわかりました。

  1. Gateway作成時にauthorizerConfigurationを省略するとCognitoがデフォルトで自動作成される
  2. ただしGatewayAuthorizerで選択可能なのは以下の2つのみ
    • GatewayAuthorizer.usingCustomJwt(...) - カスタムJWT
    • GatewayAuthorizer.usingAwsIam() - IAM認証
  3. デフォルトCognitoをカスタマイズするAPIは存在しない

そして前述のとおり、デフォルトCognitoはM2M認証(Identity→Gateway連携)には使えません。

そこでCognitoを自前で作成し、GatewayAuthorizer.usingCustomJwt()で接続するのが現時点のベストプラクティスです。

lib/gateway-with-self-cognito.ts
    // ============Cognito自前構築(M2M認証用)============
    // (一部略)
    // 3. M2M用クライアント(client_credentials + secret)
    const m2mClient = userPool.addClient('GatewayM2MClient', {
      generateSecret: true,
      oAuth: {
        flows: { clientCredentials: true },
        scopes: [invokeScope],
      },
    });

    // 4. discoveryUrlの構築
    const discoveryUrl = `https://cognito-idp.${this.region}.amazonaws.com/${userPool.userPoolId}/.well-known/openid-configuration`;

    // ============Cognito自前構築============

    // ============AgentCore Gateway============

    const gateway = new agentcore.Gateway(this, 'AgentCoreGateway', {
      // (一部略)
      // 自前CognitoをCustom JWTとして使用
	  authorizerConfiguration: agentcore.GatewayAuthorizer.usingCustomJwt({
		discoveryUrl,
		allowedClients: [m2mClient.userPoolClientId],
      }),
    });

エージェント側で必要な設定を事前に準備する

RuntimeにデプロイしたエージェントがGatewayへ接続できるようにするためには、エージェント側の実装コードで設定が必要です。

具体的には以下の3つのパラメーターが必要です。

  • AgentCore GatewayのリソースURL
  • AgentCore Identityに設定するResource Credential Providerの名前
  • Cognitoのカスタムスコープ

これらのパラメーターを、Runtimeの環境変数として予め登録しておくと、エージェント側で必要なパラメーターをサクッと取得できて非常にスマートです。なので、Runtime作成時に environmentVariables として設定してあげましょう。

GatewayのリソースURLは、L2 Constructで実装したGatewayから参照可能です。

Resource Credential Providerの名前は、ここで自由に決めてOKです。コードにベタ書きするよりはcdk.jsonなどで変更可能なパラメータとしておくのが良いかもしれません。
ただし注意点としては、ここで設定した名前を用いてこの後AgentCore IdentityコンソールからResource Credential Provider名を設定する必要があります(後述します)。

Cognitoのカスタムスコープは、マネジメントコンソールからクイック作成した際のスコープが「{Gatewayの名前}/genesis-gateway:invoke」です。
したがって、この形になるようにリソースサーバーなどを設定し、最終的にはこのスコープをRuntimeの環境変数に設定します。

ということで、こんな感じはいかがでしょうか。

lib/gateway-with-self-cognito.ts
    const { providerName, gatewayName, apiKeyName } = props;
    
    // ============Cognito自前構築(M2M認証用)============
    // 2. Resource Server + カスタムスコープ
    const invokeOAuthScope = new cognito.ResourceServerScope({
      scopeName: 'genesis-gateway:invoke',
      scopeDescription: 'Invoke AgentCore Gateway',
	});

	const resourceServer = userPool.addResourceServer('GatewayResourceServer', {
      identifier: gatewayName,
	  scopes: [invokeOAuthScope],
	});

    const invokeScope = cognito.OAuthScope.resourceServer(
      resourceServer,
      invokeOAuthScope,
    );

    // カスタムスコープ(Identity設定で使用)
    // 実態としては'<gatewayName>/genesis-gateway:invoke'な形
    const cognitoScope = `${resourceServer.userPoolResourceServerId}/${invokeOAuthScope.scopeName}`;
    // ============Cognito自前構築============

    // ============AgentCore Gateway============
    const gateway = new agentcore.Gateway(this, 'AgentCoreGateway', {
      // (中略)
    });

    // GatewayのURLはAgentCore Runtimeの環境変数に設定する
    // gatewayUrlの型が string | undefined なので、存在しない場合はエラーとしておきます
    if (!gateway.gatewayUrl) {
      throw new Error('GatewayのURLが発行されていません')
    };
    const gatewayUrl: string = gateway.gatewayUrl;
    // ============AgentCore Gateway============

    // ============AgentCore Runtime============
    // AgentCore Runtime(L2 Construct)
    const runtime = new agentcore.Runtime(this, 'AgentCoreRuntime', {
      // (中略)
      environmentVariables: {
        GATEWAY_URL: gatewayUrl,
        PROVIDER_NAME: providerName,
        COGNITO_SCOPE: cognitoScope,
      }
    });
    // ============AgentCore Runtime============

RuntimeとGatewayのサービスロールに権限追加

デフォルトで作成されるサービスロールは、結構権限足りてないことがありそうです。
ひとまず確認した範囲では、

  • Runtime:
    • CloudWatch関連の権限(ログ出力やメトリクスなど)
    • X-Ray関連の権限(GenAI Observability用)
    • Workload Access Token取得権限(※後ほど補足あり)
    • ECRからのイメージ取得権限
  • Gateway:
    • 実行するLambdaターゲットの権限

このくらいのがアタッチされています。

これだとIdentityがほとんど使えないんですよね。Identity関連の設定はCDK上で全く行えないので、L2 Constructといえど当然権限が不足しています。
RuntimeにはBedrockの呼び出し権限すらついてないので、それも追加してあげる必要があります。

AgentCore Identityはエージェントが外部サービスに安全にアクセスするための認証基盤です。以下の3つのコンポーネントで構成されています。

  • Token Vault: 認証情報(OAuth2トークンやAPI Key)の保管場所
  • Workload Identity: RuntimeやGatewayの一意なIdentityを示す
  • Credential Provider: 外部サービスへの認証方式を定義

Runtimeへの権限追加

まずはRuntimeからです。
デフォルト権限のまま実行すると、Runtimeで以下のエラーが発生します(Bedrock呼び出し権限不足のエラーは省略)。

AccessDeniedException: User: arn:aws:sts::***:assumed-role/...-AgentCoreRuntimeExecution-.../BedrockAgentCore-...
is not authorized to perform: bedrock-agentcore:GetResourceOauth2Token on resource:

  • bedrock-agentcore:GetResourceOauth2Token on resource:
    arn:aws:bedrock-agentcore:ap-northeast-1:***:token-vault/default
  • arn:aws:bedrock-agentcore:ap-northeast-1:***:token-vault/default/oauth2credentialprovider/{cdk.jsonのproviderName}
  • arn:aws:bedrock-agentcore:ap-northeast-1:***:workload-identity-directory/default
  • arn:aws:bedrock-agentcore:ap-northeast-1:***:workload-identity-directory/default/workload-identity/{Runtimeの名前}-*

AccessDeniedException: The requested operation could not be completed.
You are not authorized to perform: secretsmanager:GetSecretValue

これらの権限不足エラーは、RuntimeがIdentity経由でOAuth2トークンを取得しにいこうとして、色んなフェーズで失敗しているということです。

ということで、以下の5つを追加してあげます。

権限 リソース 目的 使用タイミング
bedrock-agentcore:GetResourceOauth2Token token-vault/default Token Vaultディレクトリへのアクセス OAuth2トークン取得時
bedrock-agentcore:GetResourceOauth2Token token-vault/default/oauth2credentialprovider/{providerName} 特定のOAuth2 Credential Providerへのアクセス OAuth2トークン取得時
bedrock-agentcore:GetResourceOauth2Token workload-identity-directory/default Workload Identityディレクトリへのアクセス Workload Access Token取得時
bedrock-agentcore:GetResourceOauth2Token workload-identity-directory/default/workload-identity/{Runtimeのname}-* Runtime自身のWorkload Identityへのアクセス Workload Access Token取得時
secretsmanager:GetSecretValue secret:bedrock-agentcore-identity!default/oauth2/{providerName}-* Cognitoクライアントシークレットの取得 client_credentials フロー実行時

ちなみにこれらの権限は公式のIdentity Quick Startでも同様の権限を追加しているので、こちらもご参照ください。

エージェントが外部サービスにアクセスする流れをざっくり可視化するとこんな感じになります。

上記でRuntime→Identityに対してGetResourceOauth2Tokenする際の権限として、上記の5つが必要になります。ここでToken Vault, Workload Identity, Credential Providerへアクセスするわけですね。

ちなみにIdentityは裏側でSecrets Managerに認証情報を保管するので、Secrets Managerの権限も必要です。

これらの権限は、エージェント側のコードで@requires_access_tokenデコレータを用いた処理で使われます。

また、しれっと上記でIdentityのサービスリンクロールというのが出てきていますが、これは2025/10/13に追加されたものです。実ロール名は AWSServiceRoleForBedrockAgentCoreRuntimeIdentity

詳しくは以下をご参照いただければと思いますが、ざっくり言うとWorkload Access TokenとOAuth認証情報を取得するものだそうです。
現在はWorkload Access Tokenこちらのロールを使うようになっているそうです。

RuntimeのL2 Construct作成時に自動作成されるIAMポリシーにおいて、デフォルトでWorkload Access Tokenを取得する権限が追加されているのですが、これは消してもちゃんと動きました。不要なので、気になる方は削除しておきましょう。

Runtime自動作成IAMポリシーで以下は不要(Identityサービスリンクロールに全く同じ権限がある)
{
    "Action": [
        "bedrock-agentcore:GetWorkloadAccessToken",
        "bedrock-agentcore:GetWorkloadAccessTokenForJWT",
        "bedrock-agentcore:GetWorkloadAccessTokenForUserId"
    ],
    "Resource": [
        "arn:aws:bedrock-agentcore:ap-northeast-1:***:workload-identity-directory/default",
        "arn:aws:bedrock-agentcore:ap-northeast-1:***:workload-identity-directory/default/workload-identity/*"
    ],
    "Effect": "Allow",
    "Sid": "GetAgentAccessToken"
}

Gatewayへの権限追加

続いてはGatewayです。
デフォルト権限のまま実行すると、Gatewayで以下のエラーが発生します。

AccessDeniedException: User: arn:aws:sts::***:assumed-role/...-AgentCoreGatewayExecution-.../BedrockAgentCore-...
is not authorized to perform: bedrock-agentcore:GetWorkloadAccessToken on resource:

  • bedrock-agentcore:GetResourceOauth2Token on resource:
    arn:aws:bedrock-agentcore:ap-northeast-1:***:workload-identity-directory/default
  • arn:aws:bedrock-agentcore:ap-northeast-1:***:workload-identity-directory/default/workload-identity/{gatewayName}-*

AccessDeniedException: User: arn:aws:sts::***:assumed-role/...-AgentCoreGatewayExecution-.../BedrockAgentCore-...
is not authorized to perform: bedrock-agentcore:GetResourceApiKey on resource:

  • arn:aws:bedrock-agentcore:ap-northeast-1:***:token-vault/default
  • arn:aws:bedrock-agentcore:ap-northeast-1:***:token-vault/default/apikeycredentialprovider/*

AccessDeniedException: The requested operation could not be completed.
You are not authorized to perform: secretsmanager:GetSecretValue

ということで、エラー文が変わっただけで、基本的には同じような権限を追加してあげればOKです。アクション名が異なるところには注意が必要です。

今回はIdentityの登録でAPI Keyを使っているので以下の権限ですが、OAuthを選んだ場合はまた変わるので注意です。

権限 リソース 目的 使用タイミング
bedrock-agentcore:GetWorkloadAccessToken workload-identity-directory/default Workload Identityディレクトリへのアクセス Outbound認証開始時
bedrock-agentcore:GetWorkloadAccessToken workload-identity-directory/default/workload-identity/{gatewayName}-* Gateway自身のWorkload Identityへのアクセス Outbound認証開始時
bedrock-agentcore:GetResourceApiKey token-vault/default Token Vaultディレクトリへのアクセス API Key取得時
bedrock-agentcore:GetResourceApiKey token-vault/default/apikeycredentialprovider/* API Key Credential Providerへのアクセス API Key取得時
secretsmanager:GetSecretValue secret:bedrock-agentcore-identity!default/apikey/{apiKeyName}-* API Keyシークレットの取得 API Key取得時

Gatewayから外部リソースにアクセスする際の流れとしては以下のとおりです。

Identityが登録するSecrets Manager名

この後、マネジメントコンソールからAgentCore Identityに2つ登録します。
その際の命名規則は以下のとおりです。

認証タイプ シークレット名パターン
OAuth2 Credential bedrock-agentcore-identity!default/oauth2/{providerName}
API Key Credential bedrock-agentcore-identity!default/apikey/{apiKeyName}

なのでこの命名規則に従って、CDK側で権限設定してあげれば最小権限で運用できるようになります。
ただし、ARNの場合は末尾にランダムなサフィックス(-xxxxxx)が付くため、IAMポリシーでは -* を付けてワイルドカード指定する必要があります。

`arn:aws:secretsmanager:${region}:${account}:secret:bedrock-agentcore-identity!default/oauth2/${providerName}-*`

`arn:aws:secretsmanager:${region}:${account}:secret:bedrock-agentcore-identity!default/apikey/${apiKeyName}-*`

CDkプロジェクトをデプロイできるようにする

上記の実装にした場合のCDKプロジェクトサンプルコードです。
以下GitHubに同じ物を置いてます。

他の実装も気になる方はこちら(長いです) ```bin/serverless-agent-app.ts import * as cdk from 'aws-cdk-lib'; import { GatewayWithSelfCognitoStack } from '../lib/gateway-with-self-cognito-stack';

const app = new cdk.App();

// cdk.jsonのcontextから読み込み
const config = app.node.tryGetContext('agentCoreConfig') as {
providerName: string;
gatewayName: string;
apiKeyName: string;
};

if (!config?.providerName || !config?.gatewayName || !config?.apiKeyName) {
throw new Error('cdk.jsonのagentCoreConfigにproviderNameとgatewayNameとapiKeyNameを設定してください');
}

new GatewayWithSelfCognitoStack(app, 'GatewayWithSelfCognitoStack', {
providerName: config.providerName,
gatewayName: config.gatewayName,
apiKeyName: config.apiKeyName,
});


```json:cdk.json
{
  "context": {
    "agentCoreConfig": {
      "providerName": "<任意のResource Credential Provider名>",
      "gatewayName": "<任意のAgentCore Gateway名>",
      "apiKeyName": "<任意のAPI Key名>"
    }
}
agent/agent.py
import os
from strands import Agent
from strands.models import BedrockModel
from strands.tools.mcp import MCPClient
from mcp.client.streamable_http import streamablehttp_client
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from bedrock_agentcore.identity.auth import requires_access_token
from typing import Dict, Any
from strands.types.content import ContentBlock

# エージェントアプリケーションの初期化
app = BedrockAgentCoreApp()


def format_content_block(block: ContentBlock) -> str:
    """ContentBlockを読みやすい文字列に変換"""
    if 'text' in block:
        return block['text']
    elif 'toolUse' in block:
        tool = block['toolUse']
        return f"[ツール呼出] {tool['name']} (入力: {tool.get('input', {})})"
    elif 'toolResult' in block:
        result = block['toolResult']
        status = result.get('status', 'unknown')
        content = result.get('content', [])
        content_str = ', '.join(
            c.get('text', c.get('json', str(c))) if isinstance(c, dict) else str(c)
            for c in content
        )
        return f"[ツール結果] {status}: {content_str}"
    elif 'reasoningContent' in block:
        reasoning = block['reasoningContent']
        text = reasoning.get('reasoningText', {}).get('text', '')
        return f"[推論] {text[:100]}..." if len(text) > 100 else f"[推論] {text}"
    else:
        return str(block)

# アプリケーションのエントリーポイント
@app.entrypoint
async def get_time_and_slack_agent(payload: Dict[str, Any]):
    """
    AgentCore Gateway+Identityを用いてツールを実行するエージェント
    Lambdaツールからは現在時間を取得し、Slackツールを使ってメッセージを取得したり書き込んだりする
    """
    print("📋 エージェント起動")
    print(f"受信したペイロード: {payload}")

    # AgentCore Identityを使用してGatewayにアクセス
    gateway_url = os.environ.get("GATEWAY_URL")
    provider_name = os.environ.get("PROVIDER_NAME")
    cognito_scope = os.environ.get("COGNITO_SCOPE")

    if not gateway_url or not provider_name or not cognito_scope:
        raise ValueError("環境変数 GATEWAY_URL, PROVIDER_NAME, COGNITO_SCOPE が設定されていません")

    @requires_access_token(
        provider_name=provider_name,
        scopes=cognito_scope.split() if cognito_scope else [],
        auth_flow="M2M",
        force_authentication=False,
    )
    async def process_with_gateway(*, access_token: str) -> str:
        """
        Gatewayへのアクセストークンを取得し、MCPクライアントで処理
        """
        # MCPクライアントの作成(AgentCore Identity認証トークン付き)
        def create_streamable_http_transport():
            return streamablehttp_client(
                gateway_url, 
                headers={"Authorization": f"Bearer {access_token}"}
            )

        client = MCPClient(create_streamable_http_transport)
        print(f"✅ MCP Client初期化完了(AgentCore Identity認証)")

        try:
            with client:
                # ツールリストを取得
                tools = client.list_tools_sync()
                print(f"🛠️ 利用可能なツール: {[tool.tool_name for tool in tools]}")

                # Bedrockモデルとエージェントの初期化
                model = BedrockModel(
                    model_id="jp.anthropic.claude-haiku-4-5-20251001-v1:0",
                )

                agent = Agent(
                    model=model,
                    tools=tools,
                    system_prompt="""
                    あなたはいろんな地域の現在時刻をチェックしてそれをSlackに送信するエージェントです。
                    指定がない場合は日本の現在時刻を教えて下さい。Slackのチャンネルは指定がなければ、 #test-strands-agents チャンネルに送信してください。
                    """
                )
                print("✅ エージェント初期化完了!")

                # ユーザー入力を処理
                user_input = payload.get("prompt", "ラスベガスの現在時刻は?")
                print(f"💬 ユーザー入力: {user_input}")

                # エージェントで処理(内部でGatewayのツールを呼び出す)
                response = agent(user_input)

                # すべてのContentBlockを整形して表示
                contents = response.message['content']
                formatted_blocks = [format_content_block(block) for block in contents]
                for i, formatted in enumerate(formatted_blocks):
                    print(f"🤖 応答[{i}]: {formatted}")

                # テキストブロックのみを結合して返す
                result = '\n'.join(
                    block['text'] for block in contents if isinstance(block, dict) and 'text' in block
                )
                return result if result else formatted_blocks[-1] if formatted_blocks else ""

        except Exception as e:
            print(f"❌ エージェント処理エラー: {e}")
            return f"エラーが発生しました: {str(e)}"

    try:
        # AgentCore Identityを使用してアクセストークンを取得し、処理を実行
        return await process_with_gateway()  # type: ignore[call-arg]  # access_tokenはデコレーターが注入
    except Exception as e:
        print(f"❌ 認証エラー: {e}")
        return f"認証に失敗しました: {str(e)}"

if __name__ == "__main__":
    app.run()

エージェントコードの以下の辺りで、Runtime側の権限を使用しています。

agent/agent.py
    # AgentCore Identityを使用してGatewayにアクセス
    gateway_url = os.environ.get("GATEWAY_URL")
    provider_name = os.environ.get("PROVIDER_NAME")
    cognito_scope = os.environ.get("COGNITO_SCOPE")

    @requires_access_token(
        provider_name=provider_name,
        scopes=cognito_scope,
        auth_flow="M2M",
        force_authentication=False,
    )
    async def process_with_gateway(*, access_token: str) -> str:
        """
        Gatewayへのアクセストークンを取得し、MCPクライアントで処理
        """
        # MCPクライアントの作成(AgentCore Identity認証トークン付き)
        def create_streamable_http_transport():
            return streamablehttp_client(
                gateway_url, 
                headers={"Authorization": f"Bearer {access_token}"}
            )

        client = MCPClient(create_streamable_http_transport)

Gatewayの権限はGateway本体が勝手に使うので、私たちが実装で気にするところはありません。

lambda/gateway_target.py
from datetime import datetime
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from typing import Dict, Any

def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
    """
    AgentCore Gatewayから呼び出される現在時刻取得ツール

    Args:
        event: {
            "timezone": "Asia/Tokyo"  # オプショナル
        }
        context: Lambda context(client_context.customにAgentCoreメタデータを含む)

    Returns:
        {
            "current_time": "2025-11-25T10:30:00+09:00",
            "timezone": "Asia/Tokyo"
        }
    """
    try:
        # タイムゾーンパラメータの取得(デフォルト: Asia/Tokyo)
        timezone_str = event.get('timezone', 'Asia/Tokyo')

        # タイムゾーンの検証と取得
        try:
            tz = ZoneInfo(timezone_str)
        except ZoneInfoNotFoundError:
            return {
                'error': f'無効なタイムゾーン: {timezone_str}',
                'error_type': 'INVALID_TIMEZONE',
                'valid_example': 'Asia/Tokyo, UTC, America/New_York'
            }

        # 現在時刻を指定されたタイムゾーンで取得
        current_time = datetime.now(tz)

        # ISO 8601形式で返す
        return {
            'current_time': current_time.isoformat(),
            'timezone': timezone_str
        }

    except Exception as e:
        # 予期しないエラー
        return {
            'error': f'内部エラーが発生しました: {str(e)}',
            'error_type': 'INTERNAL_ERROR'
        }

折り畳みここまで


上記をCDKプロジェクト内で記載した後、cdk deploy GatewayWithSelfCognitoStackを実行します。

AgentCore Gatewayに統合プロバイダーとしてのSlackをターゲットに追加する

まずは使用するSlack Bot TokenをAgentCore IdentityにAPI Keyとして登録します。
詳しい手順は以下ブログをご参照ください。

ここで作成するIdentityの名前は、先程cdk.jsonで
"apiKeyName": "<任意のAPI Key名>"
に設定したものと必ず一致するようにしてください。

その後、先程作成したGatewayに対してSlackの統合ターゲットを追加していきます。
image.png

アウトバウンド認証ではAPIキーを選択し、先程作成したIdentity名を選択します。
image.png
image.png

これでGatewayのターゲット追加は完了です。

自前実装Cognitoを用いてResource Credential Providerを作成する

CDKによって自前Cognitoがデプロイされているので、以下のようなアプリケーションクライアントが確認できるはずです。マスキングしてある部分はGatewayの名前が入っています。これができていればOKです。
image.png

このアプリケーションクライアントのクライアントID・クライアントシークレット・先程作成したGatewayのDiscovery URLをIdentityに登録していきます。こちらも詳しい手順は以下ブログをご参照ください。

ここで作成するIdentityの名前は、先程cdk.jsonで
"providerName": "<任意のResource Credential Provider名>"
に設定したものと必ず一致するようにしてください。

動作確認

以下のようなファイルを作成して実行してみます。

エージェント実行スクリプト
script/invoke_agent.py
#!/usr/bin/env python3
"""
AgentCore Runtimeを呼び出すスクリプト

使用方法:
    python scripts/invoke_agent.py "ラスベガスの現在時刻を教えて"

環境変数:
    AGENT_RUNTIME_ARN: AgentCore RuntimeのARN
"""
import boto3
import json
import sys
import os
from dotenv import load_dotenv


def invoke_agent(
    agent_runtime_arn: str,
    prompt: str,
    user_id: str = "test-user",
    qualifier: str = "DEFAULT",
    region: str = "ap-northeast-1",
) -> str:
    """
    AgentCore Runtimeを呼び出す

    Args:
        agent_runtime_arn: RuntimeのARN
        prompt: ユーザーからのプロンプト
        user_id: ユーザー識別子(必須)
        qualifier: エンドポイント名(デフォルト: "DEFAULT")
        region: AWSリージョン

    Returns:
        エージェントからの応答テキスト
    """
    client = boto3.client("bedrock-agentcore", region_name=region)

    response = client.invoke_agent_runtime(
        agentRuntimeArn=agent_runtime_arn,
        qualifier=qualifier,
        payload=json.dumps({"prompt": prompt}).encode("utf-8"),
        contentType="application/json",
        runtimeUserId=user_id,
    )

    # StreamingBodyからレスポンスを読み取り
    content = []
    for chunk in response.get("response", []):
        content.append(chunk.decode("utf-8"))

    # JSONとしてパースして結果を返す
    result = "".join(content)
    try:
        parsed = json.loads(result)
        return json.dumps(parsed, ensure_ascii=False, indent=2)
    except json.JSONDecodeError:
        return result


def main():
    load_dotenv()

    agent_arn = os.environ.get("AGENT_RUNTIME_ARN")
    if not agent_arn:
        print("エラー: 環境変数 AGENT_RUNTIME_ARN が設定されていません")
        print("使用方法: AGENT_RUNTIME_ARN=arn:aws:... python scripts/invoke_agent.py 'プロンプト'")
        sys.exit(1)

    # コマンドライン引数からプロンプトを取得、なければデフォルト
    if len(sys.argv) > 1:
        prompt = " ".join(sys.argv[1:])
    else:
        prompt = "ラスベガスの現在時刻を教えて"

    print(f"プロンプト: {prompt}")
    print("エージェントを呼び出し中...")
    print("-" * 50)

    try:
        result = invoke_agent(agent_arn, prompt)
        print(result)
    except Exception as e:
        print(f"エラー: {e}")
        sys.exit(1)


if __name__ == "__main__":
    main()
.env
# AgentCore RuntimeのARN(cdk deploy後のOutputから取得)
AGENT_RUNTIME_ARN='arn~'
ターミナル
$ uv run script/invoke_agent.py

プロンプト: ラスベガスの現在時刻を教えて
エージェントを呼び出し中...
--------------------------------------------------
"✅ ラスベガスの現在時刻を確認しました!
\n\n**ラスベガスの現在時刻:**
\n- **2025年11月29日 22:47:07**(太平洋標準時 PST)
\n\nSlackの #test-strands-agents チャンネルに送信済みです!"

(※このブログはラスベガスで書いているので↓の時間が一致しています)
image.png

日本時間にすると以下の結果になります。合ってそうですね。
image.png

その他参考資料

13
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?