Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions command.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,8 @@ add a Runtime to the current project
- `--filesystem-configurations <filesystem-configurations>`: filesystem mount configurations (JSON)
- `--role-arn <role-arn>`: IAM role ARN that provides permissions for the Runtime
- `--additional-policies <additional-policies...>`: additional IAM policy ARNs or policy document paths for the execution role
- `--container-build-role-arn <container-build-role-arn>`: existing CodeBuild execution role; selects a Container build (permissions managed externally)
- `--container-build-handler-role-arn <container-build-handler-role-arn>`: existing build-handler Lambda role; selects a Container build (permissions managed externally)
- `--authorizer-type <authorizer-type>`: inbound authorizer type (AWS\_IAM or CUSTOM\_JWT)
- `--authorizer-configuration <authorizer-configuration>`: inbound authorizer configuration (JSON)
- `--request-header-allowlist <request-header-allowlist...>`: request headers to pass through to the Runtime
Expand Down
75 changes: 75 additions & 0 deletions docs/container-build-roles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Existing roles for container builds

Add a container runtime using centrally provisioned IAM roles:

```sh
agentcore add runtime --name worker --language python \
--role-arn arn:aws:iam::111122223333:role/Runtime \
--container-build-role-arn arn:aws:iam::111122223333:role/CodeBuild \
--container-build-handler-role-arn arn:aws:iam::111122223333:role/BuildHandler
```

The runtime entry in `agentcore/agentcore.json` retains `executionRoleArn`,
`containerBuildRoleArn`, and `containerBuildHandlerRoleArn`. Build roles are
optional and only apply to Container runtimes. Supplying either build-role flag
registers a Container build. Add your code and a `Dockerfile` in `app/worker/`
before building or deploying; `add runtime` creates documentation only. Omitted roles keep automatic role
creation. Region partitions and IAM role paths are supported.

## CDK dependency

This feature requires corresponding support in `@aws/agentcore-cdk`. Version
`1.0.0` does not include it. The CLI rejects build and deploy operations with
configured build roles when the installed CDK schema lacks those fields, before
provisioning credentials or synthesizing a stack. It never silently falls back
to creating roles.

The [published-package patch](patches/agentcore-cdk-1.0.0-container-build-roles.patch)
and [verification script](patches/verify-container-build-roles.cjs) are a maintainer
handoff for the private CDK source repository. They target the published
`1.0.0` package, rather than its inaccessible TypeScript source. Maintainers
should port the changes to `ContainerBuildProject`, `ContainerImageBuilder`,
`RuntimeEnvironment`, and the runtime schema, regenerate declarations and source
maps, and publish a release before enabling this feature in deployed projects.

For local verification, extract two copies of that npm package as `original` and
`patched`, install its dependencies, then run:

```sh
patch -d patched -p1 < docs/patches/agentcore-cdk-1.0.0-container-build-roles.patch
node docs/patches/verify-container-build-roles.cjs original patched
```

The script performs local synthesis and validation. It checks unchanged default
templates, role ARNs, absence of generated IAM roles and policies when all roles
are supplied, every partial role combination, shared-project conflicts in both
creation orders, independent handler roles, separate VPC projects, and rejection
of build roles on CodeZip runtimes. Partition checks verify ARN preservation;
service availability still determines which regions can deploy a runtime. The
script does not deploy resources.

## Permissions supplied by the role owner

Imported roles are immutable. Their trust policies and permissions must already
allow the operations below; the constructs do not add policies to them.

| Role | Trusted service | Required permissions |
| ------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Runtime | `bedrock-agentcore.amazonaws.com` | Normal runtime execution permissions, including image access and the services the agent uses. See the [runtime execution role guide](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-permissions.html). |
| CodeBuild | `codebuild.amazonaws.com` | `ecr:GetAuthorizationToken` on `*`; pull/push actions on the runtime ECR repository; read/list access to the S3 source asset and decrypt access if its key requires it; create/write the build's CloudWatch log group and streams. |
| Build handler | `lambda.amazonaws.com` | `codebuild:StartBuild` and `codebuild:BatchGetBuilds` on the shared build project; create/write its CloudWatch log group and streams. |

For VPC builds, the CodeBuild role also needs EC2 network-interface creation,
deletion and describe actions, plus `ec2:CreateNetworkInterfacePermission` scoped
to the selected subnets and CodeBuild service. The handler needs the equivalent
of `AWSLambdaVPCAccessExecutionRole`. Network egress must reach CodeBuild,
CloudWatch Logs, ECR, S3, and the CloudFormation response URL.

