![[アップデート] Amazon Bedrock AgentCore Runtimeのdirect code deployがNode.jsに対応しました!](https://images.ctfassets.net/ct0aopd36mqt/7M0d5bjsd0K4Et30cVFvB6/5b2095750cc8bf73f04f63ed0d4b3546/AgentCore2.png?w=3840&fm=webp)
[アップデート] Amazon Bedrock AgentCore Runtimeのdirect code deployがNode.jsに対応しました!
はじめに
こんにちは、スーパーマーケットが大好きなコンサルティング部の神野(じんの)です。
直近のアップデートでAmazon Bedrock AgentCore RuntimeがNode.jsのdirect code deployに対応しました!
AgentCore Runtimeは元々Dockerコンテナイメージを使用すればNode.jsも動かせていたのですが、今回のリリースで.zipファイルをそのままデプロイできるdirect code deployがNode.jsでも使えるようになりました。
公式のNode.js向けドキュメントを見るとフレームワークはExpressで書かれていたのですが、せっかくならHonoで書きたい!というモチベーションで試してみました。
AIエージェント本体はTypeScript版のStrands Agents SDKを利用して、Bedrock上のClaude Haiku 4.5を呼び出してストリーミング応答を返却するところまで記事にしています!
前提
検証時の環境です。
| 項目 | バージョン・値 |
|---|---|
| Node.js(ローカル) | v24.13.0 |
| Hono | 4.12.15 |
| @hono/node-server | 2.0.0 |
| @strands-agents/sdk | 1.0.0-rc.5 |
| TypeScript | 6.0.3 |
| AWSリージョン | us-west-2 |
| Bedrockモデル | us.anthropic.claude-haiku-4-5-20251001-v1:0 |
| AgentCore Runtime ランタイム識別子 | NODE_22 |
実装
プロジェクト作成
まずはプロジェクトを初期化して、Hono、@hono/node-server、Strands Agents SDKをインストールします。
mkdir agent-strands-hono && cd agent-strands-hono
npm init -y
npm pkg set type="module"
npm install hono @hono/node-server @strands-agents/sdk
npm install -D typescript @types/node tsx
tsconfig.jsonは下記のように記載しました。
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"types": ["node"],
"rootDir": "src",
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}
エージェントコード
AgentCore Runtimeが要求しているエンドポイントは次の2つだけです。
| メソッド | パス | 役割 |
|---|---|---|
| GET | /ping |
ヘルスチェック |
| POST | /invocations |
エージェント実行 |
Honoで書くと下記のようになります。ポートは8080を開放します。
import { Hono } from "hono";
import { serve } from "@hono/node-server";
import { stream } from "hono/streaming";
import { Agent } from "@strands-agents/sdk";
import { BedrockModel } from "@strands-agents/sdk/models/bedrock";
const model = new BedrockModel({
region: process.env.AWS_REGION ?? "us-west-2",
modelId: "us.anthropic.claude-haiku-4-5-20251001-v1:0",
maxTokens: 1024,
});
const agent = new Agent({
model,
systemPrompt: "あなたは親しみやすいアシスタントです。簡潔に日本語で答えてください。",
});
const app = new Hono();
app.get("/ping", (c) => c.json({ status: "Healthy" }));
app.post("/invocations", async (c) => {
const body = await c.req.json<{ prompt?: string }>().catch(() => ({}) as { prompt?: string });
const prompt = body.prompt ?? "こんにちは";
c.header("Content-Type", "text/event-stream");
c.header("Cache-Control", "no-cache");
c.header("Content-Encoding", "Identity");
return stream(c, async (s) => {
for await (const event of agent.stream(prompt)) {
if (event.type === "modelStreamUpdateEvent") {
const inner = event.event;
if (inner.type === "modelContentBlockDeltaEvent" && inner.delta.type === "textDelta") {
await s.write(`data: ${JSON.stringify({ text: inner.delta.text })}\n\n`);
}
} else if (event.type === "agentResultEvent") {
const usage = event.result.lastMessage?.metadata?.usage;
await s.write(
`event: done\ndata: ${JSON.stringify({ stopReason: event.result.stopReason, usage })}\n\n`,
);
}
}
});
});
const port = 8080;
serve({ fetch: app.fetch, hostname: "0.0.0.0", port });
console.log(`Strands+Hono agent listening on port ${port} (Node ${process.version})`);
agent.stream(prompt) は Strands SDK の非同期ジェネレータで、エージェントの内部イベントを順次返してくれます。今回は modelStreamUpdateEvent の中の textDelta だけ拾って SSE のフォーマットでクライアントに流しています。最後に agentResultEvent を受け取ったタイミングで event: done を流して終わり、というシンプルな流れですね。
Honoのストリーミング機能については公式ドキュメントに記載してあるので必要に応じてご参照ください。
streamを使用してシンプルに実装できて良きですね。
ローカル動作確認
tsxで起動して、curlで叩いてみます。
npx tsx src/app.ts
curl -s http://localhost:8080/ping
{"status":"Healthy"}
問題なくHealthyが返ってきていますね!続いて/invocationsにプロンプトを投げます。
curl -sN -X POST http://localhost:8080/invocations \
-H "Content-Type: application/json" \
-d '{"prompt":"自己紹介を一文で。"}'
data: {"text":"こ"}
data: {"text":"んにちは!"}
data: {"text":"私は"}
data: {"text":"Cl"}
data: {"text":"audeという"}
data: {"text":"AIアシスタントで"}
...
event: done
data: {"stopReason":"endTurn","usage":{"inputTokens":49,"outputTokens":59,"totalTokens":108}}
Claudeの応答がチャンクで流れてきましたね!ローカルではうまくストリーミングできていそうです。
TypeScriptをコンパイルしてZIP化
AgentCore Runtimeのdirect code deployでは、.jsファイルと依存関係をZIPに含めてデプロイします。今回は一番シンプルに、TypeScriptをJavaScriptにコンパイルして、node_modulesごとZIP化します。
npx tsc
コンパイルするとdist/app.jsが出力されます。
ls -lh dist/app.js
ZIP化の前に、開発依存(tsxなど)をnpm pruneで外しておきます。
npm prune --omit=dev
これをやらずにZIP化すると、macOS(Apple Silicon)のnpm installで入った@esbuild/darwin-arm64やfsevents.nodeのようなネイティブバイナリが混入します。AgentCore RuntimeはLinux ARM64で動作するため、デプロイすると下記のエラーでUPDATE_FAILEDになります。
Your artifact contains binary files that are incompatible with Linux ARM64. Please rebuild your artifact with ARM64-compatible dependencies.
実際にこれを忘れてエラーになりました・・・本番デプロイには不要な依存なので、npm prune --omit=devで先に削ってからZIP化するのが安全ですね。
あとはdist/、package.json、package-lock.json、node_modules/をまとめてZIP化します。package.jsonはtype: "module"をRuntime側にも伝えるために同梱しています。
zip -r deployment_package.zip dist package.json package-lock.json node_modules
今回はまず動かすことを優先して普通にZIP化しました。
デプロイ
IAMロール
AgentCore Runtime用のIAMロールを作ります。信頼ポリシーでbedrock-agentcore.amazonaws.comを許可しておきます。
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "bedrock-agentcore.amazonaws.com" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": { "aws:SourceAccount": "<ACCOUNT_ID>" },
"ArnLike": { "aws:SourceArn": "arn:aws:bedrock-agentcore:us-west-2:<ACCOUNT_ID>:*" }
}
}]
}
権限ポリシーは、S3からコードを読み取る権限・CloudWatch Logs・Bedrock InvokeModel・X-Rayを許可しています。
runtime-policy.json(クリックで展開)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::bedrock-agentcore-code-<ACCOUNT_ID>-us-west-2",
"arn:aws:s3:::bedrock-agentcore-code-<ACCOUNT_ID>-us-west-2/*"
]
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogStreams",
"logs:DescribeLogGroups"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"bedrock-agentcore:GetWorkloadAccessToken",
"bedrock-agentcore:GetWorkloadAccessTokenForJWT",
"bedrock-agentcore:GetWorkloadAccessTokenForUserId"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"xray:PutTraceSegments",
"xray:PutTelemetryRecords",
"xray:GetSamplingRules",
"xray:GetSamplingTargets"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "cloudwatch:PutMetricData",
"Resource": "*",
"Condition": { "StringEquals": { "cloudwatch:namespace": "bedrock-agentcore" } }
}
]
}
aws iam create-role \
--role-name AmazonBedrockAgentCoreSDKRuntime-us-west-2 \
--assume-role-policy-document file://trust-policy.json
aws iam put-role-policy \
--role-name AmazonBedrockAgentCoreSDKRuntime-us-west-2 \
--policy-name AgentCoreRuntimePolicy \
--policy-document file://runtime-policy.json
S3バケット作成とアップロード
コード配布用のバケットを作って、ZIPをアップロードします。
aws s3api create-bucket \
--bucket bedrock-agentcore-code-<ACCOUNT_ID>-us-west-2 \
--region us-west-2 \
--create-bucket-configuration LocationConstraint=us-west-2
aws s3 cp deployment_package.zip \
s3://bedrock-agentcore-code-<ACCOUNT_ID>-us-west-2/agent-strands-hono/deployment_package.zip \
--region us-west-2
AgentCore Runtimeを作成
create-agent-runtimeでデプロイします。
aws bedrock-agentcore-control create-agent-runtime \
--agent-runtime-name agent_strands_hono \
--agent-runtime-artifact '{
"codeConfiguration": {
"code": {
"s3": {
"bucket": "bedrock-agentcore-code-<ACCOUNT_ID>-us-west-2",
"prefix": "agent-strands-hono/deployment_package.zip"
}
},
"runtime": "NODE_22",
"entryPoint": ["dist/app.js"]
}
}' \
--network-configuration '{"networkMode":"PUBLIC"}' \
--role-arn "arn:aws:iam::<ACCOUNT_ID>:role/AmazonBedrockAgentCoreSDKRuntime-us-west-2" \
--region us-west-2
{
"agentRuntimeArn": "arn:aws:bedrock-agentcore:us-west-2:<ACCOUNT_ID>:runtime/agent_strands_hono-XXXXXXXXXX",
"agentRuntimeId": "agent_strands_hono-XXXXXXXXXX",
"agentRuntimeVersion": "1",
"status": "CREATING"
}
数秒待つとstatusがREADYになります。
aws bedrock-agentcore-control get-agent-runtime \
--agent-runtime-id agent_strands_hono-XXXXXXXXXX \
--region us-west-2 \
--query status
"READY"
READYになっていますね!!早速試してみましょう。
動作確認
invoke-agent-runtimeでリクエストを実行してみます。
SESSION="my-session-$(date +%s)-$(head -c 22 /dev/urandom | base64 | tr -d /=+)"
ARN="arn:aws:bedrock-agentcore:us-west-2:<ACCOUNT_ID>:runtime/agent_strands_hono-XXXXXXXXXX"
aws bedrock-agentcore invoke-agent-runtime \
--agent-runtime-arn "$ARN" \
--runtime-session-id "$SESSION" \
--payload '{"prompt":"自己紹介を一文で。"}' \
--region us-west-2 \
/tmp/response.txt
{
"runtimeSessionId": "my-session-...",
"contentType": "text/event-stream",
"statusCode": 200
}
contentTypeがtext/event-streamになっていて、ちゃんとストリーミングとして転送されていますね!本文を見てみます。
data: {"text":"Ag"}
data: {"text":"entCore Runtimeが"}
data: {"text":"Node.js"}
data: {"text":"に"}
data: {"text":"対応する"}
data: {"text":"な"}
data: {"text":"ん"}
data: {"text":"て、Java"}
data: {"text":"Scriptで"}
data: {"text":"最"}
data: {"text":"先"}
data: {"text":"端の"}
...
event: done
data: {"stopReason":"endTurn","usage":{"inputTokens":69,"outputTokens":58,"totalTokens":127}}
メッセージが返却されていますね!!今回のアップデートをClaude君も喜んでくれています。
Node.js 24で動かしたかった話
ここから余談です。
ローカルではNode 24で開発していたので、せっかくならランタイムもNODE_24にしたいなと思って試してみました。
aws bedrock-agentcore-control update-agent-runtime \
--agent-runtime-id agent_strands_hono-XXXXXXXXXX \
--agent-runtime-artifact '{"codeConfiguration":{"code":{...},"runtime":"NODE_24","entryPoint":["dist/app.js"]}}' \
...
An error occurred (ValidationException) when calling the UpdateAgentRuntime operation:
1 validation error detected: Value 'NODE_24' at 'agentRuntimeArtifact.codeConfiguration.runtime'
failed to satisfy constraint: Member must satisfy enum value set:
[PYTHON_3_10, PYTHON_3_11, PYTHON_3_12, PYTHON_3_13, PYTHON_3_14, NODE_22]
見事に弾かれましたね・・・
公式ドキュメントの対応ランタイム一覧にもNODE_22しか書かれていなかったので、こちらが正でした。Pythonは3.10〜3.14と幅広いのに対して、直接コードデプロイ対応のNode.jsは22のみです。
おわりに
公式ドキュメントはExpressベースで書かれていましたが、Honoでも/pingと/invocationsの2つのエンドポイントを用意すればあっさり動かせました。Strands Agents SDKとHonoのストリーミングヘルパーを活用してシンプルに実装できて良きでした!
普段Pythonを使うことが多いので、TypeScriptでも簡単にできて面白かったですね。
コンテナイメージを使用せずに、Node.jsでAgentCore Runtimeに直接デプロイしたケースは使ってみると良いですね。ただIAMやS3回りは自分で作らないといけなく少し手間なので、個人的にはAgentCore CLIでシームレスに連携できるとより便利だなと思っているので、アップデートに期待です。
本記事が少しでも参考になりましたら幸いです。
最後までご覧いただきありがとうございました!
補足:Node.jsで直接コードデプロイする際の注意点
公式ドキュメントにNode.js固有の注意点がまとまっているので、特にハマりやすそうなポイントだけかいつまんで紹介しておきます。
エントリポイントは.jsのみ
entryPointに指定できるのは.jsファイルだけで、.tsを直接指定することはできません。本記事のようにtscで事前にJavaScriptへ変換する必要があります。
src/app.jsやdist/index.jsのようにサブディレクトリに置くこともできて、その場合はZIP内のパス通りにentryPointを指定すればOKです。node_modules/はZIPルートに置いておけばNode.jsの解決ルールでそのまま読みにいってくれます。
依存のパッケージング方法は2通り
公式が紹介している方法は次の2つです。
| 方法 | 特徴 |
|---|---|
Vendored(node_modules/を同梱) |
Node.jsの依存解決をそのまま使える。サイズは大きくなる |
| esbuildでバンドル | 単一ファイルにまとめられる。サイズは小さくなる |
ARM64必須
AgentCore RuntimeはARM64アーキテクチャのみをサポートしています。.nodeや.soといったネイティブモジュールが含まれる場合、アーキテクチャが合わないとCREATE_FAILEDになります。
ピュアJSのパッケージ(Express、Hono、Fastify、Axios、wsなど)であれば気にしなくてOKです。今回使ったHono、@hono/node-server、@strands-agents/sdkはどれもピュアJSでした。
ネイティブモジュールが必要な場合は、ARM64マシンでビルドするか、npm install --arch=arm64 --platform=linuxを使う必要があるとのことです。
package.jsonのengines.nodeに注意
package.jsonのengines.nodeフィールドは、AgentCore Runtime側でランタイムバージョンとの互換性チェックに使われます。NODE_22を指定しているのにengines.nodeが"<18"のように22を含まない範囲だと、CREATE_FAILEDでエラーになります。
しかも、自分のpackage.jsonだけでなくnode_modules/配下の依存パッケージのengines.nodeもチェックされるとのこと。今回はnode_modulesを同梱しているので、依存パッケージのenginesに古いNode.jsしか書かれていないとそこで弾かれる可能性があります。ここは気をつけたいですね。
互換性のある書き方の例はこんな感じです。
{ "engines": { "node": ">=18" } }
{ "engines": { "node": ">=14 <18 || >=20" } }
{ "engines": { "node": "22" } }
TypeScriptを使う場合
公式ドキュメントでも明記されている通り、TypeScriptは事前にコンパイルが必須です。本記事ではtscでdist/に出力して、node_modulesと一緒にZIP化するパターンで進めました。
Observabilityも有効化できます
AgentCore RuntimeにはAgentCore Observabilityというトレース・モニタリング機能があり、Node.jsエージェントでもADOT(AWS Distro for OpenTelemetry)を使った自動計装ができます。
公式手順では、先にCloudWatch Transaction Searchを有効化したうえで、ADOTパッケージを追加してentryPointの先頭にopentelemetry-instrumentを付けます。
npm install @aws/aws-distro-opentelemetry-node-autoinstrumentation
{ "entryPoint": ["opentelemetry-instrument", "dist/app.js"] }
ZIPにはnode_modules/配下のADOT関連パッケージを同梱しておく必要があります。今回のようにnode_modulesごとZIP化する構成であれば、そのまま含められるのでシンプルですね。
実際にデプロイしてinvokeすると、起動ログに次のメッセージが出てきて、Strands Agents SDKのinvoke_agentやchatスパンが取得できることまでは確認できました(モデル名・トークン使用量・レイテンシなどが自動で乗ってきます)。
AWS Distro of OpenTelemetry automatic instrumentation started successfully
ただ、本記事の構成だとAgentCore Runtimeのコンソール側ではセッションが拾えていない挙動も観測できたので別記事で改めて取り上げたいと思います!







