2026-10-27•10 min read

From Cloud Patterns to Production: Building a Bounded FinOps Agent on Amazon Bedrock AgentCore

#aws#bedrock#agentcore#cdk#python

Frontier models can solve open problems in mathematics, yet asking Claude for three ways to deploy a local PoC with a recommendation still yields unnecessarily expensive servers, several avoidable migrations, and permanent tech debt. Getting an agent to run in a notebook takes hours; getting it to production across cost, security, and operational boundaries without redoing the work three times requires knowing the cloud primitives cold. This post deconstructs a bounded FinOps agent built across 2 CDK stacks on Amazon Bedrock AgentCore in less than a week.

Every architectural choice, rejected alternative, and IAM boundary in this system is recorded in the architectural decision log at commit 127c52b, and the live deployment is available at main.dz5ronnzg5wwo.amplifyapp.com.


Maturing: 34 Days of Cloud Patterns and the Limits of GenAI Architecture

The design was settled on day one by matching market-distilled patterns to the use case. In late summer 2026, I spent 13 calendar days on the AWS Certified Solutions Architect Associate (SAA-C03, 26 August 2026) and 21 calendar days on the AWS Certified Generative AI Developer Professional (AIP-C01, 21 September 2026), the professional developer exam. The skill those exams train is straightforward: a scenario states the requirements, and the credited answer is the set of services that better meets them.

That judgement is why the repository was live in less than a week. Frontier models (Summer 2026) still propose standing VPCs, NAT gateways, EC2 or ECS instances, and broken IAM policies where a managed primitive exists. That gap shrinks once solutions-architecture data with verifiable SLOs is in the training mix. Until then, knowing what not to build is the edge. Longer term, the durable role is SLA ownership: reputational and contractual liability for a service other people rely on.


The Platform: 4 Subsystems, 1 Operational Goal

The CDK layout follows the AWS CDK Python structure, with infrastructure.py and runtime/ composed into two stacks inside component.py. The hourly billing extract is synthetic, generated by scripts/generate_synthetic_data.py.

SubsystemOperational Question AnsweredStackStructural Guarantee
Agent (src/finops_agent/ai)How can a stakeholder investigate cloud spend in plain English without hallucinated SQL or runaway tool loops?Bedrock AgentCore Runtime, AgentCore Gateway, 7-day AgentCore Memory, Bedrock Prompt Management, Strands on Amazon Nova 2 LiteThe assistant runs at most two typed queries per question. The numbers come from the same query as the charts.
Data (src/finops_agent/data)How do UI charts and the AI assistant stay mathematically identical at zero idle cost?AWS Lambda container, FastAPI + Mangum, API Gateway (AWS_IAM), read-only DuckDB over precomputed Parquet dicePrecomputed lines.parquet (2.4 MB) and dice.duckdb (3.6 MB) are baked directly into the Lambda image, serving queries from memory at zero idle database cost.
Frontend (src/finops_agent/frontend)How do we display cloud spend to technical and non-technical stakeholders with zero credentials in the browser?Next.js App Router on AWS Amplify Hosting (WEB_COMPUTE) with a dedicated SsrComputeRoleThe browser talks only to Next.js. The Amplify SSR role signs each call with SigV4. Assistant calls invoke the AgentCore Runtime. Three views: the proposal on /, the split on /split, the explorer on /explore. Each view sends its screen with the question. The drawer shows the SQL, then the answer.
Infrastructure (app.py, component.py)How do we iterate on containers and enforce hard spend limits without wiping conversation history or surprise bills?Two AWS CDK Python stacks (FinopsAgentStatefulStack and FinopsAgentStatelessStack), SynthSettings, AWS Budgets Actions, pytest CDK assertionsThe stateful stack holds only the 7-day memory. There is no data bucket. Monthly budgets of $17 and $14 deny at 100% actual spend. Template tests prove the template.

Core Architectural Patterns

Each pattern below is that same match, a requirement met by the native primitive, with the heavier option left unbuilt.

One Query, Two Doors. Eliminates divergent implementations and model-invented numbers. When an analytics dashboard and an AI assistant compute metrics through separate code paths, numbers drift and trust evaporates. Letting the model write free-form SQL directly against the analytical store invites syntax errors, high latency, and hallucinated figures. Instead, run_query in data/runtime/routes.py is the single implementation behind two doors: API Gateway (POST /investigate) and the AgentCore Gateway MCP tool run_finops_query.json exposed as finops-data___run_finops_query. The agent never imports the underlying database and never receives raw usage rows, only parameterized aggregations and the exact display SQL that produced them. See src/finops_agent/data/runtime/routes.py, src/finops_agent/ai/run_finops_query.json, and the Amazon Bedrock AgentCore Gateway documentation.

Server-Side Tool Budget. Eliminates runaway tool loops that a prompt cannot stop. System prompts asking a model to call a tool "at most twice" are polite suggestions, not execution boundaries; an edge case or hallucination in a retry loop will fire downstream invocations until the request times out. In agent.py, a server-side _QueryBudget wraps the Strands @tool closure and enforces MAX_QUERY_STEPS = 2 in Python before any network hop to the AgentCore Gateway occurs. If the model attempts a third tool call, the execution boundary halts it immediately with a typed error, while short-term conversation recall is similarly bounded to MAX_MEMORY_EVENTS = 10 per turn. See src/finops_agent/ai/runtime/agent.py, src/finops_agent/ai/constants.py, and the Strands Agents tools documentation.

