この記事は『RPAとの会話をMicrosoft Teamsに集約するしくみを作成してみた (Automation Anywhere v11編)』で利用する仲介ウェブサービスをAzure Functionsで作成する内容です。HTTPトリガーを使用した基本的な関数アプリの作成方法とストレージのバインド方法/入出力について、Microsoft Docsに書いてあるサンプル以上のことに触れているので、RPAと一緒に使うかどうかにかかわらず、Azure Functions一般的な基礎知識とはまり所が理解できます。
Azure 利用機能
- Azure Functions (Azure Portalでの関数編集、C# Script)
- Azure Table Storage
環境
- Automation Anywhere Enterprise 11.3.3
- Microsoft Teams (2020/1/28版)
- Azure Functions ランタイムバージョン v3.0.13107 (~3.x)
- インターネットにつながりHTTP通信が、使用されるURLに対して通ること
参考記事
- Azure Portal で初めての関数を作成する
- Functions を使用して Azure Storage キューにメッセージを追加する
- Azure Functions のトリガーとバインド
- Azure Functions における Azure Table Storage のバインド
- Azure Functions C# スクリプト (.csx) 開発者向けリファレンス
全体像
Microsoft Azure のサーバーレスアプリケーションを作成する仕組みであるAzure Functionsを使って、RPAとMicrosoft Teamsとの会話においてTeamsからの回答をAzure Table Storageに保存、それをRPAからリッスンして特定のトランザクションのコンテンツが保存されていれば取り出す、ということを仲介するウェブサービスを作成します。
仲介WebサービスのURLの例: ("tMXShyxV8HkosUE" はセッション文字列)
URL | 備考 |
---|---|
https://<アプリ名>.azurewebsites.net/api/OK-GET/?key=tMXShyxV8HkosUE |
このセッション文字列に対してWebサービスが呼ばれていれば、HTTP Status 200 OKを返す。 |
https://<アプリ名>.azurewebsites.net/api/OK-SET/?key=tMXShyxV8HkosUE |
呼ばれると、以後、セッション文字列に対応して返す値がHTTP Status 200 OKに変わる。 |
環境構築
Azureのサブスクリプションを持っており、Azure Portalにサインインできてサービスが使えることが前提です。環境構築の部分はありきたりなステップなのですが、Azureはよくユーザーインターフェイス (UI) が変わり、公式ドキュメントではまだ古いインターフェイスのままで最新のUIによる解説が他のサイトの記事でもなかったので、スクリーンショットもつけることにしました。
関数アプリリソースのデプロイ
Azure Portalにログインして、ポータル左上の「リソースの作成」をクリックします。
-
関数アプリの作成ウィザードで以下の項目を設定します。
-
[基本]ページ
設定後、「次:ホスト中 > 」ボタンをクリックします。- サブスクリプション:関数アプリを作成するサブスクリプションを選択します。
- リソースグループ:関数アプリを作成して保有するリソースグループを指定します。新規作成してもかまいません。後でリソースを整理する際に整理しやすいようにグループ化しておいてください。
-
関数アプリ名: グローバルで一意の関数名 (URL:
XXXX.azurewebsites.net
のXXXX部分) を指定します。他ですでに使われている名前は指定できません。 - 公開: 既定値「コード」のままにします。
- ランタイムスタック:「.NET Core」を指定します。
- リージョン: 選択肢に出てくる場所のいずれかを指定してください。
-
[ホスト中]ページ
設定後、「確認及び作成」ボタンをクリックします。- ストレージアカウント:バインドするストレージアカウントを指定します。バインドするアカウントをすでに持っている場合は既存のアカウントを、ない場合は新規で作成されているモノをそのまま指定しておきます。
- オペレーティングシステム:使いやすいOSを選んでください。どちらでもよいですが、ひとまず「Windows」としておきます。
- プラン:既定の「従量課金プラン」にしておきます。
-
HTTPトリガーされる関数アプリの作成
いよいよ実際に関数アプリを作成していきます。
- 左ナビゲーションの「関数」メニューをクリックします。
-
「新しい関数」をクリックします。
- テンプレートの選択画面で、「HTTP trigger」をクリックします。
- 表示される「HTTP trigger」ブレードで、関数名 (ここでは「OK-GET」)、承認レベルをAnonymousに設定して「作成」ボタンをクリックします。
すると、以下のような画面が得られます。Azure Portal上のウェブベースのエディタでC# Scriptを編集してプログラミングが可能です。また、左側のメニューには「統合」「管理」「監視」の各メニューが表示されます。
利用するストレージの種類の決定
ここでちょっと横道にそれて、関数アプリと一緒に使うストレージについて考えてみます。Azure Functionsでは、Azure Queue Storage、Azure Blob Storage、Azure Table Storage、Azure Files、Azure Cosmos DBなど様々なストレージと連携することが可能です。それぞれのストレージの特長はここでは触れませんが、保存できるデータ型はもちろん、どれくらいの量のトランザクションに向いているか、すべてのイベントの捕捉が保証されている/いない、トリガー/入力/出力として使える/使えない、等の癖があります。詳細は公式ドキュメントの『Azure Functions のストレージに関する考慮事項』や『Azure Storage の概要』等を参照してください。
一番スタンダードにバランスが取れているのはRDBであるAzure Table Storageになりますので、この記事ではTable Storageを使うこととします。
「統合」画面で入出力バインドの作成
さて、本題に戻りましょう。次に入出力バインドを設定します。関数へのバインドとは、関数への入出力となるリソースとのインターフェイスを宣言しておくことです。Azure Functionsでは、これを「統合」画面のUIで指定します。(これがfunction.json内の宣言コードを自動生成します。)
Azure Table Storageのデータを参照しようとしているOK-GET関数を例にとって設定します。
- OK-GET関数の「統合」画面に移動します。画面を見ると「トリガー」「入力」「出力」の3つの種類が表示されています。OK-GET関数はHTTPトリガー関数なので、トリガーのHTTP (req)と出力のHTTP($return)はあらかじめ設定されています。Azure Table Storageの値を入力に設定するために「新しい入力」をクリックします。
- 入力に指定できるリソースの種類が表示されるので「Azure Table Storage」を選択して「選択」ボタンをクリックします。
- テーブルパラメータ名はコード内で使う引数の変数名になるので、必要に応じて変更します。また、テーブル名はストレージアカウント内にある実際のテーブル名を指定してください。終わったら「保存」ボタンをクリックすると完成です。
出力側のバインドも同様に設定可能です。
コードの作成と解説
それぞれ、指定されている関数名のHTTPトリガー関数アプリをひとつずつ作成して、入出力バインドを指定してfunction.jsonを生成し、コード部分であるrun.csxにコードを貼り付けることで作成できます。
OK-SET
ウェブサービスを呼ぶときのURLのフォーマットは以下の形式になります。
URL: https://<アプリ名>.azurewebsites.net/api/OK-SET/?key=<セッション文字列>
ウェブサービスにPOSTするパラメータの内容はbodyの中にJSON形式でkeyを指定する形でもOKです。
{
"key": "<セッション文字列>"
}
function.jsonはアプリ作成画面でバインドを選択すると自動的に作成されます。
{
"bindings": [
{
"authLevel": "anonymous",
"name": "req",
"type": "httpTrigger",
"direction": "in",
"methods": [
"get",
"post"
]
},
{
"name": "$return",
"type": "http",
"direction": "out"
},
{
"type": "table",
"name": "outputTable",
"tableName": "Table",
"connection": "AzureWebJobsStorage",
"direction": "out"
}
],
"disabled": false
}
本体であるrun.csxには以下のコードを貼り付けます。ここで#r
ステートメントは、公式ドキュメントにもあまりきちんと解説がありませんが、using
と同様にアセンブリの参照に使われているようです。
#r "Newtonsoft.Json"
#r "Microsoft.WindowsAzure.Storage"
using System.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.WindowsAzure.Storage.Table;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
public static async Task<IActionResult> Run(HttpRequest req, ICollector<Transaction> outputTable, ILogger log)
{
log.LogInformation("C# HTTP trigger function processed a request.");
string key = req.Query["key"];
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
key = key ?? data?.key;
// Tableにレコードを追加。同名のPartitionkeyがすでにあると500 errorを返す
if (key!=null){
outputTable.Add(
new Transaction() {
PartitionKey = key,
RowKey = key,
Result = "1" });
return (ActionResult)new OkObjectResult($"{key}");
}
// keyに値がないと 400 bad requestを返す
return new BadRequestObjectResult("Please pass an argument correctly.");
}
public class Transaction
{
public string PartitionKey { get; set; }
public string RowKey { get; set; }
public string Result { get; set; }
}
戻り値: 正常終了 (HTTP Status=200 OK) の場合は"1"、そうでない場合はエラーメッセージ文字列が格納されます。
やっていること
keyという引数をURLとbodyの中から探します。これがセッションを一意に特定するセッション文字列の役割を果たします。
keyがきちんと指定されていれば、ICollectorインターフェイスを使って新しいデータを追加します。その際、レコードを判別するためのデータ PartitionKeyとRowKeyにはセッション文字列を、ResultにはOKを表す1を指定します。テーブルがTable Storage内にまだ作られていない場合でも、初回にこの関数が呼び出されたときに正常に作られます。Transactionクラスはテーブル内の構造を定義します。
関数をコンパイル、実行後、ためしにブラウザーを使ってテストをしてみると、以下のようになっていれば正常に実装されています。
Azure Table Storage内に正常にデータが格納されたかをチェックするには無料のツールAzure Storage Explorerが便利です。Storage Explorerを使った接続方法はこちらを参照してください。
OK-GET
URL: https://<アプリ名>.azurewebsites.net/api/OK-GET/?key=<セッション文字列>
ウェブサービスにPOSTするパラメータの内容はbodyの中にJSON形式でkeyを指定する形でもOKです。
{
"key": "<セッション文字列>"
}
function.jsonはアプリ作成画面でバインドを選択すると自動的に作成されます。
{
"bindings": [
{
"authLevel": "anonymous",
"name": "req",
"type": "httpTrigger",
"direction": "in",
"methods": [
"get",
"post"
]
},
{
"name": "$return",
"type": "http",
"direction": "out"
},
{
"type": "table",
"name": "inputTable",
"tableName": "Table",
"take": 50,
"connection": "AzureWebJobsStorage",
"direction": "in"
}
],
"disabled": false
}
本体であるrun.csxには以下のコードを貼り付けます。
#r "Newtonsoft.Json"
#r "Microsoft.WindowsAzure.Storage"
using System.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.WindowsAzure.Storage.Table;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
public static async Task<IActionResult> Run(HttpRequest req, CloudTable inputTable, ILogger log)
{
log.LogInformation("C# HTTP trigger function processed a request.");
string key = req.Query["key"];
string result="";
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
key = key ?? data?.key;
if (key!=null){
TableQuery<Transaction> rangeQuery = new TableQuery<Transaction>().Where(
TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, key));
foreach (Transaction entity in
await inputTable.ExecuteQuerySegmentedAsync(rangeQuery, null))
{
result=entity.Result;
}
// keyは指定されているが対応するレコードがないと "" を返す
return (ActionResult)new OkObjectResult($"{result}");
}
// keyに値がないと 400 bad requestを返す
return new BadRequestObjectResult("Please pass an argument correctly.");
}
public class Transaction: TableEntity
{
public string PartitionKey { get; set; }
public string RowKey { get; set; }
public string Result { get; set; }
}
戻り値: OKボタンが押されていない間は空白を、OKボタンが押されるとボタンの種類を表す数字 (OK=1)を返すものとします。いずれも正常終了 (HTTP Status=200 OK)のステータスを返します。
やっていること
keyという引数をURLとbodyの中から探します。これがセッションを一意に特定するセッション文字列の役割を果たします。
keyがきちんと指定されていれば、CloudTableオブジェクトを使って新しいデータを追加します。その際、レコードを判別するためのデータ PartitionKeyとRowKeyにはセッション文字列を指定します。Transactionクラスはテーブル内の構造を定義します。
関数をコンパイル、実行後、ためしにブラウザーを使ってテストをしてみると、以下のようになっていれば正常に実装されています。 (先にOK-SETでTable Storageに値を書き込んでいる前提。)
yesno-SET
URL: https://<アプリ名>.azurewebsites.net/api/yesno-SET/?key=<セッション文字列>
function.jsonはアプリ作成画面でバインドを選択すると自動的に作成されます。
OK-SETと似た構造になります。後日更新予定。
yesno-GET
URL: https://<アプリ名>.azurewebsites.net/api/yesno-GET/?key=<セッション文字列>
function.jsonはアプリ作成画面でバインドを選択すると自動的に作成されます。
OK-GETと似た構造になります。後日更新予定。
comment-SET
URL: https://<アプリ名>.azurewebsites.net/api/comment-SET/?key=<セッション文字列>
function.jsonはアプリ作成画面でバインドを選択すると自動的に作成されます。
OK-SETと似た構造になります。後日更新予定。
comment-GET
URL: https://<アプリ名>.azurewebsites.net/api/comment-GET/?key=<セッション文字列>
function.jsonはアプリ作成画面でバインドを選択すると自動的に作成されます。
OK-GETと似た構造になります。後日更新予定。
総括
Azure Functionsの便利なところ
Azure Functionsを使ってみて思ったのは、やはりクラウド上でのVMや開発環境の構築をほとんどしなくても実用的なウェブサービスがすぐに構築でき、かつ安価に始められるところはとても便利です。通常だとウェブサービスの土台となるVMの設計、冗長性の検討をして構築してから上物を作る必要があり、また、VMを準備するとそれなりのお金がかかります。
Azure Functionsの場合はストレージとネットワークは課金されるものの、Azure Functionsの実行はかなりの部分が無料提供に含まれています。
インフラ部分は隠蔽されているので、インフラの構築や管理の部分を気にすることなく、上物の関数アプリの作成に集中できるので、インフラ知識の乏しいディベロッパーにとってとても便利です。
注意点、はまり所
Azure Functionsは、インフラは隠蔽されていますが実際には見えないところでVMが動いており、コーディングのお作法が通常の.NET 環境と異なっていたり、ランタイムバージョンもいくつかあるため、特有のクセもあります。そこは覚えてマスターしておく必要があります。今回、はまったところをいくつか挙げます。
ストレージの種類により利用できるバインディングが異なる
前でも述べましたが、Azure Functionsと連携して使えるストレージは何種類かありますが、公式ドキュメントの一番簡単な例としてはAzure Queue Storageを使っています。Azure Queue StorageはAzure Functionsから出力をすることはできますが、Azure Queue Storage内のデータを入力バインディングに指定することはできません。このため、書き込むことはできるけど参照できないということになります。Queue Storageに書き込まれたデータをもとにトリガーで別のAzure Functionsを起動するような使われ方が想定されているようなので、素直にそのシナリオに合った使い方の時にのみ利用するのがよさそうです。
ストレージの動的バインディングはできない!?
ここでちょっと頑張って、通常の.NETコードで可能なように、コード内で動的バインディングを行うクラスライブラリを通してQueue Storageを関数アプリ内で読み込むようにすればよいのでは、というのをやってみようとしました。
『.NET を使用して Azure Queue Storage を使用する』に従い、アセンブリの参照設定を追加、
using Microsoft.Azure; // Namespace for CloudConfigurationManager
using Microsoft.Azure.Storage; // Namespace for CloudStorageAccount
using Microsoft.Azure.Storage.Queue; // Namespace for Queue storage types
その後、
// Parse the connection string and return a reference to the storage account.
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(
CloudConfigurationManager.GetSetting("StorageConnectionString"));
CloudQueueClient queueClient = storageAccount.CreateCloudQueueClient();
のようなクラスを使ってやってみました。しかし、Azure FunctionsのC# Script内では、結局このクラスを認識しませんでした。
Azure Functionsでサポートされないアセンブリをアップロードして使う方法もあるようで、これをすればできるのかもしれませんが、ライセンスや今後のバージョンアップによる変更管理等を考えると、標準環境でできないことを無理やりできるようにしても運用上うまくいかないだろうということで、あきらめました。
Functions 2.x以降では IQueryable が使えない
LINQのインターフェイスでテーブルの読み取りに便利なIQueryable
インターフェイスですが、Functions 1.xでのみサポートされ、2.x以降では利用できません。代わりにCloudTable
オブジェクトを使った読み取りになります。書式のおまじないがちょっと変わるだけなのでそんなに大きな影響はありませんが、これはちょっと残念でした。詳細は『Azure Functions における Azure Table Storage のバインド』を参照してください。
ちなみに、2.x以降でも間違ってIQueryable
インターフェイスを入力バインディングの引数に実装したままコンパイルを行っても成功してしまいます。ただし実行時に以下のエラーが出て入力バインディングが動きません。
2020-02-16T16:30:23.785 [Error] Microsoft.Azure.WebJobs.Host: Error indexing method 'Functions.XXXX'. Microsoft.Azure.WebJobs.Extensions.Storage: Can't bind Table to type 'System.Linq.IQueryable`1[Submission#0+Transaction]'.
同じ関数内で同じストレージを入出力バインディング両方に指定しないほうがいい!?
「OK-GET」と「OK-SET」の関数アプリはほぼ同じ構造なので、ひとつの関数アプリ「OK」にまとめて、Table Storageへの参照と書き込みを両方やってしまおうとも考えました。ただし、参照するTable Storageは同じテーブルとなり、特に初回実行時にテーブルが用意されていない場合にエラーになるので、分けることにしました。