1. Vol2の守備範囲とVol1との接続

- Vol1では、スキーマ駆動設計・JSリゾルバ・GraphQL Subscription・Merged API・認証・監視という土台を扱いました。
- 本Vol2は、その続編としてAppSync Events(pub/sub)・CDK実装・パフォーマンス最適化・オフライン同期に踏み込みます。Vol1が次回予告で示したテーマを実践レベルで掘り下げます。
1-1. Vol2で扱うテーマ
Vol2では、Vol1の基盤の上に立ち、本番環境で必要になる以下の5テーマを体系的に扱います。
① AppSync Events(本記事の最重要テーマ)
2024年10月30日にGAしたサーバーレスWebSocket pub/sub APIです。GraphQLスキーマを必要とせず、/namespace/channel パスにイベントを発行・購読するchannelベースの通信モデルを採用しています。Vol1で扱ったGraphQL Subscriptionとは別の通信モードであり、リアルタイム通知・チャット・ライブダッシュボードなどの大規模ブロードキャストユースケースに最適です。東京リージョン(ap-northeast-1)を含む全リージョンで利用可能です。
② AWS CDKによるIaC実装
aws-cdk-lib/aws-appsyncのL2コンストラクトを使い、GraphQL API・リゾルバ・データソース・認証設定をTypeScriptで宣言的に管理します。コードによるインフラ管理により、スキーマ変更・リゾルバ更新のCI/CDパイプライン化が実現します。
③ パフォーマンス最適化
ネストしたフィールド解決で発生するN+1問題と、BatchGetItemを活用したbatch resolverによる解決策を扱います。またX-Rayによるリゾルバ単位のレイテンシ可視化とボトルネック特定から改善までのワークフローも解説します。
④ オフライン同期と競合解決
Amplify DataStoreとDelta Syncの組み合わせによるオフラインファースト実装を扱います。接続断時のローカルキャッシュとAppSyncのversioned DynamoDBデータソースを同期するしくみ、および3種の競合解決戦略(AUTOMERGE・Optimistic Concurrency・Lambda)を解説します。
⑤ Private API
VPC内リソースからのみアクセスできるプライベートエンドポイント(2023年GA)の構成を扱います。インターネット非公開のGraphQL APIが必要な内部システム・マイクロサービス間通信のユースケースに対応します。
各テーマはVol1で設計した基盤(スキーマ駆動設計・JSリゾルバ・Subscription・Merged API・認証・監視)を前提に、本番スケールで現れる課題とその解法にフォーカスします。
1-2. 読者像と前提
本記事は以下の読者を対象としています。
- AWS AppSyncのGraphQL本番設計(Vol1相当)を理解しているバックエンド/フルスタックエンジニア
- AppSync Eventsによるサーバーレスpub/subをプロダクションに適用したい開発者
- CDKでAppSyncインフラをコード管理したい開発者
- オフライン対応(DataStore・Delta Sync)を実装したいモバイル/Webアプリ開発者
前提とする知識の目安:
| 知識領域 | 必要度 | 参照先 |
|---|---|---|
| GraphQLスキーマ設計(クエリ/ミューテーション) | 必須 | Vol1 §1 |
| AppSync JSリゾルバ(Functions・Pipeline) | 必須 | Vol1 §2〜§3 |
| DynamoDB・Lambdaデータソース連携 | 必須 | Vol1 §4 |
| GraphQL Subscription(WebSocket購読) | 推奨 | Vol1 §5 |
| AWS CDK基本操作(TypeScript) | §3のみ | — |
Vol1未読の方は、まず下記Vol1から読み始めることをお勧めします。AppSync Eventsセクション(§2)はVol1の知識なしでも読めますが、CDK(§3)・パフォーマンス(§4)・オフライン同期(§5)はVol1の基盤知識を前提とします。
▶ Vol1: スキーマ駆動設計・JSリゾルバ・Subscription・Merged API編を読む
2. AppSync Events — サーバーレスWebSocket pub/sub

