はじめに

オンラインチャットアプリを短期間で開発する案件があり、アプリのバックエンドをAWS Amplifyで構築する機会がありました。その時のAmplifyを選定した経緯や、AWS Cognitoの設定、GraphQLスキーマコードの一部、指定したドメインでアプリを公開する手続きを本ブログで紹介いたします。

本ブログはアプリのバックエンド開発を紹介していますが、共同開発者がフロントエンド開発時にあった問題とその回避策についてブログを執筆しております。ご興味があればiPhoneでバーチャルキーボード表示時に固定要素が画面外に消える問題を解決するもあわせてご参照ください。

概要

  • Amplifyを用いてオンラインチャットアプリのバックエンドを短期間かつ工数を抑えて構築できた
  • 本アプリではAWS AppSyncサービスを利用しGraphQLスキーマ設計だけで仕様を満たすことができた
  • 本アプリの要件を満たすようCognitoの設定やユーザー作成を行った

背景

複数ユーザーが特定の話題にアイデアを出しつつ発言することで、アイデアの創出とブラッシュアップを支援するモバイル向けウェブアプリのProof of Concept(PoC)向け開発を行いました。このアプリケーションの基本的な要件は、いわゆるオンラインチャットアプリのものと同じでした。

  • ユーザー認証
  • 特定の話題を扱う”スレッド”をユーザーが作成する
  • 複数のユーザーはスレッドに参加してチャットやアイデア出し等を行う

また、主に以下の要件も提示されました。

  • 開発スケジュールは短納期
  • インフラはAWSを使用する
  • PoCはクローズドだがアプリはインターネットで公開する

開発スケジュールが短納期という要件をとくに意識しながら、AWSの中から技術選択を開始しました。

Amplifyを採用した理由

オンラインチャットアプリを開発する技術をウェブで検索すると、GraphQLを推奨する記事が多く目につきました。AWSではGraphQLのマネージドサービスAWS AppSyncを提供しています。AppSyncを利用したオンラインチャットサンプルアプリChattや国内外のアプリ実装例をいくつか見つけました。Chatt等は、アプリの構築にウェブアプリケーション向け開発プラットフォームAWS Amplifyを利用していました。

Amplifyを調査していくと、本アプリで必要と思われるAWSバックエンドリソースを迅速に構築できることがわかりました。

  • GraphQLのマネージドサービスApp Sync
  • ユーザー認証基盤 AWS Cognito
  • フロントエンドアプリを配置するオブジェクトストレージAmazon S3
  • CDNであるAmazon CloudFrontAWSリソースをデプロイする時には、Infrastructure as Code の観点が大切であるとされます。これより、AWS CloudFormationTerraformなどのツールを用いてリソースをデプロイしていくのが一般的です。しかしこれらのツールでは、AWSリソースの細かな設定ができますが、通常はコード作成に時間的コストがかかります。

Amplifyではamplify cliを用いてリソースを構築します。 手順は次のようになります。
まず、デプロイ予定のAWSリソース設定を対面型式で行います。その後cliコマンドでデプロイを指示します。 Amplifyは設定情報を自動的にCloudFormationに変換します。 AWSは、変換されたCloudFormationからリソースを構築します。リソースの対面設定とデプロイ指示は短時間かつ簡単にできます。
AWSリソースを素早く簡単にデプロイできることが分かりました。

また、サンプルアプリのコードや実装例を参考に、試験的なアプリを組んでみたところ2日程度でテスト実装ができました。以上のことから、Amplifyを採用することにしました。

システム全体

下記の図は、今回のアプリケーションのために構築するAWSアーキテクチャ図です。

アーキテクチャの構築コストだけでなく管理コストも抑えるために、DBにはAmazon DynamoDBを選択しました。マネージドサービスのみでアーキテクチャを設計しました。

ユーザーは、CloudFrontからHTTPSで配信されるウェブアプリケーションを取得します。このアプリ経由でCognitoによるユーザー認証を行います。認証後、AppSyncのサービスを利用してオンラインチャットを行います。

以下の節では、Amplifyを用いてバックエンド開発したなかで、ユーザー認証、Graphスキーマ作成、アプリ公開のそれぞれにおいて紹介させていただきたい点をまとめました。

ユーザー認証基盤の設定

PoCはクローズドで行われることが計画段階で決まっていました。 ユーザー認証まわりの要件は、以下のようなものがありました。

  • テストユーザーはサインアップしない
    • テストユーザーはテスト前に報知したユーザー名とパスワードでサインインする
  • テストユーザーはパスワードを変更しない

