Skip to content

Deployment guide

This guide covers deploying ABCA into an AWS account, including compute backend choices, scale-to-zero characteristics, and the complete AWS service inventory. For day-to-day development workflow, see the Developer guide. For a quick first deployment, see the Quick start. For least-privilege IAM deployment roles, see DEPLOYMENT_ROLES.md.

ABCA deploys as a single CDK stack (backgroundagent-dev) containing all platform resources. The stack uses a ComputeStrategy interface to support three compute backends within the same stack:

AspectAgentCore (default)ECS Fargate (opt-in)Lambda MicroVMs (experimental)
ComputeBedrock AgentCore Runtime (Firecracker MicroVMs)ECS Fargate containersAWS Lambda MicroVMs
Resources2 vCPU, 8 GB RAM, 2 GB max image size2 vCPU, 4 GB RAM8 GB baseline / 32 GB peak memory
OrchestrationDurable Lambda (checkpoint/replay)Same durable Lambda via ComputeStrategySame durable Lambda via ComputeStrategy
Agent modeFastAPI server (HTTP invocation)Batch (run-to-completion)FastAPI server (lifecycle hooks)
Startup~10s (warm MicroVM)~60-180s (Fargate cold start)~6s to RUNNING (live-measured)
Max duration8 hours (AgentCore service limit)9 hours (orchestrator executionTimeout)8 hours (maximumDurationInSeconds)

All backends are orchestrated by the same durable Lambda function. The ComputeStrategy interface abstracts startSession(), pollSession(), and stopSession() — the ECS strategy calls ecs:RunTask / ecs:DescribeTasks / ecs:StopTask directly from the Lambda. No Step Functions are used.

ECS Fargate is currently opt-in — the EcsAgentCluster construct is present in the stack code but commented out. To enable it, uncomment the ECS blocks in cdk/src/stacks/agent.ts.

Not for production. lambda-microvm carries no smoke-parity guarantee for an unattended deployment. Keep production repositories on agentcore or ecs. Synth emits an unsuppressible warning to this effect whenever the backend is selected. Design detail: COMPUTE.md and ADR-021.

Selecting it is a synth-time context flag:

Terminal window
mise //cdk:deploy -- --context compute_type=lambda-microvm

You must re-bootstrap first. This is the single most common way this backend fails, and the failure does not look like a configuration problem:

  1. Check the bootstrap policy bundle already deployed in the account:

    Terminal window
    aws cloudformation describe-stacks --stack-name CDKToolkit \
    --query "Stacks[0].Outputs[?OutputKey=='BootstrapPolicyVersion'].OutputValue | [0]" --output text
  2. If it is below 1.6.0, re-bootstrap with the backend included in ComputeTypes. cdk bootstrap cannot pass template parameters, so the parameter goes on the CDKToolkit stack directly:

    Terminal window
    aws cloudformation deploy \
    --template-file cdk/bootstrap/bootstrap-template.yaml \
    --stack-name CDKToolkit --capabilities CAPABILITY_NAMED_IAM \
    --parameters ParameterKey=ComputeTypes,ParameterValue=agentcore\,lambda-microvm

    Bundle 1.6.0 adds the MicrovmPassRoles statement, without which the CDK-managed MicroVM image deploy fails with an iam:PassRole AccessDenied on the build role — an IAM error that reads like a code bug. Rationale and the live evidence for the missing iam:PassedToService condition: DEPLOYMENT_ROLES.md.

  3. Regional availability is limited (5 Regions at launch). Synth fails fast with the supported list if the stack’s Region is not among them; bgagent doctor probes it live.

Operational notes specific to this backend:

  • Nothing self-terminates. A MicroVM whose task finished, crashed, or hung stays RUNNING and billing until the 8-hour cap. The orchestrator calls TerminateMicrovm on finalize, and the heartbeat-staleness check catches a hung guest inside a healthy VM — but a leaked handle is a cost incident. The one exception: the service reaps a VM whose /run hook returns 4xx (~12s).
  • Logs land in /aws/lambda-microvms/<image-name>. Guest stdout goes there too, which is the fallback path when the agent cannot reach the application log group.
  • Deployment identifiers are not baked into the image. The snapshot carries no configuration; table names, secret ARNs, and the per-task session-role ARN arrive in the /run payload as a platform_config block. A version-skewed orchestrator that does not send it is refused rather than run with tenant scoping disabled.

