Amazon Web Services ブログ

AWS AppSync Events との連携: チャット機能付きリアルタイム Web ゲーム

この投稿では、四目並べ(プレイヤーがコマを連続して 4 つ並べて揃えるゲーム)のオンラインバージョンを作成するために必要なコアコンセプトを見ていきます。また、AWS Amplify Gen 2 が AWS バックエンドへの高速接続を可能にする方法と、WebSocket 接続を使用してプレイヤーがリアルタイムでゲームの更新を送信できるように AWS AppSync イベントの利用によってどのように実現できるかを確認します。この投稿を読み終わる頃には、IAM による承認処理を伴う AppSync イベントの使用に自信を持てるようになり、またその役割が現在のアプリケーション開発においてどのようなものであるかをよりよく理解できるはずです。

この投稿は AppSync イベントに関するシリーズの 2 件目の投稿です。アナウンス投稿にはこのサービスの概要が含まれていますが、AppSync Events を使用するための最初の投稿はこちらにあります。

アプリケーションの概要

最初に子供たちに人気のゲーム、三目並べで勝つ方法を教えたことを覚えています。ご存知ない人のために説明しておくと、これはいわゆる「解決済み」ゲームというもので、最適な戦略を取れば勝ちか引き分けを保証できる方法があります。これは子供たちにアルゴリズムを学ばせるのに最適でしたが、正直なところ、ゲームをする時間が楽しくなくなってしまったことは事実です。

幸いなことに、四目並べゲームを紹介したのは彼らがまだ幼い時でした。これは、すでに解決されたゲームですが、追跡することはより難しく、シンプルに楽しくプレーすることはずっと簡単です。

ゲームのコマを持ち運んだり、近くにいる必要はなく、私たちのアプリケーションは完全オンラインで、チャット機能をサポートしています。そして、プレイヤーにログインを要求するのではなく、ユーザー名を尋ねて、ユニークなゲームコードを生成し、そのコードをプレイ相手と共有できます。

screenshot of game creation page

ゲームを作成すると、あなたがプレイヤー 1 (赤) になります。ゲームに参加すると、あなたがプレイヤー 2 (黄色) になります。
相手のプレイヤーがいることを確認するために、ゲーム中はお互いにチャットを行えます。

2 人のプレイヤーが対戦しながらチャットしている 2 つの別々の画面のスクリーンショット

同じ色のピースが縦、横、または斜めに 4 つ並んだら、ゲームは自動的に終了します。その後、プレイヤーは新しいゲームをするか、ゲームを完全に終了するかを選択できます。

コードとそれを自身のマシン上でホストし実行する手順が書かれた readme を含むものをご覧になるには、リポジトリをご覧ください。

ゲームが短命であるため、データをデータベースに保存する必要がありません。さらに、このアプリケーションのリアルタイム機能が中心となっているため、API は必要ありません。

実際、バックエンドは 2 つの中核的な AWS サービスで構成されています。

  • Amazon Cognito: Cognito ID プールは、イベント API への未認証アクセスに対して、限定された権限を提供します
  • AWS AppSync イベント: 数百万のサブスクライバーに対応する独立した WebSocket エンドポイントです

architecture diagram of how the app flows

AWS AppSync イベント API の IAM 認証による作成

AppSync イベント API は、API キー、Cognito ユーザープール、AWS Lambda、OIDC、IAM を使って認可ができます。前回の記事では、API キーの構成方法を見ましたが、今後の記事では Lambda と Cognito の両方を紹介します。ただし、セキュアな未認証アクセスが必要なアプリケーションの場合は IAM 認証モードがよい選択肢であり、このセクションでは IAM について説明します。

Amplify では、Amplify プロジェクトを作成する際に、AWS CDK の薄いラッパーを利用します。

このプロジェクトでは AWS Amplify を使ってフルスタックアプリケーションを作成していますが、AWS AppSync のイベントを使用するのに Amplify は必須の部分ではありません。

