πŸŽ“ Claude Architect KB CCAR-F Β· CCAR-P study guide

Deployment & Operations β€” How Agents Actually Run

Every domain so far explained what the pieces are. This page answers the architect's next question: where does this code physically run, who starts it, and how do you assemble it in an enterprise cloud project?

πŸ“– New here? The "works on my laptop" gap

The gap: you've built the loop β€” it runs when you type python agent.py. But production means the loop runs when you're not there: at 2 AM for a batch, on every merge in CI, on every customer webhook. Something must host the harness, hold the credentials, restart it when it crashes, and collect its logs. That "something" is a deployment topology, and interviewers (and the exam's scenario framing) expect you to have one in mind.

The reassuring truth: an agent deploys like any other service, because the harness IS an ordinary program β€” a Python or Node process that makes HTTPS calls to the Claude API. There is no special "AI runtime" to provision, no GPU to rent (inference happens on Anthropic's side). Everything you know about deploying web services applies unchanged; this page just maps the agent-specific concerns (secrets, egress β€” outbound network connections, budgets, evals) onto that familiar shape.

So what for the exam: scenario questions say things like "the team wants nightly runs in CI" or "expose the agent to internal users" β€” each phrase implies one of the five runtime forms below. Recognize the form and the architecture answer follows.

The five runtime forms β€” recognize which one a scenario implies

Form What runs, where Triggered by Canonical example
Interactive CLI Claude Code in a developer's terminal a human typing daily development work
Headless / CI claude -p β€” same engine, no human attached pipeline events (merge, PR, nightly) auto-triage new issues on every push
Long-running service your harness as a web service (the SDK inside a container) HTTP requests / webhooks the support agent behind an endpoint
Scheduled batch a harness job spun up, run to completion, torn down cron / scheduler 10,000-invoice nightly extraction
Event-driven worker harness consuming a queue messages (Pub/Sub, SQS) "a ticket was created" β†’ agent enriches it

The first two run Claude Code itself; the last three run your own harness built on the Claude API or Agent SDK. That's the whole taxonomy β€” every "how do we ship this?" conversation starts by picking a row.

Headless mode β€” the agent with nobody at the terminal

Headless means Claude Code running non-interactively: you pass the prompt as an argument, it does the work, prints machine-readable output, and exits. Same engine, same tools, same CLAUDE.md loading β€” but no human to approve anything, which is why the flags below exist:

BASH
claude -p "Triage the new GitHub issues: label severity, close duplicates." \
  --output-format stream-json \
  --allowedTools "Read,Grep,Bash(gh issue:*)" \
  --max-turns 15

In a CI pipeline it's just a step:

YAML
# .github/workflows/triage.yml β€” an agent as a CI step
on:
  issues: { types: [opened] }
jobs:
  triage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm install -g @anthropic-ai/claude-code
      - run: |
          claude -p "Triage issue #${{ github.event.issue.number }}: apply severity label." \
            --allowedTools "Bash(gh issue:*)" \
            --max-turns 10
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}

The API key comes from the pipeline's secret store β€” never from a file in the repo. This one YAML block is the entire "AI in CI/CD" story the exam gestures at.

The harness as a service β€” it's just a program

When the agent must serve users or systems (rows 3–5), you deploy your own harness: an ordinary containerized service that happens to call the Claude API. Nothing about it is exotic β€” it follows the same operational rules as any ordinary web service: config in the environment, logs to stdout, no state baked into the container.

PYTHON
# app.py β€” the support agent as a plain web service (FastAPI)
# shape-only sketch β€” production adds auth, a typed/validated request model, size limits, error handling
from anthropic import Anthropic
from fastapi import FastAPI

app = FastAPI()
client = Anthropic()                       # reads ANTHROPIC_API_KEY from the environment

@app.post("/support")
def handle(ticket: dict):
    result = run_agent_loop(client, ticket)   # the Domain-1 loop, verbatim
    return {"resolution": result.text, "state": result.terminal_state}
🏭 Production shape: typed, authenticated support endpoint

Production adds what the sketch leaves out: a typed request model with size limits, a verified caller, an order permission check, and error handling. The agent loop itself lives in its own module (a tool_runner call, as in Domain 1).

PYTHON
import os
from typing import Literal

from anthropic import Anthropic
from fastapi import Depends, FastAPI, HTTPException
from pydantic import BaseModel, Field

from app.agent import AgentStopped, run_agent_loop  # your loop module
from app.auth import Principal, verify_caller  # your identity-provider check (401 inside)

MODEL = os.environ.get("CLAUDE_MODEL", "claude-sonnet-5")
app = FastAPI()
client = Anthropic()  # reads ANTHROPIC_API_KEY from the environment (secret manager β†’ env)


class Ticket(BaseModel):
    order_id: str = Field(min_length=1, max_length=40)
    message: str = Field(min_length=1, max_length=4000)


class Resolution(BaseModel):
    resolution: str
    state: Literal["resolved", "escalated", "needs_customer"]


@app.post("/support", response_model=Resolution)
def handle(ticket: Ticket, caller: Principal = Depends(verify_caller)) -> Resolution:
    if not caller.can_access_order(ticket.order_id):
        raise HTTPException(status_code=403, detail="Not allowed")
    try:
        result = run_agent_loop(client, MODEL, ticket, trusted_identity=caller)
    except AgentStopped:  # max_tokens, refusal, or iteration limit
        raise HTTPException(status_code=502, detail="The agent could not finish")
    return Resolution(resolution=result.text, state=result.terminal_state)

Where it lives: app/main.py (this file) + app/auth.py + app/agent.py, packaged by a Dockerfile.

Containerize it, and every platform question becomes a standard one: where do containers run, where do secrets live, where do logs go. Which brings us to the concrete version.

Deploy an agent as an authenticated cloud service

Yes: deploy the harness as a backend service with an HTTPS API. The caller sends a task to your endpoint; your application authenticates the caller, authorizes the task, and runs or queues the agent loop. The caller does not need to know how the loop is implemented. Any compatible application can integrate, but only callers you authorize should be able to start runs or retrieve results.

Concrete use case: your company hosts an order-support agent on GCP. Your website, another internal service, and an approved partner all need to submit support tasks. The agent runs on Cloud Run and calls the model provider and approved order tools. The example below is architecture guidance, not a deployment performed by this lesson.

TEXT
Website β†’ website backend ─┐
Internal service ──────────┼→ HTTPS entry + authentication
Approved partner backend β”€β”˜       ↓
                          Tenant/action authorization + quotas
                                  ↓
                          POST /v1/agent-runs
                                  ↓
                          durable run record + queue
                                  ↓
                          Cloud Run worker: harness + agent loop
                                  β”œβ†’ model API (server credential)
                                  β”œβ†’ approved tools (scoped identity)
                                  β””β†’ checkpoint/results store

Caller β†’ GET /v1/agent-runs/{run_id} β†’ authorized status/result

Three credentials that must not be confused

Credential Who supplies it? What does it authorize?
Caller access token Your user/partner obtains it from a trusted identity provider Access to your agent API under that identity and its permissions
Cloud Run invocation ID token Internal caller or gateway uses a permitted Google service identity Invocation of an IAM-protected Cloud Run service
Model API credential Your deployed harness obtains it from secret management, or uses cloud identity for a managed model endpoint Outbound model calls; never handed to callers of your agent

There may also be credentials for order/refund tools. Permission to start an agent is not permission to refund every order. Carry verified caller/tenant identity into tool authorization; do not let a model-generated customer ID override it.

Choose the entry point for your callers

Internal service β†’ private Cloud Run agent

Keep the Cloud Run service IAM-protected. Grant the calling service account roles/run.invoker on that service. The caller obtains a Google-signed ID token whose audience matches the receiving service URL (or a configured custom audience), then sends it as Authorization: Bearer .... Cloud Run checks invocation access before your handler runs. Your application still checks business permissions and request data.

This is service identity, not automatically the identity of the end user represented by that service. If an internal frontend backend acts for users, pass user context through a validated delegation design and authorize it in the agent service.

Illustrative Python caller running with a configured Google workload identity; no service-account key file needs to be embedded in the source:

PYTHON
import requests
from google.auth.transport.requests import Request
from google.oauth2.id_token import fetch_id_token

service_url = "https://YOUR-SERVICE-URL.run.app"
id_token = fetch_id_token(Request(), service_url)
response = requests.post(
    service_url + "/v1/agent-runs",
    headers={"Authorization": "Bearer " + id_token,
             "Idempotency-Key": "support-case-104-attempt-1"},
    json={"task": "Check delivery status", "order_id": "O-104"},
    timeout=30,
)
response.raise_for_status()
print(response.json())

The auth library obtains a token using the environment’s supported identity configuration; it does not grant the invoker role. Cloud Run service authentication↗.

Approved partner β†’ gateway β†’ agent service

Register the partner application with a trusted OAuth identity provider. A server-to-server integration can use an approved machine identity flow, commonly client credentials when supported by that provider. The partner obtains a short-lived access token for your API, then calls your endpoint with that token. Define permissions such as agent.runs.create and agent.runs.read in your authorization design.

A gateway can validate signed JWTs using the configured issuer, audience, expiry, and signing keys. If tokens are opaque, use an appropriate provider-supported validation/introspection mechanism instead; merely decoding a token does not authenticate it. Your backend maps the verified identity to the partner tenant and checks scopes and resource ownership.

On GCP, API Gateway can validate configured JWTs and invoke an IAM-protected Cloud Run backend using its own service identity. Grant that gateway identity invocation permission. Do not leave a separate unauthenticated backend URL that bypasses the gateway. The partner token and gateway-to-Cloud-Run ID token serve different purposes. Accept forwarded identity only through the authenticated gateway integration, never arbitrary caller-supplied identity headers. Gateway JWT validation↗.

An β€œAPI token” is a broad phrase. Ask whether it means an OAuth access token or a static API key. A managed API key may identify a consumer for quotas, but does not automatically establish an end user’s identity or permission to access an order. Do not use your Anthropic key as the partner’s agent-service token. GCP API key behavior↗.

Browser UI β†’ your backend β†’ agent service

A practical pattern is a backend-for-frontend: the user signs in through a standard identity provider, the website backend verifies the session, and that backend invokes the agent service using its service identity plus validated user context. Keep client secrets, model API keys, and service-account credentials out of browser JavaScript. Use secure, HttpOnly session cookies with appropriate CSRF protection for the browser session.

If a browser calls an API directly using a supported token flow, it still needs valid authentication and authorization. Configure CORS for the origins you intend to serve. Origin means scheme + hostname + port; it is not caller identity. CORS controls browser cross-origin access; scripts and server-to-server clients are not secured by an origin allowlist. A request from another domain with valid credentials can be permitted by your policy; a request from your own domain without valid credentials should not gain access.

What does a third-party request and response look like?

This is your application API, not the Claude Messages API. The bearer value below is a placeholder for an access token accepted by your gateway/application. It is not necessarily a Cloud Run ID token.

HTTP
POST /v1/agent-runs HTTP/1.1
Host: agents.example.com
Authorization: Bearer CALLER_ACCESS_TOKEN
Content-Type: application/json
Idempotency-Key: partner-case-104-v1

{
  "task": "Check delivery status and explain the next step",
  "order_id": "O-104"
}

Your backend derives identity and tenant from verified authentication records. A body containing "tenant_id": "someone-else" or text saying β€œI am admin” must not override that identity. A valid token still needs authorization to access O-104.

For a long task, respond after durably accepting work:

HTTP
HTTP/1.1 202 Accepted
Content-Type: application/json
Location: /v1/agent-runs/run_7d92

{
  "run_id": "run_7d92",
  "status": "queued",
  "status_url": "/v1/agent-runs/run_7d92"
}

The authenticated caller polls GET /v1/agent-runs/run_7d92. Check tenant/owner access on this endpoint too; knowing a run ID is not permission. A completed response might be:

JSON
{
  "run_id": "run_7d92",
  "status": "completed",
  "result": {
    "order_id": "O-104",
    "delivery_status": "delivered",
    "next_step": "Review the delivery photo with the customer."
  }
}

Other states can include running, waiting_for_approval, and failed. Keep status transitions in trusted application state. For short bounded tasks you can return a synchronous answer; for long loops or human approval, persist and queue work rather than keeping a browser request open for hours.

Where authentication sits in the handler

Architectural pseudocodeβ€”not a complete FastAPI implementation. The named helpers must be implemented with your identity provider, authorization policy, and durable storage. No LLM is used to decide whether a token is valid.

PYTHON
async def create_agent_run(http_request):
    principal = await trusted_authentication.verify(http_request)
    # Verify signature/issuer/audience/expiry or provider session/introspection.
    payload = validate_task_body(await http_request.json())
    authorization.require(principal, "agent.runs.create")
    authorization.require_order_access(principal, payload.order_id)
    quotas.require_available(principal.tenant_id)

    # Atomic run + outbox creation: avoid saved-but-never-enqueued jobs.
    # Idempotency is scoped to tenant + operation; reject changed payload reuse.
    run = await runs.create_with_outbox(
        principal=principal,
        task=payload,
        idempotency_key=http_request.headers["Idempotency-Key"],
    )
    return accepted(run.id)

# Queue delivery invokes a worker; it does not rely on an in-memory background task.
async def process_run(run_id):
    run = await runs.load_and_claim(run_id)
    await resume_agent_loop(run.checkpoint, trusted_identity=run.principal)
    # Authorize each tool action; save checkpoints; honor turn/time/cost limits.
🏭 Production shape: the entry handler in FastAPI

The same checks as real FastAPI code. A dependency verifies the token (401); your policy code returns 403 or 429. The helpers are your own modules. The queue later invokes a separate worker that loads, claims, and resumes the run.

PYTHON
from fastapi import Depends, FastAPI, Header, HTTPException, status
from pydantic import BaseModel, Field

from app.auth import Principal, verify_caller  # JWT signature, issuer, audience, expiry
from app.authz import require, require_order_access  # raise PermissionError when not allowed
from app.quotas import QuotaExceeded, require_available
from app.runs import runs  # durable store: run row + outbox in one transaction

app = FastAPI()


class RunRequest(BaseModel):
    task: str = Field(min_length=1, max_length=2000)
    order_id: str = Field(pattern=r"^O-\d+$")


@app.post("/v1/agent-runs", status_code=status.HTTP_202_ACCEPTED)
async def create_agent_run(
    body: RunRequest,
    idempotency_key: str = Header(alias="Idempotency-Key", max_length=100),
    caller: Principal = Depends(verify_caller),
):
    try:
        require(caller, "agent.runs.create")
        require_order_access(caller, body.order_id)
    except PermissionError:
        raise HTTPException(status.HTTP_403_FORBIDDEN, "Not allowed")
    try:
        require_available(caller.tenant_id)
    except QuotaExceeded:
        raise HTTPException(status.HTTP_429_TOO_MANY_REQUESTS, "Quota exhausted")
    run = await runs.create_with_outbox(
        principal=caller, task=body, idempotency_key=idempotency_key)
    return {"run_id": run.id, "status": "queued", "status_url": f"/v1/agent-runs/{run.id}"}

Where it lives: app/api/runs.py (this handler), app/auth.py, app/authz.py, and app/worker.py (the queue-invoked worker).

No/invalid credentials normally produce 401; authenticated but disallowed actions produce 403. Quota exhaustion can produce 429. Platform errors may use their own status mapping. The agent loop starts only after entry checks, and still enforces tool-level permissions during execution.

What gets deployed on GCP?

  1. Build a container: HTTP entry point plus harness code and dependencies. Listen on the platform’s configured port. The model runs at its provider unless you separately choose self-hosted inference.
  2. Deploy Cloud Run: assign a dedicated runtime service account and explicit invocation policy. Add a gateway/user-auth layer if serving partners or users.
  3. Configure credentials: put production model credentials in Secret Manager with access only for the runtime identity; use workload identities for Google service access.
  4. Persist and queue: store runs/checkpoints/results in a database or object storage; use a durable task/queue system for asynchronous work. Do not depend on container memory or local disk surviving.
  5. Bound and observe: set concurrency/scaling and application budgets, apply per-tenant quotas, record traces without tokens/secrets, and handle duplicate delivery safely.

Interview wording: β€œI deploy the harness as a containerized API service. I authenticate callers with an identity provider or Cloud IAM, authorize tenant and action access, then create a durable agent run. A worker executes the loop with scoped tool permissions and server-held model credentials. Clients receive a run ID and access results through authenticated endpoints.”

Reference architecture β€” the support agent on GCP

The Domain-1 walkthrough's support agent, deployed for real on Google Cloud:

flowchart TD
  U[Customer email / chat] --> GW[API Gateway / webhook endpoint]
  GW --> PS[Pub/Sub topic: support-tickets]
  PS --> CR[Cloud Run: the harness container]
  SM[Secret Manager: ANTHROPIC_API_KEY] -.mounted at startup.-> CR
  CR -->|HTTPS: model calls| CAPI[Claude API β€” api.anthropic.com]
  CR -->|tool calls| SQL[(Cloud SQL: orders DB)]
  CR -->|tool calls| MCP[MCP server: internal customer-records CRM β€” Cloud Run service]
  CR --> LOG[Cloud Logging β†’ BigQuery: traces, tokens, gate verdicts]
  SCH[Cloud Scheduler: nightly] --> EV[Eval job: golden set vs staging]
  EV --> LOG
  HITL[Approval queue: escalations UI] --- CR
Component GCP piece The agent-specific reason
Entry point API Gateway + Pub/Sub the queue absorbs traffic spikes so the harness pulls work at its own pace β€” rate-limit protection by architecture, not by retries
The harness Cloud Run scale-to-zero container (no container runs, and you pay nothing, until a request arrives); each request is one agent run; concurrency caps give you throughput/backpressure control (per-run budgets still live in the harness)
Secrets Secret Manager the Claude API key is delivered as a secret-backed environment variable on the Cloud Run revision (a new key version is picked up on the next revision deploy) β€” never baked into the image, never in git
Model calls egress to api.anthropic.com the only internet egress (outbound network connection) the container needs β€” allowlist it and block the rest, and a prompt-injected agent has nowhere to send stolen data. (Tool backends and the internal CRM are reached privately over the VPC β€” Virtual Private Cloud β€” network, not the public internet.)
Tool backends Cloud SQL, internal services least-privilege service account per tool β€” the DB user can read orders but cannot drop tables
Shared tools MCP servers as sibling Cloud Run services the Domain-4 catalog pattern deployed: one server, many agent hosts
Observability Cloud Logging β†’ BigQuery per-run traces (tokens, cost, gate verdicts) β€” Domain 5's "instrument everything," physically
Evals Cloud Scheduler job the golden set runs nightly against staging; a red run blocks the next deploy
HITL an approval queue + tiny UI the violation branch of the harness gate check β€” the decision diamond from the Domain-1 loop β€” has to land somewhere a human actually looks
πŸ” See it run β€” one customer message through the GCP topology

Entry point: a customer emails "my order #4531 never arrived" at 09:14.

  1. The email webhook posts to API Gateway, which publishes the message to the Pub/Sub topic. A push subscription then delivers each message as an authenticated HTTPS POST to the Cloud Run service; the service acks by returning 2xx, and failed deliveries are redelivered β€” which is why the harness's idempotency keys matter here too. If 500 emails arrive in one minute, they queue calmly β€” nothing melts.
  2. Cloud Run has scaled to zero overnight; the first message wakes a container. ANTHROPIC_API_KEY arrives as a secret-backed environment variable from Secret Manager on the running revision β€” the code just reads the env var, exactly like on your laptop.
  3. The harness runs the Domain-1 loop verbatim: it calls the Claude API over its one allowlisted internet egress route; the model returns tool_use: get_order; the harness queries Cloud SQL privately over the VPC using a service account that can read orders and nothing else.
  4. Turn 2's create_replacement hits the harness gate check (the decision diamond from the Domain-1 loop) β€” $89 < $100, auto-approved. (At $600 it would have landed in the approval queue, and the container would have persisted state and moved on β€” a human clicks approve later, a new run resumes from the checkpoint.)
  5. Every step streamed to Cloud Logging: prompt version, tokens (2,140 in / 380 out), cost, tool calls, gate verdicts, terminal state resolved. Tonight, BigQuery aggregates it into the cost-per-resolution dashboard; the Cloud Scheduler eval job replays the golden set against the same revision before tomorrow's deploy.

Total infrastructure the "AI part" required beyond a normal web service: one secret, one egress rule, one logging schema. That's the punchline β€” the model is a dependency, not a datacenter.

The same shape on any cloud

Concern GCP AWS Azure
Harness container Cloud Run Fargate / Lambda Container Apps
Queue Pub/Sub SQS / EventBridge Service Bus
Secrets Secret Manager Secrets Manager Key Vault
Logs/analytics Cloud Logging + BigQuery CloudWatch + Athena Monitor + Log Analytics
Scheduler (evals) Cloud Scheduler EventBridge Scheduler Logic Apps / Functions timer

The names change; the topology β€” queue in front, stateless harness container, secret store, single allowlisted egress, traces out β€” does not. (Amazon Bedrock (AWS) and Vertex AI (Google Cloud) β€” each cloud's own managed model service β€” add a variation: Claude is called through the cloud's endpoint instead of directly to api.anthropic.com β€” same harness, different base URL and cloud-IAM authentication (the cloud's own Identity and Access Management, instead of an Anthropic API key).)

Enterprise assembly checklist β€” what the architect signs off

  1. Identity & secrets: API keys in the secret store, rotated; one service account per tool backend, least-privilege each.
  2. Egress control: the harness can reach the Claude API and its tool backends β€” and nothing else. This single network rule is your strongest defense against exfiltration (smuggling data out): even a prompt-injected agent has nowhere to send it (Domain 3's layer 3, at the network layer).
  3. Budgets as infrastructure: max-turns/cost ceilings in the harness AND platform quotas (Cloud Run concurrency, spend alerts) β€” two independent layers.
  4. Observability first-class: per-run traces with correlation ids landing in a queryable store before launch, not after the first incident.
  5. Evals in the deploy path: golden set green = deployable; a config/prompt/model change that can't turn the suite red proves nothing (Domain 5's negative controls).
  6. HITL has a real surface: escalations land in a queue humans actually monitor, with an SLA β€” an unmonitored approval queue is a silent-failure machine.

πŸͺ€ Trap: "we need GPUs to deploy the agent" β€” no. Inference runs on Anthropic's side of the Claude API; your side is an ordinary container that makes HTTPS calls. The trap answer provisions infrastructure for a problem you don't have; the architect answer is the checklist above.