Amazon Web Services ブログ

フルスタックのチャットアプリケーションをAWSとNext.jsで構築する

モダンなチャットアプリはリッチな機能を必要とします。これらの機能はファイルストレージ・リアルタイムの更新、そしてクライアントとサーバーの両方からデータを取得する能力が必要です。

従来、これは多くのサードパーティサービスをつなぎ合わせるか、カスタムソリューションの作成に開発時間を費やすことを意味していました。そして、この方法では市場投入までの時間が遅くなり、複数の障害点が発生します。

チャットアプリに必要な機能と、AWS が従来の問題点をどのように解決しているかを紹介するために、我々はリアルタイムチャットアプリケーションのサンプルを更新しました。このバージョンは、ローカルと AWS の両方でアプリケーションを完全に管理・制御することがいかに簡単かを強調するために再設計されました。

このアプリケーションバージョンはこれらの技術スタックで構成されています。

フロントエンド

バックエンド

figure 1. Frontend chat UI

figure 1. Frontend chat UI

バックエンドのアーキテクチャ概要

Figure 2. Backend architecture diagram

Figure 2. Backend architecture diagram

概要

バックエンドサービスを使うための概要は下記の通りです。

  • Amazon DynamoDB: 上のスクリーンショットのように、このアプリケーションは単一テーブルではなくマルチテーブルアーキテクチャを利用します。Userテーブルは認証済みユーザーの情報を保持し、Messageテーブルはテキストメッセージだけでなく Amazon S3 にアップロードした画像 ID を保持します。そしてRoomテーブルはメッセージとリアルタイムメッセージサブスクリプションに利用されます。さらに、このアプリケーションは異なるアクセスパターンのためにグローバルセカンダリインデックス(GSI)をセットアップされています。
  • AWS AppSync GraphQL and Pub/Sub APIs: Cognito と合わせて利用することで、AMAZON_COGNITO_USER_POOLSを認証モードとして設定した API を開発し ます。AWS AppSync リゾルバーでは、API からデータベースへの直接マッピングを作成します。API エンドポイントの作成に加えて、AWS AppSync はサーバーレス WebSockets エンドポイントも作成して、リアルタイム通知を可能にします。これらはフルマネージドサービスなので、サーバーをセットアップしたり、接続プールを管理したりする必要はありません。
  • Amazon S3: ユーザーの画像をアップロードを許可し、サインインしたユーザーのみアクセスできるようにします
  • Amazon Cognito: このプロジェクトではユーザープール・IDプールに加えてユーザープールグループを利用します
  • AWS Lambda: ユーザーがアプリケーションにサインアップする際に、postConfirmation トリガーを通じて Cognito からデータベースにユーザーを追加し、ユーザーがチャットメンバーをクエリできるようにします。

Infrastructure-as-Code (IaC) の観点では、このアプリケーションは AWS CDK を利用して上記のサービスを作成します。AWS CDK は、さまざまなプログラミング言語をサポートしています。ただし、フロントエンドコンポーネントを持つチームは、バックエンドだけでなくフロントエンドでも使用できる TypeScript の恩恵を受ける可能性があります。
コンストラクトを使用することで、開発者はサービスを再利用可能なコードスタックとして構成できます。これが実際に動作していることを確認するために、バックエンドリポジトリのスニペットを次に示します。

const databaseStack = new DatabaseStack(app, 'DatabaseStack', {})

const authStack = new AuthStack(app, 'AuthStack', {
    stage: 'dev',
    hasCognitoGroups: true,
    groupNames: ['admin'],
    userpoolConstructName: 'ChatUserPool',
    identitypoolConstructName: 'ChatIdentityPool',
    userTable: databaseStack.userTable,})

const fileStorageStack = new FileStorageStack(app, 'FileStorageStack', {
    authenticatedRole: authStack.authenticatedRole,
    unauthenticatedRole: authStack.unauthenticatedRole,
    allowedOrigins: ['http://localhost:3000'],})

