かずきのBlog@hatena

日本マイクロソフトに勤めています。このブログは個人ブログなので、ここに書いている内容は個人的な意見で会社の公式見解ではない点にご注意ください。好きなものは XAML と C# 。苦手なものは型の無い言語です。

Google Assistant アプリの開発のバックエンドに Azure Functions を使おう

ということでいってみようと思います。

Dialogflow で作業

これは特に Azure に依存した話しではないので置いておいて適当にインテントとか作ります。

Azure Functions で作業

ポータルでぽちぽちやってもブラウザ上でコーディングできるのですが、今現在ではコードの補間が効かないので苦行です。お勧めは Azure Functions の CLI ツールを入れてローカルで開発する方法です。

以下のドキュメントからさくっといきましょう。今回は stable の Azure Functions v1 でやってます。v1 の残念なところはローカル開発用の CLI が Windows でしか動かないところです。v2 はプレビューですが macOS や Linux でも動きます。ただ、v2 で今回使う azure-function-express が動くかは怪しいです。

docs.microsoft.com

ということでローカルで作業します。

作業用フォルダとともに Azure Functions 用に構成されたフォルダを作成します。フォルダを作成して移動して以下のコマンドで作成できます。

func init

次に関数を作成します。func new とうつと関数を作成するウィザードみたいなのが走ります。 最初に言語を選ぶように言われるので JavaScript を選びます。その後、関数が起動するトリガーを選択するので HTTP POST を選びます。最後に関数名を入れるので dialogflow とでも入れておきましょう。

こうすると https://host/api/dialogflow に POST でアクセスできる関数が作成されます。

続けて利用するモジュールを追加します。

npm init -y
npm i express
npm i azure-function-express
npm i dialogflow
npm i dialogflow-fulfillment
npm i actions-on-google

最初の2つ目が見慣れないものになると思いますが Azure Functions で express を使えるようにする関数になります。あと、追加する依存関係は以下の package.json を参考にしました。不要なのもあるのかな?

github.com

後は、とっかかりとして必要最低限のコードを dialogflow/index.js に書きます。以下のサイトを参考に azure-function-express を使って書きます。

github.com

const createHandler = require("azure-function-express").createHandler;
const express = require("express");
const {WebhookClient} = require('dialogflow-fulfillment');
const {Card, Suggestion} = require('dialogflow-fulfillment');

process.env.DEBUG = 'dialogflow:debug';

const app = express();
app.post("/api/dialogflow", (request, response) => {
    const agent = new WebhookClient({ request, response });
    request.context.log('Dialogflow Request headers: ' + JSON.stringify(request.headers));
    request.context.log('Dialogflow Request body: ' + JSON.stringify(request.body));

    function welcome(a) {
        a.add('Welcome to my agent!');
    }
    function fallback(a) {
        a.add(`I didn't understand`);
        a.add(`I'm sorry, can you try again?`);
    }
    function now(a) {
        a.add('now!');
    }

    const intentMap = new Map();
    intentMap.set('Default Welcome Intent', welcome);
    intentMap.set('Default Fallback Intent', fallback);
    intentMap.set('now', now);

    agent.handleRequest(intentMap);
});
 
module.exports = createHandler(app);

ローカルで実行するには func host start で 7071 ポートで待ち受ける感じで動き始めます。作成した関数にアクセスするための URL が以下のように表示されると思います。

[2018/09/03 8:22:43] Job host started

Http Functions:

        dialogflow: http://localhost:7071/api/dialogflow

Debugger listening on [::]:5858

テストするには Actions on Google の Simulator で動作確認してみましょう。動作確認をするにあたっては ngrok か類似のサービスを使ってやるといいです。

ngrok.com

以下のコマンドを打ってローカルで実行してる関数にインターネットからアクセスできるようにしてあげましょう。

ngrok http 7071 -host-header="localhost:7071"

表示された URL に api/dialogflow を追加したものを Dialogflow の Fulfillment に設定します。

f:id:okazuki:20180903172744p:plain

とりあえず「いまなんじ」って聞いたら now というインテントとして認識して Fulfillment に処理を飛ばすものを Dialogflow で作成して、以下のコードを動かしてみました。

const createHandler = require("azure-function-express").createHandler;
const express = require("express");
const {WebhookClient} = require('dialogflow-fulfillment');
const {Card, Suggestion} = require('dialogflow-fulfillment');

process.env.DEBUG = 'dialogflow:debug';