Keyless Browser, IAM at Every Hop. Eliminates credential leakage and shared-secret drift. I rejected a shared header secret, an open function URL, and a Cognito login wall. A shared custom header secret risks drift and replay attacks, an open function URL exposes compute, and a Cognito wall adds unnecessary operational friction. Instead, the browser talks exclusively to the Next.js App Router proxy at route.ts, running on AWS Amplify Hosting under a dedicated SsrComputeRole defined in frontend/infrastructure.py. In component.py, that SSR role signs calls to API Gateway with SigV4 and invokes the AgentCore Runtime directly, while the runtime role uses IAM to reach the AgentCore Gateway. Every hop enforces AWS_IAM with short-lived credentials and zero keys in the browser. See src/finops_agent/frontend/app/api/backend/[...path]/route.ts, src/finops_agent/frontend/infrastructure.py, and the AWS Amplify SSR compute role documentation.

Versioned Prompt, Fail Closed. Eliminates unreviewed prompt drift between git and production, and silent degradation. Hardcoding system prompts inside container images forces a Docker rebuild for every wording change, reading DRAFT in production exposes live traffic to unreviewed console edits, and falling back to a local text file silently masks IAM regressions. The deployment workflow publishes immutable versions from scripts/system_prompt.txt into Bedrock Prompt Management via scripts/upload_prompt.py. On each request, load_system_prompt fetches only published numeric versions, ignores DRAFT, and fails closed with PromptUnavailableError (AGT-2504) if the prompt is missing or empty, stopping execution before calling the model. See src/finops_agent/ai/services/prompt.py, scripts/upload_prompt.py, and the Amazon Bedrock Prompt Management documentation.

Budget-Triggered Deny. Eliminates unbounded model spend. Email alerts do not stop an unattended loop while you sleep, and a budget filtered only on CloudFormation stack tags is blind to Bedrock model invocations because token usage does not inherit stack tags. In component.py, _spend_caps provisions two monthly budgets with automated actions: a $17 project budget across CloudFormation tags, and a $14 budget targeting Nova 2 Lite token consumption. When either budget reaches 100% of actual spend, AWS Budgets automatically attaches a managed IAM Deny policy for model invocation onto the AgentCore Runtime execution role, creating an automated circuit breaker. See src/finops_agent/component.py and the AWS Budgets actions documentation.

Two-Stack Boundary with Template Tests. Eliminates loss of retained state from compute iteration and template mistakes found at deploy time. Putting memory and stateless compute into a single CloudFormation stack risks wiping conversation history during container replacements, while discovering IAM or configuration errors only during cdk deploy turns iterations into slow cloud round-trips. In app.py, FinopsAgentStatefulStack (owning only the 7-day L2 agentcore.Memory construct in ai/infrastructure.py) is isolated from FinopsAgentStatelessStack. Architectural invariants—including the absence of public function URLs, strict role grants, and zero silent configuration defaults—are asserted offline against synthesized templates in tests/test_cdk_stack.py before reaching the cloud. See app.py, src/finops_agent/config.py, tests/test_cdk_stack.py, and the AWS CDK best practices guide.


Two Pesky Bugs

The troubleshooting log records the integration traps hit while wiring AgentCore, Bedrock, and Amplify IAM boundaries. Two of them will cost a full deploy cycle if you have not seen how Bedrock and AgentCore evaluate ARNs at runtime.

BugSymptomRoot CauseFix
Nova 2 Lite cross-region inference profile IAMOn-demand streaming fails with throughput validation errors when targeting the raw model ID, and with AccessDeniedException across regions when targeting only the inference profile.Nova 2 Lite serves on-demand traffic exclusively through cross-region inference profiles; Bedrock evaluates IAM against both the account-scoped inference-profile ARN and the regional foundation-model ARN in whichever destination region processes the request.Set MODEL_ID = "us.amazon.nova-2-lite-v1:0" in ai/constants.py and scope the execution role in ai/infrastructure.py and budget Deny policies in component.py to both the profile ARN and the regional foundation-model ARNs across US regions.
InvokeAgentRuntime denied on runtime ARN aloneNext.js SSR proxy receives AccessDeniedException when SsrComputeRole is granted permissions on the base AgentCore runtime ARN.AgentCore dispatches invocations through a runtime endpoint sub-resource ({runtime_arn}/runtime-endpoint/DEFAULT); a policy scoped only to the parent runtime ARN fails authorization at the endpoint.Call grant_invoke_runtime(self.frontend.ssr_role) in component.py to grant permissions on both the runtime ARN and wildcard sub-resources, and assert both in tests/test_cdk_stack.py.

Conclusion

Matching market-distilled cloud patterns to the use case settles the architecture on day one by choosing the native primitive and leaving the heavier option unbuilt. What shipped is a bounded deployed agent, live in less than a week, with spend caps that deny on their own. I deliberately left unbuilt CI/CD and multi-environment deploys, an evaluation harness, long-term memory, per-user identity, WAF, and a lakehouse.

The next post puts my own AWS bill behind the same query, through CUR 2.0 Data Exports, S3, Glue, and Athena, with a smaller tool vocabulary and one surprise AgentCore capability.

Thank you to Eric Z. Beard and Rico Huijbers for the AWS CDK best-practices article, to Pahud Hsieh and Nathan Peck for the Python CDK project-structure article, to Stéphane Maarek for his SAA-C03 course, to Stéphane Maarek and Frank Kane for their AIP-C01 course, and to the teams who write those AWS exams for distilling production cloud patterns into a rigorous curriculum.

For engineers and tech leads building on AWS: do you believe structured cloud certifications accelerate architectural judgment for production AI? And for already certified practitioners, how have those certifications concretely impacted your live systems?


Daniel Iván Parra Verde is an ML Engineer specializing in production AI agents, distributed systems, and ML platforms. He authored the CDK infrastructure and the runtime services for this project.

GitHub · LinkedIn


Appendix: Source Code & References