TL;DR: I configured AgentCore Runtime under this project's harness one block at a time, session lifecycle, execution role, model resolution, container backing, memory attachment and network mode, and almost every setting's real behavior only showed up on a live invoke, not in a schema or a docs page.
Part 3 of the series "Building a Partner Sales Agent on Amazon Bedrock AgentCore", built around one real project: a conversational agent that connects HubSpot CRM and AWS Partner Central for an AWS Partner's sales team. Article 2 argued why this project hands its agent loop to a managed harness at all; this article configures that harness against the runtime that executes it, block by block. Every setting can be reproduced from the AWS Management Console. My Terraform build is a automation bonus, including the places where automation hit problems a console reader never sees.
Table of Content
- Article 1: Two Systems, One Sales Motion: An AWS Partner Agent on Amazon AgentCore Harness
- Article 2: Why I Didn't Write My Own Agent Loop: The Case for AgentCore Harness
- Article 3: Configuring AgentCore Runtime: Session Lifecycle, Container Backing, Memory
- Article 4: One Gateway, Six Tools: AgentCore Gateway as Your Agent's Only Way Out (coming soon)
- Article 5: A Deterministic Backstop for Your Agent: AgentCore Policy and Cedar (coming soon)
- Article 6: Grounding an Agent Without a Vector Database: AgentCore Managed Knowledge Base (coming soon)
- Article 7: From Company Name to CRM Record: One AgentCore Conversation, End to End (coming soon)
A harness needs exactly two values at creation time: a name and an execution role. Everything else this article covers (the model reference, the memory binding, the container image, the skill, the session limits, the network mode) can be attached later. I have build the agent iterative and got surprises on nearly step in the way. So I walk the resource from the outside in and hold every block to the same question: what does this actually do at runtime and how do I know? Where the answer came from a live failure instead of documentation, I say so. That covers most of the article.
Session lifecycle: how long a conversation stays warm
A harness session opens on the first invoke and stays warm as long as calls keep landing. Two limits bound it: an idle timeout of 15 minutes and a maximum lifetime of 8 hours [1]. Each InvokeHarness call restarts the idle counter from zero. What ends a session early is a gap between calls, never a count of turns: forty short exchanges inside ten minutes leave the counter at zero, while one sixteen-minute pause terminates the session. The 8-hour ceiling is the other bound, and it holds regardless of activity.
In the console, these limits live in the harness's runtime-environment settings, which share their parameters with AgentCore Runtime itself: idle session timeout, maximum lifetime, network configuration, filesystem configuration [1]. A harness is backed by an AgentCore Runtime resource under the hood, witch you can see also twice on the IAM grant and network section. In my demo I run with the default values.
For a live demo, the numbers translate directly. A sales representative can pause mid-conversation for up to 15 minutes, take audience questions, and resume with full session state. A longer gap or the 8-hour ceiling, means the next invoke opens a fresh session silently. There is no error and no warning. There is just no continuity and an agent that suddenly asks which opportunity you meant.
The operational risk for a workshop is therefore specific: watch the gap between the last invoke and the next one. A Q&A block that runs 20 minutes resets state mid-presentation. If you ask your self how to resolve this issue, the answer is a long-term memory, that survives session boundaries by design.
Execution role: one identity for everything the agent does
Everything the agent does at runtime, it does as one IAM role. harness_exec is the harness's execution role, and it is the identity behind the Bedrock model calls, the Memory reads and writes, and the SigV4-signed calls to the Gateway, plus the ECR image pulls and the observability writes that keep the runtime diagnosable.
AWS documents the baseline fan-out for a harness execution role across four services: AgentCore Identity for workload access tokens, X-Ray for sampling rules and traces, CloudWatch for log groups and streams and ECR for image and layer access [2]. This project grants three of the four and skips Identity entirely. The harness's outbound authentication toward the Gateway is a tagged union of three variants: aws_iam (a boolean), none (a boolean) or oauth with a credential-provider ARN and scopes. Only the oauth variant consumes an AgentCore Identity resource. aws_iam = true, which this project sets, is a self-contained SigV4-signing mode: the harness signs Gateway calls with its own execution-role credentials and needs no Identity resource anywhere.
In the console, the execution role is a single field on the harness form. I have scoped a policy per capability:
each grant is its own policy document and role policy, listing only the actions one capability needs, against one ARN. No wildcard resource appears anywhere except where a service's own IAM model demands it: ecr:GetAuthorizationToken is a registry-level action that ECR refuses to scope to a repository [3], the X-Ray write actions take no resource ARN, and cloudwatch:PutMetricData is condition-scoped to the bedrock-agentcore namespace instead. Across the whole project there is exactly one further documented exception, the Knowledge Base's bedrock:AgenticRetrieveStream grant, held on the Gateway's service role, which AWS does not support scoping to a single Knowledge Base; article 6 covers it.
Here is what one of those pairs looks like in the automated build. This is the Memory grant and its action list:
data "aws_iam_policy_document" "harness_exec_memory" {
statement {
effect = "Allow"
actions = [
"bedrock-agentcore:CreateEvent",
"bedrock-agentcore:ListEvents",
"bedrock-agentcore:GetEvent",
"bedrock-agentcore:ListSessions",
"bedrock-agentcore:ListActors",
"bedrock-agentcore:RetrieveMemoryRecords",
]
resources = [awscc_bedrockagentcore_memory.agent.memory_arn]
}
}
resource "aws_iam_role_policy" "harness_exec_memory" {
name = "agentcore-memory"
role = aws_iam_role.harness_exec.id
policy = data.aws_iam_policy_document.harness_exec_memory.json
}
This policy did not exist on day one. My first live smoke test failed with an AccessDeniedException on bedrock-agentcore:ListEvents against the attached Memory resource's ARN, because the role only carried the model-invoke actions at that point. bedrock-agentcore:* is a distinct IAM namespace from bedrock:* and nothing in the toolchain flags its absence: terraform validate passed, the plan was clean, the apply succeeded, and the harness itself created without complaint. The gap surfaced at the first real invoke, as a 403.
Once tools were attached, the harness stopped calling the model through InvokeModel and switched to Bedrock's Converse and ConverseStream operations, two separate IAM actions the role had never been granted: another live AccessDeniedException. Both actions folded into the same policy that already carried the plain InvokeModel* grant, against the same ARNs (the profile-versus-region ARN split is Model resolution's subject, below):
data "aws_iam_policy_document" "harness_exec_bedrock_invoke" {
statement {
effect = "Allow"
actions = [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream",
"bedrock:Converse",
"bedrock:ConverseStream",
]
resources = [
"arn:aws:bedrock:eu-central-1:${data.aws_caller_identity.current.account_id}:inference-profile/eu.anthropic.claude-sonnet-5",
"arn:aws:bedrock:eu-central-1::foundation-model/anthropic.claude-sonnet-5",
"arn:aws:bedrock:eu-north-1::foundation-model/anthropic.claude-sonnet-5",
"arn:aws:bedrock:eu-south-1::foundation-model/anthropic.claude-sonnet-5",
"arn:aws:bedrock:eu-south-2::foundation-model/anthropic.claude-sonnet-5",
"arn:aws:bedrock:eu-west-1::foundation-model/anthropic.claude-sonnet-5",
"arn:aws:bedrock:eu-west-3::foundation-model/anthropic.claude-sonnet-5",
]
}
}
resource "aws_iam_role_policy" "harness_exec" {
name = "bedrock-invoke"
role = aws_iam_role.harness_exec.id
policy = data.aws_iam_policy_document.harness_exec_bedrock_invoke.json
}
The same switch triggered Bedrock's marketplace-subscription check for third-party models. With a new resource in one of the most recent AWS terraform provider releases, I could enable the model as well via Terraform (there is also an option in the AWS Management Console):
data "aws_iam_policy_document" "harness_exec_marketplace" {
statement {
effect = "Allow"
actions = [
"aws-marketplace:ViewSubscriptions",
"aws-marketplace:Subscribe",
]
resources = ["*"]
condition {
test = "StringEquals"
variable = "aws-marketplace:ProductId"
values = ["prod-4ezhkeia6k2cs"] # Claude Sonnet 5's marketplace product id
}
}
}
resource "aws_iam_role_policy" "harness_exec_marketplace" {
name = "marketplace-model-subscription"
role = aws_iam_role.harness_exec.id
policy = data.aws_iam_policy_document.harness_exec_marketplace.json
}
The AgentCore Gateway call needed bedrock-agentcore:InvokeGateway on the Gateway's ARN:
data "aws_iam_policy_document" "harness_exec_invoke_gateway" {
statement {
effect = "Allow"
actions = ["bedrock-agentcore:InvokeGateway"]
resources = [aws_bedrockagentcore_gateway.agent.gateway_arn]
}
}
resource "aws_iam_role_policy" "harness_exec_invoke_gateway" {
name = "invoke-gateway"
role = aws_iam_role.harness_exec.id
policy = data.aws_iam_policy_document.harness_exec_invoke_gateway.json
}
Initial I had some trouble finding out why the harness was not writing any CloudWatch logs until I figured out what permissions where missing. This is the one policy in the set with more than one statement, because logs, X-Ray, and the metrics namespace each carry their own scoping rules:
data "aws_iam_policy_document" "harness_exec_observability" {
statement {
effect = "Allow"
actions = ["logs:CreateLogGroup", "logs:DescribeLogStreams"]
resources = ["arn:aws:logs:eu-central-1:${data.aws_caller_identity.current.account_id}:log-group:/aws/bedrock-agentcore/runtimes/*"]
}
statement {
effect = "Allow"
actions = ["logs:DescribeLogGroups"]
resources = ["arn:aws:logs:eu-central-1:${data.aws_caller_identity.current.account_id}:log-group:*"]
}
statement {
effect = "Allow"
actions = ["logs:CreateLogStream", "logs:PutLogEvents"]
resources = ["arn:aws:logs:eu-central-1:${data.aws_caller_identity.current.account_id}:log-group:/aws/bedrock-agentcore/runtimes/*:log-stream:*"]
}
statement {
effect = "Allow"
actions = ["xray:PutTraceSegments", "xray:PutTelemetryRecords", "xray:GetSamplingRules", "xray:GetSamplingTargets"]
resources = ["*"] # X-Ray's write and sampling actions take no resource ARN
}
statement {
effect = "Allow"
actions = ["cloudwatch:PutMetricData"]
resources = ["*"]
condition {
test = "StringEquals"
variable = "cloudwatch:namespace"
values = ["bedrock-agentcore"]
}
}
}
resource "aws_iam_role_policy" "harness_exec_observability" {
name = "agentcore-observability"
role = aws_iam_role.harness_exec.id
policy = data.aws_iam_policy_document.harness_exec_observability.json
}
Five of the six policies here added during troubleshooting (RTFM). Only the ECR grant was added before it could fail, because the developer guide states the requirement plainly [3]. For the container I added the following permissions:
data "aws_iam_policy_document" "harness_exec_ecr" {
statement {
effect = "Allow"
actions = ["ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage"]
resources = [aws_ecr_repository.harness.arn]
}
statement {
effect = "Allow"
actions = ["ecr:GetAuthorizationToken"]
resources = ["*"]
}
}
resource "aws_iam_role_policy" "harness_exec_ecr" {
name = "ecr-image-pull"
role = aws_iam_role.harness_exec.id
policy = data.aws_iam_policy_document.harness_exec_ecr.json
}
Model resolution: the profile id, not the model id
The harness's model block takes a single id string. This project runs Claude Sonnet 5 through eu.anthropic.claude-sonnet-5, a cross-region inference profile: Bedrock's routing layer that spreads one model's traffic across a set of regional endpoints under a single addressable id [4]. This profile routes across six regions: eu-central-1, eu-north-1, eu-south-1, eu-south-2, eu-west-1, and eu-west-3.
The bare model id fails and I confirmed the mechanism directly: aws bedrock get-foundation-model reports the bare id's inferenceTypesSupported as ["INFERENCE_PROFILE"] and nothing else. There is no on-demand single-region invocation for this model. The config field would accept the bare id, nothing validates it at Terraform plan time and every invocation would then fail to reach the model.
In the console: pick the inference profile, not the foundation model, wherever the harness asks for a model id. The Bedrock console's cross-region inference page lists the profiles available in your region, with their status and routing targets.
The IAM consequence follows in the same way, because the routing layer changes what gets authorized. Bedrock checks both the profile call and the regional endpoint the call lands on, so the execution role's bedrock:InvokeModel* grant scopes to seven ARNs, not one: the inference-profile ARN, which is account-scoped, plus the six underlying per-region foundation-model ARNs, which carry no account segment because foundation models are AWS-global resources. Scope to the profile alone and the invocation fails in whichever region the router picks.
Container backing and why it exists here
Skills are filesystem-path artifacts. A purely declarative harness has no filesystem to put them on. That single sentence is the entire reason this project's harness is container-backed: the BANT-qualification Skill (budget, authority, need, timeline) ships baked into an image and nothing else about the harness needed a container at all.
The default harness environment gives the agent Python and Bash, which is enough for most configurations [5]. A custom environment is an ECR image built for linux/arm64 and it comes with one behavior that changes how you think about the image: the harness overrides the container's ENTRYPOINT and CMD, so your startup command never runs [5]. The container is a filesystem layer, nothing more. The harness's own managed agent process, the model, the tool blocks, the system prompt, runs unaffected by the conversion; your installed software, filesystem, and environment variables are simply available to it. My Dockerfile is accordingly two lines of substance, a FROM and a COPY, with no ENTRYPOINT and no application code. The one contract that matters is that the skill's configured path be byte-identical to the COPY destination [6]:
environment_artifact {
container_configuration {
container_uri = "${aws_ecr_repository.harness.repository_url}:${var.image_tag}"
}
}
skill {
path = ".agents/skills/sales-methodology-implementer"
}
The console path is: create the ECR repository -> push the image -> then set the image URI and the skill path on the harness.
Memory attachment
AgentCore Memory gives the harness two layers: short-term session events and long-term strategies that extract durable records from those events. In the console, the Memory resource is its own thing under Build, created with a name, an event-retention period and a set of strategies. The harness then binds it by ARN. This project's Memory resource carries all four built-in long-term strategies: semantic, summary, user preference and episodic with a reflection configuration (I wanted to try everything).
Console readers configure this in forms. Automation readers hit a wall first: hashicorp/aws's aws_bedrockagentcore_memory resource has no memory-strategy block anywhere in its schema. Only hashicorp/awscc's Cloud-Control-backed awscc_bedrockagentcore_memory models the four strategies. Here it is, condensed to the shape that matters:
resource "awscc_bedrockagentcore_memory" "agent" {
name = "partner_growth_agent_memory"
event_expiry_duration = 30
memory_strategies = [
{ semantic_memory_strategy = {
name = "semantic"
namespace_templates = ["/strategies/{memoryStrategyId}/actors/{actorId}/semantic"]
} },
{ summary_memory_strategy = {
name = "summary"
namespace_templates = ["/strategies/{memoryStrategyId}/actors/{actorId}/sessions/{sessionId}/summary"]
} },
{ user_preference_memory_strategy = {
name = "user_preference"
namespace_templates = ["/strategies/{memoryStrategyId}/actors/{actorId}/preferences"]
} },
{ episodic_memory_strategy = {
name = "episodic"
namespace_templates = ["/strategies/{memoryStrategyId}/actors/{actorId}/sessions/{sessionId}/episodes"]
reflection_configuration = {
# must be the same as, or a prefix of, the episodic namespace above
namespace_templates = ["/strategies/{memoryStrategyId}/actors/{actorId}"]
}
} },
]
# The awscc provider cannot flatten memory_strategies back into state;
# without this, every plan re-proposes the same phantom change forever.
lifecycle {
ignore_changes = [memory_strategies]
}
}
The namespace templates carry the design decision. Look at which strategies scope to {actorId} and which to {sessionId}. Semantic facts, user preferences and episodic reflections are statements about the actor, so they scope to {actorId} alone: that is what gives the fixed demo actor true cross-session recall, where the sales rep closes the browser, opens a new session tomorrow, and the agent still knows what it learned. Summaries and raw episodes are per-conversation artifacts, so they keep {sessionId} in their path and die with the conversation's relevance. This split is a reasoning step, not a default; the templates ship empty and force you to make it.
Two constraints are documented nowhere I could find, not in the CreateMemory API reference and not in the CloudFormation resource schema, and both surfaced only as live ValidationExceptions [8]: namespace_templates is actually required per strategy despite being marked optional (AWS's own worked memory examples all set it explicitly, which corroborates the finding [9]) and the episodic strategy's reflection namespace must be the same as, or a prefix of, the episodic namespace itself. My first attempt used a sibling path and the service rejected it with an error message.
On the actorId I had a important learning: I fix defined this id in Terraform. On the deployed system every Harness invocation passed per-user a actorId (derives from the caller's verified Cognito identity and that per-request value, not this default is what isolates users' memory from each other).
On the validation of this feature: To check cross-session recall I created a session and stated a fictional example the agent should remember. I then opend a new session a few minutes later and asked to recall that fact. It worked really well.
The same Memory resource also backs the chat UI's session-history feature, which reads and deletes conversations through its own separately scoped policies.
Network mode: PUBLIC by omission
Here is a configuration finding I have not seen written down anywhere: in the installed Terraform provider, there is no network_mode attribute on the harness resource at all. PUBLIC network mode is expressed by omitting the entire environment/network_configuration block. There is no enum to set; the block simply stays unwritten. And you should not try to write the VPC variant casually either: setting a VPC network_configuration triggers a permanent taint loop in the installed provider, an open issue at the time of writing [11].
A console reader never meets this. The console presents network mode as a visible choice between public and VPC and choosing public is unremarkable. In the automated build, the same choice looks like something is missing: grepping the Terraform for a network block and finding nothing is the configuration, not evidence that a step was skipped. The harness resource's overall shape makes the point best, with the absence marked:
resource "aws_bedrockagentcore_harness" "agent" {
harness_name = var.harness_name
execution_role_arn = aws_iam_role.harness_exec.arn
model { ... } # model resolution, above
environment_artifact { ... } # container backing, above
skill { ... }
memory { ... } # memory attachment, above
tool { ... } # the Gateway binding, article 4
# Deliberately no environment / network_configuration block.
}
The two modes on offer, public and VPC, are AgentCore Runtime's own, the runtime backing showing through one last time [12]. I had no use-case for the VPC mode, but you could used it to access private resources, internal APIs and services that run non-serverless.
Transferable patterns
Five habits from this configuration work generalize beyond the demo to any AgentCore build.
- PUBLIC-by-omission. Network mode is not a value; it is the absence of a block. Before assuming a resource type needs an explicit off setting, check whether the provider models the default as an omitted block instead of an enum.
-
The awscc-versus-aws provider check.
hashicorp/awshas no memory-strategy support on its Memory resource; onlyhashicorp/awsccreaches AgentCore's Cloud-Control-backed long-term-memory API. When a resource looks incompletely modeled in one provider, check the sibling Cloud-Control provider's schema before concluding the feature is unsupported in Terraform. - One scoped policy per capability, not one role-wide policy. Each policy-document-and-role-policy pair grants exactly the actions one capability needs, against one ARN. It costs more files up front and pays back the first time you need to reason about the blast radius of a single service or to delete a capability cleanly.
-
IAM surfaces one 403 at a time, and
terraform validatecatches none of them. A clean plan and a clean apply prove nothing about runtime authorization in a service-specific namespace likebedrock-agentcore:*. Budget a live-invoke debugging pass after every apply that touches a new capability. In this build that pass found five of the execution role's six policies.
The pattern under all of it
Step back from the sections and one shape repeats.
- Network mode: I assumed a setting existed and the truth was an absent block.
- Memory strategies: I assumed the standard provider covered the resource and the truth was a second provider by necessity.
- The execution role: I assumed the documented actions were the whole list, and the truth arrived one live 403 at a time, five policies' worth.
PUBLIC-by-omission, awscc-by-necessity, and IAM found one 403 at a time are the same finding wearing three different names: the straightforward assumption failed and the live apply is what caught it. None of these surfaced in validation, planning, or documentation review. Every one surfaced when the deployed system was made to actually do the thing.
That is no argument against the harness. When the runtime is configured, it runs reliable. I can only suggest to get your hands dirty and try it out yourself.
I hope this article was useful for you. I would love to receive feedback on what you liked and disliked, so that I can improve any future article.
Sources
[1] Amazon Bedrock AgentCore developer guide: harness/runtime session lifecycle, idleRuntimeSessionTimeout and maxLifetime, and the runtime-environment parameters shared with AgentCore Runtime. https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-lifecycle-settings.html
[2] Amazon Bedrock AgentCore developer guide: baseline execution-role permissions for the runtime (AgentCore Identity, X-Ray, CloudWatch, ECR). https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-permissions.html
[3] Amazon Bedrock AgentCore developer guide: private ECR access for custom container images, including the ecr:GetAuthorizationToken registry-level grant. https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/harness-security.html
[4] Amazon Bedrock user guide: cross-region inference and inference profiles. https://docs.aws.amazon.com/bedrock/latest/userguide/cross-region-inference.html
[5] Amazon Bedrock AgentCore developer guide: custom environment (container images); the default Python-and-Bash environment, the linux/arm64 requirement, and the ENTRYPOINT/CMD override. https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/harness-environment.html
[6] Amazon Bedrock AgentCore developer guide: filesystem-path skills and baking skill directories into a container image. https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/harness-skills.html
[7] Terraform CLI documentation: resource targeting (-target) as an exceptional-use mechanism for ordering constraints. https://developer.hashicorp.com/terraform/cli/commands/plan#resource-targeting
[8] Amazon Bedrock AgentCore API reference: CreateMemory and its memory-strategy input, where namespaceTemplates is marked optional. https://docs.aws.amazon.com/bedrock-agentcore-control/latest/APIReference/API_CreateMemory.html
[9] AWS Machine Learning Blog: "Amazon Bedrock AgentCore Memory: Building context-aware agents", whose worked examples all set namespace templates explicitly. https://aws.amazon.com/blogs/machine-learning/amazon-bedrock-agentcore-memory-building-context-aware-agents/
[10] GitHub, hashicorp/terraform-provider-aws issue #48496: the harness resource crashes plan/apply when the memory block is omitted and the harness defaults to managed memory. https://github.com/hashicorp/terraform-provider-aws/issues/48496
[11] GitHub, hashicorp/terraform-provider-aws issue #48159: setting a VPC network_configuration on the harness causes a permanent taint loop. https://github.com/hashicorp/terraform-provider-aws/issues/48159
[12] Amazon Bedrock AgentCore developer guide: network modes (public and VPC), and VPC mode's ENI-based attachment to your VPC via the AgentCore service-linked role. https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agentcore-vpc.html
[13] Docker documentation: multi-platform builds with buildx. https://docs.docker.com/build/building/multi-platform/




Top comments (0)