2-1. AppSync EventsとGraphQL Subscriptionの違い(重要な前提)
AppSync Eventsは「GraphQL Subscriptionの進化版」ではありません。同じAppSyncサービス上で動作しますが、APIの種類・スキーマ要件・イベントトリガが根本的に異なります。混同したまま設計すると、ユースケースの誤選択につながります。
AppSync Eventsとは
AppSync Eventsは2024年10月30日にGAした、GraphQLスキーマを必要としないサーバーレスWebSocket pub/sub APIです。
- API種別: Events API(GraphQL APIとは別種のAPIタイプ)
- スキーマ: 不要。
/namespace/channelパスに直接イベントを発行・購読する - プロトコル: WebSocket(購読)+ HTTP(発行)
- 認証: API Key・Amazon Cognito User Pools・AWS IAM・Lambda authorizerに対応
- リージョン: 全リージョンで利用可能(東京: ap-northeast-1 を含む)
GraphQL Subscriptionとは(Vol1の振り返り)
Vol1で扱ったGraphQL Subscriptionは、GraphQLスキーマ定義を前提とした、Mutationトリガのリアルタイム購読です。
- API種別: GraphQL API(スキーマ定義が必須)
- トリガ: 特定のMutationが実行されたときに発火
- スキーマ:
type Subscription { onCreatePost: Post @aws_subscribe(mutations: ["createPost"]) }のようにスキーマで明示的に定義
2つの通信モードの比較
| 比較項目 | AppSync Events | GraphQL Subscription |
|---|---|---|
| GraphQLスキーマ | 不要 | 必要 |
| イベントトリガ | publisherがchannelに発行 | Mutationの実行 |
| チャネル指定 | /namespace/channel パス | スキーマで定義 |
| 主なユースケース | 大規模ブロードキャスト・通知・チャット | データ変更の購読 |
| EventBridge統合 | 対応 | 非対応 |
| 型バリデーション | なし(任意JSONペイロード) | GraphQLスキーマによる型検証あり |
AppSync Eventsは「スキーマ不要の軽量pub/sub」、GraphQL Subscriptionは「スキーマ駆動のデータ変更購読」と理解すると、ユースケースの使い分けが明確になります。
2-2. channelとnamespaceの設計
AppSync Eventsのchannelは /<namespace>/<channel>/<subpath> の形式で階層的に管理されます。
Namespaceとは
Namespaceはchannelの論理グループです。認証・認可ルールとイベントハンドラ(onPublish / onSubscribe)をNamespace単位で定義します。1つのEvents APIに複数のNamespaceを作成でき、各Namespaceで独立した認証設定を持てます。
| Namespace設定 | 内容 |
|---|---|
| 認証モード | publisherとsubscriberで個別に設定可能(API Key・Cognito・IAM・Lambda) |
| onPublishハンドラ | イベント発行時に呼ばれるLambda関数 |
| onSubscribeハンドラ | クライアント購読時に呼ばれるLambda関数 |
Channelとは
ChannelはNamespace内の個別のpub/subパスです。スラッシュ区切りで任意の深さにネストできます。
channelパスの設計例:
| パス | 用途 |
|---|---|
/default/notifications/user-123 | ユーザー個別通知 |
/chat/room/general | チャットルーム |
/dashboard/metrics | ライブダッシュボード |
/orders/updates/order-456 | 注文ステータス更新 |
ワイルドカード購読
subscriberは * ワイルドカードを使い、namespace内の複数channelを一括購読できます。
/chat/room/*→ 全チャットルームを購読/default/*→ defaultネームスペース全体を購読
onPublish / onSubscribeハンドラ
| ハンドラ | タイミング | 主な用途 |
|---|---|---|
| onPublish | publisherがイベントを発行したとき | イベントのフィルタ・変換・認可チェック |
| onSubscribe | subscriberがchannelに接続したとき | 購読権限の検証・接続制御 |
onPublishハンドラでイベントを変換・フィルタすることで、publisherの生データをそのままではなくsubscriberに必要な情報だけを届ける設計が可能です。onSubscribeハンドラではユーザーの権限に応じて購読を許可・拒否するカスタムロジックを実装できます。図2はこのpublisher→namespace→channel→subscriberのフローを示しています。
2-3. ユースケースと使い分け
AppSync EventsとGraphQL Subscriptionは、アーキテクチャ上の役割と適するユースケースが異なります。以下の判断軸で使い分けを決定します。
AppSync Eventsが適するユースケース
| ユースケース | 理由 |
|---|---|
| 大規模ブロードキャスト通知 | スキーマ定義不要で多数クライアントへのイベント配信が容易 |
| チャット・メッセージング | channel/namespaceで自然にルーム分割を表現できる |
| ライブダッシュボード | メトリクスやステータスを高頻度で配信する軽量pub/sub |
| マルチテナント通知 | namespace単位の認証設定でテナント分離が容易 |
| EventBridge連携 | EventBridgeルールのターゲットとしてAppSync Eventsチャネルを指定可能 |
GraphQL Subscriptionが適するユースケース
| ユースケース | 理由 |
|---|---|
| データモデル変更の購読 | Mutationと密結合したリアルタイム更新 |
| 型安全な購読 | GraphQLスキーマによる型定義でクライアントコードの安全性を確保 |
| 既存GraphQL APIへのリアルタイム追加 | 既存のスキーマ・リゾルバ設計を流用してSubscriptionを追加 |
判断フロー
以下のフローでEventsかSubscriptionかを選択します。
- GraphQLスキーマが既に存在するか?
- Yes → GraphQL Subscriptionを検討(既存スキーマとの統一性が高い)
No → AppSync Eventsが有力候補(スキーマ設計コスト不要)
イベントはMutationにトリガされるか?
- Yes(例: データ更新後に購読者へ通知) → GraphQL Subscription
No(例: publisherが任意のタイミングで配信) → AppSync Events
大規模ブロードキャストか?
- Yes(1対多・多対多の配信) → AppSync Events
- No(特定クライアントのデータ変更通知) → GraphQL Subscription
EventBridge統合
AppSync EventsはAmazon EventBridgeと統合されており、EventBridgeルールのターゲットとしてAppSync Eventsチャネルを指定できます。これにより、S3イベント・DynamoDB Streams・CloudWatchアラームなどのAWSサービスイベントをEventBridge経由でフロントエンドへリアルタイム配信するアーキテクチャが構築できます。GraphQL Subscriptionではこの統合は提供されていないため、AWSイベント駆動のpub/subにはAppSync Eventsが適しています。
3. AWS CDKによるIaC実装

aws-cdk-lib/aws-appsync モジュールのL2コンストラクトを使うと、GraphQL APIのスキーマ・データソース・リゾルバをTypeScriptで宣言的に管理し、CDK Pipelineと組み合わせてCI/CDを自動化できます。
3-1. aws-cdk-lib/aws-appsync L2コンストラクト
GraphqlApi — APIの起点
GraphqlApi がAPIの起点です。definition: appsync.Definition.fromFile(path) でプロジェクト内の .graphql ファイルを読み込み、スキーマとインフラ定義を分離してGit管理します。
import * as appsync from 'aws-cdk-lib/aws-appsync';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
export class AppSyncStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const api = new appsync.GraphqlApi(this, 'Api', {
name: 'my-appsync-api',
definition: appsync.Definition.fromFile('graphql/schema.graphql'),
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.API_KEY,
},
},
xrayEnabled: true,
});
}
}
definition プロパティ(CDK v2.130以降推奨)は内部で SchemaFile.fromAsset に相当します。旧APIの schema: appsync.SchemaFile.fromAsset(path) も引き続き動作しますが、新規実装では Definition.fromFile を使います。
DynamoDB・Lambdaデータソースの追加
addDynamoDbDataSource / addLambdaDataSource でデータソースを追加します。返り値はデータソースオブジェクトで、リゾルバ定義に再利用します。IAMロールのアタッチはCDKが自動処理します。
const itemTable = new dynamodb.Table(this, 'ItemTable', {
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
});
// DynamoDBデータソース
const tableDs = api.addDynamoDbDataSource('ItemTableDs', itemTable);
// Lambdaデータソース
const resolverFn = new lambda.Function(this, 'ResolverFn', { /* ... */ });
const lambdaDs = api.addLambdaDataSource('LambdaDs', resolverFn);
APPSYNC_JSリゾルバのコード管理
AppSync JavaScript(APPSYNC_JS)ランタイムのリゾルバは Code.fromAsset でファイルから読み込みます。リゾルバロジックをソースファイルとして管理し、テストやlintを適用できます。
tableDs.createResolver('QueryGetItemResolver', {
typeName: 'Query',
fieldName: 'getItem',
code: appsync.Code.fromAsset('resolvers/Query.getItem.js'),
runtime: appsync.FunctionRuntime.JS_1_0_0,
});
3-2. リゾルバ・認証・データソースのコード管理
パイプラインリゾルバ
複数のAppSync Functionを順次実行するパイプラインリゾルバは AppsyncFunction と Resolver の pipelineConfig プロパティで定義します。
// AppSync Functionを個別に定義
const validateFn = new appsync.AppsyncFunction(this, 'ValidateFn', {
name: 'validateInput',
api,
dataSource: tableDs,
code: appsync.Code.fromAsset('resolvers/validateInput.js'),
runtime: appsync.FunctionRuntime.JS_1_0_0,
});
const saveItemFn = new appsync.AppsyncFunction(this, 'SaveItemFn', {
name: 'saveItem',
api,
dataSource: tableDs,
code: appsync.Code.fromAsset('resolvers/saveItem.js'),
runtime: appsync.FunctionRuntime.JS_1_0_0,
});
// パイプラインリゾルバ — pipelineConfigに実行順でFunctionを並べる
new appsync.Resolver(this, 'MutationCreateItemResolver', {
api,
typeName: 'Mutation',
fieldName: 'createItem',
code: appsync.Code.fromAsset('resolvers/pipeline.js'),
runtime: appsync.FunctionRuntime.JS_1_0_0,
pipelineConfig: [validateFn, saveItemFn],
});
複数認証モードの宣言
authorizationConfig.additionalAuthorizationModes で複数認証を宣言します。スキーマ側の @aws_auth ディレクティブと組み合わせ、フィールドレベルの認証制御をコードで管理します。
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.USER_POOL,
userPoolConfig: { userPool },
},
additionalAuthorizationModes: [
{ authorizationType: appsync.AuthorizationType.IAM },
{
authorizationType: appsync.AuthorizationType.LAMBDA,
lambdaAuthorizerConfig: { handler: authFn },
},
],
},
CDK Pipelineとの統合
aws-cdk-lib/pipelines の CodePipeline でAppSyncスタックのCI/CDを構築します。スキーマ変更・リゾルバ変更もコミットをトリガに自動デプロイできます。
import { pipelines } from 'aws-cdk-lib';
const pipeline = new pipelines.CodePipeline(this, 'Pipeline', {
synth: new pipelines.ShellStep('Synth', {
input: pipelines.CodePipelineSource.gitHub('org/repo', 'main'),
commands: [
'npm ci',
'npm run build',
'npx cdk synth',
],
}),
});
マルチ環境スタック分離
Stage クラスで環境ごとのスタックセットを定義し、pipeline.addStage で dev/stg/prod を順次追加します。環境固有のパラメータ(テーブル名・エンドポイント等)はPropsインターフェース経由で注入し、スタックコード自体は共通化します。
interface AppSyncStageProps extends StageProps {
readonly envName: 'dev' | 'stg' | 'prod';
}
class AppSyncStage extends Stage {
constructor(scope: Construct, id: string, props: AppSyncStageProps) {
super(scope, id, props);
new AppSyncStack(this, 'AppSync', {
envName: props.envName,
env: props.env,
});
}
}
// 各環境をPipelineに追加
pipeline.addStage(new AppSyncStage(this, 'Dev', {
envName: 'dev',
env: { account: DEV_ACCOUNT, region: 'ap-northeast-1' },
}));
pipeline.addStage(new AppSyncStage(this, 'Stg', {
envName: 'stg',
env: { account: STG_ACCOUNT, region: 'ap-northeast-1' },
}));
pipeline.addStage(new AppSyncStage(this, 'Prod', {
envName: 'prod',
env: { account: PROD_ACCOUNT, region: 'ap-northeast-1' },
}));
CDK PipelineにAppSyncスタックを組み込む際は、GraphQLスキーマの後方互換性を維持します。フィールド削除・引数変更はクライアント側で即時エラーになるため、additive変更(フィールド追加・typeの非破壊拡張)を徹底してください。破壊的変更が必要な場合は、新フィールドを追加→クライアント移行→旧フィールド削除の手順を踏みます。
3-3. AppSync EventsのIaC(最新状況)
AppSync Events(2024年10月30日GA)向けのCDK L2コンストラクトは、執筆時点で追加が進行中です。「L2が完備されている」と断言できる状態ではありません。実装前に aws-cdk-lib/aws-appsync のリリースノートとGitHubで最新状況を確認してください。
AppSync Events APIはGraphQL APIとは別のAPIタイプです。CDK L2が未整備の部分については、L1コンストラクト(CfnXxx)またはエスケープハッチ(node.defaultChild as CfnXxx)で補完します。
L1コンストラクトでの補完例
import * as appsync from 'aws-cdk-lib/aws-appsync';
// L1コンストラクトでEvents API関連リソースを定義
// プロパティはCDKバージョン・CloudFormationリソース仕様に依存します
// 実装前に最新のCfnリソース定義を確認してください
const cfnApi = new appsync.CfnGraphqlApi(this, 'EventsApi', {
name: 'my-events-api',
authenticationType: 'API_KEY',
});
// L2コンストラクトのエスケープハッチ
// L2が対応済みのリソースをCfnレベルで拡張する場合
const cfnChild = api.node.defaultChild as appsync.CfnGraphqlApi;
cfnChild.addPropertyOverride('SomeProperty', value);
L2整備後の移行方針
L2が追加された段階で、L1コード・エスケープハッチ部分をL2に置き換えます。CDK Pipelineとの統合は変わらず利用できるため、スタック構造を維持したままリゾルバ・データソースのみ書き換えます。
- CDK GitHub:
packages/aws-cdk-lib/aws-appsyncディレクトリのPR・CHANGELOGでL2追加を追跡 - 公式ドキュメント:
aws-cdk-lib.aws_appsync moduleの「Events API」セクション npm info aws-cdk-libで最新バージョン確認後、リリースノートを参照
4. パフォーマンス最適化