amplify/backend.ts ファイルを見ると、auth が構成されていることがわかります。

const backend = defineBackend({ auth })

この 1 行のコードで、Cognito ユーザープールと Cognito ID プールをセットアップします。ユーザープールはログイン済みユーザーを追跡するものですが、今回は使用しません。ID プールはログインしていないユーザーにアプリへのアクセスを許可するため、アプリケーションにとって重要です。この仕組みを示すために、Amplify を拡張するサービスを含む別の CDK スタックを作成します。

const customResources = backend.createStack('custom-resources-connect4')

これは単に、サービスをまとめてメイン backend スタックの中にネストするための論理的な方法です。
このbackendスタックに追加するアイテムはすべて、イベント API に関連するものになります。

const cfnEventAPI = new CfnApi(customResources, 'cfnEventAPI', {
    name: 'serverless-connect4',
    eventConfig: {
        authProviders: [{ authType: AuthorizationType.IAM }],
        connectionAuthModes: [{ authType: AuthorizationType.IAM }],
        defaultPublishAuthModes: [{ authType: AuthorizationType.IAM }],
        defaultSubscribeAuthModes: [{ authType: AuthorizationType.IAM }],
    },
})

 new CfnChannelNamespace(customResources, 'cfnEventAPINamespace', {
    apiId: cfnEventAPI.attrApiId,
    name: 'game',
})

上記のように、まず AWS CDK の L1 コンストラクタを利用してイベント API を作成します。この際、API の名前を指定し、認証プロバイダー、接続、発行、サブスクライブを許可するプロバイダーを表す設定を渡します。

さらに、game という root namespace を作成します。クライアントアプリはこの namespace に接続し、/game/gameId/chat のように root から分岐することで、受信したい接続データをさらに特定できます。

Infrastructure-as-Code (IaC) でイベント API をセットアップするには、現時点では L1 コンストラクタを使用する必要があります。これらは直接、CloudFormation リファレンスに対応しています。より高位の L2 コンストラクトが利用可能になった時点で、このシリーズの投稿は更新する予定です。

IAM を認証モードとして指定すると、イベント API を呼び出すことを許可するポリシーを Cognito Identity プールに割り当てる必要があります。

backend.auth.resources.unauthenticatedUserIamRole.attachInlinePolicy(
    new Policy(customResources, 'AppSyncEventPolicy', {
        statements: [ 
            new PolicyStatement({
                actions: [ 
                    'appsync:EventConnect',
                    'appsync:EventPublish',
                    'appsync:EventSubscribe',
                ],
                resources: [`${cfnEventAPI.attrApiArn}/*`, `${cfnEventAPI.attrApiArn}`],
            }),
        ],
    })
)

上記では、認証リソース (Cognito) から unauthenticatedUserIamRole を使用して、ポリシーを直接アタッチしています。Cognito には 2 つのロールが用意されていることに注意してください。1 つはユーザープールに格納されたログイン済みユーザー用の authenticatedRole、もう 1 つはログインしていないユーザーに対してアイデンティティプールから権限を付与する unauthenticatedRole です。

Cognito に接続されたイベント API が設定できたので、フロントエンドアプリケーションに関連する値を渡し、接続、発行、サブスクライブ (メッセージの受信) に利用できるようにします。

backend.addOutput({
    custom: {
        events: {
            url: `https://${cfnEventAPI.getAtt('Dns.Http').toString()}/event`,
            aws_region: customResources.region,
            default_authorization_type: AuthorizationType.IAM,
        },
    },
})

Amplify JavaScript ライブラリは、events オブジェクトの custom データ内に urlaws_regiondefault_authorization_type の項目が含まれていれば、イベント API と連携するようにアップデートされているので、ここのフォーマットは重要です。

このプロジェクトの readme に従えば、バックエンドが構成された後、開発者は npx ampx sandbox を実行して、これらのリソースを自分の AWS アカウントに検証環境としてデプロイできます。

