Skip to content

feat: AgentCore Instances compute type (x86_64) for managed environments - #468

Open
matheus-1618 wants to merge 20 commits into
aws-samples:mainfrom
matheus-1618:feat/agentcore-instances
Open

matheus-1618 wants to merge 20 commits into
aws-samples:mainfrom
matheus-1618:feat/agentcore-instances

Conversation

@matheus-1618

@matheus-1618 matheus-1618 commented Sep 15, 2026

Copy link
Copy Markdown

What

Adds opt-in support for the AgentCore Instances compute type to managed environments, including x86_64 environments — the first non-arm64 path in the platform.

Admins creating a managed environment can now choose its compute:

  • Serverless microVMs (arm64) — the existing default, unchanged
  • EC2 Instances (x86_64) — runs the environment's runtime on EC2 managed instances in the deployment account, provisioned through an AgentCore capacity provider

Everything is gated behind a new terraform variable enable_instances_compute (default false). Deployments that don't opt in are unaffected — with the flag off there is no amd64 image build, no operator role, and the compute selector rejects instances.

Why

  • x86_64 workloads: microVMs are arm64-only; teams that need to build/run x86 artifacts had no path.
  • Persistent workspace: the capacity provider's EBS volume replaces managed session storage at /mnt/workspace, lifting the fixed 1 GB session-storage ceiling and surviving session stops.
  • Account-level controls: instances run in the deployment account (Savings Plans/ODCRs apply, data stays in-account), with sessions of up to 14 days.

