AppSyncでリアルタイム機能実装

Arata Kurokawa
nextbeat-engineering
13 min readMay 12, 2022

ネクストビートでWebエンジニアとして働いている黒川と申します。
ネクストビートは2021年6月入社で、保育士バンク!コネクトというサービスの開発を担当しています。

今回は、リアルタイム機能のサンプルをAppSyncとAngularで実装してみたので、紹介したいと思います。

AppSyncとは?

AWS AppSync は、Amazon DynamoDB、 AWS Lambda、および HTTP APIを含む複数のソースからのデータを組み合わせて、アプリケーション開発者が利用できる堅牢でスケーラブルな GraphQL インターフェイスです。

GraphQLについてはドキュメント記事があるので、ご覧いただければと思います。

ユースケース

ユースケースはいくつかありますが、今回は下記のユースケースで紹介できればと思います。
https://aws.amazon.com/jp/appsync/#Use_cases

バックエンドで変更があった場合、リアルタイムで画面に反映するという要件があったので検証しました。

構成

SNS TopicのメッセージをトリガーにしてLambdaを起動し、
AppSyncに通知し、監視しているユーザにメッセージを伝達させます。

監視可能なデータであるか判定するための認証・認可の機能もあるのでこちらも紹介していきます。

画面イメージ

バックエンドでメッセージが追加されると、
リアルタイムで画面に表示するようにします。

ここからは設定や実装について紹介していきます。

AppSync

スキーマ(Schema)

type ChatMessage @aws_lambda @aws_iam {
id: ID!
chatRoomId: Int!
message: String!
}
type ChatRoom {
id: ID!
name: String!
}
type Mutation {
notifyAddedMessage(id: ID!, chatRoomId: Int!, message: String!): ChatMessage!
@aws_iam
}
type Subscription {
onAddChatMessage(chatRoomId: Int!): ChatMessage
@aws_subscribe(mutations: ["notifyAddedMessage"])
}

Mutationに通知用のnotifyAddedMessage、
Subscriptionにユーザーが監視するonAddChatMessageを定義します。

onAddChatMessageに@aws_subscribeを追加することで、
Mutationに定義されているnotifyAddedMessageの結果が監視しているユーザにリアルタイムで反映されます。

データソース

永続的ストレージシステム(RDB,NoSQL)またはトリガー(lambda,Httpなど)を設定します。

今回はデータソースにアクセスしないので、Noneを選択しています。

リゾルバー

notifyAddedMessageはChatMessageが返り値になっており、
リクエストの引数を元にChatMessageという結果を返すため、
Resolverというものを設定する必要があります。

リクエストマッピングテンプレート、
レスポンスマッピングテンプレートいうものがあります。

  • リクエストマッピングテンプレート
    データソースが処理するためのpayloadを生成します。
    例えばデータソースがlambdaの場合は、lambda側でpayloadを受け取りそれを元に処理します。
  • レスポンスマッピングテンプレート
    データソースから取得した情報を元にレスポンスを生成します。

今回はデータソースはNoneに設定したので、
引数をそのままレスポンスとして返します。

認証・認可

AppSyncではいくつかの認証方法がありますが、lambda認証を使用します。

クライアントからトークンを受け取り認証処理を行いますが、
今回はサンプルなので固定値です。

exports.handler = async (event, context) => {
const token = event.authorizationToken

if (token === "Fail") {
throw Error(
"Purposefully thrown exception in Lambda Authorizer."
)
}

if (token === "Authorized") {
return {
'isAuthorized': true,
'resolverContext': {
'key': 'value'
}
}
}

if (token === 'Unauthorized') {
return {
'isAuthorized': false
}
}

return {}
};

lambdaでauthorizationTokenを受け取ることができます。
返す値にはisAuthorizedは必須で認証が成功した場合はtrueを返します。

resolverContextに任意の値をセットすることができ、
リゾルバー内で参照することで認可処理も行うことができます。

実装ができたらAppSyncでlambda認証を行うよう設定をします。

設定からデフォルトの認証モードを変更します。
作成したlambda関数を指定して保存します。

フロント実装(angular)

aws-amplifyというライブラリを使用します。

npm install --save aws-amplify

最初に設定ファイルをダウンロードします。
「アプリと統合する」の「JavaScript」タグを選択し、
下にある「設定をダウンロード」をクリックするとダウンロードできます。

ダウンロードしたファイルをsrcの配下に置きます。

const awsmobile =  {
"aws_appsync_graphqlEndpoint": "https://xx.amazonaws.com/graphql",
"aws_appsync_region": "xxxxxxxxxxxxxxxx",
"aws_appsync_authenticationType": "AWS_LAMBDA",
"aws_appsync_apiKey": "xxxxxxxxxxxxxxxxxxxxxx",
};
export default awsmobile;

