前回は AppSync Events で試すイベント駆動構成 (その2) で AppSync Events を使用し Web アプリケーションのbackend, frontend をコードベースで紹介していました。
今回は cdk を用いたインフラ側の定義と、実際に画面上でどのような挙動になるのかを紹介したいと思っています。

| コンポーネント | 種別 | 役割 |
|---|---|---|
| CloudFront + S3 | frontend | Vite + React SPA のホスティング |
| AppSync Events | frontend | WebSocket Subscribe エンドポイント (tickets/orders) |
| API Gateway + Lambda | backend | POST /v1/tickets/orders を受け付け SQS に Enqueue |
| SQS + DLQ | backend | メッセージバッファ ※3 回失敗で DLQ へ移動 |
| Event Lambda | backend | SQS トリガー、DynamoDB 書き込み + AppSync Publish |
| AppSync Events | backend | HTTP Publish エンドポイント ※API キー認証 |
| DynamoDB | backend | イベント永続化 ※PK: event_id (UUID v7) |
インフラは AWS CDK TypeScript で定義しています。
参考: aws-cdk-lib.aws_appsync module
AppSync Events に関わる部分に焦点を当てて紹介します。
export class AppSyncApi extends Construct {
/** AppSync Events API インスタンス */
readonly api: appsync.EventApi
/** デフォルトチャンネル名前空間 */
readonly channelNamespace: appsync.ChannelNamespace
// AppSync Events API を定義
constructor(scope: Construct, id: string, props: AppSyncApiProps) {
super(scope, id)
// AppSync Events API を API Key 認証で定義
this.api = new appsync.EventApi(this, "EventApi", {
apiName: `${props.envName}-realtime-event-api`,
authorizationConfig: {
authProviders: [{ authorizationType: appsync.AppSyncAuthorizationType.API_KEY }],
connectionAuthModeTypes: [appsync.AppSyncAuthorizationType.API_KEY],
defaultPublishAuthModeTypes: [appsync.AppSyncAuthorizationType.API_KEY],
defaultSubscribeAuthModeTypes: [appsync.AppSyncAuthorizationType.API_KEY],
},
})
// チャンネルネームスペース "tickets" を登録 (tickets/orders の先頭セグメント)
this.channelNamespace = this.api.addChannelNamespace("tickets")
// HTTP Publish エンドポイント (バックエンド Lambda の APPSYNC_ENDPOINT 環境変数に設定する)
new cdk.CfnOutput(scope, "AppSyncHttpUrl", {
value: `https://${this.api.httpDns}`,
description: "AppSync Events HTTP Publish endpoint URL",
})
// WebSocket 接続エンドポイント (フロントエンドの VITE_APPSYNC_REALTIME_URL 環境変数に設定する)
new cdk.CfnOutput(scope, "AppSyncRealtimeUrl", {
value: `wss://${this.api.realtimeDns}`,
description: "AppSync Events WebSocket endpoint URL",
})
// API キーはデフォルトで 7 日間有効。期限切れ前に再デプロイすれば新しいキーが発行される設計
new cdk.CfnOutput(scope, "AppSyncApiKey", {
value: this.api.apiKeys["Default"].attrApiKey,
description: "AppSync API key",
})
}
}
補足として、authorizationConfig には4つのフィールドがあります。
| フィールド | 役割 |
|---|---|
authProviders | 使用できる認証方式の一覧を宣言する |
connectionAuthModeTypes | WebSocket 接続時に使う認証方式 |
defaultPublishAuthModeTypes | HTTP Publish 時に使う認証方式 |
defaultSubscribeAuthModeTypes | Subscribe 時に使う認証方式 |
AWS::AppSync::Api EventConfig - AWS CloudFormation
以下 cdk のドキュメントを見ると、 connectionAuthModeTypes / defaultPublishAuthModeTypes / defaultSubscribeAuthModeTypes は CDK では全て省略可能で、省略した場合は authProviders に定義した全プロバイダーが接続・Publish・Subscribe の全操作に使用されるようなので、意図しない操作・設定を避ける上でも明示的に設定しておくのが無難なように見えます。
EventApi - AWS CDK API Reference
If you don't specify any overrides for the connectionAuthModeTypes, defaultPublishAuthModeTypes, and defaultSubscribeAuthModeTypes parameters then all authProviders defined are included as default authorization mode types for connection, publish, and subscribe.
なお、複数の認証方式を混在させる場合は、authProviders に宣言した上で操作ごとに指定します。
Configuring authorization and authentication to secure Event APIs
When you define your API, you configure the authorization mode to connect to your Event API WebSocket. You also configure the default authorization modes to use when publishing and subscribing to messages. You can use different authorization modes for each configuration. For example, you might want your publisher to use IAM authorization for a backend process running on an Amazon EC2 instance, but you want your clients to use an API key to subscribe to messages.
例えば Lambda からの Publish は IAM SigV4、クライアント (ブラウザ) からの Subscribe は Cognito を使うといった構成では以下のようなイメージになるかと思います。※こちらの構成については別記事で紹介できたらと思います
authorizationConfig: {
authProviders: [
{ authorizationType: appsync.AppSyncAuthorizationType.AWS_IAM },
{
authorizationType: appsync.AppSyncAuthorizationType.AMAZON_COGNITO_USER_POOLS,
cognitoConfig: { userPool, appIdClientRegex: "^tenant-.*" },
},
],
connectionAuthModeTypes: [appsync.AppSyncAuthorizationType.AMAZON_COGNITO_USER_POOLS],
defaultPublishAuthModeTypes: [appsync.AppSyncAuthorizationType.AWS_IAM],
defaultSubscribeAuthModeTypes: [appsync.AppSyncAuthorizationType.AMAZON_COGNITO_USER_POOLS],
}
以下は SQS をイベントソースとした Lambda Function の定義です。
環境変数で AppSync の接続情報を渡しつつ、チャンネル名前空間に対しての権限設定が必要になります。
export class EventLambda extends Construct {
/** Event Lambda 関数 */
readonly fn: lambda.Function
constructor(scope: Construct, id: string, props: EventLambdaProps) {
super(scope, id)
// 省略: Lambda Execution Role などの定義もしておく必要あり
this.fn = new lambda.Function(this, "Function", {
functionName: `${props.envName}-realtime-event-event`,
description: "Consumes SQS events, writes to DynamoDB, and publishes to AppSync",
runtime: lambda.Runtime.PROVIDED_AL2023,
architecture: lambda.Architecture.ARM_64,
// 省略: handler, role, memorySize, timeout なども必要に応じて定義
environment: {
APP_ENV: props.envName,
EVENT_SERVICE_NAME: `${props.envName}-realtime-event-event`,
APPSYNC_API_KEY: props.appSyncApiKey, // 認証キー
APPSYNC_ENDPOINT: props.appSyncUrl, // HTTP Publish エンドポイント
APPSYNC_CHANNEL: props.appSyncChannel, // チャンネルパス (tickets/orders)
DYNAMODB_TABLE_NAME: props.table.tableName,
},
})
// 省略: 必要に応じて CloudWatch のログや、DynamoDB の書き込み権限、イベントソース(SQS) などを定義
// チャンネル名前空間への EventPublish 権限を付与 (ARN で特定の名前空間に限定する)
this.fn.addToRolePolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["appsync:EventPublish"], // 特定のチャンネルネームスペース ARN に限定して付与
resources: [props.channelNamespace.channelNamespaceArn],
}),
)
}
}
appsync:EventPublish について
AppSync には GraphQL API 向けの appsync:GraphQL と、Events API 向けの appsync:EventPublish という別々の IAM アクションがあります。
Events API を使う場合は appsync:EventPublish が必要です。
Actions, resources, and condition keys for AWS AppSync
EventPublish: Grants permission to publish events to a channel namespace
channelNamespaceArn でリソースを絞る理由
resources に API 全体の ARN を指定することもできますが、チャンネルネームスペース ARN に絞ることで「この Lambda は tickets 名前空間にしか Publish できない」という制約を課すことができます。
将来 notifications など別の名前空間を追加した場合も、この Lambda から誤って Publish されることを防止する目的として、定義しておくのが良さそうです。
以下はスタック全体の組み立てです。
各コンストラクトをインスタンス化し、AppSyncApi で生成したエンドポイント・API キー・チャンネルネームスペースを EventLambda に渡しています。
export class RealtimeEventStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: RealtimeEventStackProps) {
super(scope, id, props)
const appSyncApi = new AppSyncApi(this, "AppSyncApi", {
envName: props.config.envName,
})
const sqsQueue = new SqsQueue(this, "SqsQueue", {
envName: props.config.envName,
})
const dynamoDbTable = new DynamoDbTable(this, "DynamoDbTable", {
envName: props.config.envName,
})
new ApiLambda(this, "ApiLambda", {
envName: props.config.envName,
queue: sqsQueue.queue,
lambdaMemorySize: props.config.lambdaMemorySize,
artifactsBucketName: props.config.artifactsBucketName,
})
new EventLambda(this, "EventLambda", {
envName: props.config.envName,
queue: sqsQueue.queue,
table: dynamoDbTable.table,
channelNamespace: appSyncApi.channelNamespace, // IAM ポリシーの ARN に使う
appSyncUrl: `https://${appSyncApi.api.httpDns}`, // Lambda の環境変数に渡す
appSyncChannel: "tickets/orders", // チャンネルパスを固定値で渡す
appSyncApiKey: appSyncApi.api.apiKeys["Default"].attrApiKey, // API キーを渡す
lambdaMemorySize: props.config.lambdaMemorySize,
artifactsBucketName: props.config.artifactsBucketName,
})
new CloudFrontS3(this, "CloudFrontS3", {
envName: props.config.envName,
})
}
}
appSyncUrl と appSyncChannel の役割の違い
appSyncUrl: `https://${appSyncApi.api.httpDns}`, // Publish先のエンドポイント (どの API か)
appSyncChannel: "tickets/orders", // Publish先のチャンネルパス (どのチャンネルか)
httpDns は CDK の EventApi が持つプロパティで、HTTP Publish エンドポイントのホスト名を返します。
AppSyncApi クラスの CfnOutput でも同じ値を使用しており、「どの API に Publish するか」を示すベース URL として機能します。
一方 "tickets/orders" は「その API のどのチャンネルに Publish するか」を示すパスです。
tickets は addChannelNamespace("tickets") で定義した名前空間、/orders はその配下のサブチャンネルです。
これらの値は props として EventLambda コンストラクトに渡され、前述の EventLambda 定義の通り Lambda 関数の環境変数 APPSYNC_ENDPOINT / APPSYNC_CHANNEL にそれぞれ変換されるような動きになり、Lambda はランタイムでこれらの環境変数を読み取って Publish 先を決定する構成になります。
ダッシュボードページを開くと、自動で AppSync の tickets/orders チャンネルへの WebSocket サブスクリプションが確立されます。
右側の「イベントフィード」はイベントの到着を待機している状態です。

