9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CDKでもAgentCoreをデプロイしたい - Mastra & StrandsAgents

Posted at

吾輩は人間である

前書き

2025年9月26日、AgentCoreのCloudFormation対応がついに実装されました。

CDKを使ってAIエージェントをAgentCoreランタイムにデプロイしてみたいと思います。

現時点ではL1 Constructのみの提供ですが、L2 ConstructのRFCも既に起票されているようなので、近いうちにリリースされることでしょう。

maxresdefault.jpg

Strands Agents編

CDKがまだ未経験の方は、下記のドキュメントを参照して初期化から進めていきましょう。

npm install -g aws-cdk

mkdir agentcore-cdk && cd agentcore-cdk

# typescriptを使います。
cdk init app --language typescript

初期化が完了したら、package.jsonを確認してください。
AgentCoreを使用するにはaws-cdk-libのバージョン2.217.0以降が必要です。必要に応じて、aws-cdk-libのバージョンを更新してください。

490E119E-EA60-48EC-8480-35BFDBB45EC0_4_5005_c.jpeg

package.json
  "dependencies": {
+    "aws-cdk-lib": "2.217.0",
    "constructs": "^10.0.0"
  }

次に、libフォルダの配下にappフォルダを作成し、StrandsAgentsの関連ファイルを配置します。

実施した内容は下記のドキュメントに記載されている通りですが、使用しているPythonのバージョンは3.13です。手順はBuild and Test Locallyまで実施しました。

完了後のフォルダ構造は、下記の画像の通りです。

385C3147-FAB8-4646-BCF1-5F612A4FDFF6.jpeg

lib/agentcore-cdk-stack.tsを下記のように修正します。

lib/agentcore-cdk-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { DockerImageAsset, Platform } from 'aws-cdk-lib/aws-ecr-assets';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as agentcore from 'aws-cdk-lib/aws-bedrockagentcore';
import path from 'node:path';


export class AgentcoreCdkStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const dockerImageAsset = new DockerImageAsset(this, 'DockerImageAsset', {
      directory: path.join(__dirname, 'app'),
      platform: Platform.LINUX_ARM64,
      file: 'Dockerfile',
    });

    const agentCoreRole = new iam.Role(this, 'BedrockAgentCoreRole', {
      assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
      description: 'IAM role for Bedrock AgentCore Runtime',
    });
    const region = cdk.Stack.of(this).region;
    const accountId = cdk.Stack.of(this).account;

    agentCoreRole.addToPolicy(new iam.PolicyStatement({
      sid: 'ECRImageAccess',
      effect: iam.Effect.ALLOW,
      actions: [
        'ecr:BatchGetImage',
        'ecr:GetDownloadUrlForLayer'
      ],
      resources: [
        `arn:aws:ecr:${region}:${accountId}:repository/*`
      ]
    }));

    agentCoreRole.addToPolicy(new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      actions: [
        'logs:DescribeLogStreams',
        'logs:CreateLogGroup'
      ],
      resources: [
        `arn:aws:logs:${region}:${accountId}:log-group:/aws/bedrock-agentcore/runtimes/*`
      ]
    }));

    agentCoreRole.addToPolicy(new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      actions: [
        'logs:DescribeLogGroups'
      ],
      resources: [
        `arn:aws:logs:${region}:${accountId}:log-group:*`
      ]
    }));

    agentCoreRole.addToPolicy(new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      actions: [
        'logs:CreateLogStream',
        'logs:PutLogEvents'
      ],
      resources: [
        `arn:aws:logs:${region}:${accountId}:log-group:/aws/bedrock-agentcore/runtimes/*:log-stream:*`
      ]
    }));

    agentCoreRole.addToPolicy(new iam.PolicyStatement({
      sid: 'ECRTokenAccess',
      effect: iam.Effect.ALLOW,
      actions: [
        'ecr:GetAuthorizationToken'
      ],
      resources: ['*']
    }));

    agentCoreRole.addToPolicy(new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      actions: [
        'xray:PutTraceSegments',
        'xray:PutTelemetryRecords',
        'xray:GetSamplingRules',
        'xray:GetSamplingTargets'
      ],
      resources: ['*']
    }));

    agentCoreRole.addToPolicy(new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      actions: ['cloudwatch:PutMetricData'],
      resources: ['*'],
      conditions: {
        StringEquals: {
          'cloudwatch:namespace': 'bedrock-agentcore'
        }
      }
    }));

    agentCoreRole.addToPolicy(new iam.PolicyStatement({
      sid: 'GetAgentAccessToken',
      effect: iam.Effect.ALLOW,
      actions: [
        'bedrock-agentcore:GetWorkloadAccessToken',
        'bedrock-agentcore:GetWorkloadAccessTokenForJWT',
        'bedrock-agentcore:GetWorkloadAccessTokenForUserId'
      ],
      resources: [
        `arn:aws:bedrock-agentcore:${region}:${accountId}:workload-identity-directory/default`,
        `arn:aws:bedrock-agentcore:${region}:${accountId}:workload-identity-directory/default/workload-identity/agentName-*`
      ]
    }));

    agentCoreRole.addToPolicy(new iam.PolicyStatement({
      sid: 'BedrockModelInvocation',
      effect: iam.Effect.ALLOW,
      actions: [
        'bedrock:InvokeModel',
        'bedrock:InvokeModelWithResponseStream'
      ],
      resources: [
        'arn:aws:bedrock:*::foundation-model/*',
        `arn:aws:bedrock:${region}:${accountId}:*`
      ]
    }));

    const runtime = new agentcore.CfnRuntime(this, 'AgentCoreRuntime', {
      agentRuntimeName: 'MyAgentRuntime',
      agentRuntimeArtifact: {
        containerConfiguration: {
          containerUri: dockerImageAsset.imageUri,
        }
      },
      networkConfiguration: {
        networkMode: 'PUBLIC', 
      },
      roleArn: agentCoreRole.roleArn, 
      protocolConfiguration: 'HTTP',
    });

    runtime.node.addDependency(agentCoreRole)
    new agentcore.CfnRuntimeEndpoint(this, 'RuntimeEndpoint', {
      agentRuntimeId: runtime.attrAgentRuntimeId,
      agentRuntimeVersion: runtime.attrAgentRuntimeVersion,
      name: 'MyAgentRuntimeEndpoint',
    });
  }
}

