AWS AppSync Eventsの簡単なまとめ

## AWS AppSync Eventsとは
AWS AppSync Eventsは、接続やリソースのスケーリングを管理することなく、何百万ものサブスクライバにリアルタイムのイベントデータをブロードキャストできる、安全でパフォーマンスの高いサーバーレスWebSocket APIを作成できるサービスです。AWS AppSync Eventsを使えば、APIコードを書く必要がないので、リアルタイムのWebアプリケーションやモバイル体験を短時間で作成できます。

AWS AppSync Eventsを利用することで、メッセージ変換とブロードキャスト、パブリッシュとサブスクライブ認証、ログとメトリクスの作成などのタスクを AWS にオフロードすることができます。

AWS AppSyncのEvent APIを使用すると、以下のネットワーク通信タイプを有効にすることができます。

+ ユニキャスト
+ マルチキャスト
+ ブロードキャスト
+ バッチ パブリッシングとメッセージング

AWS AppSync Eventsを利用することで、ライブチャット、リアルタイム通知、ライブコメントなどの双方向のインタラクティブなアプリケーションの開発が容易になります。AWS AppSync Events は以下の機能を提供します。

+ WebSocket接続の自動管理とスケーリング
+ 多数のサブスクライバへのイベントブロードキャストのビルトインサポート
+ 柔軟なイベントフィルタリングと変換機能
+ きめ細かな認証と認可
+ イベントドリブンアーキテクチャのための他のAWSサービスや外部システムとのシームレスな統合

## AWS AppSync Eventsの機能

### WebSocketとHTTPのサポート
クライアントはHTTP経由でイベントを発行し、WebSocketを使用してチャンネルをサブスクライブすることができます。
Event API は、リアルタイムとpub/sub機能を可能にするWebSocketエンドポイントを提供します。

### チャンネル名前空間とチャンネル
イベントは、名前空間の下にグループ化されたチャンネルにパブリッシュされます。
名前空間では、その名前空間内のすべてのチャンネルに適用される認証と認可のルールとサーバーレス関数を定義できます。

### 名前空間ハンドラ
以下の2種類のハンドラを使用して、パブリッシュおよびサブスクライブアクションに応答して実行される関数を設定できます。

+ OnPublish - イベントがチャンネルにパブリッシュされたときに実行され、イベントの変換、フィルタリング、拒否を行います。

+ OnSubscribe - クライアントがチャネルをサブスクライブしたときに実行され、 その振る舞いをカスタマイズしたりサブスクライブリクエストを拒否したりできます。

### 柔軟な認証と認可
Event APIは様々な認証メカニズム（APIキー、IAM、Amazon Cognito、OIDC、Lambdaオーソライザー）をサポートしており、APIレベルで設定し、チャンネル名前空間レベルでカスタマイズすることができます。

### チャンネルサブスクライブ
クライアントはサブスクライブしているチャンネルのイベントを受信します。

