MastraエージェントをLambdaに載せて動かしたい！~MCPを添えて~

こんにちは、ふくちと申します。
皆様Mastra使ってらっしゃいますでしょうか？

# Mastraとは

2025年の春先から話題になっている、生成AIアプリ開発用フレームワークの1つです。
5月19日には、日本でオフラインイベントなんかも開かれていました。

https://layerx.connpass.com/event/351541/

https://qiita.com/har1101/items/1b54f739f8368cda668e

こういった生成AIアプリ開発用フレームワークはPythonであることが多いんですが、MastraはTypeScriptで書くことができます。

また、コードでサクッとエージェントやワークフローが作れるということで、私個人としてはかなり推しているフレームワークとなっています。

```typescript:サンプルエージェント.ts
import { Agent } from "@mastra/core/agent";
import { openai } from "@ai-sdk/openai";
 
export const myAgent = new Agent({
  name: "My Agent",
  instructions: "You are a helpful assistant.",
  model: openai("gpt-4o-mini"),
});
```

## Mastraのデプロイ先

ただ、私が考えているMastraの課題が1つあります。
**AWSのサーバーレスプラットフォームとの親和性が少し不足していること**です。

[Cloudflare Workers、Vercel、またはNetlifyへのデプロイ](https://mastra.ai/ja/docs/deployment/deployment)はMastra側のデフォルト機能として備わっています。

ただ、AWSへのデプロイ機能がデフォルトとして備わっているわけではありません。
今は利用者側で上手いこと実装しています。

例えば、ECS on Fargateでのデプロイ事例はすでに幾つか存在します。

https://qiita.com/Syoitu/items/bfcc49e3941e8fefe4da

https://zenn.dev/tsukuboshi/articles/mastra-mcp-with-ecs-bedrock

また、Lambda Web Adapterを使うこともできます。
※私が以前作成したアプリはLWA上できちんと動いていたんですが、突如動かなくなったのでこちらはまた別途試します…

https://speakerdeck.com/har1101/bedrockxmcpdeshe-nei-buroguzhi-bi-wen-hua-woyu-tetai

↓実装はこちらをご参考ください。

https://github.com/moritalous/nextjs-mastra-app

とまぁここまで色々ご紹介してきましたが、ここで思うわけです。
**普通のLambdaで動かないのか？** と。

ということでやってみました。

# MastraエージェントをLambdaへデプロイする！

以下の手順で進めます。
1. 必要なモジュールをインストールする
1. Lambda実行用ソースコードを書く
1. AWS CDKを用いて構築するリソースを定義する
1. デプロイしてコンソールから動作確認

また、最終的なディレクトリ構成はこうなります。node_modulesやignore系ファイルは除いています。
```
mastra-on-lambda/
├── lambda/
│   └── index.ts         # Lambda関数のソースコード
├── dist/                # ビルド成果物（自動生成）
│   └── index.mjs        # esbuildでバンドル済みLambda関数
├── package.json         # プロジェクトの依存関係
├── tsconfig.json        # TypeScriptの設定
└── cdk/                 # AWS CDKプロジェクト
    ├── lib/
    │   └── cdk-stack.ts # CDKスタック定義
    └── layer/           # Lambda Layer用の依存関係
        └── nodejs/
            └── package.json
```

## 1. 必要なモジュールをインストールする

大前提として、Lambdaにインストールされているデフォルトのパッケージだけでは動きません。
なので、必要なモジュールは別途こちらで用意して、**Lambda Layerとしてデプロイ**してあげる必要があります。

>Lambda レイヤーは、補助的なコードやデータを含む .zip ファイルアーカイブです。
レイヤーには通常、ライブラリの依存関係、カスタムランタイム、または設定ファイルが含まれています。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/chapter-layers.html

これを用いる形でやっていきましょう。
```terminal:プロジェクト作成、モジュールインストール
$ mkdir mastra-on-lambda
$ cd ./mastra-on-lambda
```

また、ルートディレクトリで以下のファイルを作成します。
```json:package.json(モジュールのバージョンは適宜アップデートしてください)
{
    "name": "mastra-on-lambda",
    "version": "0.1.0",
    "private": true,
    "type": "module",
    "devDependencies": {
        "@types/aws-lambda": "^8.10.149",
        "@types/node": "^22.15.29",
        "esbuild": "^0.25.5",
        "ts-node": "^10.9.2",
        "typescript": "^5.8.3"
    },
    "scripts": {
        "prebuild": "rm -rf dist",
        "build": "esbuild lambda/index.ts --bundle --minify --sourcemap --platform=node --target=es2022 --format=esm --external:@mastra/core --external:@ai-sdk/amazon-bedrock --outfile=dist/index.mjs"
    },
    "dependencies": {
        "@ai-sdk/amazon-bedrock": "^2.2.10",
        "@mastra/core": "^0.10.3",
        "@mastra/mcp": "^0.10.2",
        "@modelcontextprotocol/server-brave-search": "^0.6.2"
    }
}

```

```json:tsconfig.json
{
    "compilerOptions": {
      "target": "es2022",
      "strict": true,
      "preserveConstEnums": true,
      "noEmit": true,
      "sourceMap": false,
      "module": "ESNext",
      "moduleResolution": "node",
      "esModuleInterop": true,
      "skipLibCheck": true,
      "forceConsistentCasingInFileNames": true,
      "isolatedModules": true
    },
    "include": ["lambda/index.ts"],
    "exclude": ["node_modules", "dist"]
}
```

この2つのjsonファイルが作成できたら、以下コマンドを実行します。
```terminal
# package.jsonで指定したモジュールをインストールする
$ npm install
```

現状のフォルダ構造はこのような形になります。
```
mastra-on-lambda/
├── package.json         # プロジェクトの依存関係
└── tsconfig.json        # TypeScriptの設定
```
これで開発を進める準備が整いました。

## 2. Lambda実行用ソースコードを書く

Lambdaの特徴は、実行方法が少し特異なことです。
こんな感じでハンドラーを定義する必要があります。
```javascript:lambda実行方法.js
export const handler = async (event, context) => { 
  // ここに処理を記述
}
```

ということでこの形に沿いながら、TypeScriptでMastraエージェントを定義していきます。
```terminal:lambdaディレクトリ作成
# mastra-on-lambdaディレクトリで実行
$ mkdir lambda
$ touch lambda/index.ts
```

```typescript:mastra-on-lambda/lambda/index.ts
import { Handler } from 'aws-lambda';
import { Agent } from '@mastra/core/agent';
import { bedrock } from '@ai-sdk/amazon-bedrock';

interface LambdaEvent {
    question?: string;
}

export const handler: Handler<LambdaEvent> = async (event): Promise<Record<string, any>> => {

    const question = event.question || 'AIエージェントって何?';
    
    const agent = new Agent({
        name: 'Mastra',
        model: bedrock('anthropic.claude-3-5-sonnet-20240620-v1:0'),
        instructions: 'あなたはAIエージェントです。ユーザーからの質問に対し、日本語でフレンドリーな回答をお願いします。',
    });

    const response = await agent.generate(question);

    return {
        question: question,
        message: response.text,
    };
};
```

簡単にコードの解説を置いておきます。TypeScriptなので型定義が含まれているのが特徴です。
- メイン処理部分
    - export const handler
        - handlerという変数を定義
    - async (event)
        - 非同期関数で、eventパラメータ(LambdaEvent型)を受け取る
    - const agent = New Agent
        - Mastraでエージェントを定義
- 型定義部分
    - Handler\<LambdaEvent>
        - handler変数の型定義
        - aws-lambdaモジュールのHandler型を使用
        - LambdaEventはこのハンドラーに入力値として渡されるイベントの型(6-8行目で定義)
    - Promise\<Record\<string, any>>
        - この関数における返り値の型定義
    - Promise\<T>
        - 非同期処理の結果を表す型
        - Promise\<T>は「将来的にT型の値を返す」という意味
    - Record\<string, any>
        - Record<K, V>はTypeScriptのユーティリティ型
        - ここでは文字列型のキーと任意の型の値を持つオブジェクト

つまり、「LambdaEvent型のイベントを受け取り、任意のオブジェクトを返す非同期Lambda関数」という定義を行い、その中でMastraエージェントを作成して任意の質問を投げられるようにしている、というものになっています。

ここまでで、以下のディレクトリが完成しました。
```
mastra-on-lambda/
├── lambda/
│   └── index.ts         # Lambda関数のソースコード（開発用）
├── package.json         # プロジェクトの依存関係
└── tsconfig.json        # TypeScriptの設定
```

## 3. AWS CDKを用いて構築するリソースを定義する

Lambda用のコードはできましたが、このLambdaをデプロイする方法を考えなければいけません。
ここではAWS CDKを用いてデプロイを行います。

```terminal:cdkプロジェクト作成
# ルートディレクトリで実行
$ mkdir cdk && cd cdk

# cdkディレクトリで実行
$ cdk init app --language typescript
```

これでCDKをデプロイするために必要な諸々のモジュール・ファイルが自動でインストールされます。

続いて作成するリソースを定義していきます。

```typescript:mastra-on-lambda/cdk/lib/cdk-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as logs from 'aws-cdk-lib/aws-logs';
import { Construct } from 'constructs';

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

    // Lambda実行ロール
    const lambdaRole = new iam.Role(this, 'MastraLambdaRole', {
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')
      ],
    });

    // Bedrock アクセス権限を追加
    lambdaRole.addToPolicy(new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      actions: [
        'bedrock:InvokeModel',
        'bedrock:InvokeModelWithResponseStream'
      ],
      resources: ['*']
    }));

    // CloudWatch Logsグループ
    const logGroup = new logs.LogGroup(this, 'MastraLambdaLogGroup', {
      logGroupName: '/aws/lambda/mastra-lambda',
      retention: logs.RetentionDays.ONE_WEEK,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    // Lambda Layer
    const dependenciesLayer = new lambda.LayerVersion(this, 'MastraDependenciesLayer', {
      code: lambda.Code.fromAsset('./layer'),
      compatibleRuntimes: [lambda.Runtime.NODEJS_22_X],
      description: 'mastra, ai-sdk, mcp dependencies',
    });

    // Lambda関数
    const mastraLambda = new lambda.Function(this, 'MastraLambda', {
      runtime: lambda.Runtime.NODEJS_22_X,
      handler: 'index.handler',
      code: lambda.Code.fromAsset('../dist', {
        exclude: ['layer.zip', 'handler.zip', 'layer/**']
      }),
      role: lambdaRole,
      layers: [dependenciesLayer],
      timeout: cdk.Duration.seconds(300),
      memorySize: 1024,
      environment: {
        NODE_ENV: 'production',
        BRAVE_API_KEY: 'put-your-brave-api-key',
      },
      logGroup: logGroup,
    });
  }
}
```

続いて、Lambda Layer内に含めるモジュールを定義していきます。
これをcdkディレクトリ配下に含めるかは諸説ですが…今回はcdkでデプロイするので、この中に含めてしまいます。

```terminal:Lambda Layer設定
# cdkディレクトリで実行
$ mkdir ./layer/nodejs && cd ./layer/nodejs

# nodejsディレクトリで実行
$ touch package.json
```
作成したpackage.jsonには以下を入力します。
```json:package.json
{
  "name": "nodejs",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "module",
  "dependencies": {
    "@ai-sdk/amazon-bedrock": "^2.2.10",
    "@mastra/core": "^0.10.3",
  }
}
```

dependencies配下で設定したモジュールたちが、Lambda Layer内に含まれます。
ここではmastra/coreとai-sdkのBedrockですね。

ルートディレクトリのpackage.jsonに設定したモジュールと同じになっていますが、Node.jsでLambda Layerを作成する際には、「`nodejs/`ディレクトリ配下にモジュールを配置する必要がある」という仕様があるため、このような形にしています(詳しくは後述)。

そして、上記で定義したモジュールをインストールします。
```terminal:Lambda Layer用モジュールをインストール
# nodejsディレクトリで実行
$ npm install
```
これでデプロイ準備が整いました。

現在のディレクトリ構成は以下のような形になっています。
```
mastra-on-lambda/
├── lambda/
│   └── index.ts         # Lambda関数のソースコード（開発用）
├── package.json         # プロジェクトの依存関係
├── tsconfig.json        # TypeScriptの設定
└── cdk/                 # AWS CDKプロジェクト
    ├── lib/
    │   └── cdk-stack.ts # CDKスタック定義
    └── layer/           # Lambda Layer用の依存関係
        └── nodejs/
            └── package.json
```

## 4. デプロイしてコンソールから動作確認

デプロイ手順は以下の形になっています。

```terminal:ターミナル
# ルートディレクトリで実行
$ npm run build
$ cd cdk

# cdkディレクトリで実行
$ cdk deploy
```

npm run buildを実行した際に、distディレクトリを作成し、コンパイル後のLambda関数用ソースコードが格納されます。

これは一番最初に定義したpackage.jsonファイルにて定義されている、以下スクリプトを元に実行されます。

```json:package.json(一部抜粋)
"scripts": {
    "prebuild": "rm -rf dist",
    "build": "esbuild lambda/index.ts --bundle --minify --sourcemap --platform=node --target=es2022 --format=esm --external:@mastra/core --external:@ai-sdk/amazon-bedrock --outfile=dist/index.mjs"
}
```

1つずつコマンドを確認していきましょう。
1. "prebuild": "rm -rf dist"
    - ビルド前の準備
    - npm run build実行前に自動的に実行される
    - distディレクトリを完全削除してクリーンな状態にする
2. "build": "esbuild lambda/index.ts \\
--bundle \\
--minify \\
--sourcemap \\
--platform=node \\
--target=es2022 \\
--format=esm \\
--external:@mastra/core \\
--external:@ai-sdk/amazon-bedrock \\
--outfile=dist/index.mjs"
    - esbuildを使ってビルドを実行する
    - `lambda/index.ts`: 入力ファイル(TypeScriptソース)
    - `--bundle`: 必要な依存関係を単一ファイルにまとめる(Lambda関数のサイズ最適化)
    - `--minify`: コードサイズの最小化(Lambda実行時のコールドスタート改善)
    - `--sourcemap`: デバッグ用ソースマップ生成(本番エラー時の原因特定が可能)
    - `--platform=node`: Node.js環境向けの最適化
    - `--target=es2022`: ES2022仕様のJavaScript出力
    - `--format=esm`: ES Module形式での出力
    - `--external:*`: Lambda Layerから提供されるため、バンドルに含めない
    - `--outfile=dist/index.mjs`: 出力ファイル(.mjs拡張子でES Module明示)

https://esbuild.github.io/

:::note
Mastraを使うため、今回のLambda関数用コードはTypeScriptで書きました。
しかしLambdaのNode.jsランタイムはJavaScriptのみをサポートしており、TypeScriptをサポートしていません。
なので、事前にJavaScriptへのトランスパイルが必要です。

また、この後MCPを追加する都合上、CommonJSではなくES Module形式である必要があります。
したがって、*.mjs拡張子を用いて、Node.jsが自動的にES Moduleとして動作するようにしておきます。
:::

ということでビルド実行後のプロジェクト構造は以下のとおりです。
```
mastra-on-lambda/
├── lambda/
│   └── index.ts         # Lambda関数のソースコード
├── dist/                # ビルド成果物（自動生成）
│   └── index.mjs        # esbuildでバンドル済みLambda関数
├── package.json         # プロジェクトの依存関係
├── tsconfig.json        # TypeScriptの設定
└── cdk/                 # AWS CDKプロジェクト
    ├── lib/
    │   └── cdk-stack.ts # CDKスタック定義
    └── layer/           # Lambda Layer用の依存関係
        └── nodejs/
            └── package.json
```
その後cdk deploy が上手く行えていれば、成功です！

コンソールから動作確認してみましょう。
```json:コンソール上のテストイベントJSON
{
  "question": "こんにちは〜"
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/1efd5db6-4ff5-4fb4-90f1-eed265704fbd.png)

無事、Lambda上でMastraエージェントを動かせるようになりました！👏

# MCPを追加する

とはいえ、エージェントだけデプロイしても仕方がありません。
やはりエージェントが使えるツールを用意してこそ意味があります。

そこで、MCPを追加してみましょう。
今回はBrave Search API MCPサーバーを組み込んで、エージェントがネット検索できるようにしてみます。
※このMCPサーバーは2025/05/29にアーカイブされたようですが、やってみたら動いたので書いておきます。
※あくまでMCPを追加設定するための方法としてご参考ください。

https://github.com/modelcontextprotocol/servers-archived/tree/main/src/brave-search

まずはMastraのMCP用パッケージと、MCPサーバー本体をインストールします。
プロジェクト用とLambda Layer用の2箇所でインストールが必要なので、ルートディレクトリ配下のpackage.jsonとlayer/nodejs配下のpackage.jsonを以下のように修正します。

```diff_json:package.json(一部抜粋、バージョンは適宜修正してください)
"dependencies": {
    "@ai-sdk/amazon-bedrock": "^2.2.10",
    "@mastra/core": "^0.10.3",
+   "@mastra/mcp": "^0.10.2",
+   "@modelcontextprotocol/server-brave-search": "^0.6.2"
}
```

設定したら、以下をターミナルで実行します。
```terminal:ターミナルで実行
# ルートディレクトリで実行
$ npm install
$ cd cdk/layer/nodejs

# nodejsディレクトリで実行
$ npm install
```

続いて、Lambda関数のコードを修正します。MastraでのMCP定義方法に従ってコードを修正します。

https://mastra.ai/ja/docs/tools-mcp/mcp-overview

```diff_typescript:mastra-on-lambda/lambda/index.ts
import { Handler } from 'aws-lambda';
import { Agent } from '@mastra/core/agent';
import { MCPClient } from '@mastra/mcp';
import { bedrock } from '@ai-sdk/amazon-bedrock';

interface LambdaEvent {
    question?: string;
}

+ const mcp = new MCPClient({
+   id: "brave-search-mcp",
+   servers: {
+     brave_search: {
+       command: "node",
+       args: ["/opt/nodejs/node_modules/@modelcontextprotocol/server-brave-search/dist/index.js"],
+       env: {
+         BRAVE_API_KEY: process.env.BRAVE_API_KEY ?? "",
+       },
+     }
+   }
+ });

export const handler: Handler<LambdaEvent> = async (event): Promise<Record<string, any>> => {

  const question = event.question || 'AIエージェントって何?';
  
  const agent = new Agent({
    name: 'Mastra',
-   model: bedrock('anthropic.claude-3-5-sonnet-20240620-v1:0'),
+   model: bedrock('apac.anthropic.claude-sonnet-4-20250514-v1:0'),
    instructions: 'あなたはAIエージェントです。ユーザーからの質問に対し、日本語でフレンドリーな回答をお願いします。',
+   tools: await mcp.getTools(),
  });

  const response = await agent.generate(question);

  return {
    question: question,
    message: response.text,
  };
};
```

さっきとはモデルを変更して、最新モデルであるClaude Sonnet 4を指定してみました。
(@Syoitu さんがAI-SDKへコントリビュートしてくださったおかげです、ありがとうございます〜)

https://x.com/HedgehogPython/status/1929449941413634281

MCPClient内のcommand設定については、ここでは`node`コマンドを利用して実行する形にしました。

MCPサーバーの設定時、基本的には`npx`コマンドを実行するケースがほとんどです。
ただ、Lambda(もしくはLambda Layer)内では`npx`コマンドが利用できないようです。

一方で、`node`コマンドはLambdaランタイムに標準で含まれているため確実に実行可能です。
したがって、Lambda環境内では`node`コマンドを利用するのが安定だと私は考えています。

(森田さんがLambda環境における`uvx`コマンドについて以下のように発信していらっしゃったのですが、本件もこれと同じようなことだと考えています。)

https://qiita.com/moritalous/items/3e060bede2185dd739f5

また、MCPClientのargsには`/opt/nodejs/node_modules/@modelcontextprotocol/server-brave-search/dist/index.js`を設定していますが、これはLambda Layer側の仕様に則っています。

>モジュールが確実に取得されるようにするには、次のいずれかのフォルダパスのレイヤー .zip ファイルにパッケージ化します。
>これらのファイルは /opt に保存され、フォルダパスは PATH 環境変数にロードされます。
>  - nodejs/node_modules

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/nodejs-layers.html

上記の仕様に則り、argsは`/opt/nodejs/node_modules/`を指定しています。
また、この制限があるため、Lambda Layer用のモジュールを`layer/nodejs`配下に独立して配置する必要がありました。

最後に、ルートディレクトリのpackage.jsonに`--external:@mastra/mcp`を追加しましょう。

```json:package.json(一部抜粋)
"scripts": {
    "prebuild": "rm -rf dist",
    "build": "esbuild lambda/index.ts --bundle --minify --sourcemap --platform=node --target=es2022 --format=esm --external:@mastra/core --external:@ai-sdk/amazon-bedrock --external:@mastra/mcp --outfile=dist/index.mjs"
}
```

これで再度デプロイ手順を実行します。
```terminal:ターミナルで実行
# ルートディレクトリで実行
$ npm run build
$ cd cdk

# cdkディレクトリで実行
$ cdk deploy
```

デプロイできたら、再度コンソールからテストしてみましょう。
```json:コンソール上のテストイベントJSON
{
  "question": "Nintendo Switch2の発売日っていつだっけ？"
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/b2a2009b-a8c1-4f0b-a83c-d4772ab66085.png)

**楽しみですね！私はまだ当たってません！！！！！！！！😊**

# まとめ
割と楽に、LambdaでもMastraエージェントが動かせます！
そのうちもっと楽になると信じて…🙏

参考までにGitHubも置いておきます〜

https://github.com/har1101/mastra-on-lambda/

開発時に遭遇したエラーは別途ここに纏めてあります。

https://github.com/har1101/mastra-on-lambda/blob/main/errorlog.md