その後CDKをデプロイしてください、初めての方はcdk bootstrapから実施してください。

cdk deploy

デプロイが完了したら、AgentCoreのエージェントサンドボックスからテストを実行できるので、試してみてください。

033F28D5-54D8-49B6-A305-8EEE5042E629.jpeg

Mastra編

CDKを使ってMastra製のAIエージェントをAgentCoreにデプロイできるでしょうか?
試してみたらできました。

先ほどのコードをベースに少し修正を加えていきます。

libフォルダの配下にMastra製のAIエージェントを作成していきます。

cd lib
npx create-mastra@latest

初期化中にプロジェクト名を聞かれるので、今回はmastra-appと入力します。

Bedrockからモデルを使用するため、AI SDKのBedrock Providerをインストールしていきます。

cd mastra-app
npm install @ai-sdk/amazon-bedrock @aws-sdk/credential-providers

ローカル開発環境とAgentCore環境を両立させるため、
IS_LOCAL環境変数で初期化を振り分けるファイルを作成しておきます。

lib/mastra-app/src/mastra/lib/bedrock-providers.ts
import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock';
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';

const isLocal = process.env.IS_LOCAL === 'TRUE';

export const bedrock = createAmazonBedrock(
  isLocal
    ? {
        region: 'us-west-2',
        accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
        secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
        sessionToken: process.env.AWS_SESSION_TOKEN!,
      }
    : {
        region: 'us-west-2',
        credentialProvider: fromNodeProviderChain(),
      }
);

Mastra初期化された際に、天気調査AIエージェントがデフォルトで生成されているので、そのAIエージェントを改造して使用します。

lib/mastra-app/src/mastra/agents/weather-agent.ts
+ import { bedrock } from "../lib/bedrock-providers"
import { Agent } from '@mastra/core/agent';
import { weatherTool } from '../tools/weather-tool';

export const weatherAgent = new Agent({
  name: 'Weather Agent',
  instructions: `
      You are a helpful weather assistant that provides accurate weather information and can help planning activities based on the weather.

      Your primary function is to help users get weather details for specific locations. When responding:
      - Always ask for a location if none is provided
      - If the location name isn't in English, please translate it
      - If giving a location with multiple parts (e.g. "New York, NY"), use the most relevant part (e.g. "New York")
      - Include relevant details like humidity, wind conditions, and precipitation
      - Keep responses concise but informative
      - If the user asks for activities and provides the weather forecast, suggest activities based on the weather forecast.
      - If the user asks for activities, respond in the format they request.

      Use the weatherTool to fetch current weather data.
`,
+ model: bedrock("us.anthropic.claude-sonnet-4-20250514-v1:0"),
  tools: { weatherTool },
});

