Alexa-HostedスキルをMinIOを使ってローカル環境でテストする

はじめに

Alexa-Hostedスキルのセッションで扱うデータを、S3 (Simple Storage Service)を使って永続化し、ローカル環境でテストする方法を紹介します。ローカルのS3環境は、AWS S3と互換性のあるMinIO Object Storageを使用します。

対象の読者

Alexaのカスタムスキルについて基本的な知識のある方を対象とします。カスタムスキル自体やAlexa開発者コンソールの使い方についての説明は本稿には含みません。

前提条件

ローカル環境の構築については、前記事である「Alexa-HostedスキルをNGROKを使ってローカル環境でテストする」をお読みください。前記事で構築したローカル環境にMinIO環境を追加するものとします。またスキルのテスト方法なども前記事に従うものとします。

永続アトリビュートについて

Alexaのカスタムスキルでは、スキル内で使用するデータをアトリビュートとして管理できます。このアトリビュートには3種類あります。

  • リクエストアトリビュート
  • セッションアトリビュート
  • 永続アトリビュート

参考: アトリビュートの管理

本稿が扱うのは、この中の永続アトリビュートです。永続アトリビュートは、セッションを超えて記憶しておきたい情報です。例えば、ユーザーの好みであるとか、以前の注文の履歴などが相当します。この永続アトリビュートは、PersistenceAdapterを利用して、ストレージの中に保存できます。

カスタムスキルでは、S3とDynamoDBをストレージとして使った永続化がサポートされています。この記事ではS3を使って永続化します。(初稿の執筆時には、Alexa-Hosted環境ではS3のみがサポートされていたので一択でした。しかし現在はDynamoDBも選択できます。)

ローカル環境からアクセスするS3環境として、AWS S3と互換性のあるMinIO Object Storageを使用します。

参考: MinIO Object Storage

S3のローカル環境の必要性

カスタムスキルをローカル環境で実行しながら、ネットワーク越しにAWS S3にアクセスする選択肢もあります。ただAlexa-Hostedスキルの場合は、ホストされているAWS S3サービスのエンドポイントやバケットの情報を、スキルの実行時に環境変数から取り出す必要があります。ローカル環境でスキルを実行している間は、これらの環境変数にはアクセスできないため、S3環境もローカルに用意する必要があります。

全体構成

全体構成を下図に示します。

MinIOについて

MinIOは、Amazon S3と互換性のあるストレージサーバーです。クラウド上のサービスの他、幾つかのプラットフォーム向けにランタイムが配布されています。今回はDocker向けのコンテナとして配布されているものを利用します。

事前準備

ツールのインストール

前記事で構築した環境に加えて、下記のツールをインストールしているものとします。

  • Docker Compose (こちら)
    Docker本体もこちらからインストールします

この他にMinIO Object Serviceのサーバーが必要です。こちらはDockerのコンテナとして提供されているものを使うため、実行時にインストールされます。

追加モジュールのインストール

~/alexa-skill-hoge/lambdaのディレクトリで、下記のコマンドを実行します。

npm install --save ask-sdk-s3-persistence-adapter
npm install --save dotenv

環境変数の設定

カスタムスキルの中で、後述する永続性の組み込みのために、環境変数を参照しています。この環境変数を~/alexa-skill-hoge/lambda/.envの中に定義します。Webサービスとして呼び出された場合は、この環境変数を参照します。

S3_PERSISTENCE_BUCKET='mys3bucket'
MINIO_ACCESS_KEY_ID = 'myminioadmin'
MINIO_SECRET_ACCESS_KEY = 'myminioadmin'
MINIO_HOST = '127.0.0.1'
MINIO_PORT = 9000
SKILL_LOCAL = true

.envがデプロイされるのを避けるために、.gitignoreに登録しておきます。

lambda/node_modules
.ask/
ask-resources.json
lambda/.env

プロジェクトの作成

前記事で作成したプロジェクトを使用するものとします。

永続性の組み込み

MinIOコンテナの起動設定 (Docker Compose)

MinIOサーバーのコンテナを起動するために、Docker Composeを使います。Docker Composeを使うと、起動時の各種設定を~/alexa-skill-hoge/docker-compose.yamlファイルに定義できるので、コンテナの管理が楽になります。

version: "3"
services:
  s3-local:
    container_name: s3local
    image: minio/minio
    ports:
      - 9000:9000
    command: server /data
    volumes:
      - /data:/data
    environment:
      - MINIO_ACCESS_KEY=myminioadmin
      - MINIO_SECRET_KEY=myminioadmin

環境変数(environment)の、MINIO_ACCESS_KEY, MINIO_SECRET_KEYは、MinIOサーバーによって読み込まれます。ここで設定されているmyminioadminは、ブラウザへのログインや、カスタムスキルからのAPIコールでも使用されます。