AppSync イベント API への AWS Amplify による接続

Next.js アプリケーションでは、app/page.tsx にホームページがあり、app/game/[code]/page.tsx にゲームページがあります。

前のスクリーンショットに示されているように、ホームページは単にユーザーのユーザー名を取得し、ユーザーが新しいゲームを作成する場合、ゲームコードを生成します。そこから、ユーザーはcodeがゲームコードとなる動的ルートに移動します。

app/layout.tsxファイルで示されているように、Next.js アプリケーションは既に AWS バックエンドを使用するように構成されています。この設定はcomponents/configureAmplify.tsxファイルで確認できます。

app/game/[code]/page.tsx で、アプリケーションの作り込みが行われています。aws-amplify/data ライブラリの events.connect メソッドを使用して、WebSocket エンドポイントに接続します。ページが最初にロードされた時に接続を行いたいので、useEffect 呼び出し内で実装するのが最適です。

useEffect(() => {
    const subscribeToGameState = async (gameCode: string) => {
        const channel = await events.connect(`/game/${gameCode}`)
        const sub = channel.subscribe({
            next: (data) => {
                dispatch({ type: 'UPDATE_GAME_STATE', newState: data.event })
            },
            error: (err) => console.error('uh oh spaghetti-o', err),
        })
        return sub 
    }

    const subPromise = subscribeToGameState(gameCode)
    return () => {
        Promise.resolve(subPromise).then((sub) => {
            console.log('closing the connection')
            sub.unsubscribe()
        })
    }
}, [gameCode])

特定のチャネルへの接続を確立すると、そのチャネルへの発行とサブスクライブを開始できます。この使用例では、他のプレーヤーからメッセージを受信するたびに、データを reducer に送信して、新しい状態を UI にレンダリングできるようにしています。これは app/game/[code]/GameState.ts ファイルで確認できます。

最後の useEffect の部分では、ページが閉じられたりナビゲートされたりした際に、接続を切断します。これは、サブスクリプションの unsubscribe メソッドを呼び出すことで行われます。このようにすることで、メモリリークによるアプリケーションの速度低下を防ぐことができます。

イベントを発行することで、イベントソースからクライアントにデータを渡すことができます。このアプリでは、プレイヤーがボード上にピースを置いたり、「New Game」または「Reset Game」ボタンをクリックするたびに、それらの詳細を含むイベントを、チャネルの /game/${gameCode} セグメントに発行します。

await events.post(`/game/${gameCode}`, {
    board: newState.board,
    currentPlayer: newState.currentPlayer,
    winner: newState.winner,
    gameOver: newState.gameOver,
})

ご覧のとおり、フルスタックアプリケーションでイベント API を利用するのはごくわずかのコードで済みます!

チャットメッセージを送信する場合も、プロセスは同じです。別の useEffect 呼び出しで、/game/${gameCode}/chat というチャネル上でチャット接続を確立し、ユーザーがメッセージを入力して Enter キーを押すと、await events.post(`/game/${gameCode}/chat`,newMessage) の呼び出しが送信されます。

まとめ

この投稿では、Next.js のようなモダンなフロントエンドフレームワークと Amplify の機能、AppSync イベントの簡便性とスケーラビリティを組み合わせることで、アプリケーションを構築することができることを説明しました。また、Amplify の本来の機能に加えて、CDK の L1 コンストラクトを使用する方法も確認しました。

AWS AppSync イベント API は、フリーティアとして 250,000 回のイベント操作を提供し、大規模なサブスクライバー数に対応可能です。完全マネージドサービスなので、開発者は特定の API サービスに縛られることなく、アプリにリアルタイム機能を簡単に追加できます。

AWS AppSync のイベントの詳細は、ドキュメントページをご覧ください。

本記事は、2024 年 11 月 26 日に公開された Working with AWS AppSync Events: Real-time Web Games with Chat を翻訳したものです。翻訳は Solutions Architect の 吉村 が担当しました。