AWS Agent Registry is enabled by default. In an account or region where the preview service is unavailable or prohibited, omit the registry, its REST API, IAM grants, environment wiring, and stack outputs:

Terminal window
cdk deploy --context enableAgentRegistry=false

Blueprints without registry:// asset references continue to work. A remaining registry reference fails task startup rather than silently skipping the asset.

The string form is case-sensitive: use lowercase true or false. Any other value fails synthesis with an actionable validation error.

This context is an infrastructure switch, not a pause control. Applying it to an existing enabled deployment deletes the CloudFormation-managed registry and its records; re-enabling creates an empty registry that must be republished. See REGISTRY.md for the catalog migration and runtime behavior.

The shipped cdk/cdk.json sets bedrockGeoRegion: "global", so inference routes to any supported commercial Region — better throughput and resilience under burst, which matters for tasks that run for hours.

If you have a data-residency requirement, set the geography before upgrading:

Terminal window
mise //cdk:deploy -- -c bedrockGeoRegion=us # or eu, apac, jp, au, us-gov

One value drives every Bedrock grant and both injected model env vars, so the geography cannot disagree with what the agent calls. Changing it needs a redeploy, because the IAM grants are scoped to explicit profile ARNs resolved at synth. Full detail, including the per-layer model precedence, is in Model configuration.

Components that scale to zero (pay-per-use)

Section titled “Components that scale to zero (pay-per-use)”
ComponentBilling ModelIdle Cost
DynamoDB (8 core tables; integrations add more)PAY_PER_REQUEST$0
Lambda (all functions)Per invocation$0
API Gateway RESTPer request$0
ECS Fargate tasks (when enabled)Per running task$0 (cluster is free)
AgentCore RuntimePer session$0
Bedrock inferencePer token$0
AgentCore MemoryProportional to usage~$0
CognitoFree tier (50K MAU)$0

Components that do not scale to zero (always-on)

Section titled “Components that do not scale to zero (always-on)”
ComponentEst. Monthly Idle CostWhy
NAT Gateway (1x)~$32$0.045/hr fixed charge
VPC Interface Endpoints (7x, 2 AZs)~$102$0.01/hr × 7 endpoints × 2 AZs × 730 hrs
WAF v2 Web ACL~$5Base monthly charge
CloudWatch Dashboard~$3Per-dashboard charge
Secrets Manager (1+ secrets)~$0.40/secretPer-secret monthly
CloudWatch Alarms~$0.10/alarmPer standard alarm
CloudWatch Logs retention~$1-5Storage for retained logs
Total always-on baseline~$140-150/month

The dominant idle cost is VPC networking: 7 interface endpoints across 2 AZs ($102/month) plus the NAT Gateway ($32/month).

For the full cost model including per-task costs, see COST_MODEL.md.

Monthly user/team controls add one DynamoDB on-demand table with PITR and three standard CloudWatch alarms. The table, API/Lambda reads, stream rollups, and SNS notifications are usage-based; the existing list Lambda and TaskTable reconciler perform the work, so there is no additional always-running compute.

At public US East (N. Virginia) first-tier list rates verified in August 2026, the three alarms are approximately $0.30/month total. The three possible custom metric series can add up to approximately $0.90/month when active. CloudWatch’s account-wide free tier includes 10 custom metrics and 10 alarm metrics, so the incremental CloudWatch charge may be $0 when that allowance is still available. DynamoDB storage/PITR and request charges depend on task volume and the number of team scopes. See Setting up cost controls for the operational overhead and pricing links.

