[AWS AppSync] エッジデバイスのデータを AWS IoT経由で取得してGraphQLを更新してみました。

2020.01.15

1 はじめに

CX事業本部の平内(SIN)です。

AWS AppSync(以下、AppSync)は、GraphQLを利用するためのマネージド型サービスです。 今回は、このGraphQLへのMutationをAWS IoT経由で取得したデータで行うものを試してみました。

最初に動作している様子です。

デバイスからのデータをiOSで表示するだけなら、MQTTのPublishとSubscribeだけで充分だと思うのですが、ここは、今後の拡張を視野に入れて、敢えてGraphQLを挟むという事にさせて下さい。

2 構成

構成は、以下のとおりです。

エッジデバイスのデータは、MQTTでPublishされています。AWS IoTでは、ルールにより、到着したデータをすべてLambdaに送っています。

AppSyncへのMutationは、Lambdaから行われ、iOSでは、AWS Amplifyを使用して、そのデータをSubscriptionしてリアルタイムに表示しています。

なお、AppSyncのデータストアは、DynamoDBとし、認証は、APIキーのみです。

3 AppSync

AppSyncでアプリ(AppSyncSample)を作成し、スキーマーを以下のように必要最小限としました。

type Data {
	id: String!
	datetime: AWSDateTime
	value: Float
}

type Mutation {
	createData(value: Float!, datetime: AWSDateTime!): Data
}

type Query {
	getData(id: String!): Data
}

type Subscription {
	onCreateData: Data
		@aws_subscribe(mutations: ["createData"])
}

クエリ画面でmutationの動作を確認している様子です。

上記のクエリで、データストアとして設定したDynamoDBにデータが追加されていることを確認できます。

4 データの生成

エッジデバイスでは、下記の形式のデータを生成します。

{
    "value": 64.2,
    "datetime": 1578878926168
}

MQTTでPublishしているコードの抜粋は、以下のとおりです。生成されたデータを300msecごとに送っています。

"use strict";
var awsIot = require('aws-iot-device-sdk');

class Mqtt {

    constructor(){
        this.topic = 'topic_1';
        this.device = awsIot.device({
            keyPath: './private.pem',
            certPath: './cert.pem',
            caPath: './root-CA.crt',
            clientId: 'iot-data-generator',
            host: 'xxxxxxxxxx.iot.ap-northeast-1.amazonaws.com'
        });
    }

    async connect(){
        return new Promise( (resolve,reject) =>{
            this.device.on('connect', function() {
                console.log('connected');
                resolve();
            });
        });
    }

    publish(value){
        const date = new Date();
        const json = {
            value: value,
            datetime: date.getTime()
        }
        console.log(JSON.stringify(json))
        this.device.publish(this.topic, JSON.stringify(json));
    }
}

async function main() {

    const mqtt = new Mqtt();
    await mqtt.connect();
    
    while(true){
        const value = await adconv.read(); // 生成されたデータの取得
        mqtt.publish(value);
        await sleep(300);
    }
}

main();

エッジデバイスについての細部は、下記のブログ記事をご参照ください。
[AWS IoT] AWS IoTのデバッグ用にデータジェネレーターを作ってみました

5 AWS IoT

AWS IoTでは、下記のような簡単なルールを設定して、トピックに到着したデータをそのまま、Lambda(AppSyncSample)に送っています。

SELECT * FROM 'topic_1'

6 Lambda

LambdaでAppSyncにMutationしているコードは、以下のとおりです。

先のルールで起動されるLambdaでは、パラメータ(event)に、Publishされたデータがそのまま入っていますので、GraphQLで定義したスキーマの型に変換を行っています。

なお、クエリは、先程、AppSyncのコンソールで動作確認したクエリをそのままコピーしています。

AppSyncへの認証は、APIキーとなっているので、特別なパーミッションは必要ありません。

index.js

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

// クエリ
const query = gql(`mutation CreateData($value: Float!, $datetime: AWSDateTime!){
  createData(value: $value, datetime: $datetime){
    id
    value
    datetime
  }
}`);

