Running an AI agent usually means wrestling with EC2, Fargate, or complex Kubernetes setups. AWS App Runner lets you skip that boilerplate and get a secure, auto‑scaled container live in minutes. This guide shows exactly how to turn a Node.js Claude agent into a production service without managing servers. Why App Runner Beats ECS for Quick AI Agent Deploys Why it matters – When you just want an endpoint that talks to Claude, the overhead of provisioning clusters, writing task definitions, and wiring load balancers can eclipse the actual logic of your agent. Amazon Elastic Container Service (ECS) is great for large fleets, but for a single‑container AI service it adds a lot of ceremony. App Runner in plain English – Think of App Runner as a “restaurant host” that takes a ready‑made dish (your Docker image), finds a table (a compute instance), serves it to customers, and automatically cleans up when nobody is eating. You don’t need to hire chefs, bussers, or manage the kitchen yourself. Key benefits for AI agents Zero‑config networking – App Runner creates a public HTTPS URL for you; no VPC or load‑balancer fiddling. Built‑in auto‑scaling – It adds or removes compute based on request rate, so your Claude calls stay responsive under load. Integrated observability – Logs go straight to CloudWatch; you can turn on X‑Ray tracing with a single toggle. Takeaway: If your goal is “run a Claude‑backed endpoint fast,” App Runner removes the heavy lifting that ECS and Kubernetes demand. Setting Up Your Claude Agent Codebase with Native ES Modules Why start with a clean codebase? – A minimal project keeps the focus on the deployment steps instead of wrestling with build tooling. Node.js 22 supports native ECMAScript (ES) modules out of the box, so we can write modern import statements without a bundler. 1. Project layout claude-agent/ ├─ src/ │ ├─ index.ts # entry point, defines HTTP routes │ └─ claude.ts # thin wrapper around Bedrock's chat API ├─ Dockerfile ├─ package.json └─ tsconfig.json Enter fullscreen mode Exit fullscreen mode 2. package.json (only the essentials) { "name": "claude-agent", "version": "1.0.0", "type": "module", // tells Node to treat .js/.ts as ES modules "main": "dist/index.js", "scripts": { "build": "tsc", "start": "node dist/index.js" }, "dependencies": { "aws-sdk-client-bedrock-runtime": "^3.600.0", "aws-xray-sdk": "^3.5.0" }, "devDependencies": { "typescript": "^5.5.0" } } Enter fullscreen mode Exit fullscreen mode type: "module" makes every file an ES module, so we can use import … from …. 3. TypeScript configuration (tsconfig.json) { "compilerOptions": { "target": "ES2022", "module": "ES2022", "outDir": "dist", "strict": true, "esModuleInterop": true }, "include": ["src/**/*.ts"] } Enter fullscreen mode Exit fullscreen mode 4. The HTTP server (src/index.ts) // src/index.ts import http from "node:http"; // built‑in Node HTTP server import { handleAgent } from "./claude.js"; // our Claude wrapper import { URL } from "node:url"; // Simple health check – App Runner expects a 200 on '/health' function healthHandler(req: http.IncomingMessage, res: http.ServerResponse) { if (req.method === "GET" && new URL(req.url || "", `http://${req.headers.host}`).pathname === "/health") { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ status: "ok" })); return true; } return false; } // Main request router const server = http.createServer(async (req, res) => { // First, satisfy App Runner's health probe if (healthHandler(req, res)) return; const url = new URL(req.url || "", `http://${req.headers.host}`); if (req.method === "POST" && url.pathname === "/agent") { await handleAgent(req, res); // delegated to the Claude helper } else { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Not found" })); } }); const PORT = process.env.PORT ? Number(process.env.PORT) : 8080; server.listen(PORT, () => { console.log(`🚀 Claude agent listening on http://localhost:${PORT}`); }); Enter fullscreen mode Exit fullscreen mode Plain English: The server listens on the port App Runner tells it (process.env.PORT). It first answers /health with a 200 so App Runner knows the container is alive, then forwards any POST /agent calls to our Claude helper. 5. Claude wrapper (src/claude.ts) // src/claude.ts import { BedrockRuntimeClient, InvokeModelCommand } from "@aws-sdk/client-bedrock-runtime"; import { captureAWSv3Client } from "aws-xray-sdk"; import { IncomingMessage, ServerResponse } from "node:http"; // Wrap the Bedrock client with X‑Ray for tracing (optional but cheap) const bedrock = captureAWSv3Client(new BedrockRuntimeClient({ region: "us-east-1" })); // Helper to collect the request body as a string async function getRequestBody(req: IncomingMessage): Promise { const chunks: Uint8Array[] = []; for await (const chunk of req) { chunks.push(chunk); } return Buffer.concat(chunks).toString("utf-8"); } // Core logic: fetch extra info, call Claude, combine results export async function handleAgent(req: IncomingMessage, res: ServerResponse) { try { // 1️⃣ Read user prompt from request body (expects JSON { "prompt": "..." }) const body = await getRequestBody(req); const { prompt } = JSON.parse(body); // 2️⃣ Pull auxiliary data from a fake external API const infoResponse = await fetch("https://api.example.com/info"); const infoJson = await infoResponse.json(); // { "detail": "...", "metadata": { … } } // 3️⃣ Build the payload for Claude (Bedrock's chat/completions API) const payload = { modelId: "anthropic.claude-3-5-sonnet-20241022-v1:0", // Claude model identifier messages: [ { role: "system", content: "You are a helpful assistant." }, { role: "user", content: `${prompt}\nAdditional info: ${JSON.stringify(infoJson)}` } ], maxTokens: 1024, temperature: 0.7 }; // 4️⃣ Invoke the model const command = new InvokeModelCommand({ body: JSON.stringify(payload), contentType: "application/json", accept: "application/json", modelId: payload.modelId }); const bedrockResponse = await bedrock.send(command); const modelOutput = JSON.parse(Buffer.from(bedrockResponse.body).toString()); // 5️⃣ Return combined result to the caller res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ prompt, externalInfo: infoJson, claudeResponse: modelOutput })); } catch (err) { console.error("❗️ Agent error:", err); res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Internal server error" })); } } Enter fullscreen mode Exit fullscreen mode Key points We use native fetch (available in Node 22) to call https://api.example.com/info. @aws-sdk/client-bedrock-runtime is the official AWS SDK for the Claude model. aws-xray-sdk adds distributed tracing without extra code; App Runner can surface those traces in the console. Tip: Keep the /health endpoint separate from your business logic. App Runner’s health‑check runs every few seconds; a cheap 200 response avoids unnecessary Claude calls. Containerizing the Agent and Pushing to ECR Why containerize? – A Docker image captures the exact runtime (Node 22, dependencies, OS libraries). That image can run anywhere—locally for testing, or in the cloud via App Runner. 1. Dockerfile # Dockerfile # Use the official Node 22 LTS image as a base (lightweight Alpine variant) FROM node:22-alpine AS builder # Install build tools for TypeScript compilation WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci # Copy source files and compile TypeScript COPY tsconfig.json ./ COPY src ./src RUN npm run build # Runtime stage – only the compiled JS and runtime deps FROM node:22-alpine AS runtime WORKDIR /app COPY --from=builder /app/package.json . COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/dist ./dist # Expose the port that App Runner will map (default 8080, but we honor $PORT) EXPOSE 8080 ENV NODE_ENV=production # Start the service CMD ["node", "dist/index.js"] Enter fullscreen mode Exit fullscreen mode Explanation Multi‑stage build – The first stage compiles TypeScript, the second stage ships only what’s needed to run, keeping the final image small (.dkr.ecr.us-east-1.amazonaws.com # 2️⃣ Build the Docker image locally docker build -t claude-agent:latest . # 3️⃣ Tag the image with the full ECR URI docker tag claude-agent:latest .dkr.ecr.us-east-1.amazonaws.com/claude-agent-repo:latest # 4️⃣ Push to ECR docker push .dkr.ecr.us-east-1.amazonaws.com/claude-agent-repo:latest Enter fullscreen mode Exit fullscreen mode Gotcha: App Runner’s cold start (the time it takes to spin up a fresh container) can be 10‑30 seconds. For latency‑sensitive APIs you might need a warm pool or a different service, but for many internal tools that latency is acceptable. Tip: Keep the image size small; every extra megabyte adds to cold‑start time. Multi‑stage builds, as shown above, are the easiest way to stay lean. Creating an App Runner Service Linked to Your Claude API Why use the SDK instead of the console? – Automating the service creation makes the process repeatable, version‑controlled, and easy to integrate into CI/CD pipelines. 1. Minimal App Runner creation script // apprunner-create.ts import { AppRunnerClient, CreateServiceCommand, CreateServiceCommandInput } from "@aws-sdk/client-apprunner"; const client = new AppRunnerClient({ region: "us-east-1" }); async function createClaudeService() { // Service definition – tells App Runner where to find the container image const params: CreateServiceCommandInput = { ServiceName: "claude-agent-service", SourceConfiguration: { ImageRepository: { ImageIdentifier: ".dkr.ecr.us-east-1.amazonaws.com/claude-agent-repo:latest", ImageRepositoryType: "ECR", // Optional: If you have a private repo, give App Runner a role that can read it }, AuthenticationConfiguration: { // If the ECR repo is private, App Runner needs an IAM role. Omit for public repos. } }, InstanceConfiguration: { Cpu: "1vCPU", Memory: "2GB" }, HealthCheckConfiguration: { // App Runner expects a 200 on '/' by default. We override it to our '/health' endpoint. Path: "/health", Protocol: "HTTP", Interval: 10, Timeout: 5, HealthyThreshold: 1, UnhealthyThreshold: 5 }, AutoScalingConfigurationArn: undefined, // use default auto‑scaling // Enable observability features ObservabilityConfiguration: { ObservabilityEnabled: true // turns on CloudWatch Logs and X‑Ray } }; try { const command = new CreateServiceCommand(params); const response = await client.send(command); console.log("🚀 App Runner service created:"); console.log(JSON.stringify(response.Service, null, 2)); } catch (err) { console.error("❌ Failed to create service:", err); } } // Execute createClaudeService().catch(console.error); Enter fullscreen mode Exit fullscreen mode Key concepts ImageIdentifier – The full ECR URI of the image we pushed earlier. HealthCheckConfiguration.Path – We point it at /health to avoid the silent‑deploy problem where App Runner keeps waiting for a 200 on /. ObservabilityEnabled – Turns on CloudWatch Logs and X‑Ray without extra steps. In plain English: The script tells App Runner, “run this container, check /health to see if it’s alive, and send all logs and traces to AWS so I can look at them later.” 2. Deploying the service Run the script with ts-node (or compile it first): npx ts-node apprunner-create.ts Enter fullscreen mode Exit fullscreen mode After a minute or two you’ll see an output that includes a ServiceUrl. Visit that URL with a tool like curl: curl -X POST -H "Content-Type: application/json" \ -d '{"prompt":"What is the weather in Paris?"}' \ https://.awsapprunner.com/agent Enter fullscreen mode Exit fullscreen mode You should receive a JSON payload that merges the external API data with Claude’s answer. Gotcha: If you forget to set the health‑check path to /health, App Runner will mark the deployment as Pending forever because it never receives a 200 response. Observability: Hooking CloudWatch Logs and X‑Ray Why observe? – AI agents can fail silently (e.g., Claude throttling, external API downtime). Having logs and traces lets you spot those problems fast. 1. CloudWatch Logs App Runner automatically streams stdout and stderr to a CloudWatch log group named /aws/apprunner/. The console.log statements we added in index.ts and claude.ts will appear there. Quick test aws logs tail /aws/apprunner/claude-agent-service --follow Enter fullscreen mode Exit fullscreen mode You’ll see messages like 🚀 Claude agent listening on http://localhost:8080 and any error stack traces. 2. Enabling X‑Ray Tracing We already wrapped the Bedrock client with captureAWSv3Client. To make the trace data visible: Add the X‑Ray daemon permission – In the IAM role that App Runner uses (created automatically if you let the console generate one), attach the policy AWSXRayDaemonWriteAccess. Turn on tracing – The ObservabilityEnabled: true flag in the creation script activates X‑Ray for the service. Now go to the AWS X‑Ray console, choose Service map, and you’ll see a node for claude-agent-service. Clicking it shows the request flow: API Gateway (App Runner) → your /agent handler → external fetch → Bedrock → response. Tip: If you see “segment missing” warnings, double‑check that the SDK client is wrapped (the captureAWSv3Client line). 3. Adding custom annotations You can enrich traces with your own metadata. For example, add a request ID: import { Segment, subsegment } from "aws-xray-sdk"; export async function handleAgent(req: IncomingMessage, res: ServerResponse) { const segment = Segment.fromXRayTraceId(); // gets current trace segment const requestId = crypto.randomUUID(); segment.addAnnotation("requestId", requestId); // ... rest of the logic } Enter fullscreen mode Exit fullscreen mode Now every trace includes the requestId annotation, making it easy to correlate logs and traces. The Takeaway You’ve just turned a tiny Claude‑powered Node.js app into a fully managed, auto‑scaled web service using AWS App Runner. App Runner eliminates the need for clusters – no EC2, Fargate, or Kubernetes configuration required. A health endpoint (/health) is essential – without it, App Runner’s default check on “/” will keep the deployment pending. Docker multi‑stage builds keep images small, reducing cold‑start latency. The AWS SDKs (@aws-sdk/client-ecr and @aws-sdk/client-apprunner) let you script repository creation and service deployment, making the workflow reproducible. Observability comes out of the box – CloudWatch logs capture console.log output, and X‑Ray tracing reveals the end‑to‑end request path. Remember the limits – App Runner cold starts can be 10‑30 seconds and there is no persistent storage, so keep state external (e.g., DynamoDB) if you need it. Now you can iterate on your Claude prompts, add more endpoints, or spin up additional agents—all with a few commands and no server‑maintenance headaches. Happy building! Transparency notice This article was written with the help of an AI system — Groq (GPT OSS 120B). Published: 2026-11-09 · Primary focus: AppRunner All code blocks are intended to be correct and runnable, but please verify them against the official docs for the tools mentioned before using in production. Find an error? Drop a comment — corrections are always welcome.
How to Deploy a Claude‑Powered AI Agent with AWS App Runner, Explained Simply
Full Article
Original Source
Read the full article at Dev →KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.