Deployment guide
Deployment guide
Section titled “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.
Architecture overview
Section titled “Architecture overview”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:
| Aspect | AgentCore (default) | ECS Fargate (opt-in) | Lambda MicroVMs (experimental) |
|---|---|---|---|
| Compute | Bedrock AgentCore Runtime (Firecracker MicroVMs) | ECS Fargate containers | AWS Lambda MicroVMs |
| Resources | 2 vCPU, 8 GB RAM, 2 GB max image size | 2 vCPU, 4 GB RAM | 8 GB baseline / 32 GB peak memory |
| Orchestration | Durable Lambda (checkpoint/replay) | Same durable Lambda via ComputeStrategy | Same durable Lambda via ComputeStrategy |
| Agent mode | FastAPI 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 duration | 8 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.
Lambda MicroVMs backend (experimental)
Section titled “Lambda MicroVMs backend (experimental)”Not for production.
lambda-microvmcarries no smoke-parity guarantee for an unattended deployment. Keep production repositories onagentcoreorecs. 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:
mise //cdk:deploy -- --context compute_type=lambda-microvmYou must re-bootstrap first. This is the single most common way this backend fails, and the failure does not look like a configuration problem:
-
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 -
If it is below 1.6.0, re-bootstrap with the backend included in
ComputeTypes.cdk bootstrapcannot pass template parameters, so the parameter goes on theCDKToolkitstack 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-microvmBundle 1.6.0 adds the
MicrovmPassRolesstatement, without which the CDK-managed MicroVM image deploy fails with aniam:PassRoleAccessDenied on the build role — an IAM error that reads like a code bug. Rationale and the live evidence for the missingiam:PassedToServicecondition: DEPLOYMENT_ROLES.md. -
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 doctorprobes it live.
Operational notes specific to this backend:
- Nothing self-terminates. A MicroVM whose task finished, crashed, or hung stays
RUNNINGand billing until the 8-hour cap. The orchestrator callsTerminateMicrovmon 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/runhook 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
/runpayload as aplatform_configblock. A version-skewed orchestrator that does not send it is refused rather than run with tenant scoping disabled.
Optional Agent Registry
Section titled “Optional Agent Registry”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:
cdk deploy --context enableAgentRegistry=falseBlueprints 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.
Bedrock inference geography
Section titled “Bedrock inference geography”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:
mise //cdk:deploy -- -c bedrockGeoRegion=us # or eu, apac, jp, au, us-govOne 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.
Scale-to-zero analysis
Section titled “Scale-to-zero analysis”Components that scale to zero (pay-per-use)
Section titled “Components that scale to zero (pay-per-use)”| Component | Billing Model | Idle Cost |
|---|---|---|
| DynamoDB (8 core tables; integrations add more) | PAY_PER_REQUEST | $0 |
| Lambda (all functions) | Per invocation | $0 |
| API Gateway REST | Per request | $0 |
| ECS Fargate tasks (when enabled) | Per running task | $0 (cluster is free) |
| AgentCore Runtime | Per session | $0 |
| Bedrock inference | Per token | $0 |
| AgentCore Memory | Proportional to usage | ~$0 |
| Cognito | Free tier (50K MAU) | $0 |
Components that do not scale to zero (always-on)
Section titled “Components that do not scale to zero (always-on)”| Component | Est. Monthly Idle Cost | Why |
|---|---|---|
| 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 | ~$5 | Base monthly charge |
| CloudWatch Dashboard | ~$3 | Per-dashboard charge |
| Secrets Manager (1+ secrets) | ~$0.40/secret | Per-secret monthly |
| CloudWatch Alarms | ~$0.10/alarm | Per standard alarm |
| CloudWatch Logs retention | ~$1-5 | Storage 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.
Incremental cost-control cost
Section titled “Incremental cost-control cost”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.
AWS services inventory
Section titled “AWS services inventory”Compute
Section titled “Compute”| Service | Used By | Scales 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 resources | Yes |
| Service | Used By | Scales to Zero |
|---|---|---|
| Bedrock (Claude Sonnet 4.6, Opus 4.8, Opus 5, Haiku 4.5) | Agent reasoning, cross-region inference profiles | Yes |
| Bedrock Guardrails | Prompt injection detection on task input | Yes |
| Bedrock AgentCore Memory | Semantic + episodic extraction strategies | Yes |
| AWS Agent Registry (preview, default-on) | Versioned agent asset catalog and governance | N/A (managed preview service) |
Networking
Section titled “Networking”| Service | Used By | Scales to Zero |
|---|---|---|
| VPC (public + private subnets, 2 AZs) | All compute | N/A (no direct cost) |
| NAT Gateway (1x) | Private subnet internet egress | No (~$32/mo) |
| VPC Interface Endpoints (7x, 2 AZs) | AWS service connectivity from private subnets | No (~$102/mo) |
| VPC Gateway Endpoints (2x: S3, DynamoDB) | S3 and DynamoDB connectivity | Yes (free) |
| Security Groups | HTTPS-only egress | N/A |
| Route 53 Resolver DNS Firewall | Domain allowlisting for agent egress | Minimal |
Storage / Database
Section titled “Storage / Database”| Service | Used By | Scales 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 Streams | TaskEventsTable → FanOut Consumer; TaskTable → combined orchestration/budget reconciler | Yes |
| S3 | CDK 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 queues | Yes |
| Secrets Manager | GitHub PAT, webhook HMAC secrets | No (~$0.40/secret/mo) |
API / Auth
Section titled “API / Auth”| Service | Used By | Scales to Zero |
|---|---|---|
| API Gateway (REST) | Task REST API | Yes |
| Cognito User Pool | CLI/API authentication | Yes (free tier) |
| WAF v2 | API Gateway protection (managed rules + rate limiting) | No (~$5/mo base) |
Scheduling
Section titled “Scheduling”| Service | Used By | Scales to Zero |
|---|---|---|
| EventBridge (scheduled rule) | Stranded task reconciler (every 5 min) | Yes (rule is free; Lambda invocation is the cost) |
Observability
Section titled “Observability”| Service | Used By | Scales to Zero |
|---|---|---|
| CloudWatch Logs (multiple log groups) | Application, usage, model invocation, VPC flow, DNS query logs | No (storage) |
| CloudWatch Dashboard | Operational metrics visualization | No (~$3/mo) |
| CloudWatch Alarms | Operational failures plus 80%/100% monthly-budget thresholds | No (~$0.10/alarm) |
| X-Ray | AgentCore Runtime tracing | Yes |
Infrastructure / Deployment
Section titled “Infrastructure / Deployment”| Service | Used By | Scales to Zero |
|---|---|---|
| CloudFormation | Stack deployment, custom resources | N/A |
| ECR | Container image storage | Minimal |
| IAM | Roles and policies for all components | N/A |
Reference
Section titled “Reference”CI/CD pipeline (deploy.yml)
Section titled “CI/CD pipeline (deploy.yml)”The repository includes a two-stage CI/CD pipeline:
Stage 1: Build (build.yml)
Section titled “Stage 1: Build (build.yml)”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.
Stage 2: Deploy (deploy.yml)
Section titled “Stage 2: Deploy (deploy.yml)”Triggers via workflow_run when build.yml completes successfully. The pipeline:
- Skips fork PRs —
head_repository.full_name == github.repositoryprevents forks from entering the deploy flow. This is a security measure: an untrusted fork could modifybuild.ymlto produce a deploy-intent artifact, which would otherwise prompt maintainers for approval unnecessarily. - Downloads
deploy-intent.jsonfrom the triggering build run. - Resolves targets — Determines which compute types to deploy:
intent: "-"→ no-op (most PRs)intent: "labels"→ reads PR labels against an allowlistintent: "<type>"→ deploys the specified type (e.g.,agentcore)
- Requires approval — The
deployjob uses a GitHub Environment with required reviewers. Approvals are logged and the self-review rule prevents unilateral deploys. - Deploys via OIDC — Assumes an IAM role via GitHub OIDC federation (no long-lived credentials). The role is scoped to the
cdk deployaction with least-privilege policies per DEPLOYMENT_ROLES.md.
Security controls
Section titled “Security controls”| Control | Purpose |
|---|---|
Fork exclusion (head_repository check) | Prevents fork PRs from triggering deploy approval prompts |
| Environment approval | Human gate before any deploy reaches AWS |
| OIDC federation | No stored AWS credentials; tokens are request-scoped |
| Compute type allowlist | Only pre-approved types can be deployed |
| Non-cancellable concurrency | Deploy can’t be interrupted mid-flight |
For administrators
Section titled “For administrators”- Enable deploys: Set the
deployEnvironment in repo settings with required reviewers. - Configure OIDC: Set
AWS_ROLE_TO_ASSUMEsecret andAWS_REGIONvariable. - Allowlist compute types: Edit
ALLOWED_COMPUTE_TYPESindeploy.yml. - Deploy via PR label: Add the
deploy:<type>label to a PR (e.g.,deploy:agentcore).
Known deployment issues
Section titled “Known deployment issues”AgentCore unsupported Availability Zones
Section titled “AgentCore unsupported Availability Zones”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.
Which deploy paths are protected
Section titled “Which deploy paths are protected”| Path | Auto-pinned? | What you must do |
|---|---|---|
Local cdk deploy / mise //cdk:deploy (credentials resolve at synth) | Yes — for regions in the built-in map | Nothing, unless synth reports an [AgentCore AZs] error |
CI/CD (build.yml → deploy.yml) | No — the assembly is synthesized credential-less | Set the AGENTCORE_AVAILABILITY_ZONES repo/environment variable (below) |
| Any region absent from the built-in map | No | Set 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=...'tocdk deployhas no effect on the pipeline path — the template is already synthesized by then. Context only matters at synth. cdk/cdk.context.jsonis not a durable place to set this: it is gitignored, andbuild.ymlregenerates 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.
- Discover your account’s zone name-to-ID mapping:
Terminal window aws ec2 describe-availability-zones --region <region> \--query 'AvailabilityZones[].[ZoneName,ZoneId]' --output text - Pick at least two zone names (column 1) whose zone IDs (column 2) appear in the AgentCore-supported set for that region.
- Set the value — the pipeline variable above, or for a local synth either
cdk/cdk.jsoncontext:or the CLI at synth time:{ "context": { "agentcore:availabilityZones": ["us-east-1b", "us-east-1c"] } }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:
aws ec2 describe-subnets --filters "Name=vpc-id,Values=<vpc-id>" \ --query 'Subnets[].[SubnetId,AvailabilityZone,AvailabilityZoneId]' --output textBe 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:
- Config is replaced → new physical resource ID
- Association detects
ResolverQueryLogConfigIdchanged → triggers its own replacement - CloudFormation attempts Create-before-Delete on the association → Route53 Resolver rejects (one association per VPC) →
InvalidRequest
Resolution — choose one:
Option A: AWS CLI disassociation (recommended)
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.
- 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>']" - Disassociate using the
Idfrom step 1:Terminal window aws route53resolver disassociate-resolver-query-log-config \--resolver-query-log-config-id <rqlc-id> \--resource-id <vpc-id> \--region <region> - Run
mise //cdk:deploy— CloudFormation recreates both the config and association without the orphan tags. The pre-existingResolverQueryLoggingConfigis replaced as part of the same update, so an explicitdelete-resolver-query-log-configis not required.
Option B: Two-phase deploy (comment-out / re-add)
Section titled “Option B: Two-phase deploy (comment-out / re-add)”- In
cdk/src/stacks/agent.ts, comment out theDnsFirewallconstruct instantiation (~line 197):// new DnsFirewall(this, 'DnsFirewall', {// vpc: agentVpc.vpc,// additionalAllowedDomains: additionalDomains,// observationMode: true,// }); - Deploy:
mise //cdk:deploy— this deletes the query log config, association, firewall rules, and related resources - Uncomment the
DnsFirewallblock - 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.
- Open the Route 53 Resolver console
- Select the query logging configuration named
agent-dns-query-log - Under Associated VPCs, disassociate the VPC
- Delete the query logging configuration
- Run
mise //cdk:deploy(orcdk deploy) — CloudFormation will recreate both resources without tags
Related docs
Section titled “Related docs”- 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.