Amazon Web Services ブログ
フルスタックのチャットアプリケーションをAWSとNext.jsで構築する
モダンなチャットアプリはリッチな機能を必要とします。これらの機能はファイルストレージ・リアルタイムの更新、そしてクライアントとサーバーの両方からデータを取得する能力が必要です。
従来、これは多くのサードパーティサービスをつなぎ合わせるか、カスタムソリューションの作成に開発時間を費やすことを意味していました。そして、この方法では市場投入までの時間が遅くなり、複数の障害点が発生します。
チャットアプリに必要な機能と、AWS が従来の問題点をどのように解決しているかを紹介するために、我々はリアルタイムチャットアプリケーションのサンプルを更新しました。このバージョンは、ローカルと AWS の両方でアプリケーションを完全に管理・制御することがいかに簡単かを強調するために再設計されました。
このアプリケーションバージョンはこれらの技術スタックで構成されています。
フロントエンド
- React フレームワーク: Next.js
- UI ライブラリ: AWS Amplify UI
- API Bindings: AWS JavaScript library
- ホスティング: Amplify Hosting
- サンプルコードリポジトリ
バックエンド
- 認証: Amazon Cognito
- API: AWS AppSync
- データベース: Amazon DynamoDB
- ファイルストレージ: Amazon Simple Storage Service (Amazon S3)
- IaC: AWS CDK
- サンプルコードリポジトリ
バックエンドのアーキテクチャ概要
概要
バックエンドサービスを使うための概要は下記の通りです。
- 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-react
の withAuthenticator
コンポーネントを利用します。
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 バケットの管理ポリシーを作成します。これらのポリシーは、ファイルへの匿名アクセスをブロックし、サインインしたユーザーがファイルを取得してパブリックディレクトリにアップロードすることを許可します。
これを実現するために、Message
のスキーマでは MessageContent
に imageId
または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 を翻訳したものです。翻訳はソリューションアーキテクトの高柴元が担当致しました。