DevTools の Network タブを確認すると、101 Switching Protocols が返っており、WebSocket 接続が確立されていることがわかります。

Messages タブでは、ページロード直後に以下のシーケンスが自動で走っています。

| メッセージ | 方向 | 意味 |
|---|---|---|
connection_init | 送信 | WebSocket 接続の開始を宣言 |
connection_ack | 受信 | AppSync が接続を承認 |
{"channel":"tickets/orders",...} | 送信 | チャンネルへの Subscribe リクエスト |
subscribe_success | 受信 | Subscribe 完了 |
ka | 受信 | Keep-Alive (定期送信) |
その後は ka が定期的に流れ、イベントの到着を待機している状態になります。

この状態で「注文する」ボタンを押すと、API Gateway → SQS → Event Lambda → AppSync Publish という経路でイベントが流れ、イベントフィードにリアルタイムで表示されます。

WebSocket の Messages タブでも type: "data" のメッセージが受信されており、AppSync から Push されたペイロードを確認できます。

こちらで動作検証は終了です。
クライアント側で定期的に状態監視するようなリクエストを送らずとも、処理完了の通知を受信できました。
今回は AppSync Events 自体のインフラ定義と、画面上でどのような挙動になるかを紹介しました。
AppSync Events を使ったリアルタイム配信の基本的な構成を、コードベースからインフラ定義まで一通り試すことができました。WebSocket のインフラ管理を AWS 側に任せつつ、Lambda からは HTTP で Publish するだけという手軽さは、実際に動かしてみると思っていたよりシンプルに感じました。
チャンネルベースの Pub/Sub モデルや認証設定まわりはまだ奥が深そうなので、IAM 認証や Cognito を組み合わせた構成については別の機会に紹介できたらと思います。
AppSync Events を用いたイベント駆動型Webアプリケーションの実装例を紹介。API Lambda、Event Lambda、フロントエンドの実装コードと、AppSync Events への Publish 時の JSON 二重シリアライズやAPI キー認証などの注意点を解説。
AppSync Events を使用したイベント駆動型Webアプリケーション構築について、GraphQL Subscriptionとの違い、AppSync Eventsの概要、HTTP PublishエンドポイントとWebSocket Subscribeエンドポイントの役割を解説した記事。
AWS Lambdaのエラー通知をSlackに送信する際の設計ポイントを解説。ログ永続化、Subscription Filter上限対策、コスト最適化、通知遅延を考慮し、CloudWatch Logs→Lambda→Slack通知とFirehose→S3保存の構成を採用した実装例を紹介。
AWS AIPの知見を活かし、論文PDFを構造化JSONに変換するパイプラインを構築。S3、Lambda、Step Functions、Textract、Bedrockを組み合わせ、2つの抽出経路で日本語・英語PDFを処理し、メタデータ付きの構造化データをRAGのソースとして活用する仕組みを実装。
論文PDFを構造化JSONに変換するパイプラインの経路A(Textract+Bedrock)について、各ステップの設計理由と実装を詳細に解説。出力上限問題への対処として、モデルに本文を書かせず位置情報のみ返させることで、生成時間を半減させた改善事例を紹介。