ダウンロードしたファイルをsrcの配下に置き、
main.tsで初期化を行います。

import { Amplify } from 'aws-amplify'
import aws_exports from './aws-exports'
Amplify.configure(aws_exports)

設定後、バックエンドの変更が監視できるようになります。

  • authMode: AWS_LAMBDA
  • authToken: Authorized

API.graphqlの引数に上記の値を渡します。

監視を開始するとchatMessages$がリアルタイムに更新され、
画面に反映されるようになります。

import { Component, OnInit } from '@angular/core'
import { API } from 'aws-amplify'
import { Observable } from 'zen-observable-ts'
import { BehaviorSubject } from "rxjs"
const ON_ADD_CHAT_MESSAGE =
`subscription OnAddChatMessage($chatRoomId: Int!) {
onAddChatMessage(chatRoomId: $chatRoomId) {
id
chatRoomId
message
}
}`
@Component({
templateUrl: './chat-room.component.html',
styleUrls: ['./chat-room.component.scss']
})
export class ChatRoomComponent implements OnInit {
chatMessages$ = new BehaviorSubject<ChatMessage[]>([])

constructor(
private route: ActivatedRoute
) {
}
ngOnInit() {
const chatRoomId = 1
const observable = API.graphql(
{
query: ON_ADD_CHAT_MESSAGE,
variables: { chatRoomId: chatRoomId },
authMode: 'AWS_LAMBDA',
authToken: 'Authorized'
}
) as Observable<any>

// 監視
observable.subscribe(response => {
const onAddChatMessage =
response?.value?.data?.onAddChatMessage

if (onAddChatMessage) {
const current = this.chatMessages$.value
const added = [
...current,
response.value.data.onAddChatMessage
]
this.chatMessages$.next(added)
}
})
}
}

notifyAddedMessageをlambdaで実行

バックエンドの変更をSNSに通知し、
lambdaでnotifyAddedMessageを実行するようにします。

authModeをここではAWS_IAMにしています。
notifyAddedMessageはバックエンドからのみ呼び出しを想定しているためです。
lambdaにAppSyncのクエリーを実行する権限を付与する必要があります。

require("isomorphic-fetch");
const aws = require('aws-sdk');
const AWSAppSyncClient = require('aws-appsync').default;
const gql = require('graphql-tag');

const aws_exports = require('./aws-exports');

const mutationGql = gql(`
mutation NotifyAddedMessage($id: ID!, $chatRoomId: Int!, $message: String!) {
notifyAddedMessage(id: $id, chatRoomId: $chatRoomId, message: $message){
id
chatRoomId
message
}
}`);

exports.handler = async (event) => {
// SNSからのメッセージをparse
const snsMessage = JSON.parse(event.Records[0].Sns.Message)
const id = snsMessage.id
const chatRoomId = snsMessage.chatRoomId
const message = snsMessage.message

const client = new AWSAppSyncClient({
disableOffline: true,
url: aws_exports.aws_appsync_graphqlEndpoint,
region: aws_exports.aws_appsync_region,
auth: {
type: "AWS_IAM",
credentials: ()=> aws.config.credentials
}
})

await client.mutate({
mutation: mutationGql,
variables: {
id: id,
chatRoomId: chatRoomId,
message: message
}
});

return {
id: id,
chatRoomId: chatRoomId,
message: message
}
};

IAM認証するようスキーマに@aws_iamを設定します。

type ChatMessage @aws_lambda @aws_iam {
id: ID!
chatRoomId: Int!
message: String!
}
type Mutation {
notifyAddedMessage(id: ID!, chatRoomId: Int!, message: String!): ChatMessage!
@aws_iam
}

SNSで下記のようなメッセージを送信すると、
監視している画面でメッセージがリアルタイムに反映されます。

{
"id": 30,
"chatRoomId": 1,
"message": "sns topic message"
}
{
"id": 31,
"chatRoomId": 1,
"message": "sns topic message 2"
}

ここまで読んでいただきありがとうございました!

今回はリアルタイム機能のサンプルを、AppSyncとAngularで実装してみました。
リアルタイム機能の仕組みはAppSyncにあるので、簡単に実装ができました。
是非使ってみてください。

We are Hiring!

株式会社ネクストビートでは

「人口減少社会において必要とされるインターネット事業を創造し、ニッポンを元気にする。」
を理念に掲げ一緒に働く仲間を募集しております。

バックエンドにはPlay Framework(言語はScala)、フロントエンドの開発には主にAngularを用いています。フルスタックに開発したい!という方のご応募をお待ちしております。

--

--