Cognitoでよく用いられるサインアップ、サインイン手順は、次のようになります。まず、ユーザーはサインアップします。その直後にCognitoよりメールが自動配信されます。このメールに記載された一時的なパスワードを利用してユーザーはログインします。ログイン後にパスワードを変更するように要求されます。

本アプリはユーザーがサインアップをおこなわず、またメールアドレスも使用しません。 $ amplify add auth コマンドでおこなうCognitoリソースの設定において、要件にあうように以下の設定をおこないました。

  • How do you want users to be able to sign inusername
  • Email based user registration/forgot passwordを Disable

Cognitoユーザーを事前に作成する方法は後述いたします。

AppSync GraphQLスキーマ作成

GraphQLスキーマでデータモデルを設計します。チャットの仕様から、以下の様なデータ種別とデータ構造としました。

  • 特定の話題を複数人で投稿する場を表すThread
    • スレッドは投稿データPostを複数持つ
  • 投稿データPost中に保持するデータ種類を以下に分ける
    • ユーザーがスレッドに参加したことを表すUserAttend
    • ユーザーの発言を表すChat
    • 発言に対して”イイね”を行うGood
    • Slack等にある特定の発言を簡単に参照できる”ピン留め”を行うPin

データの編集に関して以下のような要件がありました。

  • ユーザーは1度作ったデータの変更や削除を行わない
  • いわゆるユーザーグループ機能はない

ユーザーに対して、変更と削除権限をつける必要がなくなりました。これにより、データモデルだけでなく、フロントエンドアプリの開発の実装工数を小さくできました。
また、ユーザーグループを考慮する必要がなくなりました。

結果として、以下のGraphQLスキーマを設計しました。

 
type User @model
@auth(rules: [
  {allow: owner, operations:[create, read]}
  {allow: private, operations: [read]}
])
{
  id: ID!
  handleName: String!
}

type Thread @model 
@auth(rules: [
    {allow: owner,  ownerField: "owner", operations:[create, read]}
    {allow: private, operations: [read]}
  ])
{
  id: ID!
  owner: User! @connection
  title: String!
  createdAt: AWSDateTime!
  posts: [Post] @connection(keyName: "byThread", fields: ["id"])
}

type Post @model 
@key(name: "byThread", fields:["threadId", "createdAt"])
@key(name: "postSortByCreatedAt", fields:["threadId", "createdAt"], queryField:"listPostSortByCreatedAt")
@auth(rules: [
    {allow: owner,  ownerField: "owner", operations:[create, read]}
    {allow: private, operations: [read]}
  ])
{
  id: ID!
  threadId: ID!
  owner: User! @connection
  createdAt: AWSDateTime!
  userAttend: UserAttend @connection
  chat: Chat @connection
}

type UserAttend @model
@key(name: "userAttendSortByCreatedAt", fields:["threadId", "createdAt"], queryField:"listUserAttendSortByCreatedAt")
@auth(rules: [
    {allow: owner, operations:[create, read]}
    {allow: private, operations: [create, read]}
  ])
{
  id: ID!
  owner: User! @connection
  threadId: ID!
  createdAt: AWSDateTime!
}

type Chat @model
@key(name: "chatSortByCreatedAt", fields:["threadId", "createdAt"], queryField:"listChatSortByCreatedAt")
@auth(rules: [
    {allow: owner, operations:[create, read]}
    {allow: private, operations: [read]}
  ])
{
  id: ID!
  contents: String!
  owner: User! @connection
  threadId: ID!
  createdAt: AWSDateTime!
}

type Pin @model
@key(name: "pinSortByCreatedAt", fields:["threadId", "createdAt"], queryField:"listPinSortByCreatedAt") 
@auth(rules: [
    {allow: owner,  operations:[create, read]}
    {allow: private, operations: [create, read]}
  ])
{
  id: ID!
  postId: ID!
  isOn: Boolean!
  owner: User! @connection
  threadId: ID!
  createdAt: AWSDateTime!
}

type Good @model
@key(name: "listGoodSortByCreatedAt", fields:["threadId", "createdAt"], queryField:"listGoodSortByCreatedAt") 
@auth(rules: [
    {allow: owner,  operations:[create, read]}
    {allow: private, operations: [create, read]}
  ])
{
  id: ID!
  postId: ID!
  isOn: Boolean!
  owner: User! @connection
  threadId: ID!
  createdAt: AWSDateTime!
}