const app = express();
app.post("/api/dialogflow", (request, response) => {
    const agent = new WebhookClient({ request, response });
    request.context.log('Dialogflow Request headers: ' + JSON.stringify(request.headers));
    request.context.log('Dialogflow Request body: ' + JSON.stringify(request.body));

    function welcome(a) {
        a.add('Welcome to my agent!');
    }
    function fallback(a) {
        a.add(`I didn't understand`);
        a.add(`I'm sorry, can you try again?`);
    }
    function now(a) {
        a.add('now!');
    }

    const intentMap = new Map();
    intentMap.set('Default Welcome Intent', welcome);
    intentMap.set('Default Fallback Intent', fallback);
    intentMap.set('now', now);

    agent.handleRequest(intentMap);
});
 
module.exports = createHandler(app);

now インテントに対応する処理を先ほどのコードに対して追加してます。これで動かすとこんな感じになりました。ちゃんと動いてますね。

f:id:okazuki:20180903172935p:plain

Azure にデプロイ

動きが確認できたら Azure Functions にデプロイしてみましょう。 ポータル上で左上のほうにあるリソースの作成を押して Function App を検索して作成します。

f:id:okazuki:20180903173216p:plain

  • アプリ名:世界で一意になる名前をつけてあげます。URL に使われます。
  • サブスクリプション:Azure のサブスクリプションは契約だと思っていただければいいと思います。この画面に来れてるということは何らかの契約(フリーでもトライアルでも企業としてでも etc...)を持ってるので何かしら出てるはずです。
  • リソースグループ:Azure 上に作成するストレージやサーバーなどはリソースと言われます。リソースグループは、これをまとめるフォルダみたいなものです。適当に名前つけてやってください。
  • OS:下で動いてる OS を何にするかです。とりあえず Windows で。Windows で感覚掴んだらドキュメントを見て Docker なり Linux なりを選んで遊んでみてください。
  • ホスティング プラン:従量課金は本当に動いた分だけ課金されます。重い処理が無いならこちらで。
  • 場所:近い場所を選びましょう。西日本か東日本が日本で使う場合はいいと思います。
  • Storage:Azure Functions のランタイムが裏で使うストレージの名前を指定します。特に名前にこだわりが無ければデフォルトでいいです。
  • Application insights:オンにするとログが保存されます。オンにしましょう。エラーログとかも見れます。
  • Application Insights の場所:日本が残念ながらないので一番近い Southeast Asia を選びましょう。

デプロイ方法は GitHub からとか Visual Studio Team Services からとか Zip デプロイとか色々ありますが開発してみてちょっと試すのにお勧めはローカル Git リポジトリと呼ばれるやつです。本番は各種 CI ツールなどからサクッとデプロイしちゃってください。

構成方法は、作成した Function App のプラットフォーム機能の展開オプションから出来ます。

f:id:okazuki:20180903175543p:plain

展開オプションを選んだら次の画面でセットアップを選択します。ソースの選択を教えてローカル Git リポジトリを選びましょう。

f:id:okazuki:20180903175723p:plain

続けてプラットフォーム機能のデプロイ資格情報で、git コマンドでデプロイするときの資格情報を設定します。

f:id:okazuki:20180903180110p:plain

そして、プラットフォーム機能の中のすべての設定から Git クローン URL を確認します。

f:id:okazuki:20180903175946p:plain

あとは以下のコマンドを打ちましょう。

リポジトリをまだローカルマシン上に作成してないなら以下のようにさくっと作成して

git init
# 必要に応じて以下のコマンドの前に .gitignore を追加 node_modules/ くらいは追加したほうがいいと思う
git add --all
git commit -m 'initial commit'

そして、リモートに先ほど確認した URL を追加して push すれば OK です。

git remote add azure https://先ほどのgitリポジトリのURL
git push azure master

そうすると、コンソールにデプロイのログが流れていきます。 npm のパッケージのリストアとかもされてるのが確認できると思います。

デプロイが成功したら、ポータル上に表示されます。関数の URL の取得を押すと関数を呼び出すための URL が出てくるので、これを Dialogflow のページの Fulfillment に設定しましょう。

f:id:okazuki:20180903182237p:plain

こんな感じで動きました。

f:id:okazuki:20180903182605p:plain

従量課金プランの問題点

安いのでお得な従量課金プランですが、暫くアクセスしてないと節約のため自動的にサーバーが寝ます。寝ると起きるのに時間がかかるので、寝た後の初回リクエストに時間がかかってタイムアウトになったりします。なので、場合によっては従量課金プランではなく App Service Plan と呼ばれる月額数千円支払って固定リソースを確保する方法を検討する必要があるかなぁと思います。

お勧めは S1 と呼ばれるプランです。S1 のプランで Function App を作ったらアプリケーション設定の画面にいって常時接続という設定項目をオンにしましょう。これで寝ないサーバーになるので、いつアクセスしてもいい感じでレスポンスを返してくれます。寝ない対策として定期的にタイマートリガーで HttpTrigger 叩くとかも考えられますがどうなんだろう?

他のサーバーレス系サービスの場合は皆どうしてるのかな?