[アップデート] Amazon Bedrock AgentCore Runtimeのdirect code deployがNode.jsに対応しました!

[アップデート] Amazon Bedrock AgentCore Runtimeのdirect code deployがNode.jsに対応しました!

2026.04.30

はじめに

こんにちは、スーパーマーケットが大好きなコンサルティング部の神野(じんの)です。

直近のアップデートでAmazon Bedrock AgentCore RuntimeがNode.jsのdirect code deployに対応しました!

https://aws.amazon.com/jp/about-aws/whats-new/2026/04/amazon-bedrock-agentcore-runtime/

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は下記のように記載しました。

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を開放します。

src/app.ts
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を使用してシンプルに実装できて良きですね。

https://hono.dev/docs/helpers/streaming

ローカル動作確認

tsxで起動して、curlで叩いてみます。

起動
npx tsx src/app.ts
ヘルスチェック
curl -s http://localhost:8080/ping
結果
{"status":"Healthy"}

問題なくHealthyが返ってきていますね!続いて/invocationsにプロンプトを投げます。

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化します。

TypeScriptのコンパイル
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-arm64fsevents.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.jsonpackage-lock.jsonnode_modules/をまとめてZIP化します。package.jsontype: "module"をRuntime側にも伝えるために同梱しています。

ZIP化
zip -r deployment_package.zip dist package.json package-lock.json node_modules

今回はまず動かすことを優先して普通にZIP化しました。

デプロイ

IAMロール

AgentCore Runtime用のIAMロールを作ります。信頼ポリシーでbedrock-agentcore.amazonaws.comを許可しておきます。

trust-policy.json
{
  "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でデプロイします。

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"
}

数秒待つとstatusREADYになります。

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でリクエストを実行してみます。

invoke
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
}

contentTypetext/event-streamになっていて、ちゃんとストリーミングとして転送されていますね!本文を見てみます。

cat /tmp/response.txt
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にしたいなと思って試してみました。

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のみです。

https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-code-deploy-supported-runtimes.html

おわりに

公式ドキュメントはExpressベースで書かれていましたが、Honoでも/ping/invocationsの2つのエンドポイントを用意すればあっさり動かせました。Strands Agents SDKとHonoのストリーミングヘルパーを活用してシンプルに実装できて良きでした!
普段Pythonを使うことが多いので、TypeScriptでも簡単にできて面白かったですね。

コンテナイメージを使用せずに、Node.jsでAgentCore Runtimeに直接デプロイしたケースは使ってみると良いですね。ただIAMやS3回りは自分で作らないといけなく少し手間なので、個人的にはAgentCore CLIでシームレスに連携できるとより便利だなと思っているので、アップデートに期待です。

本記事が少しでも参考になりましたら幸いです。
最後までご覧いただきありがとうございました!

補足:Node.jsで直接コードデプロイする際の注意点

公式ドキュメントにNode.js固有の注意点がまとまっているので、特にハマりやすそうなポイントだけかいつまんで紹介しておきます。

https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-get-started-code-deploy-node.html

エントリポイントは.jsのみ

entryPointに指定できるのは.jsファイルだけで、.tsを直接指定することはできません。本記事のようにtscで事前にJavaScriptへ変換する必要があります。

src/app.jsdist/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.jsonengines.nodeに注意

package.jsonengines.nodeフィールドは、AgentCore Runtime側でランタイムバージョンとの互換性チェックに使われます。NODE_22を指定しているのにengines.node"<18"のように22を含まない範囲だと、CREATE_FAILEDでエラーになります。

しかも、自分のpackage.jsonだけでなくnode_modules/配下の依存パッケージのengines.nodeもチェックされるとのこと。今回はnode_modulesを同梱しているので、依存パッケージのenginesに古いNode.jsしか書かれていないとそこで弾かれる可能性があります。ここは気をつけたいですね。

互換性のある書き方の例はこんな感じです。

互換性のある engines
{ "engines": { "node": ">=18" } }
{ "engines": { "node": ">=14 <18 || >=20" } }
{ "engines": { "node": "22" } }

TypeScriptを使う場合

公式ドキュメントでも明記されている通り、TypeScriptは事前にコンパイルが必須です。本記事ではtscdist/に出力して、node_modulesと一緒にZIP化するパターンで進めました。

Observabilityも有効化できます

AgentCore RuntimeにはAgentCore Observabilityというトレース・モニタリング機能があり、Node.jsエージェントでもADOT(AWS Distro for OpenTelemetry)を使った自動計装ができます。

https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-observability.html

公式手順では、先にCloudWatch Transaction Searchを有効化したうえで、ADOTパッケージを追加してentryPointの先頭にopentelemetry-instrumentを付けます。

ADOT追加
npm install @aws/aws-distro-opentelemetry-node-autoinstrumentation
entryPointの指定
{ "entryPoint": ["opentelemetry-instrument", "dist/app.js"] }

ZIPにはnode_modules/配下のADOT関連パッケージを同梱しておく必要があります。今回のようにnode_modulesごとZIP化する構成であれば、そのまま含められるのでシンプルですね。

実際にデプロイしてinvokeすると、起動ログに次のメッセージが出てきて、Strands Agents SDKのinvoke_agentchatスパンが取得できることまでは確認できました(モデル名・トークン使用量・レイテンシなどが自動で乗ってきます)。

起動ログ
AWS Distro of OpenTelemetry automatic instrumentation started successfully

ただ、本記事の構成だとAgentCore Runtimeのコンソール側ではセッションが拾えていない挙動も観測できたので別記事で改めて取り上げたいと思います!

この記事をシェアする

関連記事