DEV Community

Cover image for A Layman's Guide to Amazon Bedrock AgentCore: Identity, Gateway, Memory, and Runtime in Terraform

A Layman's Guide to Amazon Bedrock AgentCore: Identity, Gateway, Memory, and Runtime in Terraform

Contents

The AgentCore platform builds on more than 20 years of AWS experience solving the problems of Amazon's store and its end customers. The service reshapes many of the ideas we've already built up about compute, network, storage, and databases, and enables connectivity between accounts and between regions without requiring you to know software-defined networking in depth. It also gives you a way to hook legacy systems up to the new agents. The platform consists of several capabilities, which we'll go through one by one: Identity, Gateway, Memory, and Runtime.

AgentCore Identity: who can do what

AgentCore Identity manages the agent's identity and its access to external services and tools through an abstraction called workload identity. The keys for external services are kept in Secrets Manager under the prefix bedrock-agentcore-identity!. In practice, the service manages the identification flow — who wants to do what, and whether it's been approved. That way you guarantee no rogue behavior: the agent has exactly as much access as you've given it.

Two forms of authentication and authorization are on offer.

2LO (2-legged OAuth) — the agent presents itself to the external system with a pre-issued token: something like a pass issued on your behalf that defines exactly what it's allowed to do. The secrets are kept in Secrets Manager. In the iGaming sector, 2LO is something of a taboo to be avoided at all costs — the word PAT stirs up strong emotions in the infosec community, no matter whether the token is encrypted with a KMS key under a strict policy. A bit like "dracarys" around the dragons in Game of Thrones.

3LO (3-legged OAuth) — Here you have three parties: the agent, the user, and a third-party system. The agent by itself has no rights whatsoever in Jira, Confluence, or GitHub. The first time it asks to act on your behalf, the system shows you a consent screen — just like when an app asks whether it may access your camera. The screen states exactly what the agent will be able to do (for example, only read tickets), and when you hit Allow, the system issues a token with only those rights, which AgentCore Identity stores for you. From then on the agent works with that token and can perform only the approved actions — nothing more. For each of the systems (Jira, Confluence, GitHub) the consent is separate, with its own permissions.

A 2LO example:

resource "aws_bedrockagentcore_api_key_credential_provider" "github" {
  name               = "github-pat-token"
  api_key_wo         = local.envs["GITHUB_PAT"]
  api_key_wo_version = var.token_version
}
Enter fullscreen mode Exit fullscreen mode

Identity and keys are only half the story, though — the agent also needs tools it can reach with them. Enter Gateway.

AgentCore Gateway: the right tool, no wasted tokens

AgentCore Gateway lets you connect to Lambda functions, Smithy models, OpenAPI-compatible APIs, and external MCP servers. It also solves a very important problem: when you hand an agent a lot of tools, it often gets it wrong and picks the wrong one. That costs tokens, and the customer pays for them. That's why Gateway uses semantic search to find the right tool.

The Terraform code defines the gateway and its target. The OpenAPI schema can be passed inline in JSON format; if it's in YAML, you convert it with Terraform functions — jsonencode(yamldecode(...)). One quirk: if the target is an AgentCore Runtime agent, the gateway must have no protocol type set — Runtime targets can't be added to a gateway with protocol_type = "MCP".

