Upgrade to Pro — share decks privately, control downloads, hide ads and more …

コードで確認する、AWS Amplify API (GraphQL) Subscriptions によるリアルタイム通知のパターン #JAWSDAYS2021 #jawsdays2021_C

コードで確認する、AWS Amplify API (GraphQL) Subscriptions によるリアルタイム通知のパターン #JAWSDAYS2021 #jawsdays2021_C

JAWS DAYS 2021
Track C 13:00-13:40
https://jawsdays2021.jaws-ug.jp/timetable/track-c-1300/

セッション内容
Amplify の API (GraphQL) では、 AppSync をベースとした Subscriptions 機能があります。 しかし、複数の機能・設定に関わる機能であるため、関連する記述がドキュメント上のあちこちにある状態です。
そのため、まとまった理解を得るためにやや手間がかかってしまいます。

このセッションでは AWS Amplify の API (GraphQL) Subscriptions に焦点を当てて話をしていきます。
具体的なコードを使い、シンプルな例からドキュメントに明記されていない使い方までを確認していくことで、 Subscriptions によるリアルタイム通知のパターンの理解を、一緒に深めていきたいと思います。

13725f35541aa680ed5f85d16b85947a?s=128

Kihara, Takuya

March 20, 2021
Tweet

Transcript

  1. コードで確認する、 AWS Amplify API (GraphQL) Subscriptions による リアルタイム通知のパターン 木原 卓也

    / @tacck Track C / 13:00-13:40
  2. 自己紹介 名前 木原卓也 / @tacck 所属 株式会社ノースディテール コミュニティ ゆるWeb勉強会@札幌 (主催)

    Amplify Japan User Group (運営メンバー) 好きなAWSサービス Amazon Route 53 AWS Amplify 好きなフィギュアスケートの技 スプレッド・イーグル
  3. 話すこと AWS Amplify の API (GraphQL) 特に Subscription 機能とそれに関連するところ 実例として

    Vue.js (2.x系) によるコードの一部 サンプルコード: https://github.com/tacck/sample-amplify-subscriptions 上級者・開発者向けのトラックのため、 はしょる部分が多いところはご容赦ください。 わかりにくい部分は、ぜひ Twitter で指摘いただければ、 ブログやどこかの JAWS-UG 含む勉強会で補足してい きたいです。 発表できる場所あれば、どこへでも!(オンライン)
  4. 話さないこと AWS Amplify の詳細な機能 AWS Amplify の CLI の詳細な使い方 AWS

    Amplify を実際にサービスで使った場合の話 その他のサービス Vue.js の実装方法 上級者・開発者向けのトラックのため、 はしょる部分が多いところはご容赦ください。 わかりにくい部分は、ぜひ Twitter で指摘いただければ、 ブログやどこかの JAWS-UG 含む勉強会で補足してい きたいです。 発表できる場所あれば、どこへでも!(オンライン)
  5. 人間は、 Webシステムで 手軽にリアルタイム通知をしたがる 生き物

  6. リアルタイムな通知、”手軽に” 作れる? チャット的な仕組みをサイトに組み込んで、 ユーザーからの意見をすぐに受け取れるようにしたい。 多数のクライアントに、更新情報を一斉に通知したい。 ユーザーの利用状況を リアルタイムにダッシュボードで見たい。 Etc…

  7. リアルタイムな通知、実装大変ですよね WebSocket で作ることになる場合が多い • EC2やECSなどの常時稼働インスタンス + サーバー機能実装 • API Gateway

    の WebSocket でサーバー機能実装 • AWS AppSync • AWS Amplify
  8. 常時稼働インスタンス + サーバー機能実装 EC2 や ECS などを使って、インスタンス・コンテナを常時稼働。 WebSocket の制御・管理機能を、アプリケーションサーバー内に実装。 •

    大規模で利用率が常に高いのであれば、この選択。 • 多くのユースケースでは、過剰なインフラコストになりがち。 • インスタンス・ミドルウェア・コンテナのメンテナンスコストが必要。 • WebSocket クライアントの制御・管理機能をサーバー機能として実装必要。 本来作りたいサービスの機能「以外」の手間が大きい。
  9. API Gateway の WebSocket でサーバー機能実装 API Gateway + Lambda を使うことで、

    Serverless WebSocket 環境を構築可能。 WebSocket の制御・管理機能の一部を、 API Gateway が担う。 • 大規模で利用率が常に高いのであれば、この選択。 • 多くのユースケースでは、過剰なインフラコストになりがち。 • インスタンス・ミドルウェア・コンテナのメンテナンスコストが必要。 • WebSocket クライアントの制御・管理機能をサーバー機能として実装必要。 インフラ管理コストは低減できるが、 WebSocket クライアントの制御・管理機能の実装コストは引き続き残る。
  10. AWS AppSync GraphQL のフルマネージドサービス。 GraphQL の規格として Subscription 機能が存在。 • GraphQL

    を使い慣れていないと、導入にコストがかかる。 • リゾルバを VTL で定義できるが、 VTL 自体の学習コストがかかる。 AppSync / GraphQL の導入経験がないプロジェクトでは、 “手軽に” とはいかない。
  11. そこで AWS Amplify API (GraphQL) ライブラリによって、 AppSync を “手軽に” 利用可能。

    基本的な機能 (Query, Mutation, Subscription) を利用するための 標準的な GraphQL を自動生成してくれる。 開発者は JavaScript で呼び出すだけ。
  12. AWS Amplify で手軽に始められる AWS Amplify の API (GraphQL) 機能を使い Subscriptions

    を利用することで “手軽に” リアルタイムな通知を実装しよう!
  13. AWS Amplify の Subscriptions のパターン 1. Basic / @model ディレクティブのみ

    2. Filtering / @model + type Subscription 定義 3. Auth + Filtering / @model + @auth + type Subscription 定義 4. Multi-Auth + Filtering / 複数の認証先を利用して通知範囲を制御 5. Single Publisher / Lambda を利用した Subscription 制御
  14. 1.Basic / @model ディレクティブ

  15. 1.Basic / @model ディレクティブ もっとも基本的なパターン。 API (GraphQL) 機能を有効にしたら、すぐに使用可能。 書き込み内容 (Mutations)

    を、通知として受け取る。 例えば、誰でも(匿名で)書き込めるチャットサービスで、 誰かが書き込んだメッセージをすぐに画面に反映させる場合。 1.Basic
  16. $ amplify add api Initializing new Amplify CLI version... Done

    initializing new version. Scanning for plugins... Plugin scan successful ? Please select from one of the below mentioned services: GraphQL ? Provide API name: sampleamplifysubscri ? Choose the default authorization type for the API API key ? Enter a description for the API key: ? After how many days from now the API key should expire (1-365): 365 ? Do you want to configure advanced settings for the GraphQL API No, I am done. ? Do you have an annotated GraphQL schema? No ? Choose a schema template: Single object with fields (e.g., “Todo” with ID, name, description) … 1.Basic CLI
  17. type OpenChat @model { id: ID! message: String! } schema.graphql

    export const onCreateOpenChat = /* GraphQL */ ` subscription OnCreateOpenChat { onCreateOpenChat { id message createdAt updatedAt } } `; src/graphql/subscriptions.js export const createOpenChat = /* GraphQL */ ` mutation CreateOpenChat( $input: CreateOpenChatInput! $condition: ModelOpenChatConditionInput ) { createOpenChat(input: $input, condition: $condition) { id message createdAt updatedAt } } `; src/graphql/mutations.js 1.Basic
  18. this.onCreateOpenChatSubscription = API.graphql( graphqlOperation(onCreateOpenChat), ).subscribe({ next: ({ provider, value })

    => { console.log({ provider, value }) this.subscriptionMessages.push(value.data.onCreateOpenChat) }, }) 1.Basic src/views/OpenChat.vue
  19. sendMessage: async function() { const message = await API.graphql( graphqlOperation(createOpenChat,

    { input: { message: this.inputMessage }, }), ) console.log(message) this.messages.push(message.data.createOpenChat) this.inputMessage = '' }, 1.Basic src/views/OpenChat.vue
  20. this.onCreateOpenChatSubscription = API.graphql( graphqlOperation(onCreateOpenChat), ).subscribe({ next: ({ provider, value })

    => { console.log({ provider, value }) this.subscriptionMessages.push(value.data.onCreateOpenChat) }, }) 1.Basic src/views/OpenChat.vue
  21. Subscription 登録 1.Basic API.graphql(graphqlOperation(onCreateOpenChat))

  22. Mutation 実行 1.Basic API.graphql(graphqlOperation(createOpenChat,{…})

  23. リアルタイムに通知 subscribe({next: ({ provider, value }) => {…}}) 1.Basic

  24. 2.Filtering / @model + type Subscription 定義

  25. Filtering / @model + type Subscription 定義 基本的なパターン その2。 Basic

    の場合、すべての書き込み (Mutations) を、通知として受け取る。 そこで、引数として「どの情報を通知として受け取るか」を 指定することで、必要なものだけを受信できるようにする。 例えば、誰でも(匿名で)書き込めるチャットサービスで、 複数のチャットルームがある場合。 必要なのは、自分の開いているルームの最新メッセージのみ。 2.Filtering
  26. type RoomChat @model { id: ID! roomName: String! message: String!

    } type Subscription { onCreateRoomChatByRoomName(roomName: String!): RoomChat @aws_subscribe(mutations: ["createRoomChat"]) } schema.graphql 2.Filtering
  27. this.onCreateMultiRoomChatSubscriptions[roomName] = API.graphql( graphqlOperation(onCreateRoomChatByRoomName, { roomName: roomName }), ).subscribe({ next:

    ({ provider, value }) => { console.log({ provider, value }) this.subscriptionMessages[ value.data.onCreateRoomChatByRoomName.roomName ].push(value.data.onCreateRoomChatByRoomName) }, }) src/views/MultiRoomOpenChat.vue 2.Filtering
  28. sendMessage: async function() { const message = await API.graphql( graphqlOperation(createRoomChat,

    { input: { message: this.inputMessage, roomName: this.roomName }, }), ) console.log(message) this.messages[this.roomName].push(message.data.createRoomChat) this.inputMessage = '' }, src/views/MultiRoomOpenChat.vue 2.Filtering
  29. this.onCreateMultiRoomChatSubscriptions[roomName] = API.graphql( graphqlOperation(onCreateRoomChatByRoomName, { roomName: roomName }), ).subscribe({ next:

    ({ provider, value }) => { console.log({ provider, value }) this.subscriptionMessages[ value.data.onCreateRoomChatByRoomName.roomName ].push(value.data.onCreateRoomChatByRoomName) }, }) src/views/MultiRoomOpenChat.vue 2.Filtering
  30. Subscription 登録 API.graphql(graphqlOperation(onCreateRoomChatByRoomName, { roomName: ‘Room1’ })) API.graphql(graphqlOperation(onCreateOpenChat, { roomName:

    ‘Room2’ })) 2.Filtering
  31. Mutation 実行 API.graphql(graphqlOperation(createRoomChat, { input: { message: this.inputMessage, roomName: ‘Room1’}})

    2.Filtering
  32. Room1 のみ受信可能 2.Filtering subscribe({next: ({ provider, value }) => {…}})

  33. 3.Auth + Filtering / @model + @auth + type Subscription

    定義
  34. 3.Auth + Filtering / @model + @auth + type Subscription

    定義 少し応用的なパターン。 特定の認証方法で認証されたユーザーのみが書き込みを行ない、 同様に認証されたユーザーが通知を受け取る。 例えば、会員制のサイト内でのメンバー間のチャットや、 メンバーとオペレーターのメッセージのやりとり。 3.Auth + Filtering
  35. type CloseRoomChat @model @auth(rules: [{ allow: owner, provider: userPools }])

    { id: ID! roomName: String! message: String! } type Subscription { onCreateCloseRoomChatByRoomName(roomName: String!): CloseRoomChat @aws_subscribe(mutations: ["createCloseRoomChat"]) } schema.graphql 3.Auth + Filtering
  36. this.onCreateMultiRoomChatSubscriptions[roomName] = API.graphql( graphqlOperation(onCreateCloseRoomChatByRoomName, { roomName: roomName, }), ).subscribe({ next:

    ({ provider, value }) => { console.log({ provider, value }) this.subscriptionMessages[ value.data.onCreateCloseRoomChatByRoomName.roomName ].push(value.data.onCreateCloseRoomChatByRoomName) }, }) src/views/CloseChat.vue 3.Auth + Filtering
  37. sendMessage: async function() { const message = await API.graphql( graphqlOperation(createCloseRoomChat,

    { input: { message: this.inputMessage, roomName: this.roomName }, }), ) src/views/CloseChat.vue 3.Auth + Filtering
  38. this.onCreateMultiRoomChatSubscriptions[roomName] = API.graphql( graphqlOperation(onCreateCloseRoomChatByRoomName, { roomName: roomName, }), ).subscribe({ next:

    ({ provider, value }) => { console.log({ provider, value }) this.subscriptionMessages[ value.data.onCreateCloseRoomChatByRoomName.roomName ].push(value.data.onCreateCloseRoomChatByRoomName) }, }) src/views/CloseChat.vue 3.Auth + Filtering
  39. 認証済みユーザーのみSubscription 登録可能 API.graphql(graphqlOperation( onCreateCloseRoomChatByRoomName, { roomName: ‘Room1’ } )) 3.Auth

    + Filtering
  40. 認証済みユーザーで Mutation 実行 API.graphql(graphqlOperation( createCloseRoomChat, { input: { message: this.inputMessage,

    roomName: ‘Room1’} } ) 3.Auth + Filtering
  41. 認証済みユーザーのみ受信可能 subscribe({next: ({ provider, value }) => {…}}) 3.Auth +

    Filtering
  42. 4.Multi-Auth + Filtering / 複数の認証を利用して通知範囲を制御

  43. 4.Multi-Auth + Filtering / 複数の認証を利用して通知範囲を制御 応用的なパターン。 特定の認証方法で認証されたユーザーのみが書き込みを行ない、 別の認証方法で認証されたユーザーが通知を受け取る。 つまり、機能と権限の調整を行なうことができる。 例えば、書き込みは認証済みユーザーのみ可能だが、

    読み込みや通知の受信は誰でもできるような掲示板サイト。 4.Multi-Auth + Filtering
  44. type CloseRoomChat @model @auth( rules: [ { allow: owner, provider:

    userPools } { allow: public, provider: apiKey, operations: [read] } ] ) { id: ID! roomName: String! message: String! } type Subscription { onCreateCloseRoomChatByRoomName(roomName: String!): CloseRoomChat @aws_subscribe(mutations: ["createCloseRoomChat"]) @aws_api_key } schema.graphql 4.Multi-Auth + Filtering
  45. this.onCreateMultiRoomChatSubscriptions[roomName] = API.graphql({ query: onCreateCloseRoomChatByRoomName, variables: { roomName: roomName, },

    authMode: GRAPHQL_AUTH_MODE.API_KEY, }).subscribe({ next: ({ provider, value }) => { console.log({ provider, value }) this.subscriptionMessages[ value.data.onCreateCloseRoomChatByRoomName.roomName ].push(value.data.onCreateCloseRoomChatByRoomName) }, }) src/views/CloseChat.vue 4.Multi-Auth + Filtering
  46. sendMessage: async function() { const message = await API.graphql( graphqlOperation(createCloseRoomChat,

    { input: { message: this.inputMessage, roomName: this.roomName }, }), ) src/views/CloseChat.vue 4.Multi-Auth + Filtering
  47. this.onCreateMultiRoomChatSubscriptions[roomName] = API.graphql({ query: onCreateCloseRoomChatByRoomName, variables: { roomName: roomName, },

    authMode: GRAPHQL_AUTH_MODE.API_KEY, }).subscribe({ next: ({ provider, value }) => { console.log({ provider, value }) this.subscriptionMessages[ value.data.onCreateCloseRoomChatByRoomName.roomName ].push(value.data.onCreateCloseRoomChatByRoomName) }, }) src/views/CloseChat.vue 4.Multi-Auth + Filtering
  48. API_KEY で Subscription 登録 4.Multi-Auth + Filtering API.graphql({ query: …,

    variables: {…}, authMode: GRAPHQL_AUTH_MODE.API_KEY, })
  49. 認証済みユーザーで Mutation 実行 4.Multi-Auth + Filtering API.graphql(graphqlOperation( createCloseRoomChat, { input:

    { message: this.inputMessage, roomName: ‘Room1’ } } )
  50. 全ユーザーが受信可能 4.Multi-Auth + Filtering subscribe({ next: ({ provider, value })

    => {…}})
  51. 5.Single Publisher / Lambda を利用した Subscription 制御

  52. 4までの課題 1つの Mutation に対して、必ず 1つの通知が発生する。 10ユーザーが同時に書き込みをしたら? AppSync は 10の通知 を

    10ユーザーに(ほぼ)「同時」に送信 各ユーザーは 10の通知 を(ほぼ)「同時」に受信 つまり、ユーザー数が増えると、 AppSync が同時に送信すべき通知が 指数関数で増加する。 ユーザーの受信する通知は線形とはいえ、 マシン・ブラウザへの負荷に直結する。 5.Single Publisher
  53. 5.Single Publisher / Lambda を利用した Subscription 制御 Multi-Auth のさらに応用したなパターン。 通知を行なわない

    Mutation と、通知を行なう Mutation を用意し、 通知を行なう方のみユーザー認証を必要とする作りとする。 通知用の Mutation には、 @function (Lambda) を利用する。 例えば、多数の人が同時に利用・書き込みをし、 特定のユーザーが結果を全体へ定期的に通知する。 今回利用してもらった、デモのアンケートシステムがこれ。 5.Single Publisher
  54. type ResponseLateRoomChat @model(subscriptions: null) @auth( rules: [ { allow: owner,

    provider: userPools } { allow: public, provider: apiKey, operations: [read] } ] ) { id: ID! roomName: String! message: String! createdAt: AWSDateTime } schema.graphql 5.Single Publisher
  55. type Mutation { postResponses(input: PostResponsesInput): PostResponsesOutput @function(name: ”XXXXXXXXXX-${env}") } type

    Subscription { onPostResponses(roomName: String!): PostResponsesOutput @aws_subscribe(mutations: ["postResponses"]) @aws_api_key } schema.graphql 5.Single Publisher
  56. this.onPostResponses[roomName] = API.graphql({ query: onPostResponses, variables: { roomName: roomName, },

    authMode: GRAPHQL_AUTH_MODE.API_KEY, }).subscribe({ next: ({ provider, value }) => { console.log('onPostResponses', { provider, value }) value.data.onPostResponses.items.map(item => { this.subscriptionMessages[item.roomName].push(item) }) }, }) src/views/CloseChat.vue 5.Single Publisher
  57. API.graphql( graphqlOperation(createResponseLateRoomChat, { input: { message: this.inputMessage, roomName: this.roomName },

    }), ) src/views/ResponseLateRoomChatClient.vue 5.Single Publisher
  58. this.interval = setInterval(async () => { const getStartTime = new

    Date(Date.now() - this.intervalTime) const postedMessages = await API.graphql({ query: listResponseLateRoomChats, variables: { filter: { createdAt: { ge: getStartTime.toISOString() } }, }, authMode: GRAPHQL_AUTH_MODE.API_KEY, }) console.log('interval listResponseLateRoomChats', postedMessages) if (postedMessages.data.listResponseLateRoomChats.items.length > 0) { this.rooms.forEach(roomName => { const messages = postedMessages.data.listResponseLateRoomChats.items.filter( item => item.roomName === roomName, ) if (messages && messages.length > 0) { this.postResponsesToAll(roomName, messages) } }) } }, this.intervalTime) src/views/ResponseLateRoomChatClient.vue 5.Single Publisher
  59. postResponsesToAll: async function(roomName, messages) { const message = await API.graphql(

    graphqlOperation(postResponses, { input: { roomName: roomName, items: messages, }, }), ) console.log('postResponsesToAll', message) }, src/views/ResponseLateRoomChatClient.vue 5.Single Publisher
  60. this.onPostResponses[roomName] = API.graphql({ query: onPostResponses, variables: { roomName: roomName, },

    authMode: GRAPHQL_AUTH_MODE.API_KEY, }).subscribe({ next: ({ provider, value }) => { console.log('onPostResponses', { provider, value }) value.data.onPostResponses.items.map(item => { this.subscriptionMessages[item.roomName].push(item) }) }, }) src/views/ResponseLateRoomChatClient.vue 5.Single Publisher
  61. @function の方を Subscription 登録 5.Single Publisher API.graphql({ query: onPostResponses, variables:

    {…}, authMode: GRAPHQL_AUTH_MODE.API_KEY, })
  62. 登録用 Mutation 実行 API.graphql( graphqlOperation(createResponseLateRoomChat, { input: { message: this.inputMessage,

    roomName: this.roomName }, }), ) 5.Single Publisher
  63. 通知用 データを Query で取得 API.graphql({ query: listResponseLateRoomChats, variables: {…}, authMode:

    GRAPHQL_AUTH_MODE.API_KEY, }) 5.Single Publisher
  64. 通知用 Mutation 実行 API.graphql( graphqlOperation(postResponses, { input: { roomName: roomName,

    items: messages } }) ) 5.Single Publisher
  65. コントロールされた通知を受信 subscribe({ next: ({ provider, value }) => {…}}) 5.Single

    Publisher
  66. まとめ

  67. AWS Amplify の Subscriptions のパターン 1. Basic / @model ディレクティブのみ

    2. Filtering / @model + type Subscription 定義 3. Auth + Filtering / @model + @auth + type Subscription 定義 4. Multi-Auth + Filtering / 複数の認証先を利用して通知範囲を制御 5. Single Publisher / Lambda を利用した Subscription 制御
  68. もっと AWS Amplify を知りたくなったら

  69. None
  70. 発表者 タイトル 木村宏明氏 (株式会社スカラパートナーズ) バックエンドエンジニアがAmplifyで中規模案件に挑戦した話 青木 光平氏 (O: 株式会社 CTO)

    Amplify によるWeb&ネイティブアプリ開発とチーム運営 ララタン!!肉屋のCTO 谷 耕平 氏(株式会社ビータス CTO) toCサービスをAWS Amplify使って構築、運用しているお話 南島 康一 氏/向 宇 氏 (株式会社マーケットエンター プライズ Tech Lead/Front-end Engineer) レガシーなWebシステムにAmplifyを生やしてみた話 松井 英俊 氏 (株式会社スタートアップテクノロジー) Amplifyで作る配信サイト/ビデオチャットWebアプリ
  71. None