const apiStack = new APIStack(app, 'AppSyncAPIStack', {
    userpool: authStack.userpool,
    roomTable: databaseStack.roomTable,
    messageTable: databaseStack.messageTable,
    userTable: databaseStack.userTable,
    unauthenticatedRole: authStack.unauthenticatedRole,})

APIを理解する

AWS AppSync では、複数のデータソースからのデータを安全にクエリまたは更新するための単一のエンドポイントを提供することで、アプリケーション開発を簡素化するserverless GraphQL API の作成を可能にします。さらに、GraphQL subscriptions により、常にデータが更新されるリアルタイムアプリケーション体験を可能にします。
バックエンドでは、AWS AppSync はマッピングテンプレートを使用してデータソースに接続します。リクエストマッピングテンプレートは、データがどのようにデータソース (この場合は DynamoDB) に送信されるかを示す JSON ドキュメントを作成します。レスポンスマッピングテンプレートはその逆です。そのドキュメントには、データソースからのデータがどのようにAPIに送り返されるかが記載されています。これが実際に動作していることを確認するには、次のAuth Stackを参照してください。

messageTableDataSource.createResolver({
    typeName: 'Mutation',
    fieldName: 'updateMessage',
    requestMappingTemplate: MappingTemplate.fromFile(
        path.join(
            __dirname,
            'graphql/mappingTemplates/Mutation.updateMessage.req.vtl'
)
),
    responseMappingTemplate: MappingTemplate.dynamoDbResultItem(),})

requestMappingTemplate はリクエストを Mutation.updateMessage.req.vtl に応じて DynamoDB 向けに変換します

{
  "version" : "2018-05-29",
  "operation" : "UpdateItem",
  "key": {"id": $util.dynamodb.toDynamoDBJson($ctx.args.input.id)},
  "update" : {
      "expression" : "SET #updatedAt = :updatedAt, #content = :content",
      "expressionNames" : {
          "#updatedAt" : "updatedAt",
          "#content": "content"
      },
      "expressionValues" : {
          ":updatedAt" : $util.dynamodb.toDynamoDBJson($util.time.nowISO8601()),
          ":content": $util.dynamodb.toDynamoDBJson($ctx.args.input.content)
      }
  }
}

Amplify Librariesでフロントエンドをバックエンドに接続させる

サインアップの後、ユーザーは他のユーザーが参加するためのチャットルームを作成できます。チャットルームに入ると、テキストメッセージや画像を送信できます。AWS AppSync は自動的に有効なユーザーを許可し、メッセージとルームデータの両方を保存する有効なリクエストを DynamoDB に転送します。

この機能を有効にするために、バックエンドからエクスポートされた値を利用するようにフロントエンドを設定する必要があります。完全なフロントエンドの設定方法については、リポジトリの README にリンクされている getting started guide をご覧ください。
Amplify のフロントエンドライブラリを利用することで、API 呼び出しと UI 開発の両方を簡単に開発できるようになります。例として、以下のような様々なメソッドが挙げられます。

  • Storage.get(ITEM_KEY): S3 バケットからオブジェクトをフェッチする、署名済み URL を返すメソッド
  • Auth.currentAuthenticatedUser(): ユーザーの JWT パーサーとして働き、Cognito のサインイン情報をオブジェクトとして返すメソッド
  • API.graphql({query, variables, authMode}): 認証済みの GraphQL API リクエストを作成し、query, mutationを使うことができ、subscriptionを使うために拡張することもできます

ユーザーのサインアップを管理する

API は、Amazon Cognito のデフォルト認証ストラテジーで作成されています。
フロントエンドでユーザーを認証するために、@aws-amplify/ui-reactwithAuthenticator コンポーネントを利用します。

Figure 3 caption: Amplify create account screen

Figure 3 caption: Amplify create account screen

GraphQL を用いてメッセージを送信・受信する