How

  • compute: { type, architecture } field on the environment (API + UI selector on the create flow; immutable after creation, matching the AgentCore contract).
  • Capacity provider per architecture, created lazily by the status lambda with a deterministic name on first use. Capacity providers are immutable (duplicate-to-change), so they are treated as create-once resources rather than terraform state.
  • Runtime creation branches on the compute type: capacityProviderConfiguration + capacityProviderVolume (no networkConfiguration — Instances inherits the capacity provider's VPC).
  • One CodeBuild project serves both architectures via per-build overrides (environmentTypeOverride/imageOverride + an IMAGE_PLATFORM variable in the buildspec).
  • x86_64 environments build FROM an amd64 variant of the core image (opt-in terraform build). Catalog tools remain arm64-only for now and are rejected on x86_64 with a clear error.
  • The core image Dockerfile is now architecture-aware: the base image is pinned to the multi-arch index digest (the previous pin referenced the arm64 manifest, which silently ignores --platform), and Kiro/OpenCode/bun/uv select artifact + pinned sha256 by TARGETARCH. arm64 builds resolve to the exact same artifacts as before.

Fixes picked up along the way (apply to microVMs deployments too)

  • inspector2:ListCoverage/ListFindings for the status lambda — in accounts with ECR enhanced scanning, DescribeImageScanFindings is served by Amazon Inspector and every image scan fails without these.
  • iam:CreateServiceLinkedRole (scoped) — the first capacity provider in an account provisions the AWSServiceRoleForBedrockAgentCoreRuntimeInstances SLR.
  • bedrock-agentcore:PassCapacityProvider — required by CreateAgentRuntime when associating a capacity provider.
  • Transient first-invoke errors (instance provisioning) are retried instead of permanently failing the revision.
  • Block-device workspace mounts come formatted — mkfs.ext4 leaves lost+found at the volume root and git clone refuses the non-empty destination. The workspace init now clears the well-known filesystem entries before cloning (any other residue still fails loudly).

Notes and limitations

  • Burstable instance types (t family) are rejected by CreateCapacityProvider (Instance type 't3.large' is not supported); the default allowlist uses m6i.large. m5/m6i/c5/c6i were verified to be accepted.
  • x86_64 environments must derive from the Standard environment (the base swap targets the amd64 core); tool-catalog x86 binaries are a natural follow-up.
  • The environment verification guardrail now asserts the architecture from the recipe instead of hardcoding arm64.

Validation

End-to-end on a fresh us-east-1 deployment with enable_instances_compute = true:

  1. Environment created via the UI selector with compute: { type: instances, architecture: x86_64 }.
  2. CodeBuild ran on the x86 fleet with --platform linux/amd64, image built FROM the amd64 core and passed the (now architecture-aware) verification script.
  3. Capacity provider created lazily (READY), runtime created with capacityProviderConfiguration and the workspace EBS volume at /mnt/workspace.
  4. Runtime validation invoked the runtime — an m6i instance was provisioned by AgentCore and the container reported arch=x86_64, non-root, writable workspace, all four agent CLIs installed.
  5. Revision published; three further revisions repeated the pipeline cleanly with no intervention.
  6. A real intent ran end-to-end on the Instances runtime: workspace cloned onto the EBS volume, and the agent CLI (Claude Code via Bedrock) executed stages on the EC2 managed instance.
  7. linux/amd64 and linux/arm64 builds of the core image verified locally (all six CLI binaries at their pinned versions on both).

Tests: 122 backend (18 new covering the compute module and the Instances runtime path) and 569 frontend, all passing; terraform validate clean.

Managed environments can now target the AgentCore Instances compute type
(compute: { type: 'instances', architecture: 'x86_64' | 'arm64' }):

- capacity provider per architecture, created lazily by the status
  lambda with a deterministic name (immutable resource, create-once)
- runtime created with capacityProviderConfiguration; networking is
  inherited from the capacity provider and the workspace moves from
  managed session storage to a persistent EBS volume at /mnt/workspace
- x86_64 environments build FROM an amd64 variant of the core image
  (opt-in terraform build) on an x86 CodeBuild fleet via per-build
  overrides; catalog tools stay arm64-only for now
- everything is gated behind enable_instances_compute (default false)
The previous pin referenced the arm64-specific manifest, which silently
ignores --platform and blocks amd64 builds of the core image. The index
digest resolves to the exact same arm64 manifest on arm builds (no-op)
and selects the amd64 manifest when building for x86_64.
Kiro, OpenCode, bun and uv were pinned to arm64 artifacts, which made
--platform linux/amd64 builds impossible. Each download now selects the
artifact and its pinned sha256 by TARGETARCH; arm64 builds keep the
exact same artifacts as before.
Admins choose between serverless microVMs (default, arm64) and EC2
Instances (x86_64) when creating a managed environment. The choice is
immutable after creation, matching the AgentCore contract, so the
selector only renders on the create flow.
The verification prologue hardcoded the arm64 assertion, rejecting
correctly-built x86_64 images. The expected architecture now follows
the recipe (docker inspect reports x86_64 as amd64).
…counts

In accounts where ECR enhanced scanning is enabled, DescribeImageScanFindings
is backed by Amazon Inspector and the status lambda also needs
inspector2:ListCoverage/ListFindings, otherwise every image scan fails with
an authorization error.
The first CreateCapacityProvider in an account provisions the
AWSServiceRoleForBedrockAgentCoreRuntimeInstances service-linked role on
behalf of the caller, which requires iam:CreateServiceLinkedRole scoped to
that service.
CreateAgentRuntime with a capacityProviderConfiguration authorizes the
association through PassCapacityProvider on the capacity provider ARN.
…device workspace

EBS-backed workspace mounts (Instances compute type) come formatted, so
/mnt/workspace contains lost+found and git clone refuses the non-empty
destination. Remove only the well-known mkfs entries when no checkout
exists; any other residue still fails loudly.

@JWThewes JWThewes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds opt-in EC2-backed AgentCore environments with x86_64 images and persistent EBS workspaces, while retaining arm64 microVMs as the default. The focused compute module and reuse of the existing build pipeline are sensible.

Requesting changes for the five inline findings: persistent-session cleanup, cold-start retry identity, published-core image consistency, capacity-provider replacement after configuration changes, and architecture compatibility for inherited environments. These affect resource lifecycle, build reliability, and established revision semantics.

Validation on b5871c8: all 122 environment tests and 11 workspace/runtime tests passed, and the environment Lambda bundles built successfully. Additional local probes reproduced the retry, image-selection, provider-configuration, and inheritance issues; persistent-volume retention was checked against the official AWS documentation. No AWS deployment or Docker image rebuild was performed during this review.

Comment thread lambda/environments/compute.js Outdated
Comment on lines +185 to +190
volumes: [
{
ebsConfiguration: {
name: WORKSPACE_VOLUME_NAME,
sizeGiB: Number(process.env.MANAGED_INSTANCES_WORKSPACE_GIB || 50),
volumeType: 'gp3',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Add explicit cleanup for persistent Instances sessions

These volumes persist after StopRuntimeSession, idle timeout, and maximum lifetime; they are deleted only by explicit session/provider deletion (AWS lifecycle documentation). The existing validation cleanup in status.js only stops its randomly named session, and intent deletion in lambda/shared/intent-deletion.js also only stops sessions. Consequently, each validation session that provisions storage can leave a default 50-GiB volume behind, and deleting an intent leaves its workspace data and storage charges behind indefinitely.

Please retain the provider/session identifiers needed for cleanup, delete disposable validation sessions after a terminal result, and delete persistent sessions when their owning intent is permanently deleted. Add the necessary IAM permissions and lifecycle tests. Sessions temporarily stopped for park/resume should retain their volumes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 5c06bb5.

  • The capacity provider ARN is now persisted on the revision at runtime creation and carried into the intent's environment snapshot, so both cleanup paths have the identifiers they need.
  • Disposable validation sessions are deleted (DeleteCapacityProviderSession) on every terminal outcome.
  • Permanent intent deletion deletes the intent's main and lane sessions. An unexpected error aborts the cascade before the DynamoDB partition delete, so the intent still lists and the delete can simply be re-run (sessions that never existed are tolerated as misses). Park/resume is untouched — stopped sessions retain their volumes.
  • IAM: DeleteCapacityProviderSession granted to the status, intents and projects lambdas; @aws-sdk/* bumped to ^3.1135.0 (the command only exists in the data-plane client from there) via the repo's scripts/sync-aws-sdk.mjs.
  • Lifecycle tests added for both paths (lambda/shared/test/intent-deletion-sessions.test.js + status-instances.test.js).

Comment on lines +587 to +590
if (
RETRYABLE_CONTROL_ERRORS.has(error?.name) ||
(environment.compute?.type === 'instances' && INSTANCES_TRANSIENT_ERRORS.has(error?.name))
) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Retry provisioning with the same validation session

Returning pending here does not retry the session that was provisioning: the next poll reaches randomUUID() at line 506 and invokes a new session, while the finally block stops the previous agent session. If the initial invocation consistently exceeds the provisioning window, every poll repeats a cold start and the revision can remain VERIFYING indefinitely. A local probe in which the first invocation of each session fails transiently and subsequent invocations can succeed produced three different sessions across three polls, with no revision progress.

Please persist or deterministically derive one validation session ID per revision, reuse it across transient retries, and defer terminal cleanup until validation succeeds or exhausts a bounded retry/deadline policy. Cover a cold start that requires more than one poll.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in de4beb2.

The validation session ID is now persisted on the revision and reused across polls, so a transient first-invoke error keeps the provisioning session alive and the next poll reattaches to it instead of repeating the cold start. The session is only stopped on a terminal outcome, and the retry budget is bounded (MANAGED_INSTANCES_VALIDATION_MAX_POLLS, default 30 one-minute polls — exhaustion fails the revision with a clear message). microVM validation keeps the original per-poll semantics. Covered by a multi-poll cold-start test that asserts the same session ID across polls plus the exhausted-budget path.

Comment thread lambda/environments/compute.js Outdated
Comment on lines +125 to +129
const amd64 = amd64CoreImage();
return {
...recipe,
architecture: 'x86_64',
base: { ...recipe.base, imageUri: amd64.imageUri, imageDigest: amd64.imageDigest },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Resolve the amd64 image from the selected published core revision

amd64CoreImage() reads the latest deployment's environment variables, while this spread retains the selected Standard revision's revisionId. During a platform upgrade, stageCoreRevision deliberately leaves Standard's published pointer on the old revision until publication. Creating/editing/rebuilding an x86 environment during that interval therefore uses the new core image while claiming the old published base. A local probe kept the published base revision unchanged, changed only the configured amd64 digest, and obtained a different base image for that same revision.

Please store architecture-specific image references with the core revision and resolve the amd64 variant belonging to the selected published revision. Add an upgrade test where a newer core has been deployed but has not yet been published as Standard; x86 environments should continue using the published core's variant.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 5724ba0.

The amd64 variant now travels with the core revision: seedSystemEnvironments and stageCoreRevision store amd64Image on the Standard revision, and when the deployed digest matches the already-published revision the variant is backfilled onto it (pre-existing deployments gain their variant exactly). applyComputeBase resolves the amd64 refs from the selected base revision only — the env-var reader is gone — and rejects with AMD64_CORE_IMAGE_MISSING when the revision has no variant. Includes the upgrade-window test: newer core deployed but not yet published as Standard → the x86 environment keeps using the published revision's variant.

Comment on lines +148 to +151
const match = (page.capacityProviders ?? []).find((item) => item.name === name);
if (match) {
if (match.status === 'READY') return { capacityProviderArn: match.capacityProviderArn };
if (match.status === 'CREATING') return { pending: true };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Version capacity providers when their configuration changes

The lookup uses only the project/environment/architecture name, so an existing READY provider is returned without checking its configuration. For example, changing the exposed Terraform allowlist from m6i.large to m6i.xlarge updates the Lambda environment but all subsequent runtimes still use the old provider. The same applies to replaced subnets/security groups and storage settings; a local probe changed the requested subnets and instance types and still received the original provider unchanged. A failed provider also permanently occupies the same lookup identity after configuration is corrected.

Please include a fingerprint of the immutable provider configuration in its identity, or implement an explicit replacement flow, so new revisions use the intended configuration while existing revisions retain their provider. Test both configuration changes and recovery after a failed configuration is corrected.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 6ed7916.

The provider name now embeds a short hash of the create-time configuration (instance allowlist, VPC, storage, lifecycle, operator role), so a changed — or corrected-after-failure — configuration produces a fresh provider while runtimes created earlier keep the one they were built with. Superseded providers are intentionally left in place since they may still back existing runtimes (documented in the module header). Tests cover both a configuration change producing a new identity and recovery after a failed configuration is corrected.

Comment on lines +386 to +391
const recipe = compute
? applyComputeBase({ recipe: prepared.recipe, compute })
: prepared.recipe;
const flattenedRecipe = compute
? applyComputeBase({ recipe: prepared.flattenedRecipe, compute })
: prepared.flattenedRecipe;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Validate base architecture for default microVM environments too

Published x86 environments are included in the base selector, but omitting compute normalizes to null and skips the compute/base checks here. A local handler probe created a default microVM environment derived from a published x86 parent and received HTTP 201 with an amd64 base digest and no recipe architecture. Its build is then scheduled as linux/arm64, so this accepted configuration cannot pass the image architecture checks. The edit path has the same validation gap.

Please validate the target architecture against the selected base revision for every creation/revision path, including the default compute type, and reject incompatible combinations before saving or queuing a build. Filter incompatible bases in the UI as well, and add a regression test for an arm64 child of an x86 parent.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in fbaa253.

assertBaseArchitecture now runs on every creation/revision path — create, edit, and rebuild-on-latest-base — including the default (compute omitted) case, rejecting an x86_64 base for an arm64 target with a 409 BASE_ARCHITECTURE_MISMATCH before anything is saved or queued. The UI base selector filters incompatible bases as well (x86_64 targets only offer Standard; arm64 targets exclude published x86_64 environments), and switching the compute selector to x86_64 resets the base to Standard. Regression test added for an arm64 child of a published x86_64 parent.

…-start retries

The runtime validation generated a new random session on every poll, so a
transient first-invoke error (EC2 still provisioning) meant each retry
repeated the cold start while the finally block stopped the session that
was provisioning — a revision could stay VERIFYING indefinitely.

The session ID is now persisted on the revision and reused across polls;
the session is only stopped on a terminal outcome, and the retry budget is
bounded (MANAGED_INSTANCES_VALIDATION_MAX_POLLS, default 30 one-minute
polls). microVM validation keeps the original per-poll semantics.
… its identity

Capacity providers are immutable, but the lookup used only the
prefix+architecture name: changing the exposed configuration (instance
allowlist, subnets/security groups, storage, lifecycle, operator role) had
no effect on subsequent runtimes, and a failed provider permanently
occupied the lookup identity even after the configuration was corrected.

The provider name now embeds a short hash of the create-time
configuration, so a changed (or corrected) configuration produces a fresh
provider while runtimes created earlier keep the one they were built with.
Superseded providers are intentionally left in place — they may still back
existing runtimes.
…n path

A default microVM environment could derive from a published x86_64 parent:
compute normalizes to null, the compute/base checks were skipped, and the
revision was accepted with an amd64 base digest — its arm64 build could
never pass the image architecture checks. The base architecture is now
asserted on create, edit and rebuild-on-latest-base, and the UI base
selector only offers compatible bases.
… released

Instances sessions keep their persistent workspace volumes across stop,
idle timeout and maximum lifetime — only an explicit
DeleteCapacityProviderSession releases them. Validation left one default
50-GiB volume behind per revision, and deleting an intent left its
workspace volumes (and charges) behind indefinitely.

- the capacity provider ARN is persisted on the revision at runtime
  creation and carried into the intent's environment snapshot
- disposable validation sessions are deleted on every terminal outcome
- permanent intent deletion deletes the intent's main and lane sessions;
  an unexpected error aborts before the DynamoDB delete so the cascade
  stays re-runnable (sessions that never existed are tolerated)
- park/resume is untouched — stopped sessions retain their volumes
- IAM: DeleteCapacityProviderSession for the status, intents and projects
  lambdas; @aws-sdk/* bumped to ^3.1135.0 for the new command
  (scripts/sync-aws-sdk.mjs)
…ision

applyComputeBase read the amd64 core refs from the deployment's environment
variables while the recipe pinned the selected published Standard revision.
During a platform upgrade the staged core is newer than the published one,
so an x86 environment created in that window mixed the new image bytes with
the old revision identity.

The amd64 variant now travels with the core revision (seeded, staged, and
backfilled onto a published revision with the matching digest) and
applyComputeBase resolves it from the selected base revision only.
@matheus-1618

Copy link
Copy Markdown
Author

All five review findings are addressed, one commit per finding:

Finding Commit
[P1] Explicit cleanup for persistent Instances sessions 5c06bb5
[P2] Retry provisioning with the same validation session de4beb2
[P2] Resolve the amd64 image from the selected published core revision 5724ba0
[P2] Version capacity providers when their configuration changes 6ed7916
[P2] Validate base architecture for default microVM environments too fbaa253

Each carries the regression tests requested inline (details in the per-thread replies). Note for reviewers: 5c06bb5 bumps @aws-sdk/* to ^3.1135.0 via scripts/sync-aws-sdk.mjsDeleteCapacityProviderSessionCommand only exists in the data-plane client from that version.

Validation: 2871 backend + 569 frontend tests passing, terraform validate clean.

Conflicts: the @aws-sdk/* range (main stayed on ^3.1092.0, this branch
needs ^3.1135.0 for DeleteCapacityProviderSessionCommand — re-applied via
scripts/sync-aws-sdk.mjs) and the validation-session cleanup block in
status.js (kept this branch's structure, adopted main's Powertools
logger).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants