Lesson 05 · Effect AgentCore

IAM role and the AgentCore Runtime

Lesson 04 left an image sitting in ECR. An image is not a running agent — AWS will not pull and run arbitrary bytes on your behalf. This lesson adds the two resources that close that gap, both yielded in the same Alchemy Stack generator: an IAM execution role that says who the Runtime is allowed to be, and the AgentCore.Runtime resource that pulls the image and runs it. Then we type pnpm deploy — the course's first real deploy — and call the live agent.

01Two resources stand between the image and a running agent

You have an ECR repository with :latest pushed to it. To turn that into a hosted agent, two things have to exist in AWS, and the program declares them right after the ECR repo in the one Stack generator we have been growing since lesson 04:

Both are yield*-ed in the same Effect.gen body, after the repo, so the repo's repositoryUri and the role's roleArn are already in scope as typed values when the Runtime needs them. One program, one dependency graph.

ECR repo (lesson 04) IAM Role (this lesson) │ │ │ repositoryUri │ roleArn ▼ ▼ ┌───────────────────────────────────────────────┐ │ AgentCore.Runtime (given) │ │ pulls containerUri, runs it, assumes role │ └───────────────────────────────────────────────┘ │ ▼ agentRuntimeArn ──► invokeAgentRuntime(...)

src/effect-agentcore/alchemy.run.ts

02The execution role

When the Runtime runs your container, it does so as an IAM role — it calls sts:AssumeRole to take on the role's permissions. For that to work the role needs two things: a trust policy (assumeRolePolicyDocument) naming who is allowed to assume it, and permissions describing what the assumed role may then do. We grant exactly the agent's needs — call Bedrock to generate, and write its own logs — and nothing else:

const role = yield* AWS.IAM.Role("agent-exec", {
  assumeRolePolicyDocument: {
    Version: "2012-10-17",
    Statement: [{
      Effect: "Allow",
      Principal: { Service: "bedrock-agentcore.amazonaws.com" },
      Action: "sts:AssumeRole",
    }],
  },
  inlinePolicies: {
    agent: {
      Version: "2012-10-17",
      Statement: [
        { Effect: "Allow", Action: ["bedrock:InvokeModel"], Resource: "*" },
        { Effect: "Allow", Action: ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"], Resource: "*" },
      ],
    },
  },
});

The trust principal is the service bedrock-agentcore.amazonaws.com — that is the AgentCore Runtime control plane saying "let me wear this role when I run the container." The inlinePolicies map keys policies by name (agent here); AWS.IAM.Role also accepts managedPolicyArns if you prefer to attach AWS-managed policies. The role's outputs we care about are roleArn and roleName; roleArn is what the Runtime needs in the next section.

Gotcha

Resource: "*" in both statements is illustrative, to keep the lesson readable. In production, scope it: the bedrock:InvokeModel statement should name the specific foundation-model ARN(s) you call, and the logs:* statement should target the agent's own log-group ARN. Least privilege means the role can do its job and nothing more — a wildcard Resource quietly grants far more than the agent uses.

alchemy/AWS/IAM/Role.ts — RoleProps (alchemy@2.0.0-beta.44)

03The given AgentCore.Runtime resource

Alchemy ships first-class resources for ECR, IAM, SQS and the rest — but it has no AgentCore support yet. As lesson 01 flagged, the course closes that gap by shipping its own given resource: a normal Alchemy resource authored exactly like the framework's own (lesson 10 opens the hood). "Given" means you do not implement it here — you import it and consume it like any other resource:

import { Runtime } from "./given/agentcore/index.ts";

const runtime = yield* Runtime("Agent", {
  containerUri: `${repo.repositoryUri}:latest`,
  roleArn: role.roleArn,
  network: "PUBLIC",
});
return { agentRuntimeArn: runtime.agentRuntimeArn };

Notice that repo.repositoryUri and role.roleArn flow straight in as typed values — that is the payoff of declaring everything in one program. The compiler knows the repo and role produce strings, so a typo or a missing field is a build error, not a 3 a.m. console surprise.

Under the hood the resource talks to the AgentCore control plane. On create it calls createAgentRuntime with the container artifact (the image containerUri), the roleArn to assume, and a network configuration (PUBLIC here). Its read uses getAgentRuntime, and tear-down uses deleteAgentRuntime. The outputs are agentRuntimeArn, agentRuntimeId, and status — and agentRuntimeArn is the handle we return from the stack and feed to the data plane in section 06.

given/agentcore/Runtime.ts · control plane: npm @distilled.cloud/aws bedrock-agentcore-control (createAgentRuntime / getAgentRuntime / deleteAgentRuntime)

04Deploy

With the repo, the role, and the Runtime all declared, pnpm deploy (which runs alchemy deploy) walks the dependency graph and provisions them in order — the ECR repo and the role first, then the Runtime that depends on both:

$ pnpm deploy

  ✓ create  AWS.ECR.Repository       agent              (repositoryUri: …/agent)
  ✓ create  AWS.IAM.Role             agent-exec         (roleArn: arn:aws:iam::…:role/agent-exec-…)
  ✓ create  AWS.BedrockAgentCore.Runtime  Agent         (status: CREATING → READY)

  agentRuntimeArn = arn:aws:bedrock-agentcore:…:runtime/Agent-…

The Runtime's status starts at CREATING while AWS pulls the image and stands the agent up, then settles to READY. Once you see READY and the agentRuntimeArn printed, the agent you built and curled locally in lesson 02 is now running in the cloud.

This is the milestone the whole course has been building toward: deploy — the first time the program leaves your laptop and becomes real AWS infrastructure.

deploy entrypoint: src/effect-agentcore/alchemy.run.ts

05Idempotency: deploy again

Run pnpm deploy a second time with nothing changed and it is a no-op. Alchemy reads the current state, diffs each resource against what you declared, finds no difference, and creates nothing — mirroring the Alchemy v2 tutorial's idempotent re-deploy:

$ pnpm deploy

  ✓ unchanged  AWS.ECR.Repository            agent
  ✓ unchanged  AWS.IAM.Role                  agent-exec
  ✓ unchanged  AWS.BedrockAgentCore.Runtime  Agent

  agentRuntimeArn = arn:aws:bedrock-agentcore:…:runtime/Agent-…

Change a prop and only the difference is applied. Push a new image and pass the new digest as containerUri, and the Runtime is updated in place. But some fields cannot change without a fresh resource — the given resource marks agentRuntimeArn and agentRuntimeId as stables, so a change that would alter a stable field forces a replacement (delete + create) rather than an update.

Aha

You never write "create this if it doesn't exist, else update it." You declare the desired state and Alchemy converges reality toward it — create on first run, no-op when matched, in-place update on a small change, replace when a stable field moves. The deploy command is safe to run a hundred times; the program is the source of truth, not a pile of imperative steps you have to remember the order of.

idempotent re-deploy: Alchemy v2 tutorial · part 1

06Invoke the real agent

The Runtime is READY and we have its agentRuntimeArn. Calling it uses the AgentCore data plane — a different SDK surface from the control plane that created it. The data plane is where requests actually reach the running agent:

import { invokeAgentRuntime } from "@distilled.cloud/aws/bedrock-agentcore";

const res = yield* invokeAgentRuntime({
  agentRuntimeArn,
  runtimeSessionId,   // AWS-required, ≥33 chars; microVM affinity (lesson 07 contrasts it with the body sessionId)
  payload: JSON.stringify({ prompt: "what is the runtime contract?" }),
});

Look at that payload: { prompt: "..." } — the exact body the lesson-02 server decoded with its InvocationRequest schema and answered with curl on localhost:8080. Same contract, same body shape; the only thing that changed is the host. Locally it was a Node process on your machine; now it is the AgentCore Runtime pulling your ECR image and running it under the execution role you just declared.

Why this is the pivot of the course

Lessons 02–04 made a contract-complete agent and packaged it. This lesson is the first one whose output lives in AWS. Everything after it — Gateway tools (06), Memory (07), observability (08) — is now a few more resources added to a stack that is already deployed and answering. The hard architectural choice (one program, typed values flowing between resources) is what makes those additions small.

Milestone reached: deploy — your first real deploy is live and answering the same body the local server did.

data plane: npm @distilled.cloud/aws bedrock-agentcore (invokeAgentRuntime) · local contract from src/agent.ts (lesson 02)