data "aws_iam_policy_document" "gtw_plcy" {
  statement {
    sid     = "GetResourceOauth2Token"
    effect  = "Allow"
    actions = ["bedrock-agentcore:GetResourceApiKey"]
    resources = ["arn:aws:bedrock-agentcore:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:token-vault/default/apikeycredentialprovider/${aws_bedrockagentcore_api_key_credential_provider.github.name}",
      "arn:aws:bedrock-agentcore:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:workload-identity-directory/default/workload-identity/${aws_bedrockagentcore_gateway.gtw.gateway_id}",
      "arn:aws:bedrock-agentcore:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:workload-identity-directory/default",
      "arn:aws:bedrock-agentcore:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:token-vault/default"
    ]
  }
  statement {
    sid     = "GetWorkloadAccessToken"
    effect  = "Allow"
    actions = ["bedrock-agentcore:GetWorkloadAccessToken"]
    resources = [
      "arn:aws:bedrock-agentcore:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:workload-identity-directory/default",
      "arn:aws:bedrock-agentcore:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:workload-identity-directory/default/workload-identity/${aws_bedrockagentcore_gateway.gtw.gateway_id}"
    ]
  }
  statement {
    effect  = "Allow"
    actions = ["secretsmanager:GetSecretValue"]
    resources = [
      aws_bedrockagentcore_api_key_credential_provider.github.api_key_secret_arn[0].secret_arn
    ]
  }
}
data "aws_iam_policy_document" "assume_role" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["bedrock-agentcore.amazonaws.com"]
    }
    condition {
      test     = "StringEquals"
      values   = [data.aws_caller_identity.current.account_id]
      variable = "aws:SourceAccount"
    }
  }
}
resource "aws_iam_role" "gtw_role" {
  name               = "bedrock-agentcore-gateway-role"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

resource "aws_iam_policy" "gtw_plcy" {
  policy = data.aws_iam_policy_document.gtw_plcy.json
  name   = "bedrock-agentcore-gateway-plcy"
}

resource "aws_iam_role_policy_attachment" "gtw_role_plcy_attach" {
  policy_arn = aws_iam_policy.gtw_plcy.arn
  role       = aws_iam_role.gtw_role.name
}

resource "aws_bedrockagentcore_gateway" "gtw" {
  name            = "gateway"
  role_arn        = aws_iam_role.gtw_role.arn
  authorizer_type = "AWS_IAM"
  protocol_type   = "MCP"
  exception_level = "DEBUG"
  protocol_configuration {
    mcp {
      search_type = "SEMANTIC"
    }
  }
}

resource "aws_bedrockagentcore_gateway_target" "github_target" {
  name               = "api-target"
  gateway_identifier = aws_bedrockagentcore_gateway.gtw.gateway_id
  description        = "External API target with API key authentication"

  credential_provider_configuration {
    api_key {
      provider_arn              = aws_bedrockagentcore_api_key_credential_provider.github.credential_provider_arn
      credential_location       = "HEADER"
      credential_parameter_name = "Authorization"
      credential_prefix         = "Bearer"
    }
  }

  target_configuration {
    mcp {
      open_api_schema {
        inline_payload {
          payload = jsonencode(yamldecode(file("${path.module}/github-tools.yaml")))
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Sometimes, beyond access to external services, we want the agent to remember information about the end user, so it can offer more valuable solutions based on their context. That's done with AgentCore Memory.

AgentCore Memory: what the agent remembers

Memory is divided by strategy into: user preference (things the user likes), semantic (facts from the context), summarization, and custom. Extraction of the long-term records is asynchronous and takes time. There's also a quirk at creation time: each strategy blocks the creation of the next one, i.e. if you're setting up more than one, you have to wait between them — empirically around 190 seconds; in the code below I've set 300 to be safe. In Terraform it looks like this:

resource "aws_iam_role" "bedrock_memory_role" {
  name               = "bedrock-agentcore-memory-role"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

resource "aws_iam_role_policy_attachment" "bedrock_plcy_attach" {
  role       = aws_iam_role.bedrock_memory_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonBedrockAgentCoreMemoryBedrockModelInferenceExecutionRolePolicy"
}

resource "aws_bedrockagentcore_memory" "memory" {
  name                      = "agent_memory"
  event_expiry_duration     = 30
  memory_execution_role_arn = aws_iam_role.bedrock_memory_role.arn
}

resource "aws_bedrockagentcore_memory_strategy" "user_preference" {
  name        = "userpreference_strategy"
  memory_id   = aws_bedrockagentcore_memory.memory.id
  type        = "USER_PREFERENCE"
  description = "user preference strategy"
  namespaces  = ["/users/{actorId}/preferences/"]
}

resource "time_sleep" "wait_after_usr_pref_strategy" {
  depends_on       = [aws_bedrockagentcore_memory_strategy.user_preference]
  create_duration  = "300s"
  destroy_duration = "300s"
  triggers = {
    strategy_id = aws_bedrockagentcore_memory_strategy.user_preference.memory_strategy_id
  }
}

resource "aws_bedrockagentcore_memory_strategy" "semantic" {
  name        = "semantic_strategy"
  memory_id   = aws_bedrockagentcore_memory.memory.id
  type        = "SEMANTIC"
  description = "semantic strategy"
  namespaces  = ["/users/{actorId}/facts/"]
  depends_on  = [time_sleep.wait_after_usr_pref_strategy]
}

resource "time_sleep" "wait_after_semantic_strategy" {
  depends_on       = [aws_bedrockagentcore_memory_strategy.semantic]
  create_duration  = "300s"
  destroy_duration = "300s"
  triggers = {
    strategy_id = aws_bedrockagentcore_memory_strategy.semantic.memory_strategy_id
  }
}

resource "aws_bedrockagentcore_memory_strategy" "summarization" {
  name        = "summarization_strategy"
  memory_id   = aws_bedrockagentcore_memory.memory.id
  type        = "SUMMARIZATION"
  description = "summarization strategy"
  namespaces  = ["/summaries/{actorId}/{sessionId}/"]
  depends_on  = [time_sleep.wait_after_semantic_strategy]
}
Enter fullscreen mode Exit fullscreen mode

The records live in hierarchical namespaces, where {actorId} and {sessionId} are substituted with the real values at write time.

Here's how everything so far comes together in the agent itself — reading from long-term memory, talking to the tools through Gateway (MCP with SigV4 signing), and recording the turn as an event:

"""Repo Ops agent — AgentCore Runtime entrypoint.

Flow of every request:
  1. recall()  -> reads long-term memory (LTM) for this user
  2. Agent()   -> LLM conversation with the tools from Gateway (MCP over SigV4)
  3. create_event() -> records the turn as an event (STM); the strategies
                       extract LTM asynchronously after ~1 minute
"""
import os

import boto3
import httpx
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
from bedrock_agentcore.memory import MemoryClient
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from mcp.client.streamable_http import streamablehttp_client
from strands import Agent
from strands.models import BedrockModel
from strands.tools.mcp import MCPClient

REGION = os.environ.get("AWS_REGION", "eu-central-1")
GATEWAY_URL = os.environ["GATEWAY_URL"]   # .../mcp
MEMORY_ID = os.environ["MEMORY_ID"]
MODEL_ID = os.environ["MODEL_ID"]

app = BedrockAgentCoreApp()               # starts an HTTP server on :8080
memory = MemoryClient(region_name=REGION)


class SigV4HTTPXAuth(httpx.Auth):
    """Signs every MCP request with SigV4.

    The gateway has authorizer_type = "AWS_IAM", i.e. it expects a
    signature exactly like any other AWS API call. The Runtime role
    must have bedrock-agentcore:InvokeGateway on the gateway's ARN.
    """

    requires_request_body = True

    def __init__(self, service: str, region: str):
        self._creds = boto3.Session().get_credentials()
        self._service = service
        self._region = region

    def auth_flow(self, request: httpx.Request):
        aws_req = AWSRequest(
            method=request.method,
            url=str(request.url),
            data=request.content,
            headers={"host": request.url.host},
        )
        SigV4Auth(self._creds, self._service, self._region).add_auth(aws_req)
        request.headers.update(dict(aws_req.headers))
        yield request


def recall(actor_id: str, query: str) -> str:
    """Semantic search over long-term memory.

    The namespaces must match the ones in Terraform EXACTLY
    (including the trailing slash).
    """
    namespaces = [
        f"/users/{actor_id}/preferences/",
        f"/users/{actor_id}/facts/",
    ]
    chunks = []
    for ns in namespaces:
        try:
            records = memory.retrieve_memories(
                memory_id=MEMORY_ID,
                namespace=ns,
                query=query,
                top_k=3,
            )
            for rec in records:
                text = rec.get("content", {}).get("text", "")
                if text:
                    chunks.append(f"- {text}")
        except Exception as exc:
            # Memory must never take the agent down
            print(f"[memory] retrieve failed for {ns}: {exc}")
    return "\n".join(chunks)


def remember(actor_id: str, session_id: str, prompt: str, answer: str) -> None:
    """Records the turn as an event in short-term memory."""
    try:
        memory.create_event(
            memory_id=MEMORY_ID,
            actor_id=actor_id,
            session_id=session_id,
            messages=[(prompt, "USER"), (answer, "ASSISTANT")],
        )
    except Exception as exc:
        print(f"[memory] create_event failed: {exc}")


@app.entrypoint
def invoke(payload, context):
    prompt = payload.get("prompt", "")
    actor_id = payload.get("actor_id", "default")

    # session_id comes from --runtime-session-id at invocation time
    session_id = getattr(context, "session_id", None) or "local-dev-session-0000000000000000"

    memories = recall(actor_id, prompt)

    system_prompt = (
        "You are Repo Ops — a concise assistant for GitHub operations. "
        "Use the available tools for anything related to repositories, "
        "pull requests, and issues. Never make up data — if a tool "
        "returns an error, tell the user about it. Ask for confirmation "
        "before creating an issue."
    )
    if memories:
        system_prompt += f"\n\nWhat you remember about this user:\n{memories}"

    gateway = MCPClient(
        lambda: streamablehttp_client(
            GATEWAY_URL,
            auth=SigV4HTTPXAuth("bedrock-agentcore", REGION),
        )
    )

    with gateway:
        tools = gateway.list_tools_sync()
        print(f"[gateway] tools: {[t.tool_name for t in tools]}")
        agent = Agent(
            model=BedrockModel(model_id=MODEL_ID, region_name=REGION),
            tools=tools,
            system_prompt=system_prompt,
        )
        answer = str(agent(prompt))

    remember(actor_id, session_id, prompt, answer)
    return {"result": answer}


if __name__ == "__main__":
    app.run()
Enter fullscreen mode Exit fullscreen mode

This code has to live somewhere, though. Time for the part I always freeze on in interviews.

AgentCore Runtime: Firecracker, MicroVMs, and containers

I've always wondered how to explain what AgentCore Runtime is. Questions about MicroVMs, virtual machines, containers, and processes are always hard to talk through. Maybe I don't fully understand it myself; maybe the rabbit hole is so deep that it's hard to explain simply. So I decided to start from a distance — perhaps also because these are exactly the questions I freeze on in interviews.

To bust the myth, I'll start with the MicroVM.

Two of the options for deploying an application (including an AI application) are a virtual machine and a container. The difference: containers package all of the application's dependencies and libraries and share a common kernel and operating system with the host, while each virtual machine has its own kernel, its own operating system, and a dedicated slice of the resources. The kernel manages the hardware through drivers, i.e. it has unrestricted access to CPU time and memory; applications, on the other hand, do not.

It's commonly assumed that a virtual machine is more secure than containers. Why? Perhaps because containers share a kernel with the host.

Whether that makes containers insecure compared to a virtual machine is debatable. With a virtual machine you get patching, driver management, and slower startup. There's also the distroless container option — no shell and no package managers, only what the end application needs.

The MicroVM tries to solve the slow-startup challenge while also offering the stronger isolation characteristic of virtual machines: each machine comes with its own virtualized kernel. Firecracker — the MicroVM technology behind AWS Lambda — lets you launch up to 150 MicroVMs per second, with each one booting in about 125 ms.

Firecracker

So what do virtual machines and containers have to do with AgentCore Runtime? Firecracker sits behind AgentCore Runtime: you deploy a container, and it runs on top of a Firecracker virtual machine. That gives you maximum isolation — one Firecracker machine can't use another's resources, that's forbidden at the hypervisor level — and at the same time the easiest possible deployment of any AI application in the form of a container with all its dependencies. Alongside the Firecracker process itself, the host also runs a program called the jailer — a second layer of defense (a sandbox) that wraps the virtual machine in case it gets compromised.

Firecracker design

The technology guarantees security and isolation of every customer's data, memory, and processes. AgentCore Runtime stands on Amazon's experience with Lambda, containers, and instances.

Runtime lets you deploy any AI agent or MCP server, regardless of SDK or framework, as long as it exposes HTTP on port 8080 (for MCP servers the port is 8000). AWS says the idea is for the service to be framework agnostic — the future secure home for your agent, guarding a company's most valuable asset: its data. Together with memory, access management, connectivity to other services, and agent evaluation, this shapes a complete AI platform.

Preparing the application takes three steps.

  • Creating a container image registry where the application will live:
resource "aws_ecr_repository" "agent_repo" {
  name                 = "agent-repo"
  image_tag_mutability = "IMMUTABLE"
  image_scanning_configuration {
    scan_on_push = false
  }
  force_delete = true
}
Enter fullscreen mode Exit fullscreen mode
  • A Dockerfile with all of the application's dependencies:
# AgentCore Runtime runs ONLY on Graviton (linux/arm64).
# That's why the build must go through buildx with --platform linux/arm64.
FROM public.ecr.aws/docker/library/python:3.13-slim

WORKDIR /app

# Dependencies first — Docker caches the layer, so subsequent builds are fast
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

# The Runtime contract: an HTTP server listening on 8080
EXPOSE 8080

CMD ["python", "app.py"]
Enter fullscreen mode Exit fullscreen mode
  • Building and pushing the image:
resource "random_id" "agent_rand_tag" {
  keepers = {
    agent_img_tag = local.agent_code_hash
  }
  byte_length = 2
}

resource "null_resource" "build_and_push_agent" {
  triggers = {
    dockerfile_hash = local.agent_code_hash
  }

  provisioner "local-exec" {
    command = <<-EOT
      aws ecr get-login-password --region ${data.aws_region.current.region} | docker login --username AWS --password-stdin ${aws_ecr_repository.agent_repo.repository_url}
      docker build -t ${aws_ecr_repository.agent_repo.repository_url}:${random_id.agent_rand_tag.hex} ${path.module}/
      docker push ${aws_ecr_repository.agent_repo.repository_url}:${random_id.agent_rand_tag.hex}
    EOT
  }
}
Enter fullscreen mode Exit fullscreen mode

The Terraform code assembles from the building blocks: to bring up AgentCore Runtime, you pass in everything else and create it last — like Voltron, if you've seen it, this is the head — with the required role:

data "aws_iam_policy_document" "runtime_permissions" {
  statement {
    actions   = ["ecr:GetAuthorizationToken"]
    effect    = "Allow"
    resources = ["*"]
  }

  statement {
    actions = [
      "ecr:BatchGetImage",
      "ecr:GetDownloadUrlForLayer",
    ]
    effect    = "Allow"
    resources = [aws_ecr_repository.agent_repo.arn]
  }
  statement {
    actions = [
      "logs:DescribeLogStreams",
      "logs:CreateLogGroup",

    ]
    effect    = "Allow"
    resources = ["arn:aws:logs:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:log-group:/aws/bedrock-agentcore/runtimes/*"]
  }
  statement {
    actions = [
      "logs:DescribeLogGroups"

    ]
    effect    = "Allow"
    resources = ["arn:aws:logs:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:log-group:*"]
  }
  statement {
    actions = [
      "logs:CreateLogStream",
      "logs:PutLogEvents"

    ]
    effect    = "Allow"
    resources = ["arn:aws:logs:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:log-group:/aws/bedrock-agentcore/runtimes/*:log-stream:*"]
  }
  statement {
    actions = [
      "cloudwatch:PutMetricData"
    ]
    resources = ["*"]
    effect    = "Allow"
    condition {
      test     = "StringEquals"
      values   = ["bedrock-agentcore"]
      variable = "cloudwatch:namespace"
    }
  }
  statement {
    actions = [
      "bedrock:InvokeModel",
      "bedrock:InvokeModelWithResponseStream"
    ]
    resources = ["arn:aws:bedrock:*::foundation-model/*",
    "arn:aws:bedrock:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:*"]
    effect = "Allow"
  }
  statement {
    sid     = "GetWorkloadAccessToken"
    effect  = "Allow"
    actions = ["bedrock-agentcore:GetWorkloadAccessToken"]
    resources = [
      "arn:aws:bedrock-agentcore:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:workload-identity-directory/default",
      "arn:aws:bedrock-agentcore:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:workload-identity-directory/default/workload-identity/${aws_bedrockagentcore_gateway.gtw.gateway_id}"
    ]
  }
  statement {
    effect  = "Allow"
    actions = ["bedrock-agentcore:InvokeGateway"]
    resources = [
      aws_bedrockagentcore_gateway.gtw.gateway_arn
    ]
  }
  statement {
    effect = "Allow"
    actions = ["bedrock-agentcore:ListEvents",
      "bedrock-agentcore:CreateEvent",
    "bedrock-agentcore:RetrieveMemoryRecords"]
    resources = [
      aws_bedrockagentcore_memory.memory.arn
    ]
  }
}

resource "aws_iam_role" "rntime_role" {
  name               = "bedrock-agentcore-rntime-role"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

resource "aws_iam_policy" "rntime_plcy" {
  policy = data.aws_iam_policy_document.runtime_permissions.json
  name   = "bedrock-agentcore-rntime-plcy"
}

resource "aws_iam_role_policy_attachment" "rntime_role_plcy_attach" {
  policy_arn = aws_iam_policy.rntime_plcy.arn
  role       = aws_iam_role.rntime_role.name
}

resource "aws_bedrockagentcore_agent_runtime" "example" {
  agent_runtime_name = "gitops_agent_runtime"
  role_arn           = aws_iam_role.rntime_role.arn

  agent_runtime_artifact {
    container_configuration {
      container_uri = "${aws_ecr_repository.agent_repo.repository_url}:${random_id.agent_rand_tag.hex}"
    }
  }

  network_configuration {
    network_mode = "PUBLIC"
  }
  environment_variables = {
    GATEWAY_URL = aws_bedrockagentcore_gateway.gtw.gateway_url
    MEMORY_ID   = aws_bedrockagentcore_memory.memory.id
    MODEL_ID    = "eu.anthropic.claude-sonnet-4-5-20250929-v1:0"
  }
  depends_on = [null_resource.build_and_push_agent]
}
Enter fullscreen mode Exit fullscreen mode

Bonus: cross-account and cross-region without network magic

On top of everything else, the services above — and Gateway in particular — solve a problem that usually calls for more complex network constructions: calls between accounts and between regions. Two examples from real life. In both, the scenario is the same: the gateway lives in platform account A in eu-west-1, while the MCP server (mcp-atlassian) and Jira DC live in another account B and another region, eu-west-2. The difference is the price you pay.

Example 1: Runtime target — the network isn't there, because you don't need it

AgentCore Runtime

A gateway with no protocol type passes the Okta JWT onward, and with a single InvokeAgentRuntime over the AWS backbone it reaches the Runtime in the other account and region — no peering, no PrivateLink; this is an API, and the network stays hidden. The Runtime runs the forked mcp-atlassian (arm64, stateless, MCP on :8000) and goes out through egress ENIs straight to Jira DC with the user's token. The price: Identity has to live in the Runtime's account — the secrets move to the agent — and you lose both aggregation and semantic search, since the gateway is just a proxy.

Example 2: MCP target — semantic search, but you build the network yourself

Lattice

Here the gateway uses the classic MCP protocol, since the target is not an AgentCore Runtime: MCP, CUSTOM_JWT, aggregation, and semantic search, with Identity exchanging the JWT for the user's token — the 3LO vault stays with the platform. The price is the network, and it's a whole chain: Lattice resource gateway (only in the gateway's region) → cross-region interface endpoint → PrivateLink → endpoint service in account B → internal NLB with TLS :443 and a public ACM certificate → EC2 with mcp-atlassian on :8000 → Jira DC. Every link is managed — but every link is yours.

Which one, when

If you just need to reach an agent or MCP server in another account and region — Runtime target: zero network work, but the vault moves to the runtime and you lose aggregation. If you're building a platform — one endpoint, semantic search, tokens and consents on your side — MCP target: you get all of that, but the Lattice, the cross-region PrivateLink, and the certificates are your problem.

What it costs

AgentCore has no monthly subscription — each capability has its own meter, and you pay only for what you use. Prices as of July 2026:

  • Runtime — $0.0895 per vCPU-hour and $0.00945 per GB-hour, billed per second. CPU is charged only while the agent is actually processing: I/O waiting is free, and agents spend 30–70% of a session waiting on the model and the tools.
  • Gateway — $0.005 per 1,000 invocations (ListTools, InvokeTool), $0.025 per 1,000 semantic searches, and $0.02 per 100 indexed tools per month.
  • Memory — $0.25 per 1,000 events (short-term memory); long-term is $0.75 per 1,000 records per month with the built-in strategies (the model is included in the price) and $0.50 per 1,000 retrievals. Three strategies mean three separate extractions from the same events.
  • Identity — $0.010 per 1,000 token requests… but free when it goes through Runtime or Gateway. In our architecture: zero.
  • Observability — at CloudWatch rates (about $0.35/GB for spans).

On the side you have ECR for the image, Secrets Manager for the PAT, and — the biggest line item — the tokens to the model, which are often 50–70% of the whole bill. That's exactly why Gateway's semantic search isn't a luxury but FinOps: a wrongly chosen tool costs tokens, and tokens cost the most.

In lieu of a conclusion

AgentCore isn't just hosting for agents. Identity decides who can do what, Gateway — which tool is the right one, Memory — what's worth remembering, Runtime — where all of it lives in isolation. And when you have to cross an account and a region, you now have a choice: you pay either with functionality or with network plumbing. Example 1 delivers on the intro's promise literally — no network at all. Example 2 shows that even when you do need the network, it snaps together from managed blocks like Lego, with no need to touch route tables.

Top comments (0)