ServiceUsed ByScales to Zero
Bedrock AgentCore Runtime (MicroVMs)Agent sessions (default)Yes
ECS Fargate (when enabled)Agent sessions (opt-in)Yes
AWS Lambda MicroVMs (when enabled)Agent sessions (experimental, --context compute_type=lambda-microvm)Yes
Lambda (Node.js 24, ARM64)Orchestrator, API handlers, fanout consumer, reconcilers, custom resourcesYes
ServiceUsed ByScales to Zero
Bedrock (Claude Sonnet 4.6, Opus 4.8, Opus 5, Haiku 4.5)Agent reasoning, cross-region inference profilesYes
Bedrock GuardrailsPrompt injection detection on task inputYes
Bedrock AgentCore MemorySemantic + episodic extraction strategiesYes
AWS Agent Registry (preview, default-on)Versioned agent asset catalog and governanceN/A (managed preview service)
ServiceUsed ByScales to Zero
VPC (public + private subnets, 2 AZs)All computeN/A (no direct cost)
NAT Gateway (1x)Private subnet internet egressNo (~$32/mo)
VPC Interface Endpoints (7x, 2 AZs)AWS service connectivity from private subnetsNo (~$102/mo)
VPC Gateway Endpoints (2x: S3, DynamoDB)S3 and DynamoDB connectivityYes (free)
Security GroupsHTTPS-only egressN/A
Route 53 Resolver DNS FirewallDomain allowlisting for agent egressMinimal
ServiceUsed ByScales to Zero
DynamoDB (8 core tables, PAY_PER_REQUEST)Task state, events, nudges, concurrency, monthly budgets, webhooks, repo config, approvals. Enabling integrations adds their mapping, registry, and deduplication tables.Yes
DynamoDB StreamsTaskEventsTable → FanOut Consumer; TaskTable → combined orchestration/budget reconcilerYes
S3CDK asset bucket, ECR image layers, FUSE session storage, trace artifacts (7-day lifecycle)Minimal
SQS (DLQ)FanOut, approval metrics, screenshot, and orchestration/budget reconciler dead-letter queuesYes
Secrets ManagerGitHub PAT, webhook HMAC secretsNo (~$0.40/secret/mo)
ServiceUsed ByScales to Zero
API Gateway (REST)Task REST APIYes
Cognito User PoolCLI/API authenticationYes (free tier)
WAF v2API Gateway protection (managed rules + rate limiting)No (~$5/mo base)
ServiceUsed ByScales to Zero
EventBridge (scheduled rule)Stranded task reconciler (every 5 min)Yes (rule is free; Lambda invocation is the cost)
ServiceUsed ByScales to Zero
CloudWatch Logs (multiple log groups)Application, usage, model invocation, VPC flow, DNS query logsNo (storage)
CloudWatch DashboardOperational metrics visualizationNo (~$3/mo)
CloudWatch AlarmsOperational failures plus 80%/100% monthly-budget thresholdsNo (~$0.10/alarm)
X-RayAgentCore Runtime tracingYes
ServiceUsed ByScales to Zero
CloudFormationStack deployment, custom resourcesN/A
ECRContainer image storageMinimal
IAMRoles and policies for all componentsN/A

The repository includes a two-stage CI/CD pipeline:

Triggers on every PR and push to main. Runs mise run build (compile, test, lint, synth) and uploads the synthesized cdk.out/ as a deploy-intent artifact. The intent file declares whether a deploy should happen and for which compute types.

Triggers via workflow_run when build.yml completes successfully. The pipeline:

  1. Skips fork PRs — head_repository.full_name == github.repository prevents forks from entering the deploy flow. This is a security measure: an untrusted fork could modify build.yml to produce a deploy-intent artifact, which would otherwise prompt maintainers for approval unnecessarily.
  2. Downloads deploy-intent.json from the triggering build run.
  3. Resolves targets — Determines which compute types to deploy:
    • intent: "-" → no-op (most PRs)
    • intent: "labels" → reads PR labels against an allowlist
    • intent: "<type>" → deploys the specified type (e.g., agentcore)
  4. Requires approval — The deploy job uses a GitHub Environment with required reviewers. Approvals are logged and the self-review rule prevents unilateral deploys.
  5. Deploys via OIDC — Assumes an IAM role via GitHub OIDC federation (no long-lived credentials). The role is scoped to the cdk deploy action with least-privilege policies per DEPLOYMENT_ROLES.md.
ControlPurpose
Fork exclusion (head_repository check)Prevents fork PRs from triggering deploy approval prompts
Environment approvalHuman gate before any deploy reaches AWS
OIDC federationNo stored AWS credentials; tokens are request-scoped
Compute type allowlistOnly pre-approved types can be deployed
Non-cancellable concurrencyDeploy can’t be interrupted mid-flight
  • Enable deploys: Set the deploy Environment in repo settings with required reviewers.
  • Configure OIDC: Set AWS_ROLE_TO_ASSUME secret and AWS_REGION variable.
  • Allowlist compute types: Edit ALLOWED_COMPUTE_TYPES in deploy.yml.
  • Deploy via PR label: Add the deploy:<type> label to a PR (e.g., deploy:agentcore).

Affects: Fresh deploys in accounts whose default Availability Zones don’t line up with the zones AgentCore supports for the region.

