🥸

AgentCore ランタイムを (とりあえず) CDK カスタムリソースでデプロイする方法

に公開

2025年 7月に Amazon Bedrock AgentCore がプレビューとしてリリースされました。プレビュー版だけに 2025年 9月時点ではまだ AgentCore の CloudFormation は公開されていません。ゆえに CDK の Constructs も提供されていません。AgentCore ランタイムのデプロイ方法として、Starter Toolkit を使う方法マニュアルでのセットアップ方法がドキュメントでは案内されていますが、CDK 厨としてはやはり CDK でデプロイしたいものです。

CDK では AWS API を使ってカスタムリソースとしてリソースを作成できるので AgentCore のリソースも作成できそうです。正式に CDK で AgentCore をサポートするまでの暫定的な方法になりますが、参考までに紹介したいと思います。

(最終的な成果物 (Construct) はこちら)

CDK における AWS API を使ったカスタムリソース

詳しくはドキュメントを参照いただければですが、簡単に紹介すると cdk.custom_resources.AwsCustomResource という Construct を使って、以下のように記述できます。

new cdk.custom_resources.AwsCustomResource(this, 'AgentCoreRuntime', {
  onCreate: {
    // 新規作成時に利用する API
    service: 'Bedrock-AgentCore-Control',
    action: 'createAgentRuntime',
    parameters: {}, // API に渡すパラメータ
    physicalResourceId,
  },
  onUpdate: {
    // 更新時に利用する API。onCreate と同じように設定
  },
  onDelete: {
    // 削除時に利用する API。onCreate と同じように設定
  },
  policy: // 内部的に作成される Lambda 関数に与える IAM ポリシー
  //  : その他のオプション
});

スタックの新規作成、更新、削除にそれぞれ呼び出す API を定義してあげます。

内部的には Lambda 関数が作成され、AWS SDK for JavaScript を利用して API が呼び出されます。なので例えば AgentCore ランタイムを作成する API createAgentRuntime に渡すパラメータは AWS SDK ドキュメントの CreateAgentRuntimeCommandを参考にすることができます。

AgentCore ランタイムをデプロイするカスタムリソース

AgentCore ランタイムをカスタムリソースとしてデプロイするには、サービスとして Bedrock-AgentCore-Control、アクションとして createAgentRuntimeupdateAgentRuntimedeleteAgentRuntime をそれぞれ利用します。
最終的なコードが知りたい方はこちらに。

onCreate (作成時) 部分

SDK のドキュメント CreateAgentRuntimeCommand を参考に、今回は必須のパラメータを設定していきます。

AwsCustomResource onCreate 部分
onCreate: {
  service: 'Bedrock-AgentCore-Control',
  action: 'createAgentRuntime',
  parameters: {
    // AgentCore ランタイムの名前。有効な文字は a~z、A~Z、0~9、_ (アンダースコア)
    agentRuntimeName: 'MyAgent',
    agentRuntimeArtifact: {
      containerConfiguration: {
        // コンテナイメージのリポジトリ URI
        containerUri: `${repository.repositoryUri}:latest`,
      }
    },
    // AgentCore ランタイムに付与する実行ロール
    roleArn: executionRole.roleArn,
    networkConfiguration: {
      // ネットワークモードに現在指定可能なのは 'PUBLIC' のみ
      networkMode: 'PUBLIC',
    }
  },
  // 物理リソース ID は API の実行結果のレスポンスから 'agentRuntimeId' の値を設定
  physicalResourceId: cr.PhysicalResourceId.fromResponse('agentRuntimeId'),
},

物理リソース ID physicalResourceId として、API のレスポンス結果から agentRuntimeId の値を参照して設定します。これによって、更新時、削除時に参照できるようになります。

今回は必須パラメータのみ指定しましたが、他に以下のようなパラメータが指定できます。詳細、および全てのパラメータは SDK ドキュメントを参照してください。

  • authorizerConfiguration: JWT で認証する場合に指定します
  • environmentVariables: ランタイムに設定する環境変数を指定します
  • protocolConfiguration: プロトコル (HTTP か MCP) を指定します

onUpdate (更新時) 部分

SDK のドキュメント UpdateAgentRuntimeCommand を参考に、同様に必須のパラメータを設定していきます。作成時と比較して AgentCore ランタイムの名前の代わりにランタイムの ID が必須パラメータになります。

AwsCustomResource onUpdate 部分
onUpdate: {
  service: 'Bedrock-AgentCore-Control',
  action: 'updateAgentRuntime',
  parameters: {
    // ランタイムの ID が必須指定。作成時に設定した物理リソース ID を指定
    agentRuntimeId: new cr.PhysicalResourceIdReference(),
    agentRuntimeArtifact: {
      containerConfiguration: {
        containerUri: `${repository.repositoryUri}:latest`,
      }
    },
    roleArn: executionRole.roleArn,
    networkConfiguration: {
      networkMode: networkMode,
    },
  },
},

onDelete (削除時) 部分

SDK のドキュメント DeleteAgentRuntimeCommand を参考に、同様に必須のパラメータを設定していきます。削除時はランタイムの ID だけが必須パラメータになります。

AwsCustomResource onDelete 部分
onDelete: {
  service: 'Bedrock-AgentCore-Control',
  action: 'deleteAgentRuntime',
  parameters: {
    // ランタイムの ID が必須指定。作成時に設定した物理リソース ID を指定
    agentRuntimeId: new cr.PhysicalResourceIdReference(),
  },
},

policy 部分

AWS API によるカスタムリソースは内部的に作成された Lambda 関数によって実行されます。その Lambda 関数に付与する権限を policy で設定します。呼び出す API の権限だけ必要な場合は、例えば以下のような記述で良いのですが、これでは権限不足になってしまいます。

AwsCustomResource policy 部分 (これでは権限不足)
policy: cr.AwsCustomResourcePolicy.fromSdkCalls({
  resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE,
}),

AgentCore ランタイムを作成する場合は、他にも以下の権限が必要になります。

  • bedrock-agentcore:CreateAgentRuntimeEndpoint
  • bedrock-agentcore:CreateWorkloadIdentity,
  • bedrock-agentcore:UpdateAgentRuntimeEndpoint,
  • bedrock-agentcore:UpdateWorkloadIdentity,
  • bedrock-agentcore:DeleteAgentRuntimeEndpoint,
  • bedrock-agentcore:DeleteWorkloadIdentity,

また、Lambda 関数が AgentCore に対して実行ロールを付与する権限も必要です。

  • iam:PassRole

まとめると、cr.AwsCustomResourcePolicy.fromStatements() を使って以下のように指定します (長いので折りたたみます)。

AwsCustomResource policy 部分
AwsCustomResource policy 部分
policy: cr.AwsCustomResourcePolicy.fromStatements(
  [
    new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      actions: [
        'bedrock-agentcore:CreateAgentRuntime',
        'bedrock-agentcore:CreateAgentRuntimeEndpoint',
      ],
      resources: [ '*' ],
    }),
    new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      actions: [
        'bedrock-agentcore:CreateWorkloadIdentity',
        'bedrock-agentcore:UpdateWorkloadIdentity',
        'bedrock-agentcore:DeleteWorkloadIdentity',
      ],
      resources: [
        // ARN を組み立て
        cdk.Stack.of(this).formatArn({
          service: 'bedrock-agentcore',
          resource: 'workload-identity-directory',
          resourceName: `*`,
        }),
      ],
    }),
    new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      actions: [
        'bedrock-agentcore:UpdateAgentRuntime',
        'bedrock-agentcore:UpdateAgentRuntimeEndpoint',
        'bedrock-agentcore:DeleteAgentRuntime',
        'bedrock-agentcore:DeleteAgentRuntimeEndpoint',
      ],
      resources: [
        // ARN を組み立て
        cdk.Stack.of(this).formatArn({
          service: 'bedrock-agentcore',
          resource: 'runtime',
          resourceName: `${agentCoreRuntimeName}*`,
        }),
      ],
    }),
    new iam.PolicyStatement({
      actions: [
        'iam:PassRole',
      ],
      resources: [ executionRole.roleArn ],
    }),
  ]
),

他のオプション

念のため、最新の SDK を利用するようにします (デプロイ時間が少し長くなります)。また、Lambda が出力するログもデフォルト 2年間なので不要であれば短くします。

AwsCustomResource 他のオプション
installLatestAwsSdk: true,
logRetention: cdk.aws_logs.RetentionDays.ONE_WEEK,

ランタイム作成時には実在するコンテナイメージが必要

ランタイム作成時にはプッシュ済みのコンテナイメージがリポジトリに存在している必要があります。今回は CDK でコンテナイメージもプッシュしたかったので、cdklabs/cdk-docker-image-deployment というものを利用しました。

cdk-docker-image-deployment のインストール
npm install cdk-docker-image-deployment

以下のように記述します。

コンテナイメージのプッシュ
import * as imagedeploy from 'cdk-docker-image-deployment';

const deployedImage = new imagedeploy.DockerImageDeployment(this, 'ImageDeploymentWithTag', {
  // Dockerfile があるディレクトリを指定
  source: imagedeploy.Source.directory('assets/agent/'),
  // ECR リポジトリとタグを指定
  destination: imagedeploy.Destination.ecr(repository, {
    tag: 'latest',
  }),
});

全体的なコード

Construct として以下についてまとめました。

  • ECR リポジトリの作成
  • コンテナイメージのプッシュ
  • AgentCore ランタイムに付与する実行ロールの作成
  • AgentCore ランタイムをカスタムリソースとして作成
Construct としてまとめたコード
agentcore-runtime.ts
import * as cdk from 'aws-cdk-lib';
import {
  aws_iam as iam,
  custom_resources as cr,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as imagedeploy from 'cdk-docker-image-deployment';

type AgentCoreRuntimeProps = {
  agentRuntimeName: string; // AgentCore ランタイムに設定する名前
}

export class AgentCoreRuntime extends Construct {

  public readonly agentRuntimeArn: string;

  constructor (scope: Construct, id: string, props: AgentCoreRuntimeProps){
    super(scope, id);

    const {
      agentRuntimeName,
    } = props;

    // -----------------------------
    // ECR repository
    // -----------------------------
    const repository = new cdk.aws_ecr.Repository(this, 'Repository', {
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      emptyOnDelete: true,
    });

    // Container image push
    const deployedImage = new imagedeploy.DockerImageDeployment(this, 'ImageDeploymentWithTag', {
      source: imagedeploy.Source.directory('assets/agent/'),
      destination: imagedeploy.Destination.ecr(repository, {
        tag: 'latest',
      }),
    });

    // -----------------------------
    // Execution Role
    // -----------------------------
    const executionRole = new iam.Role(this, 'ExecutionRole', {
      assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com').withConditions({
        StringEquals: {
          'aws:SourceAccount': cdk.Stack.of(this).account,
        },
        ArnLike: {
          'aws:SourceArn': cdk.Stack.of(this).formatArn({service: 'bedrock-agentcore', resource: '*'}),
        },
      }),
      inlinePolicies: {
        'policy': new iam.PolicyDocument({
          statements:[
            new iam.PolicyStatement({
              effect: iam.Effect.ALLOW,
              actions: [
                'bedrock:InvokeModel',
                'bedrock:InvokeModelWithResponseStream',
              ],
              resources: [ '*' ],
            }),
            new iam.PolicyStatement({
              effect: iam.Effect.ALLOW,
              actions: [
                'ecr:GetAuthorizationToken',
              ],
              resources: [ '*' ],
            }),
            new iam.PolicyStatement({
              effect: iam.Effect.ALLOW,
              actions: [
                'ecr:GetDownloadUrlForLayer',
                'ecr:BatchGetImage',
              ],
              resources: [
                repository.repositoryArn
              ],
            }),
            new iam.PolicyStatement({
              effect: iam.Effect.ALLOW,
              actions: [
                'bedrock-agentcore:GetWorkloadAccessToken',
                'bedrock-agentcore:GetWorkloadAccessTokenForUserId',
                'bedrock-agentcore:GetWorkloadAccessTokenForJWT',
              ],
              resources: [ '*' ],
            }),
            new iam.PolicyStatement({
              effect: iam.Effect.ALLOW,
              actions: [
                'cloudwatch:PutMetricData',
              ],
              resources: [ '*' ],
              conditions: {
                StringEquals: {
                  'cloudwatch:namespace': 'bedrock-agentcore',
                },
              },
            }),
            new iam.PolicyStatement({
              effect: iam.Effect.ALLOW,
              actions: [
                'logs:DescribeLogStreams',
                'logs:CreateLogGroup',
              ],
              resources: [
                cdk.Stack.of(this).formatArn({service: 'logs', resource: 'log-group:/aws/bedrock-agentcore/runtimes/*'}),
              ],
            }),
            new iam.PolicyStatement({
              effect: iam.Effect.ALLOW,
              actions: [
                'logs:CreateLogStream',
                'logs:PutLogEvents',
              ],
              resources: [
                cdk.Stack.of(this).formatArn({service: 'logs', resource: 'log-group:/aws/bedrock-agentcore/runtimes/*:log-stream:*'}),
              ],
            }),
            new iam.PolicyStatement({
              effect: iam.Effect.ALLOW,
              actions: [
                'logs:DescribeLogGroups',
              ],
              resources: [
                cdk.Stack.of(this).formatArn({service: 'logs', resource: 'log-group:*'}),
              ],
            }),
            new iam.PolicyStatement({
              effect: iam.Effect.ALLOW,
              actions: [
                'xray:PutTelemetryRecords',
                'xray:GetSamplingRules',
                'xray:GetSamplingTargets',
                'xray:UpdateTraceSegmentDestination',
                'xray:PutTraceSegments',
              ],
              resources: [ '*' ],
            }),
          ]
        })
      }
    });

    // -----------------------------
    // AgentCore Runtime using Custom Resource
    // -----------------------------

    // Common parameter on create and update
    const commonParameter = {
      agentRuntimeArtifact: {
        containerConfiguration: {
          containerUri: `${repository.repositoryUri}:latest`,
        }
      },
      roleArn: executionRole.roleArn,
      networkConfiguration: {
        networkMode: 'PUBLIC',
      },
      //// Optional Parameters
      // description: '...',
      // authorizerConfiguration: {
      //   customJWTAuthorizer: {
      //     discoveryUrl: `https://cognito-idp.${REGION}.amazonaws.com/${COGNITO_USER_POOL_ID}/.well-known/openid-configuration`,
      //     allowedClients: [ `${COGNITO_CLIENT_ID}` ]
      //   }
      // },
      // environmentVariables: {
      //   "<keys>": "STRING_VALUE",
      // },
      // protocolConfiguration: {
      //   serverProtocol: 'MCP' || 'HTTP',
      // },
    };

    // AgentCore Runtime using AwsCustomResource
    const agentCoreRuntime = new cr.AwsCustomResource(this, 'AgentCoreRuntime', {
      onCreate: {
        service: 'Bedrock-AgentCore-Control',
        action: 'createAgentRuntime',
        parameters: {
          agentRuntimeName: agentRuntimeName,
          ...commonParameter,
        },
        physicalResourceId: cr.PhysicalResourceId.fromResponse('agentRuntimeId'),
      },
      onUpdate: {
        service: 'Bedrock-AgentCore-Control',
        action: 'updateAgentRuntime',
        parameters: {
          agentRuntimeId: new cr.PhysicalResourceIdReference(),
          ...commonParameter,
        },
      },
      onDelete: {
        service: 'Bedrock-AgentCore-Control',
        action: 'deleteAgentRuntime',
        parameters: {
          agentRuntimeId: new cr.PhysicalResourceIdReference(),
        },
      },
      policy: cr.AwsCustomResourcePolicy.fromStatements(
        [
          new iam.PolicyStatement({
            effect: iam.Effect.ALLOW,
            actions: [
              'bedrock-agentcore:CreateAgentRuntime',
              'bedrock-agentcore:CreateAgentRuntimeEndpoint',
            ],
            resources: [ '*' ],
          }),
          new iam.PolicyStatement({
            effect: iam.Effect.ALLOW,
            actions: [
              'bedrock-agentcore:CreateWorkloadIdentity',
              'bedrock-agentcore:UpdateWorkloadIdentity',
              'bedrock-agentcore:DeleteWorkloadIdentity',
            ],
            resources: [
              cdk.Stack.of(this).formatArn({service: 'bedrock-agentcore', resource: 'workload-identity-directory', resourceName: `*`}),
            ],
          }),
          new iam.PolicyStatement({
            effect: iam.Effect.ALLOW,
            actions: [
              'bedrock-agentcore:UpdateAgentRuntime',
              'bedrock-agentcore:UpdateAgentRuntimeEndpoint',
              'bedrock-agentcore:DeleteAgentRuntime',
              'bedrock-agentcore:DeleteAgentRuntimeEndpoint',
            ],
            resources: [
              cdk.Stack.of(this).formatArn({service: 'bedrock-agentcore', resource: 'runtime', resourceName: `${agentRuntimeName}*`})
            ],
          }),
          new iam.PolicyStatement({
            actions: [
              'iam:PassRole',
            ],
            resources: [ executionRole.roleArn ],
          }),
        ]
      ),
      installLatestAwsSdk: true,
      logRetention: cdk.aws_logs.RetentionDays.ONE_WEEK,
    });

    // Wait for image deployed
    agentCoreRuntime.node.addDependency(deployedImage);

    // AgentCore Runtime ARN
    const agentRuntimeArn = cdk.Stack.of(this).formatArn({
      service: 'bedrock-agentcore',
      resource: 'runtime',
      resourceName: `${agentCoreRuntime.getResponseField('agentRuntimeId')}`
    });
    this.agentRuntimeArn = agentRuntimeArn;

    // -----------------------------
    // Output
    // -----------------------------

    new cdk.CfnOutput(this, 'EcrRepository-Uri', {
      description: 'AgentCore ECR repository URI',
      value: repository.repositoryUri,
    });

    new cdk.CfnOutput(this, 'AgentCoreExecutionRole-Arn', {
      description: 'AgentCore Execution Role Arn',
      value: executionRole.roleArn,
    });

    new cdk.CfnOutput(this, 'AgentCoreRuntime-Arn', {
      description: 'AgentCore Runtime Arn',
      value: agentRuntimeArn,
    });
  }
}