AgentCoreからMastraのAIエージェントがどのように呼び出されるのか、内部の実装が分からないため、StrandsAgentsのAgentCore実装を参考にして、Expressを使用したAPIサーバーを作成します。

lib/mastra-app/server.ts
import express, { Request, Response } from 'express';
import { mastra } from "./src/mastra/index.js"

const app = express();
const port = 8080;

app.use(express.json());

interface InvocationRequest {
    input: {
        prompt?: string;
        [key: string]: any;
    };
}

interface InvocationResponse {
    output: {
        message: string;
        timestamp: string;
        model: string;
    };
}

app.post('/invocations', async (req: Request<{}, InvocationResponse, InvocationRequest>, res: Response<InvocationResponse>) => {
    try {
        const userMessage = req.body.input?.prompt;
        console.log(userMessage)

        if (!userMessage) {
            return res.status(400).json({
                output: {
                    message: "No prompt found in input. Please provide a 'prompt' key in the input.",
                    timestamp: new Date().toISOString(),
                    model: "mastra-agent"
                }
            });
        }

        const agent = mastra.getAgent("weatherAgent");
        const result = await agent.generateVNext(userMessage);

        const response: InvocationResponse = {
            output: {
                message: result.text,
                timestamp: new Date().toISOString(),
                model: "mastra-agent"
            }
        };

        return res.json(response);
    } catch (error) {
        console.error("Agent processing failed:", error);
        return res.status(500).json({
            output: {
                message: `Agent processing failed: ${error instanceof Error ? error.message : String(error)}`,
                timestamp: new Date().toISOString(),
                model: "mastra-agent"
            }
        });
    }
});

app.get('/ping', (_req: Request, res: Response) => {
    return res.json({ status: "healthy" });
});

if (import.meta.url === `file://${process.argv[1]}`) {
    app.listen(port, '0.0.0.0', () => {
        console.log(`Server is running at http://localhost:${port}`);
    });
}

export default app;

次に、Dockerfileも作成していきます。

lib/mastra-app/Dockerfile
FROM --platform=linux/arm64 node:23-alpine
 
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY src ./src
COPY server.ts ./
RUN npx mastra build
RUN apk add --no-cache gcompat
 
RUN addgroup -g 1001 -S nodejs && \
  adduser -S mastra -u 1001 && \
  chown -R mastra:nodejs /app
 
USER mastra
 
ENV PORT=8080
ENV IS_LOCAL=FALSE
ENV NODE_ENV=production
 
EXPOSE 8080
 
CMD ["npm", "run", "server"]

最後に、.envファイルも作成しておきます、ローカルで動作しない場合は作らなくても大丈夫です。

lib/mastra-app/.env
IS_LOCAL=TRUE
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_SESSION_TOKEN=

ローカルでも実行できるように、server scriptsを作成しておきます。
実行用のライブラリであるtsxも追加します。

npm i tsx
lib/mastra-app/package.json
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "mastra dev",
    "build": "mastra build",
    "start": "mastra start",
+    "server": "tsx server.ts"
  },

完成したフォルダ構成は下記の画像の通りです。

793B2ABB-AE16-4A3D-85A6-3133FCD328CA_4_5005_c.jpeg

既存のCDKスタックを修正します。変更箇所は1行のみです。

lib/agentcore-cdk-stack.ts
    const dockerImageAsset = new DockerImageAsset(this, 'DockerImageAsset', {
+      directory: path.join(__dirname, 'mastra-app'),
       platform: Platform.LINUX_ARM64,
       file: 'Dockerfile',
    });

最初に作成したStrandsAgentsを使って実装したAIエージェントが存在する場合は、一度destroyしてください。

cdk destroy

クリーンな状態になったら、再度デプロイを実行してください。

cdk deploy

デプロイが完了し、AgentCoreのエージェントサンドボックスからテストしてみたところ、問題なく動作しました。

B35F4273-E236-4B60-84D8-F85ADDF9F89C.jpeg

感想

StrandsAgentsやその他のPython製フレームワークに関しては、現状ではBedrock AgentCore Starter Toolkitの方が使いやすいというのが率直な感想です。

しかし、Python以外の言語のフレームワークにとっては、CDKでデプロイできるようになったことは朗報と言えるでしょう。

参考資料

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?