👻
AppSync における汎用 Pub/Sub API をサクッと試す
これは何
- AWS AppSync における汎用 Pub/Sub API を試したメモ
- AppSync, Amplify Framework, React を使用してシンプルな Pub/Sub アプリケーションを動かす
- 汎用 Pub/Sub API は AppSync や GraphQL 自体に詳しくなくとも、リアルタイムのアプリケーション構築に便利に使うことが出来る
実際に試す手順
マネジメントコンソールから AppSync の GraphQL API を作成
マネジメントコンソールで AppSync を開く
API の作成から「汎用リアルタイム API を作成」を選択して「開始」をクリック
適当な API 名を設定
API の作成完了を待つ
API の作成が完了したら、画面下部の「ダウンロード」をクリックして、生成された JavaScript コード (generated.js) を取得する
React App の実装
create-react-app で React App の雛形を作成
% npx create-react-app appsync-generic-pubsub-sample
npm install aws-amplify
で Amplify Framework をインストール
% npm install aws-amplify
generated.js をプロジェクトに配置
先程ダウンロードした generated.js を src
ディレクトリにコピーしておく
App.js を実装
マネジメントコンソールで生成されたコードは import の書き方が古いため、以下のように書き換え
import './App.css'
import { useEffect, useState } from 'react'
import { Amplify } from '@aws-amplify/core'
import * as gen from './generated'
App.js 全文
import './App.css'
import { useEffect, useState } from 'react'
import { Amplify } from '@aws-amplify/core'
import * as gen from './generated'
Amplify.configure(gen.config)
function App() {
const [send, setSend] = useState('')
const [received, setReceived] = useState(null)
//Define the channel name here
let channel = 'robots'
//Publish data to subscribed clients
async function handleSubmit(evt) {
evt.preventDefault()
evt.stopPropagation()
await gen.publish(channel, send)
setSend('Enter valid JSON here... (use quotes for keys and values)')
}
useEffect(() => {
//Subscribe via WebSockets
const subscription = gen.subscribe(channel, ({ data }) => setReceived(data))
return () => subscription.unsubscribe()
}, [channel])
//Display pushed data on browser
return (
<div className="App">
<header className="App-header">
<p>Send/Push JSON to channel "{channel}"...</p>
<form onSubmit={handleSubmit}>
<textarea rows="5" cols="60" name="description" onChange={(e) => setSend(e.target.value)}>
Enter valid JSON here... (use quotes for keys and values)
</textarea>
<br />
<input type="submit" value="Submit" />
</form>
<p>Subscribed/Listening to channel "{channel}"...</p>
<pre>{JSON.stringify(JSON.parse(received), null, 2)}</pre>
</header>
</div>
)
}
export default App
generated.js を編集
コピーしてきた generated.js も import の書き方が古いため、以下のように書き換え
import { API, graphqlOperation } from '@aws-amplify/api'
generated.js 全文
import { API, graphqlOperation } from '@aws-amplify/api'
export const config = {
"aws_appsync_graphqlEndpoint": "https://example.appsync-api.ap-northeast-3.amazonaws.com/graphql",
"aws_appsync_region": "ap-northeast-3",
"aws_appsync_authenticationType": "API_KEY",
"aws_appsync_apiKey": "myapikey",
}
export const subscribeDoc = /* GraphQL */ `
subscription Subscribe($name: String!) {
subscribe(name: $name) {
data
name
}
}
`
export const publishDoc = /* GraphQL */ `
mutation Publish($data: AWSJSON!, $name: String!) {
publish(data: $data, name: $name) {
data
name
}
}
`
/**
* @param {string} name the name of the channel
* @param {Object} data the data to publish to the channel
*/
export async function publish(name, data) {
return await API.graphql(graphqlOperation(publishDoc, { name, data }))
}
/**
* @param {string} name the name of the channel
* @param {nextCallback} next callback function that will be called with subscription payload data
* @param {function} [error] optional function to handle errors
* @returns {Observable} an observable subscription object
*/
export function subscribe(name, next, error) {
return API.graphql(graphqlOperation(subscribeDoc, { name })).subscribe({
next: ({ provider, value }) => {
next(value.data.subscribe, provider, value)
},
error: error || console.log,
})
}
/**
* @callback nextCallback
* @param {Object} data the subscription response including the `name`, and `data`.
* @param {Object} [provider] the provider object
* @param {Object} [payload] the entire payload
*/
動作確認
-
npm start
コマンドで React App を localhost で動かす - 2つ のブラウザタブでアプリを開いて、任意の JSON を入力して Submit すればどちらのアプリにもリアルタイムに JSON データが反映されることが確認できる
% npm start
仕組みについて
プロトコル
- 汎用 Pub/Sub API は AppSync GraphQL の Subsription を使用して実現している
- これは WebSocket で Pub/Sub を実現している
GraphQL スキーマとクエリ
以下のような GraphQL スキーマが生成される
type Channel {
name: String!
data: AWSJSON!
}
type Mutation {
publish(name: String!, data: AWSJSON!): Channel
}
type Query {
getChannel: Channel
}
type Subscription {
subscribe(name: String!): Channel
@aws_subscribe(mutations: ["publish"])
}
クエリ例
mutation PublishData {
publish(data: "{\"msg\": \"hello world!\"}", name: "robots") {
data
name
}
}
subscription SubscribeToData {
subscribe(name:"channel") {
name
data
}
}
リゾルバーやデータソースについて
- ミニマムな汎用 Pub/Sub API では、特に Lambda や DynamoDB といったサービス連携は不要なので、シンプルな構成になっている
- Mutation の publish には NoneDatasource というローカルリゾルバーが設定されている
- データソースとしては NoneDatasource が設定されている
- VTL のマッピングテンプレートとして以下のような設定がされている
リクエストマッピングテンプレート
{
"version": "2017-02-28",
"payload": {
"name": "$context.arguments.name",
"data": $util.toJson($context.arguments.data)
}
}
レスポンスマッピングテンプレート
$util.toJson($context.result)
感想
- サクッと Pub/Sub が実現できて便利
- サンプルコードの例では、以下のような汎用性がある
- チャンネルを設定して複数の Pub/Sub を独立して扱える
- 任意の JSON を Pub/Sub で扱える
- 応用として AppSync, Amplify を組み合わせて IAM や Cognito といった認証機能を付けることも可能
- API Gateway の WebSocket API や AWS IoT を使って Pub/Sub を実現することも可能だが、個人的には最もマネージドでサクッと Pub/Sub が実現できて好感触
参考情報
Creating generic pub/sub APIs powered by serverless WebSockets - AWS AppSync
Tutorial: Local Resolvers - AWS AppSync
Simple serverless WebSocket real-time API with AWS AppSync (little or no GraphQL experience required) | Front-End Web & Mobile
API (GraphQL) - Subscribe to data - JavaScript - AWS Amplify Docs
試した環境
% sw_vers
ProductName: macOS
ProductVersion: 13.3
BuildVersion: 22E252
package.json
{
"name": "appsync-generic-pubsub-sample",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"aws-amplify": "^5.0.25",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
Discussion