### ワイルドカードチャンネルサブスクライブ
クライアントは、ワイルドカード構文（「namespace/channel/*」など）を使用して、関連するチャンネルのグループをサブスクライブすることができます。

### スケーラブルなイベントブロードキャスト
Event APIは、大量の同時接続を処理するために自動的に拡張され、サブスクライブされたすべてのクライアントに効率的にイベントをブロードキャストすることができます。

### AWS エコシステムとの統合
AWS AppSync Eventsは、Amazon CloudWatch Logs、CloudWatch metrics、AWS WAFのような他のAWSサービスと統合します。Amazon EventBridgeやAWS Lambdaのようなサービスから直接パブリッシュすることで、イベントドリブンアーキテクチャを簡単に実装できます。Amazon Cognitoは認可タイプとして直接サポートされています。

## AWS AppSync Eventsの価格について

AWS AppSync Eventsを利用する場合、最低料金や強制的なサービス利用はなく、利用した分だけの支払いになります。詳細については、[AWS AppSyncの価格](https://aws.amazon.com/jp/appsync/pricing/)を参照してください。

## AWS AppSync Event APIを作成してみた
AWS AppSync Event API を作成し、クライアントからアクセスすることで、AWS AppSync Events をすぐに使い始めることができます。AWS AppSync Events Developer Guideの記述に従って、AWS AppSyncコンソールでAWS AppSync Event APIを作成する手順を説明します。

### AWS AppSync Event API の作成
AWS AppSync Events で、アプリケーションのリアルタイム機能を実現するためのEvent APIを作成します。ここでは、デフォルトのチャンネル名前空間を持つAPIを作成します。その後、AWS AppSyncコンソールを使用してメッセージを発行し、名前空間内のチャンネルに送信されたメッセージを購読します。

#### ステップ 1: AWS AppSyncコンソールを使用してイベントAPIを作成する

1. AWS Management Consoleにサインインし、AWS AppSyncコンソールを開きます。
1. AWS AppSyncコンソールの[API types]で、Event APIを選択し、[Event APIを作成]を選択します。

    ![図1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83238/545604f1-af3c-93a1-d2ca-72620017b1d1.png)
    

1. [Event APIを作成]ページの [APIの詳細] セクションでAPI名にAPIの名前を入力し、[作成]を選択します。

     ![図2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83238/1b93a701-7298-186b-f613-4dbdff65962b.png)


1. APIが作成されます。APIは、接続、パブリッシュ、サブスクライブアクションの認証モードとしてAPI Keyで構成され、APIキーが作成されます。default" という名前のデフォルト・チャネル・ネームスペースも作成されます。

   ![図3](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83238/ff8811a0-0a18-f0e5-467d-76201538cb8f.png)


認可のカスタマイズの詳細については、[Configuring authorization and authentication to secure Event API](https://docs.aws.amazon.com/appsync/latest/eventapi/create-event-api-tutorial.html#:~:text=Configuring%20authorization%20and%20authentication%20to%20secure%20Event%20APIs)を参照してください。チャンネル名前空間についての詳細は、[Understanding channel namespaces](https://docs.aws.amazon.com/appsync/latest/eventapi/channel-namespaces.html)を参照してください。

#### ステップ2: 最初のイベントをパブリッシュし、受信登録する
以下の手順でイベントを発行します。

1. AWS AppSyncコンソールで、ステップ 1で作成したEvent APIの[Pub/Subエディタ]タブを選択します。
1. [パブリッシュ]セクションで、[チャンネル]の[名前空間を選択]のテキストボックスに`default`と入力し、2番目のテキストボックスに /messagesと入力します。
1. コードエディタで、以下のJSONペイロードを入力します。


```json
[{"message": "Hello world!"}]
```

[パブリッシュ]をクリックします。
パブリッシャーのログテーブルに以下のような応答が表示され、成功が確認されます。

```json
{
  "channel": "default/messages",
  "events": [
    {
      "message": "Hello world!"
    }
  ]
}
```

次の手順で、メッセージの受信を登録します。

1. エディタの[サブスクライブ]セクションで、[接続]をクリックしてWebSocketエンドポイントに接続します。
1. [チャンネル]に、サブスクライブしたいチャネルの名前、/default/messagesを入力し、[サブスクライブ]を選択します。
1. コード・エディタで、もう一度[パブリッシュ]をクリックします。サブスクライバのログ・テーブルに、パブリッシュされたイベントを含む新しいデータ・イベントを受信することが確認できます。

    ![図4](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83238/9dae0f07-ade7-c96b-02e1-c64da4f781fb.png)


## Amplify Eventクライアントの実装
ViteとAmplifyクライアントを使用してReactアプリを作成します。そして、このAmplifyクライアントを使用して、メッセージのパブリッシュとサブスクライブを行います。

AWS AppSync Event APIには、任意のHTTPおよびWebSocketクライアントを使用して接続でき、JavaScript用のAmplifyクライアントも使用できます。ここでは、AWS AppSync Events Developer Guideの手順に従って、JavaScript ReactアプリケーションからEvent APIに接続する方法を説明します。

### ViteでReactアプリをデプロイする
1. ローカルの作業環境から、ターミナルウィンドウで以下のコマンドを実行し、React用の新しいViteアプリを作成します。

    ```shell
    npm create vite@latest appsync-events-app -- --template react
    ```

1. 以下のコマンドを実行して、appsync-events-appディレクトリに移動し、依存関係とAmplifyライブラリをインストールします。

    ```Shell
    cd appsync-events-app
    npm install
    npm install aws-amplify
    ```

1. 別のターミナル・ウィンドウを開き、appsync-events-appディレクトリにcdします。以下のコマンドを実行し、devモードでサーバーを起動します。

    ```shell
    npm run dev
    ```

1. ブラウザでlocalhostの指定のポートを開くとReactアプリケーションが表示されます。

    ![図5](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83238/2bbee860-449f-721b-e147-19ff69e34214.png)

### Amplifyクライアントの設定

1. AWS Management Console にサインインし、AWS AppSyncコンソールを開きます。
1. 上記で作成したEvent APIの[統合]タブを開きます。
1. [設定をダウンロード]をクリックして設定ファイルをダウンロードします。
1. amplify_outputs.jsonファイルをプロジェクトのsrcディレクトリに保存します。設定ファイルは以下のようになります。

```json
{
  "API": {
    "Events": {
      "endpoint": "https://xxxx.appsync-api.us-west-2.amazonaws.com/event",
      "region": "us-west-2",
      "defaultAuthMode": "apiKey",
      "apiKey": "xxxxx..."
    }
  }
}
```

:::note warn
重要
defaultAuthModeはAPI_KEYではなく、apiKeyに設定する必要があります。
:::

### チャンネルに接続し、メッセージを受信する
1. App.jsxファイルを以下のコードで更新します。

    ```javascript
    import { useEffect, useState, useRef } from 'react'
    import './App.css'
    
    import { Amplify } from 'aws-amplify'
    import { events } from 'aws-amplify/data'
    import config from './amplify_outputs.json'
    
    Amplify.configure(config)
    
    export default function App() {
      const [messages, setMessages] = useState([])
      const [room, setRoom] = useState('')
      const counterRef = useRef(null)
    
      useEffect(() => {
        if (!room || !room.length) {
          return
        }
        let timeoutID
        const pr = events.connect(`/default/${room}`)
        pr.then((channel) => {
          channel.subscribe({
            next: (data) => {
              setMessages((messages) => [...messages, data.message])
              if (timeoutID) {
                clearTimeout(timeoutID);
              }
              counterRef.current?.classList.add('animate-bounce')
              timeoutID = setTimeout(() => {
                counterRef.current?.classList.remove('animate-bounce')
              }, 3000);
            },
            error: (value) => console.error(value),
          })
        })
    
        return () => {
          pr?.then((channel) => channel?.close())
        }
      }, [room])
    
      return (
        <div className='max-w-screen-md mx-auto'>
          <h2 className='my-4 p-4 font-semibold text-xl'>AppSync Events - Messages</h2>
          <button
            type="button"
            className='border rounded-md px-4 py-2 items-center text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-sky-200  shadow hover:bg-sky-200/90'
            onClick={() => {
              const room = prompt('Room:')
              if (room && room.length) {
                setMessages([])
                setRoom(room.trim().replace(/\W+/g, '-'))
              }
            }}
          >
            set room
          </button>
          <div className='my-4 border-b-2  border-sky-500 py-1 flex justify-between'>
            <div>
              {room ? (
                <span>
                  Currently in room: <b>{room}</b>
                </span>
              ) : (
                <span>Pick a room to get started</span>
              )}
            </div>
            <div className='flex items-center uppercase text-xs tracking-wider font-semibold'>
              <div className='mr-2'>Messages count:</div>
              <span ref={counterRef} className='transition-all inline-flex items-center rounded-md bg-sky-100 px-2.5 py-0.5 text-xs font-medium text-sky-900'>{messages.length}</span></div>
          </div>
          <section id="messages" className='space-y-2'>
            {messages.map((message, index) => (
              <div
                key={index}
                className='border-b py-1 flex justify-between px-2'
              ><div>
                  {message}
                </div>
                <div> </div>
              </div>
            ))}
          </section>
        </div>
      )
    }
    ```

1. ブラウザでアプリを開き、[set room]ボタンを選択してルームを`greetings`に設定する。
1. AWS AppSyncコンソールを開き、APIの[Pub/Subエディタ]タブに移動します。
1. チャンネルを `/default/greetings`に設定します。`default`名前空間を選択し、残りのパスを`/greetings`に設定します。
1. イベントを送信するために、エディタに以下を貼り付けます。

    ```json
    [
        {
            "message": "hello world!"
        }
    ]
    ```

    ![図6](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83238/876c6252-01ef-4863-ba78-df3a2a04b7d7.png)

アプリにメッセージが表示されるはずです。

![図7](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83238/3efd6d99-24c5-ab57-c0ec-d00b7f78cb40.png)


別の部屋を選択するために、もう一度set roomボタンを選択します。Pub/Sub Editorで別のイベントを/default/greetingsチャンネルに送信します。アプリにそのメッセージは表示されません。

### アプリからメッセージを送る
App.jsxを開き、App関数の先頭に以下のコードを追加する。

```javascript
const [message, setMessage] = useState('')
```

同じファイルで、最後の`section`要素を探し、その次の要素として以下のコードを追加する。

```javascript
<section>
        <form
          disabled={!room}
          className='w-full flex justify-between mt-8'
          onSubmit={async (e) => {
            e.preventDefault()
            const event = { message }
            setMessage('')
            await events.post(`/default/${room}`, event)
          }}
        >
          <input
            type="text"
            name="message"
            placeholder="Message:"
            className='flex flex-1 rounded-md border border-input px-3 py-1 h-9 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 bg-transparent'
            value={message}
            disabled={!room}
            onChange={(e) => setMessage(e.target.value)}
          />
          <button
            type="submit"
            className='ml-4 border rounded-md px-4 flex items-center text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-sky-200  shadow hover:bg-sky-200/90'
            disabled={!room || !message || !message.length}>
            <svg xmlns="http://www.w3.org/2000/svg" className='size-4' width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z" /><path d="m21.854 2.147-10.94 10.939" /></svg>
          </button>
        </form>
      </section>
```

これでアプリから直接、選択した部屋にメッセージを送れるようになりました。


## まとめ
いかがでしょうか。AWS AppSync Eventsを利用することで、リアルタイムな双方向のアプリケーションの開発において、開発者の実装の負荷を軽減して容易に構築できることが理解できたと思います。今回はre:Invent 2024直前に新たにリリースされたAWS AppSync Eventsについて簡単に紹介しましたが、この記事が何かのお役に立てば幸いです。


## 免責
本投稿は個人の意見であり、所属する企業や団体は関係ありません。
また掲載しているサンプルプログラム等の動作に関しても一切保証しておりません。