ASK SDK Express Adapterを使って、Alexaスキルを作る


Alexaスキルをhttpsホスティングする

ついに公式サポートです。
今だとalexa-hostedスキルやお手軽なものだとBluePrintなんかも登場し、より開発の敷居が下がっていますがAlexaが日本ローンチされた直後は、Lambdaとhttpsホスティングの2種類しか選ぶことが出来ませんでした。2種類の選択肢はあったもののhttpsホスティングする際に求められる要件の検証などが発生するため、事実上はLambdaだけでカスタムスキルを作るということが一般的でした。
そんな少し面倒だったhttpsホスティングスキルが簡単に作れるようになったよというお話です。

Alexaとのやり取りを少しでも速くしたかった

VUIをデザインする上で、最も大切なのは情報過多になりすぎず、人との対話に近づけられるかということです。
人対人で話す時に、問いかけに対して何らかの返答をタイムラグなく相手に返すということを当たり前のように行っています。もちろん、難しい質問に対しては人間も悩むことがあるので、そのような状況に近いレスポンスを作るのであればワンクッション挟んで、「考えてました」とか少し相槌を打つことで違和感を消せるとは思います。
しかし、普通の応答で遅延していたら「人対人の対話」に近づくことは難しいと個人的には考えてます。

そんな状況下で、試行錯誤した結果前職ではhttpsホスティングでAlexaスキルを提供し、Lambda環境よりも1秒レスポンスを速くする施策なんかも挑戦していました。
そんな戦いの記録はこちらからどうぞ。
交通情報系スキルを事例に見る日常生活に溶け込むスキル開発のテクニック

httpsホスティングすると何がいいのか?

Lambdaのコールドスタートによる遅延をなくすことができます。
なので、コールドスタート時に必要になるコンテナの作成時間を削ることが可能になります。
あとは、趣味用のVPSなどでも動かせるのでAWSを使わずとも遊び程度でスキル開発出来るようになるということも1つの利点かなと思います。

コールドスタートってなに?

Lambdaはリクエストが来るたびコンテナを作成し、その上でプログラムを動かし、レスポンスを生成し、戻すという流れを行っています。
作成したコンテナは、一切リクエストがない状況が10分から20分程度続くと終了されます。
定期的にリクエストがあれば、コンテナは生き続けるので、生きているコンテナが再利用されます。再利用時は、コンテナ作成のステップが省略されるので、コールドスタートよりも速い時間で処理を完了します。(再利用する場合のことをウォームスタートと言います。)
このコールドスタート時のコンテナ作成時間は、デプロイパッケージやLambdaに割り当てたメモリ量によって左右されますが、ウォームスタートよりは確実に遅くなってしまいます。アプリのAPIなどであれば、多少のタイムラグも目をつむることも出来ますが、Alexaとの対話として考えた時この余計な時間が邪魔に感じることもあります。

ASK SDK Express Adapter

はい、今回の本題です。
実際にhttpsホスティングできるスキルのコードを見てみましょう!

Install

npm install --save ask-sdk-express-adapter

プロジェクトへのask-sdk-express-adpterのインストールは簡単です。
npmに公開されているask-sdk-express-adapterをインストールするだけでOKです。
これ以外にalexaスキルに必要な、ask-sdk-coreまたは、ask-sdkがプロジェクトに含まれている必要があります。

Sample code

index.js
// ask-sdkの読み込み
const Alexa = require('ask-sdk-core');
// expressの読み込み
const express = require('express');
// ExpressAdapterの読み込み
const { ExpressAdapter } = require('ask-sdk-express-adapter');
// Handlerの判定お助けツールを読み込む
const Util = require('./Utility');

// MARK: 定数群
const PORT_NO = 3000; 
const SKILL_PATH = '/';

// MARK: Handlerの定義

// スキル起動のHandler
const LaunchHandler = {
    canHandle(handlerInput) {
        return Util.checkIntentTypeName(handlerInput, 'LaunchRequest');
    },
    async handle(handlerInput) {
        return handlerInput.responseBuilder
            .speak('ハローワールド') 
            .getResponse();
    }
}

// ヘルプのHandler
const HelpIntentHandler = {
    canHandle(handlerInput) {
        return (Util.checkIntentTypeName(handlerInput, 'IntentRequest', 'AMAZON.HelpIntent'));
    },
    async handle(handlerInput) {
        return handlerInput.responseBuilder
            .speak('使い方を読み上げますよ') 
            .getResponse();
    }
}


// スキル終了のHandler
const SkillEndHandler = {
    canHandle(handlerInput) {
        return (
            Util.checkIntentTypeName(handlerInput, 'IntentRequest', 'AMAZON.CancelIntent')
            || Util.checkIntentTypeName(handlerInput, 'IntentRequest', 'AMAZON.StopIntent')
        );
    },
    handle(handlerInput) {
        return handlerInput.responseBuilder
            .speak('スキルを終了します。') 
            .getResponse();
    }
}

// エラー発生時のHandler
const ErrorHandler = {
    canHandle() {
        return true;
    },
    handle(handlerInput, error) {
        console.log(`Error handled: ${error.message}`);
        return handlerInput.responseBuilder
            .speak('申し訳ありません。内部エラーが発生致しました。再度、スキルを立ち上げ直してください。') 
            .getResponse();
    },
};