トラブルシューティング

今回のコードに辿り着くまでなんだかんだ試行錯誤がありました。どなたかのお役に立てるかもしれないので記録しておきます。

1. "UnknownError" が出力される

CDK でデプロイしようとしたところ、以下のように UnknownError というエラーが出力されました。

cdk deploy
2:45:10 | CREATE_FAILED  | Custom::AWS  | AgentCoreAgentCoreRuntimeA1B2C3D4
Received response status [FAILED] from custom resource. Message returned: UnknownError (RequestId: XXXXXXXX)

これはカスタムリソースを作成する際の Lambda のポリシーに権限が足りない場合に出力されました。前述の policy に指定する権限 を参照ください。

2. WorkloadIdentity に関する権限不足

cdk deploy
4:18:22 | CREATE_FAILED  | Custom::AWS  | AgentCoreAgentCoreRuntimeB04EC36A
Received response status [FAILED] from custom resource. Message returned: User: arn:aws:sts::123456789012:assumed-role/XXXXX/XXXXX
 is not authorized to perform: bedrock-agentcore:CreateWorkloadIdentity on resource: arn:aws:bedrock-agentcore:us-east-1:123456789012:workload-identity-directory/default
because no identity-based policy allows the bedrock-agentcore:CreateWorkloadIdentity action (Service: AgentCredentialProvider, Status Code: 403, Request ID: XXXXXXXX) (SDK Attempt Count: 1) (RequestId: XXXXXXXX)