Symptom: The AWS::BedrockAgentCore::Runtime resource fails to stabilize (NotStabilized — “subnets are in unsupported availability zones”) and the stack rolls back.

Root cause: AgentCore Runtime only places its network interfaces in a subset of each region’s Availability Zones, published as physical zone IDs (e.g. use1-az1, use1-az2, use1-az4 for us-east-1). Zone IDs are stable across accounts, but zone names (us-east-1a) are aliased per-account — so us-east-1a can map to a different physical zone in your account than in another. Left to its default, CDK picks zones by name and can land the Runtime subnets in an unsupported zone. See the AWS Supported Availability Zones table for the per-region set; the same table is snapshotted as AGENTCORE_SUPPORTED_AZ_IDS in cdk/src/constructs/agentcore-azs.ts.

PathAuto-pinned?What you must do
Local cdk deploy / mise //cdk:deploy (credentials resolve at synth)Yes — for regions in the built-in mapNothing, unless synth reports an [AgentCore AZs] error
CI/CD (build.yml → deploy.yml)No — the assembly is synthesized credential-lessSet the AGENTCORE_AVAILABILITY_ZONES repo/environment variable (below)
Any region absent from the built-in mapNoSet the context override (below)

Auto-pin (local deploys). With a concrete account and region, synth confirms the credentials belong to that account (sts:GetCallerIdentity), reads the account’s zone name-to-ID mapping (ec2:DescribeAvailabilityZones), and pins the VPC to the first two AZ names — sorted, so the pin is stable across synths — whose zone IDs are AgentCore-supported. Two zones matches AgentVpc’s default maxAzs, so enabling this does not widen an already-working topology.

If auto-pin is attempted and cannot finish — lookup denied or throttled, credentials pointing at a different account, or fewer than two supported zones — synth fails with an [AgentCore AZs] error rather than quietly falling back. That is deliberate: a silent fallback produces a template that looks pinned but is not, which is the failure this section exists to prevent. Fix the cause or set the override.

CI/CD deploys are not auto-pinned. Auto-pin needs a bound account at synth time and the pipeline has none: build.yml synthesizes cdk.out without credentials (env-agnostic) and deploy.yml deploys that pre-built assembly (--app cdk/cdk.out). Two consequences worth being explicit about:

  • Passing -c 'agentcore:availabilityZones=...' to cdk deploy has no effect on the pipeline path — the template is already synthesized by then. Context only matters at synth.
  • cdk/cdk.context.json is not a durable place to set this: it is gitignored, and build.yml regenerates the whole file.

Instead, set the repo (or environment) variable AGENTCORE_AVAILABILITY_ZONES to a JSON array of zone names. build.yml’s “Generate CDK context” step folds it into the context that the uploaded assembly is synthesized with, and an unset variable simply leaves the stack unpinned (synth logs an [AgentCore AZs] warning):

AGENTCORE_AVAILABILITY_ZONES = ["us-east-1b","us-east-1c"]

Choosing the values.

  1. Discover your account’s zone name-to-ID mapping:
    Terminal window
    aws ec2 describe-availability-zones --region <region> \
    --query 'AvailabilityZones[].[ZoneName,ZoneId]' --output text
  2. Pick at least two zone names (column 1) whose zone IDs (column 2) appear in the AgentCore-supported set for that region.
  3. Set the value — the pipeline variable above, or for a local synth either cdk/cdk.json context:
    { "context": { "agentcore:availabilityZones": ["us-east-1b", "us-east-1c"] } }
    or the CLI at synth time:
    Terminal window
    cdk deploy -c 'agentcore:availabilityZones=["us-east-1b","us-east-1c"]'

The override is validated at synth time, and both the JSON-array and -c string forms behave identically. Synth fails with a message naming the key when the value is not an array, has an empty/non-string entry, lists fewer than two distinct zones, contains zone IDs instead of names (use1-az2 — a common column mix-up), or names zones outside the target region. When the account’s mapping is knowable, the override is additionally cross-checked against the supported set, and unsupported or nonexistent zones fail synth.

Upgrading an existing stack. Auto-pin is on by default, so a local cdk deploy against a stack created before this change may select different zones than the deployed subnets use. Subnet.AvailabilityZone is create-only, so that is a replacement of the subnets and the resources bound to them (route tables, NAT gateway/EIP, VPC endpoints). Run mise //cdk:diff first. If the diff shows subnet replacement and you would rather keep the current topology, pin the override to the zones already deployed:

Terminal window
aws ec2 describe-subnets --filters "Name=vpc-id,Values=<vpc-id>" \
--query 'Subnets[].[SubnetId,AvailabilityZone,AvailabilityZoneId]' --output text

Be aware that destroying a VPC whose subnets held AgentCore ENIs can take 20–40 minutes while AWS reclaims them (see the DELETE_FAILED note in the quick start troubleshooting table).

DNS Query Log Config replacement cascade (upgrading from pre-v0.5)

Section titled “DNS Query Log Config replacement cascade (upgrading from pre-v0.5)”

Affects: Stacks deployed before the tag-exclusion fix (#222). Stacks created after this fix are not affected.

Symptom: UPDATE_FAILED on AWS::Route53Resolver::ResolverQueryLoggingConfigAssociation with error InvalidRequest: Cannot create association — one already exists for this VPC.

Root cause: The ResolverQueryLoggingConfig resource is create-only in CloudFormation — any property change (including Tags) triggers a full replacement. Pre-fix stacks have github:sha and other tags on this resource. Although the new code excludes it from future tag applications, CloudFormation still attempts to remove the now-excluded tags from the existing resource during the update, triggering the replacement cascade:

  1. Config is replaced → new physical resource ID
  2. Association detects ResolverQueryLogConfigId changed → triggers its own replacement
  3. CloudFormation attempts Create-before-Delete on the association → Route53 Resolver rejects (one association per VPC) → InvalidRequest

Resolution — choose one:

Section titled “Option A: AWS CLI disassociation (recommended)”

Fastest, scriptable, no console access required. Replace <vpc-id> with the agent VPC ID and <region> with your stack’s region.

  1. List the association for your VPC to get the ResolverQueryLogConfigId:
    Terminal window
    aws route53resolver list-resolver-query-log-config-associations \
    --region <region> \
    --query "ResolverQueryLogConfigAssociations[?ResourceId=='<vpc-id>']"
  2. Disassociate using the Id from step 1:
    Terminal window
    aws route53resolver disassociate-resolver-query-log-config \
    --resolver-query-log-config-id <rqlc-id> \
    --resource-id <vpc-id> \
    --region <region>
  3. Run mise //cdk:deploy — CloudFormation recreates both the config and association without the orphan tags. The pre-existing ResolverQueryLoggingConfig is replaced as part of the same update, so an explicit delete-resolver-query-log-config is not required.

Option B: Two-phase deploy (comment-out / re-add)

Section titled “Option B: Two-phase deploy (comment-out / re-add)”
  1. In cdk/src/stacks/agent.ts, comment out the DnsFirewall construct instantiation (~line 197):
    // new DnsFirewall(this, 'DnsFirewall', {
    // vpc: agentVpc.vpc,
    // additionalAllowedDomains: additionalDomains,
    // observationMode: true,
    // });
  2. Deploy: mise //cdk:deploy — this deletes the query log config, association, firewall rules, and related resources
  3. Uncomment the DnsFirewall block
  4. Deploy again: mise //cdk:deploy — resources are recreated cleanly without tags

Option B is more disruptive (two deploys, brief DNS logging gap) but requires no AWS API access beyond cdk deploy.

Option C: Manual disassociation via AWS Console

Section titled “Option C: Manual disassociation via AWS Console”

For users without AWS CLI access.

  1. Open the Route 53 Resolver console
  2. Select the query logging configuration named agent-dns-query-log
  3. Under Associated VPCs, disassociate the VPC
  4. Delete the query logging configuration
  5. Run mise //cdk:deploy (or cdk deploy) — CloudFormation will recreate both resources without tags
  • Quick start — Zero-to-first-PR in 6 steps.
  • Developer guide — Local development, testing, repository onboarding.
  • User guide — API reference, CLI usage, task management.
  • DEPLOYMENT_ROLES.md — Least-privilege IAM policies for CloudFormation execution.
  • COST_MODEL.md — Per-task costs, cost guardrails, cost at scale.
  • COST_ATTRIBUTION.md — Operator FinOps setup for per-user/per-repo Bedrock chargeback (Cost Explorer / CUR 2.0, invocation-log forensics).
  • COMPUTE.md — Compute backend architecture and trade-offs.
  • ADR-021 — Lambda MicroVMs backend decision, phased rollout, and live-verification evidence.