- N+1問題: batch resolver(BatchGetItem)またはDataLoaderで集約解決
- キャッシュ: per-resolver / full-request TTL戦略で読み取り負荷を削減
- コールドスタート: Provisioned Concurrency / SnapStart / JSリゾルバ移行
- DynamoDB: GSI設計とKeyConditionExpressionによるアクセスパターン最適化
- 計測・改善: X-Rayトレースでリゾルバ単位のボトルネックを特定
- 保護: クエリ深度制限・スロットリング・負荷テストで本番品質を確保
4-1. N+1問題とbatch resolver
GraphQLの自由なネスト構造は、データソース呼び出しの爆発(N+1問題)を引き起こしやすい。
# スキーマ例
type Query {
posts: [Post]
}
type Post {
id: ID!
author: User# このフィールド解決がN+1の温床
}
postsクエリでN件の投稿を返すと、各Postのauthorフィールドを解決するためにリゾルバがN回個別呼び出しされる。合計N+1回のDynamoDB呼び出しが発生する。
Unit resolverはN+1を防げない
Unit resolver(1フィールド=1リゾルバ)は独立して実行されるため、前後のフィールド解決を集約する仕組みがない。N件のPostがあればauthorリゾルバはN回独立に呼ばれる。
unit vs pipeline の選択基準
| 観点 | Unit resolver | Pipeline resolver |
|---|---|---|
| 単一データソース・単純処理 | 推奨 | 過剰 |
| 複数データソース横断 | 不可 | 必須 |
| N+1防止バッチ集約 | 不可 | Function間で集約可 |
| ロジック再利用 | 不可 | Function共有可 |
N+1が懸念される1対多リレーションにはpipeline resolver + batch resolverを基本とする。
DynamoDBのbatch resolverの実装例
// JSリゾルバ(request)
export function request(ctx) {
return {
operation: 'BatchGetItem',
tables: {
UsersTable: {
keys: ctx.source.map(post => ({
PK: { S: `USER#${post.authorId}` },
})),
},
},
};
}
// response
export function response(ctx) {
return ctx.result.data.UsersTable;
}
BatchGetItemは1リクエストで最大100件のキーを一括取得する。authorフィールドN件の解決がDynamoDBへの1回の呼び出しに集約される。
Lambda batch invokeによる集約
LambdaデータソースにBatch enableを設定すると、AppSyncが同一クエリ内の複数フィールド解決をひとつのLambda呼び出しにまとめ、イベントペイロードにsourceの配列を渡す。Lambdaハンドラ内でPromise.allやBatchGetを使い一括処理する。
4-2. DataLoaderパターンとキャッシュ最適化
DataLoaderパターン
Lambda統合でNode.jsを使う場合、dataloaderライブラリでリクエストスコープのバッチ集約とメモイゼーションを実装できる。
import DataLoader from 'dataloader';
const userLoader = new DataLoader<string, User>(async (userIds) => {
const users = await batchGetUsers(userIds as string[]);
return userIds.map(id => users.find(u => u.id === id) ?? null);
});
// Lambdaハンドラ内のリゾルバ
export const resolvers = {
Post: {
author: (post: Post) => userLoader.load(post.authorId),
},
};
dataloaderは同一リクエスト内で同じキーがload()された場合はキャッシュを返すため、N+1に加えて重複呼び出しも防ぐ。DataLoaderインスタンスはリクエストごとに生成してスコープを限定する。
AppSyncキャッシュの2つのレベル
AppSyncのAPIキャッシュ(Elasticache Serverless使用)にはfull-requestとper-resolverの2段階がある。
| キャッシュ種別 | 動作 | 適用場面 |
|---|---|---|
| full-request | クエリ全体の結果をキャッシュ。クエリ文字列・変数・認証情報の組み合わせをキーとする | 参照系クエリが大半を占め、全結果をまとめてキャッシュできる場合 |
| per-resolver | リゾルバ単位でキャッシュ。キャッシュキーを個別に設定可能 | 認証ユーザーごとに異なるデータを返す部分フィールドに適用する場合 |
TTLは1〜3600秒の範囲で設定する。Mutationが実行されたら該当キャッシュをevictFromApiCacheミューテーションで明示的に削除するか、TTLを短縮して鮮度を保つ。
per-resolverキャッシュキーの設計
per-resolverでは$context.arguments・$context.identity.sub・任意のリクエストヘッダをキャッシュキーに含めることができる。プライベートデータを返すリゾルバでは$context.identityの識別子を必ずキーに含め、他ユーザーのデータが流用されないよう設計する。MutationリゾルバとSubscriptionにはキャッシュを適用しない。
4-3. Lambda統合コールドスタート対策
Lambda統合リゾルバはコールドスタートのレイテンシが本番品質に影響する。対策は3段階で考える。
Provisioned Concurrency
常時ウォームな実行環境を指定数確保しコールドスタートを排除する。アクセスが集中する時間帯に合わせてAuto Scalingと組み合わせる。
// CDKでの設定例
const fn = new lambda.Function(stack, 'AuthorResolver', { ... });
const alias = fn.addAlias('live');
alias.addAutoScaling({ minCapacity: 2, maxCapacity: 10 })
.scaleOnUtilization({ utilizationTarget: 0.7 });
new lambda.CfnProvisionedConcurrencyConfig(stack, 'PC', {
functionName: fn.functionName,
qualifier: alias.aliasName,
provisionedConcurrentExecutions: 2,
});
未使用時間にもコストが発生するため、アクセスパターンと費用対効果を確認して適用する。
Lambda SnapStart(Java 11/17/21)
Javaランタイムのみ利用可能。デプロイ時にスナップショットを作成し、コールドスタート時はそこから実行環境を復元することで初期化時間をほぼ排除する。TypeScript/Pythonは元々起動が軽量なため効果の差は小さい。
JSリゾルバへの移行(コールドスタートの根本排除)
AppSync JS(JavaScript/TypeScriptランタイム)を使えばLambdaが不要になり、コールドスタート問題が根本的になくなる。単純なDynamoDB操作やデータ変換はJSリゾルバへの移行を先に検討する。Lambda固有のロジック(外部API呼び出し・複雑なビジネスロジック)にのみLambdaを残すのが推奨パターンである。
4-4. DynamoDBアクセスパターン最適化
GSI設計とアクセスパターンの網羅
AppSync→DynamoDB直結のリゾルバはGraphQLのクエリ構造とDynamoDBのアクセスパターンを1対1で対応させることが重要。事前にすべてのQueryパターンをリストアップし、必要なGSI(Global Secondary Index)を設計する。
Scan操作はコスト・レイテンシ両面で非推奨。KeyConditionExpressionを使ったQuery操作に徹するFilterExpressionは読み取り済みItemに対して適用されるためRCU節約にならない。不要なItemはKeyConditionExpressionで取得前に絞り込む
並列クエリの実装
Pipeline resolverのFunction間は直列チェーンのため真の並列実行にはならない。独立した複数DynamoDBクエリを並列化するにはLambda内でPromise.allを使う方が柔軟で確実である。
// Lambda内での並列クエリ
const [posts, tags] = await Promise.all([
ddb.query({ TableName: 'PostsTable', KeyConditionExpression: 'PK = :pk', ... }).promise(),
ddb.query({ TableName: 'TagsTable', KeyConditionExpression: 'PK = :pk', ... }).promise(),
]);
DynamoDB Accelerator(DAX)の検討
マイクロ秒レベルのレイテンシが必要な読み取り集中型ワークロードではDAXを検討する。AppSyncのAPIキャッシュとDAXは独立しており、両方適用して二重のキャッシュ層を構成することもできる。
4-5. X-Ray分析に基づくリゾルバ改善
AppSync + X-Rayの有効化
AppSyncコンソールの「設定」→「X-Rayトレース」をONにするか、CDKでAPIにxrayEnabled: trueを設定する。
const api = new appsync.GraphqlApi(stack, 'Api', {
name: 'MyApi',
xrayEnabled: true,
// ...
});
トレースで読み取れる情報
X-Rayのトレースには以下のセグメントが記録される。
GraphQL— クエリ全体の処理時間Resolve <FieldName>— フィールド単位のリゾルバ実行時間DynamoDB GetItem / BatchGetItem / Query— DynamoDB呼び出し単位のレイテンシとRCULambda— Lambda呼び出し時間(コールドスタートの有無も確認可能)
N+1問題の検出
同一フィールドのResolveセグメントが繰り返し多数表示される場合はN+1が発生している証拠である。X-Ray Analyticsでリゾルバ名をフィルタしてヒット数を確認し、batch resolverへの移行を検討する。
改善サイクル
- X-Rayでリゾルバ単位のp50/p99レイテンシを計測する
- 高レイテンシリゾルバを特定する
- N+1パターン → batch resolver または DataLoader を適用する
- コールドスタート → Provisioned Concurrency またはJSリゾルバ移行を実施する
- 繰り返しアクセスするデータ → per-resolver cacheのTTLを設定する
- 改修後に再計測し効果を定量確認する
4-6. クエリ深度制限・サーキットブレーカー・負荷テスト
クエリ深度制限(Max Query Depth)
悪意あるクライアントや設計ミスによる深くネストしたクエリはデータソースへの過剰なリクエストを招く。AppSyncのAPI設定で最大クエリ深度(queryDepthLimit)と解決するリゾルバの上限(resolverCountLimit)を設定する。
// CDKでの設定(L1 CfnGraphQLApi)
const cfnApi = api.node.defaultChild as appsync.CfnGraphQLApi;
cfnApi.queryDepthLimit = 7;
cfnApi.resolverCountLimit = 10000;
推奨値はスキーマの最大ネスト深さ+2程度に設定する。深すぎると正当なクエリを拒否し、浅すぎるとN+1攻撃を防げない。
スロットリングとサービスクォータ
AppSyncのデフォルトスロットリング上限はAWSサービスクォータで確認する。高トラフィックが予想される場合は事前に上限緩和を申請する。Lambda統合では下流のLambdaの同時実行数上限も合わせて確認する。
負荷テストパターン
# Artilleryによる負荷テスト(HTTPクエリ)
# artillery.yml
config:
target: 'https://xxxxxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql'
phases:
- duration: 60
arrivalRate: 100
scenarios:
- name: GetPosts
flow:
- post:
url: /graphql
headers:
x-api-key: '{{ $env.APPSYNC_API_KEY }}'
json:
query: 'query { posts { id title } }'
WebSocket(Subscription / Events)の同時接続数テストはk6のWebSocket拡張を使う。接続数が増えるとAppSync側のスロットリングが先に発動するか、Lambda同時実行数が限界に達するかを確認する。
- N+1問題: Pipeline resolver + batch resolver(BatchGetItem 最大100件)またはLambda DataLoaderで集約解決
- キャッシュ: full-requestはクエリ全体、per-resolverはフィールド単位。Mutationはevict or TTL短縮で整合性維持
- コールドスタート: Provisioned Concurrency(常時ウォーム)/ SnapStart(Java)/ JSリゾルバ移行(根本排除)
- DynamoDB: Scan禁止・KeyConditionExpression + GSI設計・並列クエリはLambda内 Promise.all
- X-Ray: リゾルバ単位のp50/p99で計測 → N+1/コールドスタート/キャッシュ候補を特定 → 改善 → 再計測
- 保護: queryDepthLimit設定・スロットリング確認・負荷テストで本番品質を保証
5. オフライン同期と競合解決