@model注釈は、記述したデータ型毎にDynamoDBのテーブルを作成することを指示します。
上記@auth注釈の書き方で、データ作成者がデータの作成と読み込み権限をもち、Cognitoへ認証したユーザーが読み込み権限を持つことを指定しています。
また@key(name:..., field:["threadId", "createdAt"], qeuryField...)注釈を設定しました。これは、各データをthreadIDごとに分け、さらに”createdAt”順に並べることを指定します。この指定により、アプリの仕様上データアクセス効率が上がりました。

このスキーマを設計していくのに、Amplifyのmock機能が非常に有用でした。amplify mockコマンドで、ローカル環境にGraphiQL IDEを立ち上げられます。
コマンド中にGraphQLスキーマを変更すると、その変更を検知して環境に自動的に反映してくれます。この機能によりスピーディに「スキーマ変更→スキーマの確認」のサイクルが回せました。結果として、この機能が開発の時間的コストの縮小に寄与しました。

Cognitoユーザーを開発側で事前作成する

PoCに参加するユーザーのCognitoユーザーアカウントをaws CLIを用いて事前に作成しました。

まずはaws cognito-idp admin-create-userコマンドでCognitoユーザーを作成します。

 aws cognito-idp admin-create-user \
  --user-pool-id ${POOLID} \
  --username ${USERNAME} \
  --no-force-alias-creation \
  --profile ${PROFILE} \
  --region ${REGION}

Cognitoユーザーは作成されますが、Cognito上のステータスがFORCE_CHANGE_PASSWORD状態となります。これは、ユーザーがログインすると、パスワードを変更するよう要求される状態をしめします。このままですと、「テストユーザーはパスワードを設定・変更しない」という要件を満たせません。ですので、開発側でパスワードを設定し、かつ恒久的な状態にします。これをcognito-idp admin-set-user-passwordコマンドで行います。

aws cognito-idp admin-set-user-password \
  --user-pool-id ${POOLID} \
  --username ${USERNAME} \
  --password ${PASSWORD} \
  --permanent \
  --profile ${PROFILE} \
  --region ${REGION}

これでテストユーザーを作成できました。

指定したドメインでアプリケーションを公開する

フロントエンドアプリを配信するため、オブジェクトストレージとCDNのリソース構築設定をamplify add hostingコマンドで行います。設定後に、amplify publishコマンドでリソースの構築とアプリケーションの公開を同時にできます。この時に、エンドポイントは以下の型式が自動的に割り当てられます。

https://XXXXXXXXX.cloudfront.net

要件に「ドメインを指定してHTTPSアクセスできるようにする」がありました。要件を満たすために、CloudFrontにある機能”代替ドメイン(CNAME)の設定”を行いました。

AWSではドメインを取得するところから、CloudFront Distributionのリソースにドメインでアクセスできるように設定するまで、すべてAWS内のサービス内で手続きを完了できます。このように手続きすると、各手続きの入力項目に値の候補を自動的に設定してくれます。

  1. Amazon Route53でドメインを登録する
  2. AWS Certificate Manager (ACM)でSSL証明書をリクエストする
  3. SLL証明書を用いてCloudFrontでCNAME代替の設定をする
  4. Route53のエイリアスレコードを設定する

取得するドメイン名やAWSの認可状況にもよりますが、本アプリの場合では上記の手続きは約2時間で完了できました。

所感

Amplifyを用いることで、対面型式でAWSリソースの構築設定しmock機能を利用してGraphQLスキーマを素早く設計しcliコマンドでバックエンドを簡単にデプロイができました。結果として、アプリバックエンド構築コストを大幅に下げ、かつ短期間で行うことができました。

しかし、プロジェクトの要件によっては、AWSリソースの詳細な設定や連携が必要になります。そうなると、Amplifyでは要件を満たすのが難しいのではないかと感じます。また、他プロジェクトとの共通化という観点はAmplifyにはないようです。ですので、これらが求められる時には、CloudFormationやTerraformを用いて開発する必要があります。プロジェクトの要件に合わせて最適な選択肢をとることが肝要であることも感じました。

本記事がAmplifyを用いたアプリケーション開発の参考になれば幸いです。

なお、GraphQLの有用性の詳細な説明がFintanのAWS AppSync(GraphQL)を用いたバックエンドの構築事例に掲載されています。ご興味があればこちらの記事もあわせてご参照ください。


本コンテンツはクリエイティブコモンズ(Creative Commons) 4.0 の「表示—継承」に準拠しています。