スマートデバイスとクラウドサービスとの連携を容易にするiPaaSの活用① ～AlexaからAPI呼び出しでDatabaseを参照してみた～

# はじめに

近年、スマートデバイスの種類や用途は広がってきており、身近なものになりつつあります。今後は、さらに活用のパターンは広がっていくと思われますが、その際にはデバイス間の連携や、クラウドサービスとの連携のニーズも高まっていくと考えられます。
様々なデバイスでデータ取得のためのAPIが提供されていたり、各種プログラミング言語ではサービス連携のためのライブラリーも提供されていると思います。しかし、この方法だけでは、将来的にシステム連携が増えていくと、各クラウドサービス用の連携開発や認証情報の管理など、効率性が損なわれてしまう恐れがあります。
このような課題に対応するために、近年、iPaaS（integration Platform as a Service）と呼ばれるサービスに注目が集まっています。クラウドサービス種別としてPaaSはよく知られていますが、その中でも特にintegration（システム連携）に特化したプラットフォームサービスとなります。このソリューションでは、様々なクラウドサービスと連携するためのアダプターが提供されており、システム連携の課題を解決することができます。

今回は、スマートデバイスとクラウドサービスとの連携をiPaaSを活用して実装した例をご紹介します。
実装のアーキテクチャーは以下のような概要となります。クラウド側はIBM Cloudの無料サービスで実現しています。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/f6083db2-15b2-4ee5-41cc-48977c84040b.png)


# 1. IBM Cloud 実装の流れ

## 1.1 Cloudant

CloudantはIBM Cloudの無料枠でインスタンスを作成しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/807db0dc-f2b0-f287-b1ec-f027f1d708b9.png)


Cloudantのデータベース内にはユーザーの健康状態（statusフィールド）が含まれています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/b2eb365d-5721-1fbe-693a-24f072c922b4.png)


## 1.2 App Connect

IBMでは、iPaaSソリューションとしてApp Connect on Cloud機能を提供しています。
こちらもIBM Cloudの無料枠でインスタンスを作成しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/275de47c-627c-0092-3cf2-6575b950fc0c.png)

インスタンス作成後は、コンソールにアクセスでき、フローを開発することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/1b538ef0-2308-4a00-ca90-bcbbd7bf4886.png)

今回は、1.1で作成したCloudantに接続するためのAPIを登録したいので、「Create flows for an API」を選択します。

＜データモデルの登録＞
APIの呼び出し、応答で使用するデータモデルを設定します。
今回は、Databaseにあるnameとstatusを設定しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/b54232c0-104c-1bb3-59b2-8ce33ceb10ab.png)

＜操作の登録＞
APIの操作を登録します。
今回は、Databaseを参照しますので、Retrieve（GETメソッド）を登録します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/9bd054f7-0adf-3848-c488-fab898eba2c5.png)


＜フロー開発＞
API操作に対して具体的な処理を登録していきます。
全体の流れは以下のようになります。APIが呼び出されれるとフローが開始し、

1. 「IBM Cloudant」でCloudantからデータ取得
2. 「JSON Parser」で取得したJSONデータをパース
3. 「Response」でJSONデータを応答

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/82c02a2f-4bf6-f009-3f07-5ff03d5c277c.png)

以下、もう少し詳しく、各設定値を見ていきます。

### 1. 「IBM Cloudant」でCloudantからデータ取得

+ボタンで「IBM Cloudant」アプリケーションを登録します。
接続先として以下のように「ホスト」と「API Key」を設定して接続します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/4833b734-8253-a081-873a-d2408309ef4b.png)

一度、アカウント登録すると認証情報が保管され、次からは再利用することができます。
今回はCloudantのDocumentを取得したいので「Retrieve documents」を選択します。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/80789606-2e9c-982d-a473-419883640a73.png)

無事に接続できるとDatabase情報が取得されます。
取得条件としてIDに設定している「name」を指定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/174d21bb-7a3b-ea17-3aa3-72e419d9f7d8.png)

### 2. 「JSON Parser」で取得したJSONデータをパース

この後の処理として、Toolboxの「JSON Parser」を登録します。
この機能のInputとして「IBM Cloudant」機能で取得した「Document data」を設定します。
これにより、IBM Cloudantから取得したローデータをフローで参照可能なJSON形式に変換することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/e9e13cae-e51c-2482-c168-169e84e16ac8.png)

また、後続の処理でJSONの構造を参照できるように「Output Schema」を設定します。
今回は、CloudantのサンプルデータからJSON Schemaを生成しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/887dc7b6-a275-a7bc-e733-238bc65181ed.png)


### 3. 「Response」でJSONデータを応答

先ほどのOutput Schemaを参照して、応答データに「name」と「status」を設定していきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/aa35ce05-59ed-f6b0-79e8-4c142d662760.png)

以上で設定は完了です。
実際にこのフローをテストしてみましょう。
テストの際は「Request」から具体的なデータを設定できます。
今回は要求のIDとして「yamada」を設定しています。次に操作名の隣にあるテストボタンを押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/31adb076-b145-92a7-b4b5-28781773b6d1.png)

テストに問題がなければ、Cloudantから情報が取得できていることが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/b8a92742-2d05-88eb-a7db-10c185fd90fa.png)