5-1. Amplify DataStoreとDelta Sync(重要な前提)
Amplify DataStoreはオフラインファーストのローカルストレージAPIで、デバイス上のSQLite(React Native/iOS/Android)またはIndexedDB(Web)にデータを保存し、バックグラウンドでAppSyncと自動同期します。同期にはDelta Syncプロトコルを使用します。
Delta Syncの前提:versioned DynamoDB
Delta SyncはAppSyncのデータソースとしてバージョニングされたDynamoDB(versioned DynamoDB)が必須です。テーブルには_version(変更バージョン番号)、_lastChangedAt(最終変更タイムスタンプ)、_deleted(論理削除フラグ)の各属性が必要で、これらがDataStoreの同期制御に使われます。Amplify CLIまたはAmplify Studioでスキーマを定義すると、これらの属性とテーブル設定が自動生成されます。
3テーブル構成
Delta Syncは以下3種のテーブルで差分同期を実現します。
| テーブル | 用途 | 備考 |
|---|---|---|
| Base table | 全データのスナップショット保持 | 初回同期・base query期限超過時に参照 |
| Delta table | base query期間内の変更差分を記録 | TTL付き。差分同期の主なソース |
| Sync table | サブスクリプション再接続時の同期制御 | 切断中の差分補完に使用 |
Base queryの間隔
DataStoreは最後のBase query実行時刻を追跡し、次回同期で経過時間を判定します。Base queryの実行間隔は既定で24時間(分単位で設定変更可能)です。間隔内であればDelta tableから差分のみを取得する差分同期を行い、期限を超えるとBase tableへのBase queryを実行してフルスキャンし直します。
Delta Syncの動作フロー
- 初回同期(Base query): Base tableからすべてのデータを取得しローカルDBに保存する
- 差分同期(Delta query): 前回同期以降にDelta tableに記録された変更のみを取得・適用する
- 削除検知(tombstone): 削除されたレコードは物理削除されず
_deleted: trueのtombstone(削除マーカー)としてDelta tableに保持され、DataStoreがローカルからも該当データを削除する
DataStoreの基本API
import { DataStore } from 'aws-amplify/datastore';
import { Post } from './models';
// クエリ(オフライン時はローカルDBから返却)
const posts = await DataStore.query(Post);
// 作成(オフライン中はキューに保存→接続復帰時に自動送信)
await DataStore.save(new Post({ title: 'AppSync記事', content: '本文...' }));
// 更新
await DataStore.save(Post.copyOf(post, updated => {
updated.title = '更新タイトル';
}));
// 削除(tombstoneとして同期)
await DataStore.delete(post);
// 同期開始
await DataStore.start();
オフライン中の書き込みはデバイスのアウトボックス(送信キュー)に保持され、接続復帰時にAppSyncへ自動送信・再試行されます。DataStore.start()はアプリ起動時に呼び出すと、初回同期とサブスクリプション確立が開始されます。
5-2. 3つの競合解決戦略(重要な前提)
AppSync + DataStoreの競合は「同じデータを複数クライアントが同時に更新した場合」または「オフライン中のローカル変更とサーバー変更が接続復帰時に衝突した場合」に発生します。解決戦略は以下3種から選択します。
① AUTOMERGE(既定)
AppSync・DataStoreのデフォルト戦略です。GraphQLスキーマのデータ型に応じて競合を自動解決します。
| データ型 | 解決ルール |
|---|---|
| List | 競合した双方のリストをマージ(重複を含む形で結合) |
| Set | 競合した双方のSetをunion(重複排除で結合) |
| スカラー(String、Int等) | サーバー側の既存値を優先(ローカル変更を棄却) |
スキーマ側での追加設定は不要で、Amplify CLIでconflict_detection: VERSIONとconflict_handler: AUTOMERGEを指定するだけで有効になります。コンテンツ管理・メモ帳・タスクリストなど、リスト・Set操作が中心のユースケースに最適です。
type Post @model
@auth(rules: [{allow: owner}]) {
id: ID!
title: String!
tags: [String]# List → 競合時はマージ
}
② Optimistic Concurrency
楽観的同時実行制御に基づく戦略です。Mutationリクエストには_versionフィールドを含め、AppSyncがサーバー現行バージョンと照合します。バージョンが一致しない(他クライアントが先に更新済み)場合、Mutationは拒否され、サーバーの最新アイテムがレスポンスとして返されます。クライアントは最新データを受け取り、競合を認識した上で自身のロジックで再試行します。
mutation UpdatePost($id: ID!, $title: String!, $expectedVersion: Int!) {
updatePost(input: {
id: $id
title: $title
_version: $expectedVersion # バージョン不一致→拒否+最新item返却
}) {
id
title
_version
_lastChangedAt
}
}
Eコマースの在庫更新・金融取引・予約システムなど、「後から来た更新を自動マージせず競合を検知してクライアントに委ねたい」ユースケースに適します。
③ Lambda
Lambda関数にカスタム競合解決ロジックを実装する戦略です。AppSyncは競合発生時にLambda関数を呼び出し、関数はconflictItem(送信されたデータ)とexistingItem(サーバー現行データ)の両方を受け取ります。関数は解決したアイテムを返すか、nullを返して競合を拒否します。
def handler(event, context):
conflict_item = event['arguments']['input']
existing_item = event['source']
# カスタムロジック例:priorityフィールドは大きい方を採用
resolved = existing_item.copy()
resolved['priority'] = max(
int(conflict_item.get('priority', 0)),
int(existing_item.get('priority', 0))
)
# _versionは既存値を引き継ぐ
resolved['_version'] = existing_item['_version']
return resolved
AUTOMERGE/Optimistic Concurrencyでは対応できない複雑なビジネスルール(フィールド単位の優先度制御・マルチテナント分離・外部システム整合性確認など)が必要な場合に選択します。Lambda呼び出しコストが発生するため、適用は必要最小限のデータソースに絞ります。
戦略の選定基準
| 状況 | 推奨戦略 |
|---|---|
| リスト・Set中心のコンテンツ、マージ可能 | AUTOMERGE(既定) |
| 整合性重視・競合時にユーザーへ通知し再試行させたい | Optimistic Concurrency |
| フィールド単位の優先度制御など複雑なルールが必要 | Lambda |
AUTOMERGE → Optimistic Concurrency → Lambda の順でコストと実装複雑度が上がります。まずAUTOMERGEで開始し、実際の競合パターンを観測してから移行を検討するのが実践的なアプローチです。
5-3. オフライン対応設計の実践
接続復帰時の同期シーケンス
DataStoreはネットワーク接続復帰を検知すると、以下のシーケンスで自動同期を行います。
- アウトボックス送信: オフライン中にローカルで発生した書き込み(Create/Update/Delete)をAppSyncへ順次送信する。競合が発生した場合は設定済みの競合解決戦略が呼び出される
- Delta同期実行: 切断中にサーバー側で発生した変更をDelta tableから取得し、ローカルDBへ適用する
- サブスクリプション再開: GraphQL Subscriptionを再確立し、リアルタイム更新を再開する
DataStoreのHub eventsをリッスンすることで、同期状態をアプリのUIに反映できます。
import { Hub } from 'aws-amplify';
Hub.listen('datastore', ({ payload: { event, data } }) => {
switch (event) {
case 'networkStatus':
console.log('ネットワーク:', data.active ? 'オンライン' : 'オフライン');
break;
case 'outboxStatus':
console.log(
'アウトボックス:',
data.isEmpty ? '送信待ちなし' : '送信待ちあり'
);
break;
case 'outboxMutationProcessed':
console.log('Mutation送信完了:', data.element.model);
break;
case 'syncQueriesStarted':
console.log('同期クエリ開始:', data.models);
break;
case 'ready':
console.log('DataStore同期完了');
break;
}
});
競合戦略の選び方
- データがリスト・Set中心でマージ可能 → AUTOMERGE(既定のまま)。追加設定不要で即時適用
- 整合性重視・競合時にユーザーへ通知し再試行させたい → Optimistic Concurrency。在庫・予約・金融系に適合
- フィールド単位の優先度制御など複雑なルールが必要 → Lambda。Lambdaコスト発生のため必要最小限の適用に絞る
UX設計の考慮点
- 楽観的UI(Optimistic UI): オフライン書き込みはローカルDBへ即反映し、同期の成否(成功・競合)を非同期で通知することでユーザー体験を向上させます。Hub eventsの
outboxMutationProcessedを使い、送信完了をUIに反映します - 競合通知UI: Optimistic Concurrency採用時はバージョン競合をUIでユーザーに提示し、「サーバーの最新値を使う」「自分の変更を使う」の選択肢を提供します。
DataStore.query()で最新データを再取得し、ユーザーに比較表示するパターンが一般的です - 同期インジケーター: DataStoreのHub events(
outboxStatus/ready/networkStatus)を使い、「同期中」「オフライン」「同期済み」のステータスをアイコンやバナーでUI上に可視化します - 書き込みの冪等性: アウトボックスの再送時に同じMutationが複数回送信される可能性があります。リゾルバ側でバージョンチェックを行い、古いバージョンの再送が二重書き込みにならないよう設計します。Delta Syncのバージョニング機構がこの冪等性を自動的に担保します
- オフライン期間の上限設計: Delta tableのTTLを超えるオフライン期間(既定24時間以上の非同期)が発生すると、次回接続時にBase queryが実行されます。大規模テーブルでは初回Base queryのダウンロードコストが大きくなるため、ページネーションや同期対象モデルの絞り込みを検討します
6. 運用監視とアラート設計
6-1. AppSyncフィールドレベルログの設定
AppSyncのログ機能はフィールドレベルログを有効化することで、リゾルバ単位の実行詳細をCloudWatch Logsへ出力します。ログレベルはNONE / ERROR / ALLの3段階で、本番ではERRORを基本とし、デバッグ時のみALLに切り替えるのが標準的な運用です。
CDKでの設定例:
import * as appsync from 'aws-cdk-lib/aws-appsync';
import * as logs from 'aws-cdk-lib/aws-logs';
const api = new appsync.GraphqlApi(this, 'AppSyncApi', {
name: 'production-api',
schema: appsync.SchemaFile.fromAsset('schema.graphql'),
logConfig: {
fieldLogLevel: appsync.FieldLogLevel.ERROR, // 本番: ERROR
excludeVerboseContent: false, // requestMappingTemplate/responseMappingTemplateを含める
retention: logs.RetentionDays.ONE_MONTH,
},
xrayEnabled: true,
});
excludeVerboseContent: falseにするとrequestMappingTemplate・responseMappingTemplateの実行内容がログに含まれ、リゾルバのデバッグが容易になります。FieldLogLevel.ALLは全リクエストの詳細を記録するためコストが高くなります。デバッグ完了後はERRORに戻すことを徹底してください。
6-2. CloudWatch Logs Insightsによるリゾルバ分析
フィールドレベルログが有効なAPIでは、Logs Insightsを使ってリゾルバ別のエラー率・レイテンシを計測できます。
リゾルバ別エラー率(4xx/5xx合算):
fields @timestamp, @message
| filter @logStream like /RequestSummary/
| filter statusCode >= 400
| stats count() as errorCount by resolverArn
| sort errorCount desc
| limit 20
P99レイテンシの時系列推移:
fields @timestamp, duration
| filter @logStream like /RequestSummary/
| stats
pct(duration, 50) as p50_ms,
pct(duration, 95) as p95_ms,
pct(duration, 99) as p99_ms
by bin(5min)
未解決フィールドの検出(パイプラインリゾルバのデバッグ):
fields @timestamp, @message
| filter @logStream like /Tracing/
| filter @message like /Unresolved/
| stats count() by fieldName
CloudWatch Logsのロググループは/aws/appsync/apis/{apiId}に自動作成されます。Logs Insightsウィジェットをダッシュボードに埋め込むことで、リゾルバの健全性を常時可視化できます。
6-3. CloudWatchアラームの設計
AppSyncが自動発行するメトリクスを使ってアラームを設定します。主要メトリクスと推奨閾値を示します。
| メトリクス | 推奨閾値 | 評価期間 | 用途 |
|---|---|---|---|
4XXError | エラー率 > 1% | 5分 | クライアント認証エラー多発検知 |
5XXError | 1件以上 | 1分 | リゾルバ/サービス障害の即時検知 |
Latency P99 | 3000ms超 | 5分 | 応答遅延の早期検知 |
ConnectSuccess | 前比-50%以上の急落 | 5分 | Subscription断絶の検知 |
ConnectClientError | 10件/分超 | 5分 | 認証失敗多発の検知 |
CDKでのアラーム設定例:
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import { Duration } from 'aws-cdk-lib';
// 5XXエラーアラーム(1件でも即アラーム)
new cloudwatch.Alarm(this, 'AppSync5xxAlarm', {
metric: new cloudwatch.Metric({
namespace: 'AWS/AppSync',
metricName: '5XXError',
dimensionsMap: { GraphQLAPIId: api.apiId },
statistic: 'Sum',
period: Duration.minutes(1),
}),
threshold: 1,
evaluationPeriods: 1,
comparisonOperator:
cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
// P99レイテンシアラーム(3秒超、5分間3回中2回)
new cloudwatch.Alarm(this, 'AppSyncP99LatencyAlarm', {
metric: new cloudwatch.Metric({
namespace: 'AWS/AppSync',
metricName: 'Latency',
dimensionsMap: { GraphQLAPIId: api.apiId },
statistic: 'p99',
period: Duration.minutes(5),
}),
threshold: 3000,
evaluationPeriods: 3,
datapointsToAlarm: 2,
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
アラームをSNSトピックに接続し、Slackや運用チームへの通知を設定することで、障害の初動対応を自動化できます。
6-4. EMFによるビジネスKPIメトリクスの送信
AWS Embedded Metrics Format (EMF) を使うと、Lambdaリゾルバのログから非同期でCloudWatchカスタムメトリクスを発行できます。クエリ種別ごとの利用量計測や、ビジネス指標の可視化に有効です。
import json, time
def handler(event, context):
field_name = event.get('info', {}).get('fieldName', 'unknown')
# EMF形式のログ出力 — CloudWatchが非同期でメトリクス化
print(json.dumps({
"_aws": {
"Timestamp": int(time.time() * 1000),
"CloudWatchMetrics": [{
"Namespace": "AppSync/Business",
"Dimensions": [["QueryType"]],
"Metrics": [{"Name": "InvocationCount", "Unit": "Count"}]
}]
},
"QueryType": field_name,
"InvocationCount": 1
}))
return resolve(event)
EMFで集計したメトリクスをCloudWatch Dashboardに表示することで、どのフィールドへのクエリが多いか・コストに寄与しているかを把握できます。aws-embedded-metricsライブラリを使うと、EMF形式のJSONを手書きせずに済みます。
6-5. ダッシュボード設計の指針
- ゴールデンシグナル行: エラー率(4XX/5XX)・レイテンシP50/P95/P99・Subscription接続数 — 1行で全体健全性を俯瞰
- リゾルバ詳細行: Logs Insightsウィジェットでリゾルバ別エラー件数・実行時間上位20件を常時表示
- コスト行: クエリ件数・Subscription接続分・リアルタイムデータ転送量(月次コスト見積もりのベース)
- ビジネスKPI行: EMFで発行したQueryType別InvocationCountを積み上げグラフで表示
GrafanaをAmazon Managed Grafanaとして導入し、CloudWatchをデータソースに追加すれば既存のGrafana環境と統合できます。AppSync固有のメトリクス(ConnectSuccess等)はCloudWatch Dashboardでも十分カバーでき、標準ウィジェットのみで監視基盤を構築可能です。
7. セキュリティ強化と本番強化Tips

7-1. WAF統合によるDDoS・過剰クエリ防御
AppSyncはAWS WAFと統合できます。WAFルールをAssociateすることでレートリミット・IPブラックリスト・マネージドルールセットを適用し、過剰なGraphQLリクエストやDDoSから保護できます。
import * as wafv2 from 'aws-cdk-lib/aws-wafv2';
const webAcl = new wafv2.CfnWebACL(this, 'AppSyncWAF', {
scope: 'REGIONAL',
defaultAction: { allow: {} },
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: 'AppSyncWAF',
sampledRequestsEnabled: true,
},
rules: [
// AWSマネージドルール: 一般的な脅威を自動ブロック
{
name: 'AWSManagedRulesCommonRuleSet',
priority: 1,
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesCommonRuleSet',
},
},
overrideAction: { none: {} },
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: 'CommonRuleSet',
sampledRequestsEnabled: true,
},
},
// レートリミット: 同一IPから5分間1000リクエスト超でブロック
{
name: 'RateLimitRule',
priority: 2,
statement: {
rateBasedStatement: { limit: 1000, aggregateKeyType: 'IP' },
},
action: { block: {} },
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: 'RateLimit',
sampledRequestsEnabled: true,
},
},
],
});
// AppSyncにWAFを関連付け(L1 Cfnコンストラクト)
new wafv2.CfnWebACLAssociation(this, 'AppSyncWAFAssociation', {
resourceArn: api.arn,
webAclArn: webAcl.attrArn,
});
WAFブロック件数はAWS/WAFV2名前空間のメトリクスで取得できます。アラームを設定してセキュリティチームへの通知を自動化してください。
7-2. フィールド認可の深化: @auth + Lambdaオーソライザー
Vol1で扱った@authディレクティブによるフィールドレベル認可に加え、Lambdaオーソライザーを組み合わせることで動的な認可ロジックを実装できます。テナントIDやカスタムクレームに基づく制御に特に有効です。
Lambdaオーソライザーの実装例(JWTクレームに基づく動的認可):
const authLambda = new lambda.Function(this, 'AppSyncAuthFn', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda/authorizer'),
});
const api = new appsync.GraphqlApi(this, 'AppSyncApi', {
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.LAMBDA,
lambdaAuthorizerConfig: {
handler: authLambda,
resultsCacheTtl: Duration.seconds(300), // 5分キャッシュ
},
},
additionalAuthorizationModes: [
{
authorizationType: appsync.AuthorizationType.USER_POOL,
userPoolConfig: { userPool },
},
],
},
});
Lambdaオーソライザーが返すresolverContextはリゾルバ内で$ctx.identity.resolverContext.tenantIdとして参照できます。これを使ってDynamoDBのパーティションキーにテナントIDを含め、データ分離を実現できます。
オーソライザーのresultsCacheTtl(デフォルト300秒)を適切に設定することで、Lambda呼び出しコストを削減できます。トークンの失効を即時反映させたい場合はDuration.seconds(0)でキャッシュを無効化してください。
7-3. クエリ深度・複雑度制限とIntrospection無効化
GraphQLはネストしたクエリで意図せず大量のデータを取得できます。以下の対策を組み合わせてリクエストの爆発を防ぎます。
クエリ深度の検証(Lambdaオーソライザー内で実装):
// authorizer/index.js
const MAX_DEPTH = 5;
function getQueryDepth(query, depth = 0) {
if (!query.selectionSet) return depth;
return Math.max(
...query.selectionSet.selections.map(s => getQueryDepth(s, depth + 1))
);
}
exports.handler = async (event) => {
const parsed = parseGraphQL(event.requestContext.queryString);
if (getQueryDepth(parsed) > MAX_DEPTH) {
return { isAuthorized: false };
}
return { isAuthorized: true, ttlOverride: 0 };
};
Introspectionの制御: AppSyncコンソールおよびAPIレベルでIntrospectionを制限できます。外部ユーザーがスキーマを探索できないようにするため、本番APIではIntrospectionを無効化することがセキュリティベストプラクティスです(設定方法は執筆時点のAWS公式ドキュメントを確認してください)。
7-4. Private API: VPC内限定公開パターン
AppSync Private APIはVPCインターフェースエンドポイント経由でのみアクセスできるGraphQL APIを作成します。インターネット経由のアクセスを完全に遮断し、内部サービス間APIや管理用APIに最適なパターンです。
import * as ec2 from 'aws-cdk-lib/aws-ec2';
// VPCインターフェースエンドポイント作成
const appSyncEndpoint = vpc.addInterfaceEndpoint('AppSyncEndpoint', {
service: ec2.InterfaceVpcEndpointAwsService.APP_SYNC,
subnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
securityGroups: [endpointSg],
privateDnsEnabled: true,
});
// CDK L1でAPIをPRIVATEに設定
const cfnApi = api.node.defaultChild as appsync.CfnGraphQLApi;
cfnApi.visibility = 'PRIVATE';
Private APIはリソースポリシーでアクセスを許可するVPCやアカウントをさらに絞り込めます。aws:SourceVpc条件キーを使って特定のVPCからのみ許可するポリシーを設定することを推奨します。
7-5. Secrets Managerとの統合パターン
LambdaリゾルバからデータベースのCredentialを取得する場合、環境変数ではなくAWS Secrets Managerを使用します。
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
const dbSecret = secretsmanager.Secret.fromSecretNameV2(
this, 'DbSecret', 'prod/appsync/db-credentials'
);
// LambdaリゾルバにSecrets Managerへのアクセス権付与
resolverFn.addEnvironment('SECRET_ARN', dbSecret.secretArn);
dbSecret.grantRead(resolverFn);
Lambdaのコードでは、初回呼び出し時にシークレットを取得してモジュールスコープにキャッシュします。Lambdaの実行コンテキストが再利用される間はAPI呼び出しが発生せず、コストと遅延を最小化できます。Secrets Managerの自動ローテーションを有効化し、ローテーション時のLambda再起動(または初回呼び出し時の再取得)も考慮した実装にしてください。
- WAFのレートリミットルールを有効化し、CloudWatchでブロック件数を監視している
- 本番APIでIntrospectionを無効化している(またはIPベースで制限している)
- Lambdaオーソライザーのキャッシュ(resultsCacheTtl)を用途に応じて設定している
- Private APIのVPCエンドポイントにセキュリティグループとリソースポリシーで最小権限を設定している
- LambdaリゾルバのCredentialはSecrets Managerから取得し、自動ローテーションが設定されている
- クエリ深度制限をLambdaオーソライザーまたはリゾルバで実装している
8. まとめとVol3予告
8-1. Vol2の実装まとめ
本記事では、AppSync本番運用をさらに深化させる以下の機能・設計パターンを解説しました。
- AppSync Events(GraphQL Subscription非依存のサーバーレスpub/sub)を理解し、チャット・通知システムへの適用を判断できる
- CDK L2コンストラクト(GraphqlApi / SchemaFile / Resolver)でAppSyncをIaC管理できる
- N+1問題をbatch resolverで緩和し、X-Rayでリゾルバボトルネックを特定できる
- Amplify DataStoreによるオフライン同期とDelta Syncの3テーブル構成を設計できる
- 競合解決戦略(AUTOMERGE / Optimistic Concurrency / Lambda)を用途に応じて選択できる
- CloudWatch Logs Insights・EMF・アラームでリゾルバ別監視を本番運用できる
- WAF統合・フィールド認可・クエリ制限・Private APIによる多層セキュリティを適用できる
8-2. Vol1・Vol2でAppSync本番運用の全体像が揃う
Vol1ではスキーマ駆動設計・JSリゾルバ・GraphQL Subscription・Merged API・認証・基礎監視という土台を構築しました。Vol2ではその上にAppSync Events・CDK・パフォーマンス最適化・オフライン同期・セキュリティ強化を積み上げました。両Volを合わせることで、AppSyncを本番環境で設計・実装・運用するための体系的な知識が揃います。
- Vol1: スキーマ駆動設計・JSリゾルバ・Subscription・Merged API・認証・監視
- Vol2(本記事): AppSync Events・CDK・パフォーマンス最適化・オフライン同期・セキュリティ強化
8-3. Vol2で使用した主要サービス・機能の整理
本記事で登場したAWSサービスと機能を整理します。
| サービス/機能 | 本記事での役割 | 関連セクション |
|---|---|---|
| AppSync Events | スキーマ不要のサーバーレスpub/sub | §2 |
| aws-cdk-lib/aws-appsync | GraphQL APIのIaC定義 | §3 |
| AWS X-Ray | リゾルバのトレーシングとボトルネック特定 | §4 |
| Amplify DataStore | オフライン同期とDelta Sync | §5 |
| CloudWatch Logs Insights | リゾルバ別エラー率・レイテンシ分析 | §6 |
| CloudWatch Metrics/Alarms | 4XX/5XX/Latency/ConnectSuccessの監視 | §6 |
| EMF (Embedded Metrics Format) | LambdaからのカスタムKPIメトリクス発行 | §6 |
| AWS WAF | DDoS・過剰クエリのレートリミット | §7 |
| Lambdaオーソライザー | 動的フィールド認可・テナント分離 | §7 |
| AppSync Private API | VPC内限定のGraphQL APIエンドポイント | §7 |
| AWS Secrets Manager | LambdaリゾルバのCredential管理 | §7 |
8-4. 次のステップ: 公式リソース
より深く学ぶための公式リソースを紹介します。
- AppSync Events 開発者ガイド: AppSync Eventsのchannelとnamespace設計、onPublish/onSubscribeハンドラの実装
- CDK aws-appsync モジュール API リファレンス: L2コンストラクト(GraphqlApi / Resolver / DataSource)の最新プロパティ一覧
- Amplify DataStore 公式ドキュメント: Delta Sync・競合解決戦略の詳細設定と実装ガイド
- AppSync セキュリティのベストプラクティス: WAF統合・Introspection無効化・Private APIの公式推奨事項
- CloudWatch AppSync メトリクス一覧: 全メトリクスのディメンション定義と集計方法
8-5. Vol3予告: AppSync × Step Functions / EventBridge統合
Vol3では、AppSyncをAWSサービスとより深く連携させるアーキテクチャパターンを扱う予定です。
- AppSync × Step Functions: GraphQL MutationでStep Functionsワークフローをトリガーし、非同期処理の進捗をSubscriptionでリアルタイム通知するパターン
- AppSync × EventBridge: EventBridgeのイベントをHTTPデータソース経由でAppSyncに連携し、イベント駆動のGraphQL APIを構築するアーキテクチャ
- Merged APIの本番活用: マイクロサービスごとにAPIを分割してMerged APIで統合するドメイン駆動設計への応用
▶ Vol3: Step Functions・EventBridge統合編を読む(近日公開)
▶ 関連: AWS App Integration Vol1 — SQS×SNS×EventBridge×API GW×AppSyncの全体像