// MARK: 初期化関連
const app = express();

const skillBuilder = Alexa.SkillBuilders.custom()
                            .addRequestHandlers(
                                LaunchHandler,
                                HelpIntentHandler,
                                SkillEndHandler
                            )
                            .addErrorHandlers(ErrorHandler)

const skill = skillBuilder.create();
const adapter = new ExpressAdapter(skill, true, true);

app.post(SKILL_PATH, adapter.getRequestHandlers());
app.listen(PORT_NO);

サンプルコードはこちらのリポジトリに置きました。
Github - MasamiYamate/AlexaHttpsHostedSkill

肝の部分

基本的にHandlerの定義などはLambda上で動かすAlexaスキルを作る時と差はありません。
肝となる部分は、下記の部分です。

index.js
const skill = skillBuilder.create();
const adapter = new ExpressAdapter(skill, true, true);

app.post(SKILL_PATH, adapter.getRequestHandlers());
app.listen(PORT_NO);

通常であれば、skillBuilder.lambda();としますが、skillBuilder.create();とします。
skillBuilder.create();にすることで、スキルリクエストのレスポンスをjson形式で取得できるようになります。
ちなみにask-sdk-express-adapterが出る前は、下記のように工夫することでレスポンスのjsonを取得することが出来ました。
過去の例として下記のリポジトリを参考にしてみてください。
Github - MasamiYamate/katana-alexa-https-hosting

レスポンスのjsonを生成するcreate()メソッドを今回新たに公開されたask-sdk-express-adpterを生成するときの引数として渡して、expressの任意のパスをpostで呼び出す時にask-sdk-express-adpterのgetRequestHandlers()を呼び出すようにすれば完了です。

index.js
app.post(SKILL_PATH, adapter.getRequestHandlers());

あとは、expressでListenするポート番号を指定してあげるだけ。
これだけで今まで必要だったリクエストの検証などが一切不要になり、さくらのVPSなどワンコインVPSなどでもAlexaスキルを動かせるようになります。

サーバー側の設定

  • forever...デーモン化ツール
  • nginx...httpsのリクエストを任意のlocalhostに向けるために必要(リバースプロキシ)

上記の2つが必要です。
サーバーにログインしてスキルを立ち上げてもログアウトしてしまえばセッションがなくなりプログラムが止まってしまいます。
なので、ログアウトしていてもスキルが永続的に動くようにforeverを利用してスキルのスクリプトを永続化させます。

また、Alexaへのリクエストはhttpsなので443番のポート番号指定で飛んできます。単純に上記スクリプトのポートを443に指定してあげれば直接Node.jsのサーバーに外部からリクエストを受けることが出来ます。しかし、1024番以下のポートでの実行には管理者権限が必要でセキュリティ的に良くないです。
そんなこともあり、Node.jsでは一般的に3000番などのポートでプログラムを動かし、外部からのリクエストは一度nginxで受け取って、nginxからlocalの3000番に渡すようなことをするのが一般的です。
なので、別途nginxのリバースプロキシの設定をすることをおすすめします。
大体のやり方は、先人の知恵をお借りしたので下記の記事を参照してみてください。
Node.js + Express + forever を構成して nginx から流す

スキルのエンドポイント設定


サーバー側の設定が終わったら、Alexa developer consoleのhttpsホスティングを行いたいスキルの画面からエンドポイントの設定を行います。
DefaultではARNの項目にチェックが入っていますので、httpsの項目を選択しエンドポイントを入力します。入力後、証明書の種別を選択して保存すれば作業は完了です。

注意点 - DynamoDBのコールは出来ない

AWS EC2上で動かす場合は、ロールにDynamoDBの権限を付与するだけでhttpsホスティングのスキルスクリプトからDynamoDBにアクセスすることが出来ます。しかし、ワンコインVPSなどの環境からだとDynamoDBをそのまま使うことは出来ません。
以前はAWS EC2上で動かしていたので、まだ未検証ですがおそらくaws-sdkを初期化したりsecretkey等々アクセスに必要な権限などの準備が別途必要になると思います。

また、ask-sdk-express-adpterではこれも未検証ですが、httpsホスティングスキルをAWS EC2上で動かし、実行ロールにDynamoDBの権限を付与していてもaws-sdkにリージョンの指定をしてあげる必要がありました。動き的にexpressで動かすだけのように見えますので、ask-sdk-express-adpterを利用してEC2上でスキルを動かしてかつDynamoDBを利用したい時にうまく動かないことが発生したらまずリージョンの指定がちゃんと出来ているか確認することをおすすめします。
コレに関しては後日追試して確認する予定です。

まとめ

個人的にはalexa-sdk時代からhttpsホスティングできるスキルの開発を行ってきたこともあり公式サポートしてくれたことは非常に嬉しく感じます。
Lambdaでお手軽に実行することに比べると運用を少し気を使わないといけなかったり、スキルのスクリプトのバージョン管理などの手間も発生してしまいます。
ただ、それでもトリッキーなことを試したり、AWSを使わなくてもスキル開発を誰でも簡単にできるようになるパーツとしては良いモノなので、Alexa開発にある程度精通してきた方は挑戦してみると色々楽しいかもしれません!