Runtimes sharing the same build network share a CodeBuild project and must specify
the same `containerBuildRoleArn`, including whether it is omitted. Conflicts are
rejected regardless of runtime order. Each runtime has its own build handler and
may use a different handler role. Distinct build networks may use different
CodeBuild roles.

CDK bootstrap infrastructure is provisioned separately. Other resources in the
project, such as harnesses, may require additional roles of their own.
242 changes: 242 additions & 0 deletions docs/patches/agentcore-cdk-1.0.0-container-build-roles.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
--- a/dist/cdk/constructs/components/container/ContainerBuildProject.d.ts
+++ b/dist/cdk/constructs/components/container/ContainerBuildProject.d.ts
@@ -45,11 +45,12 @@
* (each is triggered by its own custom resource), so this is purely a project-reuse optimization —
* there is no requirement that they share a VPC.
*/
- static getOrCreate(scope: Construct, networkConfig?: BuildNetworkConfig): ContainerBuildProject;
+ static getOrCreate(scope: Construct, networkConfig?: BuildNetworkConfig, executionRoleArn?: string): ContainerBuildProject;
readonly project: codebuild.Project;
- readonly role: iam.Role;
+ readonly role: iam.IRole;
+ readonly executionRoleArn?: string;
readonly networkConfig?: BuildNetworkConfig;
- constructor(scope: Construct, id: string, networkConfig?: BuildNetworkConfig);
+ constructor(scope: Construct, id: string, networkConfig?: BuildNetworkConfig, executionRoleArn?: string);
/**
* Grant the build project permission to push to a specific ECR repository.
*/
--- a/dist/cdk/constructs/components/container/ContainerBuildProject.js
+++ b/dist/cdk/constructs/components/container/ContainerBuildProject.js
@@ -96,7 +96,7 @@
* (each is triggered by its own custom resource), so this is purely a project-reuse optimization —
* there is no requirement that they share a VPC.
*/
- static getOrCreate(scope, networkConfig) {
+ static getOrCreate(scope, networkConfig, executionRoleArn) {
const stack = cdk.Stack.of(scope);
// The public (no-VPC) build keeps the original fixed id so existing deployed container stacks see
// no resource replacement on upgrade. VPC builds are net-new, so a config-derived id suffix has no
@@ -104,32 +104,41 @@
const id = networkConfig ? `ContainerBuildProject-${networkConfigHash(networkConfig)}` : 'ContainerBuildProject';
const existing = stack.node.tryFindChild(id);
if (existing) {
+ if (existing.executionRoleArn !== executionRoleArn) {
+ throw new Error('Conflicting containerBuildRoleArn values for a shared CodeBuild project. All runtimes and harnesses sharing a build network must use the same role configuration.');
+ }
return existing;
}
- return new ContainerBuildProject(stack, id, networkConfig);
+ return new ContainerBuildProject(stack, id, networkConfig, executionRoleArn);
}
project;
role;
networkConfig;
- constructor(scope, id, networkConfig) {
+ executionRoleArn;
+ constructor(scope, id, networkConfig, executionRoleArn) {
super(scope, id);
this.networkConfig = networkConfig;
+ this.executionRoleArn = executionRoleArn;
const partition = cdk.Stack.of(this).partition;
const region = cdk.Stack.of(this).region;
const account = cdk.Stack.of(this).account;
- this.role = new iam.Role(this, 'ContainerBuildExecutionRole', {
- assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'),
- });
- // ECR auth token (must be * per AWS API — does not support resource-level permissions)
- this.role.addToPolicy(new iam.PolicyStatement({
- actions: ['ecr:GetAuthorizationToken'],
- resources: ['*'],
- }));
- // CloudWatch Logs for build logs
- this.role.addToPolicy(new iam.PolicyStatement({
- actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'],
- resources: [`arn:${partition}:logs:${region}:${account}:log-group:/aws/codebuild/*`],
- }));
+ this.role = executionRoleArn
+ ? iam.Role.fromRoleArn(this, 'ContainerBuildExecutionRole', executionRoleArn, { mutable: false })
+ : new iam.Role(this, 'ContainerBuildExecutionRole', {
+ assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'),
+ });
+ if (!executionRoleArn) {
+ // ECR auth token (must be * per AWS API — does not support resource-level permissions)
+ this.role.addToPrincipalPolicy(new iam.PolicyStatement({
+ actions: ['ecr:GetAuthorizationToken'],
+ resources: ['*'],
+ }));
+ // CloudWatch Logs for build logs
+ this.role.addToPrincipalPolicy(new iam.PolicyStatement({
+ actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'],
+ resources: [`arn:${partition}:logs:${region}:${account}:log-group:/aws/codebuild/*`],
+ }));
+ }
// Pass an imported (immutable) role to the Project so CDK doesn't auto-add
// policies that reference the project name, which would create a circular
// dependency when we add DependsOn below.
@@ -179,28 +188,30 @@
// only fires when a full ec2.IVpc is passed to the L2 prop). The project also
// uses an imported immutable role, so we add the EC2 permissions CodeBuild
// needs to create its VPC network interface to the real (mutable) role.
- this.role.addToPolicy(new iam.PolicyStatement({
- actions: [
- 'ec2:CreateNetworkInterface',
- 'ec2:DescribeNetworkInterfaces',
- 'ec2:DeleteNetworkInterface',
- 'ec2:DescribeSubnets',
- 'ec2:DescribeSecurityGroups',
- 'ec2:DescribeDhcpOptions',
- 'ec2:DescribeVpcs',
- ],
- resources: ['*'],
- }));
- this.role.addToPolicy(new iam.PolicyStatement({
- actions: ['ec2:CreateNetworkInterfacePermission'],
- resources: [`arn:${partition}:ec2:${region}:${account}:network-interface/*`],
- conditions: {
- StringEquals: {
- 'ec2:AuthorizedService': 'codebuild.amazonaws.com',
- 'ec2:Subnet': networkConfig.subnets.map(s => `arn:${partition}:ec2:${region}:${account}:subnet/${s}`),
- },
- },
- }));
+ if (!executionRoleArn) {
+ this.role.addToPrincipalPolicy(new iam.PolicyStatement({
+ actions: [
+ 'ec2:CreateNetworkInterface',
+ 'ec2:DescribeNetworkInterfaces',
+ 'ec2:DeleteNetworkInterface',
+ 'ec2:DescribeSubnets',
+ 'ec2:DescribeSecurityGroups',
+ 'ec2:DescribeDhcpOptions',
+ 'ec2:DescribeVpcs',
+ ],
+ resources: ['*'],
+ }));
+ this.role.addToPrincipalPolicy(new iam.PolicyStatement({
+ actions: ['ec2:CreateNetworkInterfacePermission'],
+ resources: [`arn:${partition}:ec2:${region}:${account}:network-interface/*`],
+ conditions: {
+ StringEquals: {
+ 'ec2:AuthorizedService': 'codebuild.amazonaws.com',
+ 'ec2:Subnet': networkConfig.subnets.map(s => `arn:${partition}:ec2:${region}:${account}:subnet/${s}`),
+ },
+ },
+ }));
+ }
}
this.project.node.addDependency(this.role);
}
@@ -208,7 +219,7 @@
* Grant the build project permission to push to a specific ECR repository.
*/
grantPushTo(repository) {
- repository.grantPullPush(this.role);
+ if (!this.executionRoleArn) repository.grantPullPush(this.role);
}
}
exports.ContainerBuildProject = ContainerBuildProject;
--- a/dist/cdk/constructs/components/container/ContainerImageBuilder.d.ts
+++ b/dist/cdk/constructs/components/container/ContainerImageBuilder.d.ts
@@ -3,6 +3,8 @@
import type { Asset } from 'aws-cdk-lib/aws-s3-assets';
import { Construct } from 'constructs';
export interface ContainerImageBuilderProps {
+ /** Existing immutable Lambda execution role; the caller supplies permissions. */
+ executionRoleArn?: string;
/**
* The shared CodeBuild project used for building container images.
*/
--- a/dist/cdk/constructs/components/container/ContainerImageBuilder.js
+++ b/dist/cdk/constructs/components/container/ContainerImageBuilder.js
@@ -59,6 +59,7 @@
const { buildProject, sourceAsset, repository, dockerfile, buildArgs } = props;
const hasBuildArgs = buildArgs !== undefined && Object.keys(buildArgs).length > 0;
const handler = new lambda.Function(this, 'ContainerBuildHandler', {
+ ...(props.executionRoleArn && { role: iam.Role.fromRoleArn(this, 'BuildHandlerRole', props.executionRoleArn, { mutable: false }) }),
runtime: lambda.Runtime.NODEJS_22_X,
handler: 'index.handler',
code: lambda.Code.fromInline(container_build_handler_1.CONTAINER_BUILD_HANDLER_CODE),
@@ -73,10 +74,10 @@
};
// The L1 override bypasses CDK's automatic VPC-access grant; attach the
// managed policy that lets the Lambda manage its ENI in the VPC.
- handler.role?.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaVPCAccessExecutionRole'));
+ if (!props.executionRoleArn) handler.role?.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaVPCAccessExecutionRole'));
}
// Grant Lambda permission to trigger builds and read results
- handler.addToRolePolicy(new iam.PolicyStatement({
+ if (!props.executionRoleArn) handler.addToRolePolicy(new iam.PolicyStatement({
actions: ['codebuild:StartBuild', 'codebuild:BatchGetBuilds'],
resources: [buildProject.project.projectArn],
}));
--- a/dist/cdk/constructs/l3/RuntimeEnvironment.js
+++ b/dist/cdk/constructs/l3/RuntimeEnvironment.js
@@ -61,12 +61,13 @@
name: spec.name,
});
// Get or create the shared build project (one per distinct network config) at stack level
- const buildProject = ContainerBuildProject_1.ContainerBuildProject.getOrCreate(this, buildNetwork);
+ const buildProject = ContainerBuildProject_1.ContainerBuildProject.getOrCreate(this, buildNetwork, spec.containerBuildRoleArn);
buildProject.grantPushTo(ecrRepo.repository);
// Grant CodeBuild read access to source asset
- sourceAsset.asset.grantRead(buildProject.role);
+ if (!spec.containerBuildRoleArn) sourceAsset.asset.grantRead(buildProject.role);
const builder = new ContainerImageBuilder_1.ContainerImageBuilder(this, 'ContainerImageBuilder', {
buildProject,
+ executionRoleArn: spec.containerBuildHandlerRoleArn,
sourceAsset,
repository: ecrRepo,
// dockerfile is resolved relative to the build context (the source asset root), which
--- a/dist/schema/schemas/agent-env.d.ts
+++ b/dist/schema/schemas/agent-env.d.ts
@@ -266,6 +266,8 @@
}, z.core.$strict>>;
}, z.core.$strip>>;
executionRoleArn: z.ZodOptional<z.ZodString>;
+ containerBuildRoleArn: z.ZodOptional<z.ZodString>;
+ containerBuildHandlerRoleArn: z.ZodOptional<z.ZodString>;
additionalPolicies: z.ZodOptional<z.ZodArray<z.ZodString>>;
bedrockManagedAgents: z.ZodOptional<z.ZodBoolean>;
tags: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
--- a/dist/schema/schemas/agent-env.js
+++ b/dist/schema/schemas/agent-env.js
@@ -341,6 +341,8 @@
authorizerConfiguration: auth_1.AuthorizerConfigSchema.optional(),
/** ARN of an existing IAM execution role to use instead of creating a new one. */
executionRoleArn: zod_1.z.string().optional(),
+ containerBuildRoleArn: zod_1.z.string().max(2048).regex(/^arn:[^:]+:iam::[0-9]{12}:role\/[^\s]+$/).optional(),
+ containerBuildHandlerRoleArn: zod_1.z.string().max(2048).regex(/^arn:[^:]+:iam::[0-9]{12}:role\/[^\s]+$/).optional(),
/**
* Additional IAM policies attached to the runtime execution role. Each entry is either a
* managed-policy ARN (attached directly) or a `.json` policy-document file path relative to
@@ -440,7 +442,7 @@
}
// Container-only fields. If adding more, extend this list (and consider consolidating into a
// containerConfig object — see the networkConfig pattern).
- for (const field of ['dockerfile', 'buildContextPath', 'customDockerBuildArgs']) {
+ for (const field of ['dockerfile', 'buildContextPath', 'customDockerBuildArgs', 'containerBuildRoleArn', 'containerBuildHandlerRoleArn']) {
if (data.build !== 'Container' && data[field]) {
ctx.addIssue({
code: zod_1.z.ZodIssueCode.custom,
--- a/dist/schema/schemas/agentcore-project.d.ts
+++ b/dist/schema/schemas/agentcore-project.d.ts
@@ -369,6 +369,8 @@
}, z.core.$strict>>;
}, z.core.$strip>>;
executionRoleArn: z.ZodOptional<z.ZodString>;
+ containerBuildRoleArn: z.ZodOptional<z.ZodString>;
+ containerBuildHandlerRoleArn: z.ZodOptional<z.ZodString>;
additionalPolicies: z.ZodOptional<z.ZodArray<z.ZodString>>;
bedrockManagedAgents: z.ZodOptional<z.ZodBoolean>;
tags: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
Loading
Loading