ClovaのExtensionサーバーをLambdaで構築する方法
1. Clova Extensions Kit SDKについて
CEKが、会話の解析をして、JSON形式のデータを作成しています。そのデータを、少ない労力で処理できるように、Clova Extensions Kitソフトウェア開発キット(CEK SDK)が、用意されています。
もちろん、単なる、JSON形式のメッセージのやり取りに過ぎないので、自前で実装することも可能です。
実際にやり取りされるメッセージについては、「ClovaとIoT家電 Clova ⇒ Extensionサーバー」を参照して下さい。
提供される言語は、Node.js、Python、Java、Swift、Kotlin、Elixir、Goです。ここではPythonを使用します。下記手順により開発を進めていきます。
2. Python3.7.2環境作成
## Python 3.7.2のコンパイル・ビルド・インストール cd ~ mkdir tmp cd tmp wget https://www.python.org/ftp/python/3.7.2/Python-3.7.2.tgz tar zxvf Python-3.7.2.tgz cd Python-3.7.2 ./configure --prefix=/usr/bin/python3.7.2 make make install ## virtualenvを用いてPython 3.7.2仮想環境を作成し、バージョンを3.7.2に切りかえる cd ~ virtualenv my_env_python --python=/usr/bin/python3.7.2/bin/python3.7 cd my_env_python source bin/activate python -V ## CEK SDKをgit cloneしてテスト実行 git clone https://github.com/line/clova-cek-sdk-python.git cd clova-cek-sdk-python python -m unittest discover -s ./test -p 'test_*.py'
Pythonの実行環境依存のライブラリィも必要なため、Amazon Linuxで構築したEC2インスタンス上で実行しています。EC2の他にAmazon Linuxのdockerイメージを利用する方法もあります。
Amazon Linux上で行う理由は、Lambdaの実行環境のOSが、Amazon Linuxのためです。
詳細は、AWS Lambda Runtimesを参照して下さい。
8~10行目で、ソースからPython3.7.2をコンパイルしてビルドしています。14行目でvirtualenvを使用して、既存の環境を壊さずにPython 3.7.2の仮想環境を作成しました。virtualenvの使用は必須ではありません。
また3.7.2のバージョンを使用しているのは、Lambda(ランタイム Python 3.7)で使用しているバージョンは、3.7.2 (default, Mar 1 2019, 11:28:42)※1で、それに合わせたためです。
22行目でテスト実行しています(初回時に必要なライブラリィがイントールされる。インストールされたライブラリィは、Lambdaで実行するときに必要になります)。
テストの成功時には、「Ran 53 tests in 0.008s OK」※2と表示されます。
※1 print(sys.version)の実行結果で確認しました。(確認日 2019年4月)
※2 clova-cek-sdk-pythonのバージョンが1.1.1の場合
3. ライブラリィ収集
mkdir -p python_layer/python/lib/python3.7/site-packages cd python_layer/python/lib/python3.7/site-packages ## 必要なpythonのソース及びコンパイルされた環境依存のライブラリィをコピーします cp -r /home/ec2-user/my_env_python/lib/python3.7/site-packages/asn1crypto . cp -r /home/ec2-user/my_env_python/lib/python3.7/site-packages/cffi . cp -r /home/ec2-user/my_env_python/lib/python3.7/site-packages/cryptography . cp -r /home/ec2-user/my_env_python/lib/python3.7/site-packages/idna . cp -r /home/ec2-user/my_env_python/lib/python3.7/site-packages/pycparser . cp -r /home/ec2-user/my_env_python/lib/python3.7/site-packages/.libs_cffi_backend . cp -r /home/ec2-user/my_env_python/lib/python3.7/site-packages/six.py . cp -r /home/ec2-user/my_env_python/lib/python3.7/site-packages/easy_install.py . cp -r /home/ec2-user/my_env_python/lib/python3.7/site-packages/_cffi_backend.cpython-37m-x86_64-linux-gnu.so . cp -r /home/ec2-user/my_env_python/clova-cek-sdk-python/cek . ## 不要なpythonキャッシュの削除 find ./ -type d -name "__pycache__" -prune -exec rm -r {} \; ## ライブラリィをzip形式に圧縮 (Lambda Lyaerを使用しない場合) zip -r site-packages.zip . ## ライブラリィをzip形式に圧縮 (Lambda Lyaerを使用する場合) ## python_layerに移動 cd ../../../.. zip -r layer.zip python/
Lambdaで提供されていないライブラリィは、自分でアップロードする必要があります。Pythonのライブラリィには、コンパイルされた環境依存のネイティブライブラリィが
必要な場合もあります。
13行目がOS依存となるネイティブライブラリィとなります。その他のフォルダ配下にもネイティブライブラリィがあります。AWS IoT coreにアクセスするライブラリィが、TLS1.2以上を使用するため、Lambdaのランタイムは、Python 3.7を使用しています。
3.6の場合エラーになります。(バージョンによりOpenSSLのバージョンが違い、そのため対応しているTLSのバージョンに相違があます。)
17行目で、不要なPythonのキャッシュを削除をしているのは、アップロードするサイズが一定サイズ以上になると、LambdaのGUI上でソースが修正できなくなるため削除しています。
20行目では、Lamada Layerを使用しない場合の、zip圧縮方法です。使用する場合は、20行目を実行しないで、24行目以降を実行して下さい。
Lamada Layerは、2018年11月29日に発表され機能で、共通ライブラリィを登録でき、共通ライブラリィのバージョン管理を行うことができます。
注意事項として、ここで記載しているSKDを実行するのに必要なライブラリィは、clova-cek-sdk-pythonのバージョンが1.1.1の場合であり、将来、変わる可能性があります。
4. ハンドラー定義
下記インテントに対するハンドラーを定義して、LED照明を操作しています。
家電の操作は、AWS IoT Topic経由で、ラズベリーパイにメッセージを送付しています。そのあとに、赤外線を照射して家電を操作しています。
- LEDColorManagement
照明の色を変える - LEDOnManagement
照明を消す - LEDOffManagement
照明をつける - LEDIlluminanceUpManagement
照明を明るくする - LEDIlluminanceDownManagement
照明を暗くする
import boto3 import cek import json from cek import Clova from cek.core import ApplicationIdMismatch from cryptography.exceptions import InvalidSignature def lambda_handler(event, context): speech_builder = cek.SpeechBuilder() response_builder = cek.ResponseBuilder() clova_handler = cek.RequestHandler("xxx.xxx.xxx") reprompt_message = "マスター、他に何かご用件はございますか。" @clova_handler.launch def launch_request_handler(clova_request): return response_builder.simple_speech_text("IoT管理を行います!") @clova_handler.intent("LEDColorManagement") def color_handler(clova_request): # スロット取得 if clova_request.slot_value("led_color") != None: led_color = clova_request.slot_value("led_color") responseText = "照明を" + led_color + "色にしました。" publishTopic("light", "color", color=led_color) else: responseText = "使用できる色は、青、赤です。" return response_builder.simple_speech_text(responseText) @clova_handler.intent("LEDOnManagement") def turn_on_handler(clova_request): responseText = "照明をつけました。" publishTopic("light", "power", power="on") return response_builder.simple_speech_text(message=responseText) @clova_handler.intent("LEDOffManagement") def turn_off_handler(clova_request): responseText = "照明を消しました。" publishTopic("light", "power", power="off") return response_builder.simple_speech_text(message=responseText) @clova_handler.intent("LEDIlluminanceUpManagement") def illuminance_up_handler(clova_request): responseText = "照明を明るくしました。" publishTopic("light", "power", power="up") return response_builder.simple_speech_text(message=responseText) @clova_handler.intent("LEDIlluminanceDownManagement") def illuminance_down_handler(clova_request): responseText = "照明を暗くしました。" publishTopic("light", "power", power="down") return response_builder.simple_speech_text(message=responseText) @clova_handler.default def default_handler(clova_request): return response_builder.simple_speech_text("指示を正しく認識できませんでした。") @clova_handler.end def end_handler(clova_request): return response_builder.simple_speech_text("IoT管理を終了します。") try: response = clova_handler.route_request(request_body=event['body'].encode("UTF-8"),\ request_header_dict=event['headers']) plain_text = speech_builder.plain_text(reprompt_message) response.reprompt = speech_builder.simple_speech(plain_text) return get_lambda_response(200, response) except InvalidSignature: return get_lambda_response(401, {"error": "InvalidSignature"}) except ApplicationIdMismatch as e: return get_lambda_response(401, {"error": "WrongApplicationId"}) except Exception as e: return get_lambda_response(500) def get_lambda_response(status_code, response={}): return { "statusCode": status_code, "headers": {"Content-Type": "application/json"}, "body": json.dumps(response), "isBase64Encoded": False } def publishTopic(device_type, action, power=None, color=None): if device_type == "light": if action == "power": payload = '{"data": {"device_type": "' + device_type + '", "action": "' + action + '", "power": "' + \ power + '"}}' elif action == "color": payload = '{"data": {"device_type": "' + device_type + '", "action": "' + action + '", "color": "' + \ color + '"}}' else: return; else: return; iot = boto3.client("iot-data") iot.publish(topic="iot/1", qos=0, payload=payload)
公式SDKと、Python標準機能のデコレータ機能により、ハンドラーの実装は難しくありません。見てのとおり、Clovaとの会話のやり取りは非常に簡単です。 以下にそれぞれの処理について解説します。
- 11行目で、「Application ID」を指定しています。ここでの「Application ID」は、スキル設定基本情報の「Extention ID」に該当します。 また、「debug_mode=True」と指定することで、HTTP Bodyに対する電子署名の検証を省略することができます。テスト時は、電子署名付きで呼び出すことができないため、そのようなパラメータが用意されています。
- 12行目は、5秒間、Clovaと会話がなかった場合の、Clovaが話す内容を設定しています。
- 18行目の「@clova_handler.intent("LEDColorManagement")」の「LEDColorManagement」は、CEKのGUI画面で定義したインテントに該当します。
- 26行目は、赤、青以外の色を指定した場合の処理です。ここの処理が行われる場合は、 色の認識に失敗したか、ただ単に、色の定義漏れかのどちらかです。
- 53行目では、デフォルトハンドラーの定義で、定義済みのハンドラーで処理できなかった場合に、呼び出されます。 この処理が呼ばれる場合は、Clova側で、話す内容がきちんと認識されなかった場合か、ただ単に、ハンドラーの定義漏れの場合です。
- 62、63行目の「route_request」メソッドの呼出しで、「会話=インテント」に該当するハンドラーが実行されます。
- 67行目では、HTTP Bodyに対する電子署名の検証時に例外が発生した処理になります。HTTPステータスコード401 (Unauthorixed)で返しています。詳細は、「5. セキュリティについて」を参照して下さい。
- 69行目では、11行目で指定したApplicationIdと、CEKのスキル基本情報のExtension IDが不一致の場合の例外処理になります。HTTPステータスコード401 (Unauthorixed)で返しています。 「ApplicationIdMismatch」例外は、バージョン1.1.2で追加された例外です。
- 95行目で、送信するメッセージを生成し、AWS IoTのトピックに送信しています。メッセージ仕様については、下記を参照して下さい。
- Lambdaの初回実行時は、コンパイルするのに時間がかかるため、Lambdaのタイムアウト値は、初期値の3秒より、かなり大きい値を設定したほうが良いです。そうしない場合、API Gatewayからの呼び出しで、かなり高い確率でエラーとなります。
5. セキュリティについて
CEKから呼び出される場合、HTTP Bodyの電子署名結果を、HTTPリクエストヘッダのパラメータに付与して呼ばれます。そのあとSDKの中で電子署名の検証を行います。検証エラーの場合は、 InvalidSignature例外が発生します。呼出元は、必ず電子署名の秘密鍵を保持しているCEKとなり、電子署名により改竄は困難になります。 また、電子署名の検証をすることでアクセス元の検証を行うことができます。 詳細は、公式ドキュメントの「リクエストメッセージを検証する」 を参照して下さい。 また、HTTPS通信を使用することで通信の内容は暗号化されます。
6. メッセージ仕様について
メッセージは、Node-Redで汎用的に処理できるようにしました。device_typeで操作対象を特定し、actionで操作内容を示しています。 ここでは、照明しか操作していないが、エアコン、テレビを操作する場合は、device_typeには、aricon、televisionが設定されます。
照明をつけて{ "data": { "device_type": "light", "action": "power", "power": "on" } }照明を消して
{ "data": { "device_type": "light", "action": "power", "power": "off" } }照明を青にして
{ "data": { "device_type": "light", "action": "color", "color": "青" } }照明を明るくして
{ "data": { "device_type": "light", "action": "power", "power": "up" } }
7. API Gateway定義
API GatewayのPOSTメソッドの定義する際に、必ず「Lambda プロキシ統合の使用」の使用にチェックして、定義して下さい。 定義したメソッドをデプロイして、Clova Developer Centerの「ExtensionサーバのURL」に、エンドポイントを設定します。