ユーザーがアプリケーションで認証されたら、彼らは API をコールできるようになります。例として、ホームページ上で参加可能なチャットルームのリストを表示する場合は以下のようになります。

API.graphql({
    query: listRooms,}).then(({ data }) => {
    setRooms(data.listRooms.items)
})

クライアントサイドで認証済みのクエリはうまく動作します。しかし、サーバーサイドで API を呼び出すほうが簡単な場合もあります。これを示すためにwithSSRContext を利用して Cookie セッションに保存されている詳細情報にアクセスします。

export async function getServerSideProps(context) {
    // 👇 pass the context to an Amplify function
    const { API, Auth } = withSSRContext(context)
    try {
        const user = await Auth.currentAuthenticatedUser()
        const { data } = await API.graphql({
            query: listRooms,
        })
        const currentRoomData = data.listRooms.items.find(
            (room) => room.id === context.params.roomId
        )
        return {
            props: {
                currentRoomData,
                username: user.username,
                roomsList: data.listRooms.items,
            },
        }
        // 👇 if there's an error, perform a server-side redirect
    } catch (err) {
        return {
            redirect: {
                destination: '/',
                permanent: false,
            },
        }
    }
}

このように、ユーザーの存在を確認して、チャットルームのリストを取得します。もしエラーが発生した場合はフロントエンドにリダイレクトします。

画像アップロードを管理する

アプリケーションのビジュアルとして重要なことは、ユーザーが画像をアップロードできるようにすることが挙げられます。バックエンドスタックでは、Amplify のベストプラクティスに従った S3 バケットの管理ポリシーを作成します。これらのポリシーは、ファイルへの匿名アクセスをブロックし、サインインしたユーザーがファイルを取得してパブリックディレクトリにアップロードすることを許可します。

Figure 4. Two separate chat apps side by side, sending message

Figure 4. Two separate chat apps side by side, sending message

これを実現するために、Messageのスキーマでは MessageContentimageIdまたはtextのいずれかを含めることができる型として定義しています。

type Message {
id: ID!
content: MessageContent!
owner: String!
createdAt: AWSDateTime!
updatedAt: AWSDateTime!
roomId: ID!
}

type MessageContent {
text: String
imageId: String
}

Amplify のStorageモジュールを使い、ファイルデータとファイル名を指定することで自動的に Amazon S3 に画像を配置し、key が返されます。

const uploadFile = async (selectedPic) => {
    const { key } = await Storage.put(selectedPic.name, selectedPic, {
        contentType: selectedPic.type,
    })

    return key
}

まとめ

この記事では、ユーザーが期待する豊富な機能を備えたスケーラブルなチャットアプリケーションをチームがどのように構築できるかを紹介しました。AWS CDK を利用することで、チームはフロントエンドチームとバックエンドチームがそれぞれのスタックで柔軟に作業できると同時に、フルスタックの開発者が両方のスタックを共通の言語で管理できるようになります。

また、AWS AppSync では、サーバーについて心配したり、複雑なバックエンドを抽象化したりすることなく、単一のエンドポイントをフロントエンドチームに公開する方法も確認しました。フロントエンドチームは、GraphQL スキーマによって提供される厳密に型指定されたデータモデルを使用して簡単に推論できるため、エンドユーザーエクスペリエンスに集中できます。

フロントエンドでは、チームは Amplify UI と JavaScript バインディングを使用して作成されたバックエンドサービスの使用に集中できます。

今後の記事で説明するように、このアプリケーションを拡張して通知やサブスクリプション処理を強化することができます。次のプロジェクトで AWS AppSync を活用する方法の詳細については、サービスの概要ページにアクセスし、フロントエンドでの使用方法とセットアップに関するAmplify Library のドキュメントを参照してください。

この記事は、Building a full-stack chat application with AWS and NextJS を翻訳したものです。翻訳はソリューションアーキテクトの高柴元が担当致しました。