書き込まれたデータは、ローカルマシンの/dataに書き込まれます。この設定はvolumesに定義されているので、必要ならば変更してください。Windows環境の場合は、C:/data:/dataのような形式になります。コロンの前がホスト環境、後ろがコンテナ環境のパスになります。

カスタムスキルの変更

カスタムスキルのコード(~/alexa-skill-hoge/lambda/index.js)を変更して、セッションの結果を保存します。

「ほげを開いて」と発話すると実行されるLaunchRequestHandlerを書き換えて、「○回目の呼び出しです」と応答するようにします。呼び出された回数をS3に記録します。

モジュールの読み込み

必要なモジュールを読み込みます。

const AWS = require('aws-sdk');
const Adapter = require('ask-sdk-s3-persistence-adapter');
require('dotenv').config()

永続化アダプタの構成情報の生成

永続化するデータの読み書きには、ASK SDKの提供するS3PersistenceAdapterを使用します。S3PersistenceAdapterの作成時に、構成情報を渡す必要があります。この構成情報の中に、S3のエンドポイントやバケット名を指定します。

この構成情報を生成する関数を下記に示します。

function createS3Config() {
    const bucketName = process.env.S3_PERSISTENCE_BUCKET;
    const isLocal = process.env.SKILL_LOCAL ? true : false;
    let s3Config;
    if (isLocal) {
        console.log('Local Environment');
        console.log(`S3 Bucket Name = ${bucketName}`);
        const accessKeyId = process.env.MINIO_ACCESS_KEY_ID;
        const secretAccessKey = process.env.MINIO_SECRET_ACCESS_KEY;
        const minioHost = process.env.MINIO_HOST;
        const minioPort = process.env.MINIO_PORT;
        console.log(`Access Key = ${accessKeyId}`);
        console.log(`Secret Access Key = ${secretAccessKey}`);
        console.log(`MINIO Host = ${minioHost}`);
        console.log(`MINIO Port = ${minioPort}`);
        const s3Client = new AWS.S3({
            accessKeyId: accessKeyId,
            secretAccessKey: secretAccessKey,
            endpoint: `http://${minioHost}:${minioPort}/`,
            s3ForcePathStyle: true,
            signatureVersion: 'v4'
        });
        s3Config = {
            bucketName: bucketName,
            objectKeyGenerator: (param) => {return 'myObjectKey';},
            // pathPrefix: 'chandora',
            s3Client: s3Client
        };
    }
    else {
        const bucketName = process.env.S3_PERSISTENCE_BUCKET;
        console.log('Remote Environment');
        console.log(`S3 Bucket Name = ${bucketName}`);
        s3Config = {
            bucketName: bucketName
        };
    }
    return s3Config;
}
バケット名の取得

S3のバケット名を取得します。Alexa-HostedスキルがホストするLambda環境では、環境変数S3_PERSISTENCE_BUCKETにバケット名が設定されています。ローカル環境の場合は、.envの中に同じ環境変数を定義し、mys3bucketをバケット名として設定しています。

ローカル環境の判別

ローカル環境で実行されている場合は、環境変数SKILL_LOCALtrueに設定されています。これを使ってローカル環境かどうかを判別しています。

S3クライアントの作成

ローカル環境で実行されている場合は、S3にアクセスするためのクライアントを作成します。このクライアントは、aws-sdkの提供しているS3クラスから作成します。作成時にMinIOサーバーにアクセスするための情報を指定します。それらの情報を~/alexa-skill-hoge/lambda/.envの中に環境変数として定義しています。

リモート環境で実行されている場合は、PersistenceAdapterの中でクライアントが作成されるため、何もする必要はありません。

キージェネレータの指定

キージェネレータは、S3にデータを保存する時のキーを提供するものです。何も指定しないと、デフォルトではuser idをキーに使うようになっています。このuser idは、Alexaサーバーから送られてきたメッセージに含まれているものです。

ローカル環境で実行されている場合は、キージェネレータを指定して、user idではなくmyObjectKeyという固定の文字列を使うように変更しています。

当初ローカル環境でも、キージェネレータを指定していませんでした。しかしS3への書き込みでエラーが発生してしまいました。

エラーについて調べたところ、キーを元にファイルシステムにディレクトリを作成しており、そこでエラーが発生していました。キーの長さが非常に長かったことから、ファイルシステムのパスの文字数の制約に引っかかっている可能性が考えられました。キージェネレータを指定し、短いキーに変更したところエラーが解消しました。厳密な問題判別は行っていませんが、現象的には上記の推測で合っていそうです。

この問題が発生したのは、Windows環境です。Windows環境ではパスの長さは合計で260文字という制約があります。筆者は普段Windows環境を使っているため、この制約に引っかかってしまったようです。なお本稿の内容を検証するために、Ubuntuの環境を構築してテストしています。そちらの環境ではこの問題は再現していません。