exports.handler = async (event: any) => {
  
  // event = {
  //   "value": 23.5,
  //   "datetime": 1578879458040
  // }
  
  const url = process.env.END_POINT;
  const region = process.env.REGION;
  const apiKey = process.env.API_KEY;
  const authType = 'API_KEY';

  const client = new AWSAppSyncClient({
    url: url,
    region: region,
    auth: {
      type: authType,
      apiKey: apiKey
    },
    disableOffline: true,
    fetchPolicy: 'network-only'
  });

  if(event.value && event.datetime){
    const dt = new Date(event.datetime);
    const params =  {
      "datetime": dt.toISOString(), // AWSDateTimeへの変換 2011-10-05T14:48:00.000Z
      "value": parseFloat(event.value) // Floatへの変換
    }
    try {
      await client.mutate({
        variables: params,
        mutation: query
      });
    } catch (err) {
      console.log(JSON.stringify(err));
    }
  }
}

Lambdaから、GraphQLへのクエリについての詳しい説明は、下記をご参照ください。
[AWS AppSync] LambdaからGraphQLのクエリを送ってみる

7 iOS

Xcodeでプロジェクトを作成した後の手順は、以下のとおりです。(コマンドは、プロジェクトのトップで実行しています)

※プログレスバーは、KDCircularProgressを利用させて頂きました。

(1) Cocoapods

CocoaPodsでAWSAppSyncとKDCircularProgressを追加しています。

$ pod init
$ vi Podfile

target 'AppSyncSample' do
  use_frameworks!
  pod 'AWSAppSync', '~> 2.15.0'
  pod 'KDCircularProgress'
end

$ pod install

(2) Amplify初期化

amplify initでAmplifyの初期化(CFnのスタック生成等)を行います。

$ amplify init

(3) GrapQLドキュメントの生成

amplify add codegenでiOSからアクアセスするためのクラスが生成されます。

$ amplify add codegen --apiId xxxxxxxxxxxxxxxxxxxxx

また、 amplify pushして、反映させて置きます。

$ amplify push
$ amplify status
Current Environment: demo

| Category | Resource name | Operation | Provider plugin |
| -------- | ------------- | --------- | --------------- |
| Api      | AppSyncSample | Create    |                 |

生成されたファイル(awsconfiguration.json及び、API.swift)は、プロジェクトに追加します。

(4) AWSAppSyncClientのインスタンス生成

下記は、AppDelegate.swiftで、AWSAppSyncClientのインスタンスを生成しているコードの抜粋です。

import UIKit
import AWSAppSync

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var appSyncClient: AWSAppSyncClient?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        do {
          let cacheConfiguration = try AWSAppSyncCacheConfiguration()
          let appSyncConfig = try AWSAppSyncClientConfiguration(appSyncServiceConfig: AWSAppSyncServiceConfig(), cacheConfiguration: cacheConfiguration)
          appSyncClient = try AWSAppSyncClient(appSyncConfig: appSyncConfig)
        } catch {
          print("Error initializing appsync client. \(error)")
        }
        return true
    }

(5) subscriptionによるデータ取得

API.swift で提供されるクラスでGraphQLにアクセス可能になります。リアルタイムでデータを取得して、プログレスバーを更新しているコードの一部です。

class ViewController: UIViewController {
    
    var appSyncClient: AWSAppSyncClient?
    
    @IBOutlet weak var progress: KDCircularProgress!
    @IBOutlet weak var label: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()

        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
            return
        }
        appSyncClient = appDelegate.appSyncClient
        subscribe()
        
    }
    
    func setValue(value:Double) {
        self.progress.angle = value/100 * 360.0
        self.label.text = String(value)
    }
    
    var discard: Cancellable?
    func subscribe() {
        do {
            discard = try appSyncClient?.subscribe(subscription: OnCreateDataSubscription(), resultHandler: { (result, transaction, error) in
                if let result = result {
                    self.setValue(value: result.data?.onCreateData?.value as! Double)
                } else if let error = error {
                    print(error.localizedDescription)
                }
            })
        } catch {
            print("Error starting subscription.")
        }
    }
}

AmprifyによるiOSからのGraphQLへのアクセスについては、下記を参考にさせて頂きました。
参考:https://aws-amplify.github.io/docs/sdk/ios/start

8 最後に

今回は、AppSyncを使用して、エッジデバイスのデータ変化をiOSで表示してみました。実際に物理的な動作(ボリュームを回す)が、リアルタイムに遠隔表示できると、ちょっと、気持ち良かったです。

すべてのコードは、下記に置きました。
https://github.com/furuya02/AppSyncSample

9 参考リンク


Monitoring IoT devices in real time with AWS AppSync