はじめに
AWS Amplify Gen 2 でカスタム Lambda Resolver を増やしていくと、CloudFormation の 500 リソース上限 に当たることがあります。カスタム Query / Mutation はデフォルトで FunctionDirectiveStack という NestedStack に集約されるため、API が増えるほどこのスタックが膨らみ、新規 API の追加やデプロイが失敗します。
2026 年 5 月、@aws-amplify/backend-data 1.7.0 に defineData() の stackMappings が追加され、@aws-amplify/backend 1.23.0 から使えます。AppSync Resolver を別 NestedStack へ分散配置できます。
本記事では、なぜこの機能が必要になるのか、どう使うのか、移行先をどう選ぶのかをまとめます。
なぜ stackMappings が必要になるか
Amplify Gen 2 の Resolver 配置
Resolver の置き場所は、種類によって違います。
| 種類 | 例 | デフォルトの配置 |
|---|---|---|
| モデル自動生成の CRUD | CreateOrderResolver |
各モデル用 NestedStack |
カスタム Lambda(a.handler.function(...)) |
QuerysearchItemsResolver |
FunctionDirectiveStack |
カスタム JS(a.handler.custom(...)) |
共有 DataSource 上の Resolver | data 親スタック(stackMappings の対象外) |
上限対策の主戦場は a.handler.function(...) です。ユーザー管理・検索・通知・ファイル処理など、Lambda 連携の GraphQL フィールドが多いプロジェクトでは、FunctionDirectiveStack のリソース数が急速に増えます。1 Resolver あたり IAM Role / Policy、DataSource、FunctionConfiguration、Resolver が載るため、数十個でも上限が見えてきます。
a.handler.custom(...) は FunctionDirectiveStack を膨らませません。公開型どおり mapping 対象外なので、本記事の手順では扱いません。
直面する CloudFormation 制約
| 制約 | 内容 |
|---|---|
| リソース数 | 1 CloudFormation スタックあたり 500 リソース |
| テンプレートサイズ | S3 上のテンプレート 1MB 上限 |
| 公式の位置づけ | リソース数上限に遭遇していない場合の利用は 非推奨(graphql-api-construct の型コメント) |
親 data スタックのテンプレートが 1MB を超える問題も起こり得ます。NestedStack を増やすと、親に AWS::CloudFormation::Stack・Parameters・Outputs・DependsOn が増えます。data スタックの 1MB 超過は amplify-backend#3066 でも報告されています。
上限にまだ余裕がある場合は、公式どおり stackMappings は使いません。すでに上限が見えている場合が、この機能の出番です。
よく見るエラー
デプロイログを検索するときの手がかりです。FunctionDirectiveStack が 500 を超えると、次のようなメッセージになります。
[TooManyResourcesInStack] Number of resources in stack
'amplify-.../data/.../FunctionDirectiveStack': 517 is greater than allowed maximum of 500:
AWS::IAM::Role (...), AWS::IAM::Policy (...), AWS::AppSync::DataSource (...),
AWS::AppSync::FunctionConfiguration (...), AWS::AppSync::Resolver (...), AWS::CDK::Metadata (1)
sandbox / synth の警告は次の形です。
Number of resources in stack 'FunctionDirectiveStack': 505 is greater than allowed maximum of 500
親 data テンプレートが 1MB を超えると、次が出ます。
Template may not exceed 1000000 bytes in size.
Template size exceeds limit: 1022366/1000000. Split resources into multiple stacks
or set suppressTemplateIndentation to reduce template size.
ネストスタックの一括更新が大きすぎると、次も出ます。
Limit on the number of resources in a single stack operation exceeded
モデルスタックへ誤って載せた場合など、循環依存は次の形です。
The CloudFormation deployment failed due to circular dependency found between nested stacks
stackMappings とは
defineData() に追加するオプションで、AppSync Resolver の CloudFormation 論理 ID → 移行先 NestedStack 名 を指定します。@aws-amplify/backend 1.23.0 以上が前提です(機能追加は @aws-amplify/backend-data 1.7.0。リリースコミット)。
カスタム Lambda Resolver を退避する例です。論理 ID は Query<フィールド名>Resolver / Mutation<フィールド名>Resolver になります。
export const data = defineData({
schema,
authorizationModes: {
defaultAuthorizationMode: "userPool",
},
stackMappings: {
QuerysearchItemsResolver: "SharedResolvers",
MutationexportReportResolver: "SharedResolvers",
},
});
値は 任意の NestedStack 名 です。存在しない名前を指定すると、その名前の NestedStack が新規作成されます。公開型定義では Resolver のみ が対象で、DataSource や FunctionConfiguration は mapping しません。
Lambda 本体の置き場所は stackMappings では変わりません。通常は Function スタック側に残ります。resourceGroupName: 'data' で data 側へ寄せていると、後述のとおり親テンプレート容量と循環依存の両方で不利になりやすいです。
公式ドキュメントの例は、モデル自動生成の CRUD を業務単位へ振る書き方です。FunctionDirectiveStack の分散とは別用途ですが、指定の形は同じです。
stackMappings: {
CreateOrderResolver: 'OrderMutations',
UpdateOrderResolver: 'OrderMutations',
}
移行手順
全体フロー
Step 1: 対象 Resolver の特定
論理 ID は推測せず、公式どおり npx ampx sandbox または cdk synth の成果物 から取得します。CloudFormation コンソールのテンプレートでも確認できます。
カスタム Query / Mutation の論理 ID は、だいたい次の形です。
- Query:
Query<フィールド名>Resolver(例:searchItems→QuerysearchItemsResolver) - Mutation:
Mutation<フィールド名>Resolver(例:exportReport→MutationexportReportResolver)
大文字小文字を含め、成果物と完全一致させてください。同じ Lambda を複数フィールドが使う場合は、同じ移行先にまとめると運用しやすいです。
Step 2: 移行先の選定
stackMappings の値は 任意の NestedStack 名 です。公式例はドメイン / 操作種別への振り分けです。それ以外は実務で使う切り方で、どれか 1 つに決め打ちする必要はありません。
| 分け方 | 内容 | 向くケース |
|---|---|---|
| ドメイン / 操作種別 |
OrderMutations のように業務や CRUD で切る |
公式例。チームやデプロイ境界を分けたいとき |
| 退避スタック 1 本 | 溢れた Resolver を同じ先(例: SharedResolvers)へ |
まず 500 上限を回避したいとき |
| 既存モデルスタックへ相乗り |
Todo など、すでに存在するモデル用 NestedStack へ載せる |
そのモデル専用で本数が少なく、循環依存がないとき |
| 番号付き分割 |
CustomStack1, CustomStack2 … |
1 本の退避先も上限に近づいたとき |
運用上、移行先にしないスタック:
-
FunctionDirectiveStack(移行元そのもの) -
ConnectionStack/AmplifyTableManagerなど、責務が不明な Amplify 内部スタック
新規 NestedStack を Lambda ごと・API ごとに増やすと、親 data テンプレートに AWS::CloudFormation::Stack が追加され、別の容量問題になります。切り方を決めたら、同じ方針でまとめる方が安全です。
既存モデルスタックへ載せる場合は、移行先に十分な余裕があること(500 リソース / 1MB に近づいていないこと)と、後述の循環依存に当たらないことを確認してください。
Step 3: デプロイ(未デプロイ vs デプロイ済み)
未デプロイ(初回追加前)なら、stackMappings を足してからデプロイするだけです。
デプロイ済みの Resolver は、CloudFormation 上スタック間をインプレース移動できません。公開型定義どおり、一度アプリから外して別スタックで再追加します。2 段階デプロイが必要です。
デプロイ 1: Resolver 削除
- 対象 Query / Mutation をスキーマから一時削除する
- 呼び出し元が対象 API を使用しない状態を確認する
- 開発環境 / ステージング / 本番へデプロイし、Resolver 削除を完了する
デプロイ 2: mapping 付き再追加
- Query / Mutation を元の契約(フィールド名・引数・戻り値・認可)で戻す
- 同時に Resolver 論理 ID の
stackMappingsを追加する - 再デプロイする
ステージングや本番では API 一時停止 が発生します。削除デプロイ前に影響範囲を確認してください。フィールド名・引数・戻り値・認可規則は変更しません。移行はインフラ配置だけの変更です。
Step 4: 検証
npx ampx sandbox などでデプロイしたあと、.amplify/artifacts/cdk.out の NestedStack テンプレート、または CloudFormation コンソールで次を確認します。
- synth / デプロイ成功
- 対象 Resolver が 移行先テンプレートに存在し、
FunctionDirectiveStackに存在しない - DataSource / FunctionConfiguration は 元スタックに残る
- 各スタックが 500 リソース未満、各テンプレートが 1MB 未満
- API の正常系・認可拒否が従来どおり
運用上の注意点
ここからは公開型定義にない、実運用で確認した点です。
循環依存を避ける
公式の循環依存トラブルシュートが扱うのは、主に data スタック ↔ function スタック です。回避策として resourceGroupName: 'data' で Lambda を data 側へ寄せる案内がありますが、stackMappings が必要になる規模ではあまり向きません。Lambda / IAM が data 親に増え、500 リソースや 1MB の別枠を圧迫します。
加えて、data 配置の Lambda がモデルテーブルを CDK の Construct 参照(backend.data.resources.tables など)で繋いでいると、そのモデルスタックへ Resolver を mapping したときに循環依存になります。
resourceGroupName: 'data' の Lambda
--(テーブルの CFN 参照)--> モデル NestedStack
^ |
| v
FunctionDirectiveStack <--(stackMappings)-- 同モデル NestedStack
この組み合わせでは、該当モデルへは載せないか、先にテーブル参照を環境変数など CDK 非参照の方法へ切り替えてください。Resolver 用 Lambda は Function スタックのまま、テーブル名を環境変数で渡す方が、容量と循環の両方を避けやすいです。
段階的に移行する
一度にすべての Resolver を移す必要はありません。
- まず上限に近い / 追加予定の Resolver から着手する
- ドメイン単位・退避 1 本・モデル相乗りなど、プロジェクトで使う切り方を決める
- 新規 Resolver は 未デプロイのうち mapping を追加 する
ハマりどころ
| 問題 | 対処 |
|---|---|
| デプロイ済み Resolver の移動 | 2 段階デプロイ必須(API 一時停止) |
| 論理 ID の推測 | sandbox / synth 成果物から取得(大文字小文字含め完全一致) |
a.handler.custom に mapping しても効かない |
対象は a.handler.function の Resolver のみ。custom は data 親へ直接生成される |
| 循環依存 | テーブルを CDK 参照しているモデルスタックへは載せない。resourceGroupName: 'data' で Lambda を寄せると親容量も増える |
TooManyResourcesInStack / 500 超過 |
FunctionDirectiveStack の Resolver を stackMappings で分散 |
Template may not exceed 1000000 bytes |
新規 NestedStack の乱立を避ける。親 data の 1MB も確認 |
ロールバック
sandbox で試している場合は、直前に足した mapping だけを外して再デプロイすれば戻ります。本番へ 2 段階デプロイ済みの場合は、同じ手順を逆向き(mapping 付き削除 → 元の配置で再追加)になり、こちらも API 一時停止が発生します。
まとめ
カスタム Resolver が増えると、FunctionDirectiveStack が 500 リソース / 1MB 上限 に当たることがあります。当たったら(または直前になったら)stackMappings で分散します。
-
理由: カスタム Lambda Resolver(
a.handler.function)はFunctionDirectiveStackに集約され、上限に達するとデプロイが止まる -
使い方: Amplify Backend 1.23.0(backend-data 1.7.0)の
stackMappingsで、Resolver 論理 ID を別 NestedStack へ振り分ける。対象は Resolver のみ - 移行先: 任意の NestedStack 名を指定できる。公式例はドメイン / 操作種別。実務では退避 1 本やモデル相乗りも使う。スタックの乱立は避ける
- デプロイ: 未デプロイなら mapping 追加のみ。デプロイ済みは削除 → mapping 付き再追加の 2 段階
公式は、上限に遭遇していない場合の利用を非推奨としています。Resolver を継続的に追加するプロジェクトでは、上限の手前で移行計画を持っておくと、デプロイ停止を防げます。
参考リンク
- DataProps.stackMappings(Amplify Toolbox)
- Amplify GraphQL API construct の stackMappings 型コメント(非推奨条件・デプロイ済みは削除して再追加)
- Amplify Backend 1.23.0 release(backend-data 1.7.0 で stackMappings 追加)
- AWS CloudFormation quotas
- Troubleshoot circular dependency issues(Amplify Gen 2)
- amplify-backend#3066(data NestedStack の 1MB 超過)
- Amplify Gen2 の Lambda リゾルバーは何個まで定義できるのか?(FunctionDirectiveStack の上限検証)