スキルを公開すると、複数のユーザーからのリクエストを受け付けるようになります。その場合は、S3に保管されるデータは、ユーザーごとに別れている必要があります。このために、user idがキーとして使われているもと思われます。これを固定の文字列にしてしまうと、ユーザーごとのデータを保持できません。

ローカル環境のテストでは、単一ユーザーからの発話のみを受け付ける前提であれば、これで問題ありません。もし複数ユーザーからの発話を受け付けながらテストする場合は、Linux環境に構築してキージェネレータを使わないようにしてください。

永続化アダプタの生成とスキルへの組み込み

永続化アダプタを生成します。

const persistenceAdapter = new Adapter.S3PersistenceAdapter(createS3Config());

永続化アダプタをスキルに組み込みます。

const handlers = Alexa.SkillBuilders.custom()
    .addRequestHandlers(
        LaunchRequestHandler,
        HelloWorldIntentHandler,
        HelpIntentHandler,
        CancelAndStopIntentHandler,
        FallbackIntentHandler,
        SessionEndedRequestHandler,
        IntentReflectorHandler)
    .addErrorHandlers(
        ErrorHandler)
    .withCustomUserAgent('sample/hello-world/v1.2')
    .withPersistenceAdapter(persistenceAdapter);

ハンドラの変更

ハンドラを変更して、呼び出されるたびにカウントアップし、「○回目の呼び出しです」と返事するようにします。

const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
    },
    async handle(handlerInput) {
        const attributesManager = handlerInput.attributesManager;
        let s3Attributes = await attributesManager.getPersistentAttributes() || {counter: 0};
        let counter = s3Attributes.counter || 1;
        const speakOutput = `${counter}回目の呼び出しです`;
        ++ counter;
        s3Attributes.counter = counter;
        attributesManager.setPersistentAttributes(s3Attributes);
        await attributesManager.savePersistentAttributes();        
        return handlerInput.responseBuilder
            .speak(speakOutput)
            .getResponse();
    }
};

プロジェクトのファイル構成

プロジェクトのディレクトリには、この時点で下記のようなファイルが作成されています。

alexa-skill-hoge
 ├── docker-compose.yaml
 ├── lambda
 │   ├── index.js
 │   ├── package.json
 │   ├── server.js
 │   └── util.js
 └── skill-package/
      ├── interactionModels/
      │   └── custom/
      │       └── ja-JP.json
      └── skill.json

ローカル環境の起動

MinIOサーバーの起動

~/alexa-skill-hogeディレクトリで、MinIOサーバーを、Docker Composeを使って起動します。

sudo docker-compose up

下記のような画面が表示されたらMinIOサーバーが起動されています。

初回の起動後はバケットを作成する必要があります。ブラウザでhttp://127.0.0.1:9000にアクセスします。ログイン画面が表示されたら、Access Key, Secret Keyに、myminioadminと入力します。

ログインできたら、右下のプラス(+)ボタンを押し、次にその上に表示されるCreate bucketボタンを押します。バケットの名前を求められるので、mys3bucketと入力します。

バケットが作成されました。

バケットは一度作成すると永続化されるので、MinIOサーバーを再起動しても、再度作成する必要はありません。

カスタムスキルの起動

前記事に従ってカスタムスキルを起動します。

npm start

NGROKの起動

前記事に従ってNGROKを起動します。

ngrok http 3000

エンドポイントの設定

前記事に従ってエンドポイントを設定します。

カスタムスキルのテスト

Alexa開発者コンソールでテストタブを選びます。Alexaシミュレータから、「ほげを開いて」と入力します。「1回目の呼び出しです」と返事が返ってきます。何度か「ほげを開いて」と入力してみてください。「○回目の呼び出しです」と回数が1つずつ増えて行けば期待通りに動いています。

おわりに

Alexa-Hostedスキルを、S3によるセッションの永続化も含めて、ローカル環境でテストする方法を紹介しました。

スキルの使い勝手を向上するには、過去のやり取りからユーザーの好みに対応したり、会話のコンテキストを維持しながらマルチターンのやり取りを行うなど、ステートフルな処理が必要になってきます。そのためには、S3を利用したセッションの永続化は、重要な役割を果たします。このS3の永続化も含めてローカル環境でテストできることは、開発者にとって大いに役立つものと思います。

付録. ソースコード

Githubのレポジトリ

更新記録

日付内容
2021/07/17docker-compose.yamlファイのVolumesの定義例で、Windows環境の場合”C:\data:/data”としていたものを”C:/data:/data”に変更。VS CodeのDocker Extensionから起動した場合、Volumeの構成が正しく行われないため (本稿のコマンドラインからの起動例では問題ない)。
2021/04/11ソースコードをGithubに移動
.gitignoreの.envのパスを修正
2021/03/14タイトル変更
ASK CLI V2に対応した記述に変更
2020/02/02初版公開                    

 

Alexa-Skill
スポンサーリンク