動作は問題ありませんので、これでAPIを開始します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/1c58d7d2-cd18-8484-594b-d8a614d6fd11.png)

Runningになりましたら「Manage」画面のルート情報からAPIのエンドポイントを確認できます。
今回は、簡易的なテストのためこのAPIにセキュリティー設定はしていません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/e13fbe13-400f-2c6c-acd2-ee556a5689fd.png)



# 2. Smart Device 実装の流れ

今回は、1で公開したAPIをAlexaのスキルから呼び出していきます。

## 2.1 Alexa Console

[alexa developer console](https://developer.amazon.com/alexa/console/ask)からスキルを作成します。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/54026be6-19b1-5a03-6eff-0de1b4371eb5.png)

スキルを作成します。今回はカスタムモデル、Alexa-hosted（Node.js）を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/f381b031-9711-66f2-64b7-4cdb3b9c79cb.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/188441ff-7430-a5ec-a82c-5b4b0f53a27a.png)



スキルが作成されましたら、「コードエディタ」タブからAPIを呼び出す処理を実装します。
特にAlexaとのやり取りは設定せず、静的API呼び出しでyamadaさんの健康状態を取得しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/db9c8ab4-d6d3-52f7-cd6a-6a9f736a25dd.png)



```node.js:index.js
const Alexa = require('ask-sdk-core');
const http = require('http');
const https = require('https');

const LaunchRequestHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'LaunchRequest';
  },
  async handle(handlerInput) {
 
　      // CreateMessage関数からAPIを呼び出し結果を取得
        try {
            const response = await CreateMessage();
            const speakOutput = response;
            handlerInput.responseBuilder
                .speak(speakOutput)   
        } catch (error) {
            handlerInput.responseBuilder
                .speak("すみません、健康状態が取得できませんでした")
        }
        return handlerInput.responseBuilder
            .getResponse();
  }
};

const HelloIntentHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'IntentRequest'
      && handlerInput.requestEnvelope.request.intent.name === 'HelloIntent';
  },
  handle(handlerInput) { 
    const speechText = handlerInput;
    return handlerInput.responseBuilder
      .speak(speechText)
      .withSimpleCard()
      .getResponse();
  },
};

const HelpIntentHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'IntentRequest'
      && handlerInput.requestEnvelope.request.intent.name === 'AMAZON.HelpIntent';
  },
  handle(handlerInput) {
    const speechText = '「ヘルプ」と話しかけてください。';

    return handlerInput.responseBuilder
      .speak(speechText)
      .reprompt(speechText)
      .withSimpleCard()
      .getResponse();
  },
};

const CancelAndStopIntentHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'IntentRequest'
      && (handlerInput.requestEnvelope.request.intent.name === 'AMAZON.CancelIntent'
        || handlerInput.requestEnvelope.request.intent.name === 'AMAZON.StopIntent');
  },
  handle(handlerInput) {
    const speechText = 'さようなら。';

    return handlerInput.responseBuilder
      .speak(speechText)
      .withSimpleCard('こんにちは。', speechText)
      .getResponse();
  },
};

const SessionEndedRequestHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'SessionEndedRequest';
  },
  handle(handlerInput) {
    console.log(`Session ended with reason: ${handlerInput.requestEnvelope.request.reason}`);

    return handlerInput.responseBuilder.getResponse();
  },
};

const ErrorHandler = {
  canHandle() {
    return true;
  },
  handle(handlerInput, error) {
    console.log(`Error handled: ${error.message}`);

    return handlerInput.responseBuilder
      .speak('すみません。理解できませんでした。もう一度お願いします。')
      .reprompt('すみません。理解できませんでした。もう一度お願いします。')
      .getResponse();
  },
};

const skillBuilder = Alexa.SkillBuilders.custom();

exports.handler = skillBuilder
  .addRequestHandlers(
    LaunchRequestHandler,
    HelloIntentHandler,
    HelpIntentHandler,
    CancelAndStopIntentHandler,
    SessionEndedRequestHandler
  )
  .addErrorHandlers(ErrorHandler)
  .lambda();



//CreateMessage関数
const CreateMessage = function() {
  
    // App Connect on cloudのAPIエンドポイント
    var url = "https://***.***.apigw.appdomain.cloud/xdBXrU/user_status/yamada" ;

    return new Promise((resolve, reject) => {
        const request = https.get(url, response => {
            response.setEncoding('utf8');

            let returnData = '';
            if (response.statusCode < 200 || response.statusCode >= 300) {
                return reject(new Error(`${response.statusCode}: ${response.req.getHeader('host')} ${response.req.path}`));
            }

            response.on('data', chunk => {
                returnData += chunk;
            });
            response.on('end', () => {

                let res = JSON.parse(returnData);
                let name = res.name;
                let status = res.status;
                let speakOutput = name + "さんの健康状態は" + status + "です";
                resolve(speakOutput);
            });
            response.on('error', error => {
                console.log(error);
                let errorSpeakOutput = "すみません、健康状態を取得できませんでした。";
                reject(errorSpeakOutput);
            });
        });
        request.end();
    });
};
```


実際にテストしてみると以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/459cc5b7-05b8-bf8d-1d17-a2dd7709b632.png)

以上です。