こちらも 1. と同様にカスタムリソースを作成する際の Lambda のポリシーに権限が足りない場合に出力されました。前述の policy に指定する権限 を参照ください。

3. "The image with identifier 'ImageIdentifier(ImageTag=latest)' does not exist in the repository ..." と出力される

cdk deploy
3:11:10 | CREATE_FAILED  | Custom::AWS  | AgentCoreAgentCoreRuntimeA1B2C3D4
Received response status [FAILED] from custom resource. Message returned: The image with identifier 'ImageIdentifier(ImageTag=latest)' does not exist in the re
pository with name '<RepositoryName>' in registry with id '123456789012'. (RequestId: XXXXXXXX)

AgentCore を作成する際に、コンテナイメージのリポジトリ URI を指定しますが、そのイメージが実在している必要があります。今回は CDK からコンテナイメージもプッシュしたかったので、cdklabs/cdk-docker-image-deployment というものを利用しました。前述の ランタイム作成時には実在するコンテナイメージが必要 を参照ください。

4. それでも解決できない場合

カスタムリソースとして実行される Lambda が API 実行のログを CloudWatch Logs に出力していますので、確認してみましょう。

ロググループは /aws/lambda/<スタック名>-AWS<ランダム文字列>-<ランダム文字列> という形式になっていると思います。

まとめ

AgentCore ランタイムをカスタムリソースでデプロイする方法をご紹介しました。API 実行だけでデプロイできるリソースであれば、カスタムリソースとして Lambda を書かずともできる点もポイントですかね。作ったり消したりする機会が多いので、どうしても CDK でデプロイできるようにしておきたかったのです。あくまで CDK で正式にサポートされるまでの暫定的な方法になるかと思いますのでご留意ください。

アマゾン ウェブ サービス ジャパン (有志)

Discussion