なんか、一番好きな Windows アプリ作りは苦痛そうな雰囲気を多少感じたので、Webアプリ方面に手を出してみようと思っていたところ最適そうな本があったのでアマゾンでぽちりました!
※:アフィリエイトリンクなので嫌な人は気を付けて!
本当はプログラミング言語Goが欲しかったけどKindle無かったので2番目に欲しい!!って思ったWebアプリ開発に特化した奴を買いました。楽しみ!
なんか、一番好きな Windows アプリ作りは苦痛そうな雰囲気を多少感じたので、Webアプリ方面に手を出してみようと思っていたところ最適そうな本があったのでアマゾンでぽちりました!
※:アフィリエイトリンクなので嫌な人は気を付けて!
本当はプログラミング言語Goが欲しかったけどKindle無かったので2番目に欲しい!!って思ったWebアプリ開発に特化した奴を買いました。楽しみ!
Go には標準で Web アプリを作るための機能が入ってるらしい!? 他の言語でいうところの Web アプリ開発のためのフレームワークというのは、あるんだろうなぁと思って調べてみたら凄く素敵なまとめをみつけました。
いいね。本読み終わったら次に手を出してみようかなぁ。
とりあえず net/http
をインポートしてたら簡単なハローワールドは出来上がる。
ドキュメントはここみたい。
http - The Go Programming Language
書籍の最初では、Web アプリを作るための書籍なので HandleFunc
でリクエスト受け取るのに使ってますがドキュメントを見ると以下のようになってる。
Package http provides HTTP client and server implementations.
なるほど、クライアントとサーバー兼ね備えてるんですね。じゃぁ早速クライアント側試してみようと思います。 試すのはこれ。
http://zipcloud.ibsnet.co.jp/api/search
に対して zipcode パラメータつけて GET するだけで OK。とても直感的ですね。
早速
package main import ( "fmt""io/ioutil""net/http" ) func main() { res, err := http.Get("http://zipcloud.ibsnet.co.jp/api/search?zipcode=7830060") if err != nil { fmt.Println(err.Error()) return } defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) if err != nil { fmt.Println(err.Error()) return } fmt.Print(string(body)) }
実行するとこんな感じ。
{ "message": null, "results": [ { "address1": "高知県", "address2": "南国市", "address3": "蛍が丘", "kana1": "コウチケン", "kana2": "ナンコクシ", "kana3": "ホタルガオカ", "prefcode": "39", "zipcode": "7830060" } ], "status": 200 }
いいね!せっかくなので JSON もパースしておきたいですね。愛用している https://quicktype.ioで上記 JSON を張り付けて Go 言語用の構造体とかを作ります。サクッと出来ていいですね。コピペして完了。
// To parse and unparse this JSON data, add this code to your project and do://// zipCodeResult, err := UnmarshalZipCodeResult(bytes)// bytes, err = zipCodeResult.Marshal()package main import"encoding/json"func UnmarshalZipCodeResult(data []byte) (ZipCodeResult, error) { var r ZipCodeResult err := json.Unmarshal(data, &r) return r, err } func (r *ZipCodeResult) Marshal() ([]byte, error) { return json.Marshal(r) } type ZipCodeResult struct { Message interface{} `json:"message"` Results []Result `json:"results"` Status int64`json:"status"` } type Result struct { Address1 string`json:"address1"` Address2 string`json:"address2"` Address3 string`json:"address3"` Kana1 string`json:"kana1"` Kana2 string`json:"kana2"` Kana3 string`json:"kana3"` Prefcode string`json:"prefcode"` Zipcode string`json:"zipcode"` }
そして、これを使って main 関数を書いて
package main import ( "fmt""io/ioutil""net/http" ) func main() { res, err := http.Get("http://zipcloud.ibsnet.co.jp/api/search?zipcode=7830060") if err != nil { fmt.Println(err.Error()) return } defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) if err != nil { fmt.Println(err.Error()) return } r, err := UnmarshalZipCodeResult(body) if err != nil { fmt.Println(err.Error()) return } fmt.Printf("status code: %v, address1: %v, address2: %v, address3: %v\n", r.Status, r.Results[0].Address1, r.Results[0].Address2, r.Results[0].Address3) }
実行するとこうなりました。
status code: 200, address1: 高知県, address2: 南国市, address3: 蛍が丘
うん。ばっちり。あれ?本読んでたけど違うことしちゃってた。まぁいっか。
先ほど脱線して http のクライアント側の機能に走ってしまったので気を取り直してサーバー側に行きたいと思います。
といってもハローワールドするだけなら凄く簡単。本当に凄く
package main import ( "fmt""net/http" ) func main() { http.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) { fmt.Fprintf(res, "Hello world") }) http.ListenAndServe(":8080", nil) }
HandleFunc でハンドラーを登録して ListenAndServe で待つ。以上終了。できた。
試しに http://localhost:8080/hoge/foo?bar=puyopuyo
を叩いてハンドラーの関数にブレークポイントはってリクエストの構造体の中身を見てみた。
当然と言えば当然だけど基本的なものは詰まってるので、後は頑張ればいけそう。
ちなみにパスに対して何かしらするものを登録するのは HandleFunc の他に Hundle というのがあるみたい。 Handle には以下のような Handler インターフェースを渡す。
type Handler interface { ServeHTTP(ResponseWriter, *Request) }
つまり、さっきのをこれを使う用に書き直すとこんな感じ。
package main import ( "fmt""net/http" ) type MyHandler struct { } func (*MyHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) { fmt.Fprintf(res, "Hello world!! Handler version.") } func main() { http.Handle("/", &MyHandler{}) http.ListenAndServe(":8080", nil) }
今回は MyHandler がデータを何も持ってない構造体なので、何も嬉しくないけど設定情報とかを持たせれる点で HandleFunc より柔軟そう。
現に以下の FileServer 関数の例では http.FileServer
を使って特定ディレクトリの下のファイルを返すようにしてるみたい。
https://golang.org/pkg/net/http/#FileServer
試しに以下のコードを動かすと C:\Work
の下のファイルを返してくれるようになった。
package main import ( "fmt""net/http" ) type MyHandler struct { } func (*MyHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) { fmt.Fprintf(res, "Hello world!! Handler version.") } func main() { http.Handle("/", http.FileServer(http.Dir("/work"))) //http.Handle("/", &MyHandler{}) http.ListenAndServe(":8080", nil) }
http.FileServer
関数で作ったやつは、素直にパスをみてファイルを返してくれるみたいなので/static/
とかにマッピングすると C:\Work
の下にも static フォルダを切らないと NotFound になる。
基本的には、パスから static を外して欲しいから、そこらへんのことをしてくれると思われるのが http.StripPrefix
関数。こんな感じでいける。
package main import ( "fmt""net/http" ) type MyHandler struct { } func (*MyHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) { fmt.Fprintf(res, "Hello world!! Handler version.") } func main() { http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("/work")))) http.Handle("/", &MyHandler{}) http.ListenAndServe(":8080", nil) }
これで http://localhost:8080/static/hello.txt
にアクセスすると c:\work\hello.txt
の中身が返ってくるし、http://localhost:8080/hogehoge
みたいなのにアクセスすると MyHandler のほうに処理がいく。
結構単純でいいね。
Prism v7.1 が2週間ちょっと前にリリースされてました。
かなり大きな更新に見えます。 個人的にインパクトが大きそうだと思ったのが Autofac と MEF が Prism の将来のサポート対象から外れてしまうことでしょうか。 Autofac はイミュータブルになってるので、Prism の提供するモジュールまわりの機能と相性がよくないみたいですね。
ということで、特に理由が無ければ Unity (こっちはこっちで名前空間が昔と変わったりしてる)を使うのがいいと思います。
Xamarin.Forms 向けでは XAML でナビゲーションを定義出来たり、あと一番アツイのが ContainerProvider
です。今まで Converter とかに対して DI しようと思ったらコードで DI コンテナからインスタンス取ってきて ResourceDictionary に自分で追加とかしないといけなかったけど、それが XAML でいける。ヤバイ。
あとは、この v7.1 系が WPF 向けの最初の v7 系の Prism のリリースというのも個人的にアツイです。6.3 から 7.1 への更新ということなので結構な破壊的変更のオンパレードです。 v6 系で作ってるひとは、アップデートするときにはちょっと苦痛が伴うかもしれません。
例えば Bootstrapper クラスが非推奨になって PrismApplication を使うようになっていたり、今までは DI コンテナへのインスタンスの登録は生の DI コンテナの API をたたいてたけど抽象化するレイヤーが設けられていたり(多分がんばれば裏で動いてる DI コンテナのインスタンスにダイレクトアクセスして、既存の登録ロジックに回したりは出来ると思う「要出典」)
モジュール系のインターフェースが Prism.Wpf に行ってたり、Unity のバージョンを上げたので、そっちの破壊的変更にも引きずられたり(名前空間が違う)。
とりあえず、新しいものが出てくると楽しいですね。
しないんですけど!?下の issue の通りみたい。
まじかぁ…
The sign in card's button has an ActionType of signin and teams does not support this ActionType. In order to make this work as present time you need to go in and change the ActionType to ActionType.OpenUrl There are a few other issues discussing this one is #2104
この issue で提示されてる解決策は v3 ベースっぽいので v4 ではそのまま使えないくさい。 ということで、OAuthPrompt.cs をリポジトリからこぴってきて。
botbuilder-dotnet/OAuthPrompt.cs at master · Microsoft/botbuilder-dotnet · GitHub
SendOAuthCardAsync メソッドで2か所 SignIn の ActionTypes を使ってる箇所があるので OpenUrl に変えます。 そしてオリジナルの OAuthPrompt を使ってる箇所を、自分で作成したオレオレ OAuthPrompt に差し替えれば動きます。
もうちょっと詳しく Bot Builder SDK v4 の処理を追うと、issue でやってるようにメッセージ送信途中で何かしら処理をフックしてアクションを書き換えることで対応できるんじゃないかと思ってますが、とりあえずの方法としてはこれで…。
Dialogflow v2 に対応した C# の SDK が実はこっそりあります。 1.0.0-beta2 (2018/1104 現在) なので正式版ではないですが、きっと近いうちに出ると思う!!
以下のパッケージを入れましょう。
Visual Studio で追加するときはプレリリースパッケージのチェックを入れてから検索しましょう。
入れたらあとは JSON をパースしたりするだけです。ただ、JSON.NET ではなく Google の提供している JSON のパーサーを使います。Google.Protobuf の名前空間にあります。
using System; using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.AspNetCore.Http; using Microsoft.Azure.WebJobs.Host; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Google.Cloud.Dialogflow.V2; using Google.Protobuf; usingstatic Google.Cloud.Dialogflow.V2.Intent.Types.Message.Types; usingstatic Google.Cloud.Dialogflow.V2.Intent.Types; using Microsoft.Azure.WebJobs.Hosting; namespace HogeHoge { publicstaticclass Function1 { [FunctionName("Function1")] publicstatic async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequest req, ILogger log) { var parser = new JsonParser(JsonParser.Settings.Default.WithIgnoreUnknownFields(true)); // パーサーを作る var webhookRequest = parser.Parse<WebhookRequest>(await req.ReadAsStringAsync()); // パースする// 例えば Webhook がすべてのパラメータがそろった状態で呼び出されたかを確認したりif (!webhookRequest.QueryResult.AllRequiredParamsPresent) { log.LogInformation("This request does not have all required parameters"); returnnew OkResult(); } // パラメーターをとって色々したり var requestedParameters = webhookRequest.QueryResult.Parameters; // 戻りは、WebhookResponse を返せば OK var webhookResponse = new WebhookResponse(); webhookResponse.FulfillmentText = $"応答内容"; // 結果の JSON も Google.Protobuf を使ってシリアライズreturnnew ProtcolBufJsonResult(webhookResponse, JsonFormatter.Default); } } // Google.Protobuf を使ってシリアライズする IActionResult の実装publicclass ProtcolBufJsonResult : IActionResult { privatereadonlyobject _obj; privatereadonly JsonFormatter _formatter; public ProtcolBufJsonResult(object obj, JsonFormatter formatter) { _obj = obj; _formatter = formatter; } public async Task ExecuteResultAsync(ActionContext context) { context.HttpContext.Response.Headers.Add("Content-Type", new Microsoft.Extensions.Primitives.StringValues("application/json")); var stringWriter = new StringWriter(); _formatter.WriteValue(stringWriter, _obj); await context.HttpContext.Response.WriteAsync(stringWriter.ToString()); } } }
あとはデプロイして関数のエンドポイントを Dialogflow のフルフィルメントに設定して完了。
早く正式版でないかなぁ。
スマートスピーカーのスキルは、基本的にスマートスピーカーが受け取った音声をテキスト化する部分、テキストから意図(インテント)とキーワード(スロットとかエンテティとかって言われる)を抜き出すところまで、各スマートスピーカーを提供してくれているベンダーが面倒見てくれます。
そして、そこから解析結果が詰まった JSON を Webhook めがけて投げてくれます。 開発者がゴリゴリコードを書いてカスタマイズできる部分は、この Webhook の先のコードという形が一般的です。
ちなみに、Microsoft の Bot Framework を使った場合は意図やキーワードを抜き出すことはしてくれないです。 その代わり SDK 側に Microsoft の LUIS と呼ばれるテキストから意図やキーワードを抜き出すサービスとの連携機能があるので、自分で好きなように呼び出すことが出来るようになっています。
例えば、その時の会話の状態に応じて LUIS を呼び分けるといったことが出来ます。最初はおおまかに何がしたいかを判別することに特化して学習させた LUIS に解析をお願いして、その先では、それぞれのやりたいことに特化して学習させた LUIS を呼び出すといったことが出来ます。 手間がかかるぶんこったものを実現可能になっている感じですね。
さて、話しを戻します。
Webhook で飛んでくる JSON ですがスマートスピーカーのプラットフォームごとに互換性がありません。ですが、その先でやりたいことはだいたい共通でしょう。
ということで、受け口と必要なデータを抜き出す部分だけ別関数(HTTPのエンドポイントが別になる)に分けておいて、そこから共通ロジックを呼ぶ形で 1 つの Azure Functions で対応可能になります。
このとき node.js でやるなら公式の SDK が使えるのでは…と思います。express に依存している場合は azure-functions-express というパッケージで結構動きます。例えば Clova の場合は以下のような感じ。
C# の場合は、野良 SDK などを駆使することになります。
Clova の場合はこんな感じ。
Google Home の場合はこんな感じ。唯一公式 SDK (ただし、まだベータ)
Alexa の場合は、ブログには書いてませんが Alxa.NET というパッケージがあります。
以下の記事は Lambda を使ってますが、基本的にリクエストのボディを SkillRequest に JSON.NET を使ってパースして、 SkillResponse を作って new OkObjectResult(response);
で返してやる感じです。
ということで、複数スマートスピーカーに対応したい人は昔から GUI アプリケーション開発で言われていた見た目とロジックの分離と同じようにスマートスピーカーに依存する部分とロジックを分離することで、低コストで対応が可能になるのではと思います!
ここでは Azure Functions と言ってますが、何を使っててもプラットフォーム依存の部分とそうでない部分を分離して書くことで今回のと同じように 1 つのアプリで 3 プラットフォーム対応とか出来ます。AWS Lambda でも Heroku 上にデプロイしたものでも、レンタルサーバー上にデプロイしたアプリでも!
それでは、良い VUI ライフを!
2018/11/05 - 2018/11/07 で開催された Microsoft Tech Summit 2018 で登壇してきました。
その時のセッション用に準備したコードを以下のリポジトリに公開しました。
AI とか無関係じゃないし、新機能とかもあるし、デスクトップアプリケーションがつまらないプラットフォームではないということをお伝え出来ればよかったかなと思います。
肩身狭いけどね!
なんとなく見かけたので Azure でもやってみましょう。 Azure なら一番簡単に REST API 作るんだったら Azure Functions かなぁ。ログは Application Insights かなぁ。
Azure Functions のドキュメント - チュートリアル | Microsoft Docs
Azure Application Insights とは何か | Microsoft Docs
Azure Functions は Lambda と違って HTTPTrigger というものがあって、Functions だけで REST API 作れます。 本格的に API として管理したければ API Management を使えば OK。
じゃぁやってみましょう。本格的に開発するときはローカルに開発環境を作ってやるのが王道ですが、今回は使い捨てレベルくらいのものなのでポータル上で開発してみようと思います。
適材適所。
ポータルから Function App を作ります。作るときに一緒に Application Insights を作るのを忘れずに。 作り忘れた場合は、Application Insights を別途作って Function App のアプリケーション設定に Application Insights のキーを設定します。
作成画面はこんな感じ。
そして関数を作っていく。今回は先ほど言ったようにポータル内を選ぶ。
webhook + API を選ぶ。
暫くまつと HttpTrigger1 というトリガーが出来るのでコードをさくっと以下のようにします。
#r "Newtonsoft.Json"using System.Net; using System.IO; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; using Newtonsoft.Json; publicstatic async Task<IActionResult> Run(HttpRequest req, ILogger log) { log.LogInformation(await new StreamReader(req.Body).ReadToEndAsync()); returnnew OkObjectResult(new { message = "Powered by Azure" }); }
引数にわたってくる ILogger にログはけば Application Insights に行ってくれる。
実行ボタンを押すとテスト実行が出来ます。実行ボタンを押すと以下のようにログも出るのでちゃんと動いてることがわかります。
関数の URL の取得で、この関数を叩くための URL がゲットできるので Postman あたりで叩いてみます。
こんな感じ
Application Insights のライブメトリクス ストリームを選ぶとリアルタイムでログが流れていくのが見えます。
数分まつと Application Insights の検索からもひっかかるようになります。
リクエストで出た一連のログとかも見れるので割と便利。
C# になじみのない人は JavaScript でもいけます。
Function App 作るときにランタイムを dotnet ではなく JavaScript のほうを選んで作ると JavaScript でいけます。C# の時と同じ手順で webhook + API を作るといけます。
index.js の編集画面になるので以下のコードをさくっと書きましょう。 こういうのには個人的には JavaScript お手軽なので好きです。
module.exports = async function (context, req) { context.log(req.body); context.res = { body: { message: "Powered by Azure." } }; };
JavaScript のほうは引数にわたってくる context.log で出力した内容が Application Insights に行きます。
Postman などで叩くと同じようにライブメトリクス ストリームや検索でリクエストのボディを確認できるようになります。
今回は JavaScript 本当に簡単でいい。(今回の例では C# ちょっとめんどい)
最近できた Microsoft Learn を使うと実際の Azure 環境を無料でクレカ不要で学習用だけに使うことが出来るので、そこの Azure Functions あたりのコースを流してみるのをお勧めします。
Go 言語で Web アプリを作るときにもう一つ外したらいけなさそうなものとして Mux というのがあるみたい。 Multiplexer っていうのかな。
package main import ( "fmt""net/http" ) func handleRequest(w http.ResponseWriter, req *http.Request) { name, ok := req.URL.Query()["name"] if ok { w.Write([]byte(fmt.Sprintf("Hello %v.", name[0]))) } else { w.Write([]byte("Hello unknown user")) } } func main() { mux := http.NewServeMux() mux.HandleFunc("/", handleRequest) http.ListenAndServe("localhost:8080", mux) }
使うとこんな感じ。今まで http に対して直接やってたことを mux に対してやるイメージ。 世の中の Go 向けの Web フレームワークは mux をベースにしてるものもあるらしい。
クッキーを使うには、http.Cookie を使う。http.Request から Cookie メソッドを呼ぶと引数に指定した名前の Cookie のポインタとエラーが返ってくる。クッキーの設定は http.SetCookie 関数を使う。http.ResponseWriter と *Cookie を渡す感じ。
挙動を見る感じ body への出力より前にクッキーは設定しないとダメみたい。
package main import ( "fmt""net/http""strconv" ) func handleRequest(w http.ResponseWriter, req *http.Request) { count, err := req.Cookie("_counter") if err != nil { count = &http.Cookie{Name: "_counter", Value: "0", HttpOnly: true} } currentCount, err := strconv.ParseInt(count.Value, 0, 64) count.Value = strconv.Itoa(int(currentCount + 1)) http.SetCookie(w, count) name, ok := req.URL.Query()["name"] if ok { w.Write([]byte(fmt.Sprintf("Hello %v. You have been visiting this site %v times.", name[0], count.Value))) } else { w.Write([]byte("Hello unknown user")) } } func main() { mux := http.NewServeMux() mux.HandleFunc("/", handleRequest) http.ListenAndServe("localhost:8080", mux) }
こうすると何回かアクセスするとカウントアップしていく。
Go 言語って標準ライブラリにテンプレートまであるのか。便利。
ということで使ってみましょう。
使い方は簡単。template.Must(template.ParseFiles("templateFilePath1", "templateFilePath2", ...) みたいにしてテンプレートをパースする。パースしたら ExecuteTemplate メソッドで出力先とテンプレート名とテンプレートに渡す値を指定して完成。
package main import ( "html/template""net/http" ) type pageData struct { Title string Message string } func main() { templateFiles := []string{"templates/index.html", "templates/test.html"} templates := template.Must(template.ParseFiles(templateFiles...)) mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { templateName := "index" templateNames, ok := req.URL.Query()["template"] if ok { templateName = templateNames[0] } templates.ExecuteTemplate(w, templateName, pageData{Title: "Template sample title", Message: "Template sample message"}) }) http.ListenAndServe("localhost:8080", mux) }
テンプレートは以下のような感じになります。
まずは templates/index.html
{{ define "index" }} <!DOCTYPE html><htmllang="ja"><head><metacharset="utf-8"><title>Hello</title></head><body><h1>{{ .Title }}</h1><p>{{ .Message }}</p></body></html> {{ end }}
templates/test.html はこんな感じ。
{{ define "test" }} <!DOCTYPE html><htmllang="ja"><head><metacharset="utf-8"><title>Test</title></head><bodystyle="background-color: yellow"><h1>{{ .Title }}</h1><p>{{ .Message }}</p></body></html> {{ end }}
基本的には {{ }}
でくくった中に何か書く。先頭は define でレイアウト名(ExecuteTemplate で指定する名前)を指定して最後に end でおしまい。
{{ .プロパティ名 }} とかで ExecuteTemplate で渡されたデータにアクセスできる感じっぽい。
動かしてみるとちゃんと動いた。
今回のプログラムは URL のパラメータでテンプレート名指定するからこんな感じでも動く。
{{ range .xxx }} でループも行ける。テンプレートをいじって
{{ define "index" }} <!DOCTYPE html><htmllang="ja"><head><metacharset="utf-8"><title>Hello</title></head><body><h1>{{ .Title }}</h1><p>{{ .Message }}</p><hr/><ul> {{ range .Records }} <li>{{ .Name }}</li> {{ end }} </ul></body></html> {{ end }}
main.go もそれにあわせて変更。
package main import ( "html/template""net/http" ) type record struct { Name string } type pageData struct { Title string Message string Records []record } func main() { templateFiles := []string{"templates/index.html", "templates/test.html"} templates := template.Must(template.ParseFiles(templateFiles...)) mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { templateName := "index" templateNames, ok := req.URL.Query()["template"] if ok { templateName = templateNames[0] } templates.ExecuteTemplate(w, templateName, pageData{ Title: "Template sample title", Message: "Template sample message", Records: []record{ record{Name: "aaaaaaaa"}, record{Name: "bbbbbbbb"}, record{Name: "cccccccc"}, record{Name: "dddddddd"}, record{Name: "eeeeeeee"}, }, }) }) http.ListenAndServe("localhost:8080", mux) }
いい感じに動く。
さらに if やカスタムの関数とかも定義出来るみたい。今回は偶数番目のデータと奇数番目のデータで色を変えたかったので、mod という関数を追加したうえでパースして実行してみました。
package main import ( "html/template""net/http" ) type record struct { Name string } type pageData struct { Title string Message string Records []record } func main() { templateFiles := []string{"templates/index.html", "templates/test.html"} templates := template.Must(template.New("").Funcs(template.FuncMap{ "mod": func(x int, y int) int { return x % y }, }).ParseFiles(templateFiles...)) mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { templateName := "index" templateNames, ok := req.URL.Query()["template"] if ok { templateName = templateNames[0] } templates.ExecuteTemplate(w, templateName, pageData{ Title: "Template sample title", Message: "Template sample message", Records: []record{ record{Name: "aaaaaaaa"}, record{Name: "bbbbbbbb"}, record{Name: "cccccccc"}, record{Name: "dddddddd"}, record{Name: "eeeeeeee"}, }, }) }) http.ListenAndServe("localhost:8080", mux) }
テンプレートはこんな感じになりました。
{{ define "index" }} <!DOCTYPE html><htmllang="ja"><head><metacharset="utf-8"><title>Hello</title></head><body><h1>{{ .Title }}</h1><p>{{ .Message }}</p><hr/><ul> {{ range $index, $record := .Records }} {{ $isEvenRow := eq (mod $index 2) 0 }} <listyle="color: {{ if $isEvenRow}} red {{ else }} black {{ end }}">{{ $index }}{{ $record.Name }}</li> {{ end }} </ul></body></html> {{ end }}
if や range のインデックスの取得方法と関数呼び出しと盛りだくさん。実行するとこんな感じです。
あとはサニタイズとかもあるみたいですね。普通に Go 側とこういう風に書くと…
package main import ( "html/template""net/http" ) type record struct { Name string } type pageData struct { Title string Message string Records []record } func main() { templateFiles := []string{"templates/index.html", "templates/test.html"} templates := template.Must(template.New("").Funcs(template.FuncMap{ "mod": func(x int, y int) int { return x % y }, }).ParseFiles(templateFiles...)) mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { templateName := "index" templateNames, ok := req.URL.Query()["template"] if ok { templateName = templateNames[0] } templates.ExecuteTemplate(w, templateName, pageData{ Title: "Template sample title", Message: "<script type='text/javascript`>alert('Template sample message')</script>", // !? Records: []record{ record{Name: "aaaaaaaa"}, record{Name: "bbbbbbbb"}, record{Name: "cccccccc"}, record{Name: "dddddddd"}, record{Name: "eeeeeeee"}, }, }) }) http.ListenAndServe("localhost:8080", mux) }
素晴らしい。
あえてエスケープしたくないときは template.HTML を使う。まぁレアケースだけどマークダウンエディターみたいなものを作りたいときとかは必要。
こんな感じで
package main import ( "html/template""net/http" ) type record struct { Name string } type pageData struct { Title string Message template.HTML Records []record } func main() { templateFiles := []string{"templates/index.html", "templates/test.html"} templates := template.Must(template.New("").Funcs(template.FuncMap{ "mod": func(x int, y int) int { return x % y }, }).ParseFiles(templateFiles...)) mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { templateName := "index" templateNames, ok := req.URL.Query()["template"] if ok { templateName = templateNames[0] } templates.ExecuteTemplate(w, templateName, pageData{ Title: "Template sample title", Message: template.HTML("<script type='text/javascript'>alert('Template sample message')</script>"), Records: []record{ record{Name: "aaaaaaaa"}, record{Name: "bbbbbbbb"}, record{Name: "cccccccc"}, record{Name: "dddddddd"}, record{Name: "eeeeeeee"}, }, }) }) http.ListenAndServe("localhost:8080", mux) }
実行するとこうなる。ばっちり。
Pull Request を送って頂いたので取り込んでリリースしました。
それに合わせて Visual Studio 拡張機能も更新しています。
誰かドキュメント書いてプルリクエストください。
この記事は Serverless2 Advent Calendar 2018 の 2 日目の記事です。
最近 Docker でパッケージングしたらどこでも動くが実現してるようなものなので、実質 Docker でパッケージング出来たら run anywhere な感じということです!
そして、Azure Functions は Docker をサポートしています。(2018/12/02 時点ではプレビュー) ということでやってみましょう。私は Windows 10 に Docker を入れて試しました。
基本的には以下のドキュメントに沿ってやります。
ではやってみましょう。
といっても難しいことは特になくて、func コマンドでプロジェクトを作るときに --docker
オプションをつけてやります。任意のフォルダで以下のコマンドをうってみましょう。
func init --docker
そうすると dotnet, node, python から選択できるので今回は node で行ってみようと思います。実行結果は以下のような感じのログになります。Dockerfile まで出来ちゃってますね!
Select a worker runtime: node Writing .gitignore Writing host.json Writing local.settings.json Writing C:\Users\kaota\.vscode\extensions.json Writing Dockerfile
早速 Visual Studio Code で開いてみましょう。
FROM mcr.microsoft.com/azure-functions/node:2.0 ENV AzureWebJobsScriptRoot=/home/site/wwwroot COPY . /home/site/wwwroot
思ったよりシンプル。確かに node の場合はビルドとかいりませんしね。因みに dotnet を選択すると以下のような Dockerfile が生成されます。
FROM microsoft/dotnet:2.1-sdk AS installer-env COPY . /src/dotnet-function-app RUN cd /src/dotnet-function-app && \ mkdir -p /home/site/wwwroot && \ dotnet publish *.csproj --output /home/site/wwwroot FROM mcr.microsoft.com/azure-functions/dotnet:2.0 ENV AzureWebJobsScriptRoot=/home/site/wwwroot COPY --from=installer-env ["/home/site/wwwroot", "/home/site/wwwroot"]
こちらも十分シンプルに見えます!公式がおぜん立てしてくれたベースイメージがあるのは強いですね。
このままだと空の関数になってしまうので func new
をうちこんで HttpTrigger の関数を 1 つ作ります。
関数名は Echo を指定しました。そして生成されたファイルをちょっと変えて以下のようにします。
{"disabled": false, "bindings": [{"authLevel": "anonymous", "type": "httpTrigger", "direction": "in", "name": "req", "methods": ["get" ]}, {"type": "http", "direction": "out", "name": "res" }]}
module.exports = async function (context, req) { context.log('JavaScript HTTP trigger function processed a request.'); context.res = { status: 200, body: `You said '${req.query.message}'` }; };
まずは、func start
でローカルで実行してみます。先ほど作成した Echo 関数がちゃんと認識されてることがわかります。
Http Functions: Echo: [GET] http://localhost:7071/api/Echo
curl コマンドで叩いてみるとこんな感じ。ばっちりですね。
> curl http://localhost:7071/api/Echo?message=Hello You said 'Hello'
そして docker build -t okazuki/serverless2018:v1
と打ち込んで docker でパッケージングします。
そして、docker run -p 8080:80 okazuki/serverless2018:v1
と打ち込んで実行しましょう。docker ps
をして動いてるのを確認!
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 2ce0a0ec453c okazuki/serverless2018:v1 "/bin/sh -c /azure-f…" About an hour ago Up 9 seconds 0.0.0.0:8080->80/tcp gallant_hugle
先ほどと同じように curl コマンドで docker run で指定した 8080 ポートに対して関数のエンドポイントを叩いてみます。
>curl http://localhost:8080/api/Echo?message=Hello You said 'Hello'
ばっちりですね。docker kill で止めておきましょう。
ここまで出来たら後はお好みの環境で動かすだけですね。 まぁ Azure Functions の機能自体が Azure Storage に依存してたりするので(Durable Functions とか) そういうのを使おうとすると結局 Azure のサービスが必要でレイテンシを考えると Azure でホストするしかないのかぁ…となりそうな気もします。
でも、Azure Functions はバインディング(トリガーやアウトプットやインプット)は自作出来るようになってるので例えば AWS 系サービスのトリガーとかを作ったらいい感じに AWS 上で Azure Functions を動かすことも出来るかもしれませんね。
IoT デバイスで Azure Functions を動かすのも Docker を使ってるので、Azure Functions の Docker 対応は要注目な気がします。
Azure Functions は Docker イメージ作れるのでどこでも動く。
Azure Functions に興味がある人は、無料で教育用の Azure 環境をクレカ不要で使いながらテキストを進めることが出来る Microsoft Learn というサイトがあるので、そこから試してみてください。因みに無料枠でも Azure Functions 出来ます。
Azure Functions on AWS とかしてくれないかな?
これは Serverless2 Advent Calendar 2018 の 4 日目の記事です。
前日は miyake さんによる Serverlessconf Tokyo で Durable Functions にコントリビュートしました | PaaSがかりの部屋でした。
先日、Azure Functions を Docker を使って Docker イメージにパッケージングしました。
Advent Calendar も、空白があるようなので折角なので、今回は AWS の Amazon ECS にデプロイしてみようと思います。
とりあえず手動 docker push
がだるかった(and 何故か自宅からやると凄く重かった)ので、以下のような azure-pipelines.yml
を書いて Azure DevOps (旧 Visual Studio Team Services) で docker build
と docker push
をしてもらうようにしました。
とりあえず現在のビルド ID をタグにしたイメージと、同じものを latest タグに突っ込むようにしたけどいいのかな。
pool:vmImage:'Ubuntu 16.04'variables:imageName:'$(dockerId)/serverless2018:$(build.buildId)'latestImageName:'$(dockerId)/serverless2018:latest'steps:- script: | docker build -f Dockerfile -t $(imageName) . docker tag $(imageName) $(latestImageName) displayName:'docker build'- script: | docker login -u $(dockerId) -p $(pswd) docker push $(imageName) docker push $(latestImageName) displayName:'docker push'
変数部分は、別途変数用の設定画面でさくっと設定してビルド設定をしておきます。このビルドを走らせると以下のようにさくっと docker push
されました。
(自宅で何時間放置しても終わらなかったのに…)
Docker は初心者なのでよくわかってないのですが Tags ってところに作られてるから大丈夫なのかな。
はじめて、ちゃんと開いた AWS のコンソールで ECS を探します。こんな感じに入力して更新
タスク定義はそのままで、ロードバランサーは Application Load Balancer を選びました。あとは Yes マンです。 知らないクラウドの、いったい今何が作られてるんだろう…という不安感はたまらないですね(後で全部ちゃんと消せるんだろうか)
作成されたロードバランサーを見ると凄く長い URL が生成されてます。そこに向けて curl コマンドを叩くと…。
>curl http://ec2co-ecsel-1g11swshqzpgu-1462563513.us-west-2.elb.amazonaws.com/api/Echo?message=Hello You said 'Hello'
動いた!!
まだプレビュー段階ですが Azure Functions の Docker サポートを使うと AWS で Azure Functions を動かすことも出来ますね!やったね。
先日の Connect(); 2018 で .NET Core 3.0 Preview 1 が出ましたね!そして、前々から噂されてた WPF / Windows Forms のサポートが試せます。まぁ、前々から alpha 版とか使って試せましたが alpha はちょっと…という感じでも Preview なら許容範囲かな?という人は入れて試してみましょう。
alpha のころに試した記事は以下になります。
.NET Core 3.0 preview を入れたら以下のコマンドで WPF や Windows Forms のプロジェクトを作れます。
dotnet new wpf dotnet new winforms
そして、Visual Studio 2019 Preview で開いて開発出来ます。
デザイナーは、まだサポートされてませんが実行してデバッグすることが出来ます。
WPF や WinForms アプリは、ほぼほぼ .NET Framework をターゲットにしたライブラリに依存してると思います。.NET Core 3.0 自体は .NET Standard をサポートしてるので、既存の .NET Standard 対応のライブラリは使えるのですが .NET Framework をターゲットにした奴は使えるの?という話し。 例えば Prism とかですね。
一応 NuGet Package Manager から追加は出来ますが、互換性に関する警告が出ます。
実際どうなの?というところは .NET Portability Analyzer で確認できます。
VS 2019 ではこれの拡張機能が、まだサポートされてないのでコマンドラインツールを以下から入れて使う感じになります。
インストール後に、プロジェクトの出力フォルダで以下のコマンドを実行するとチェックが走ります。
<Path to APIPort.exe folder>\ApiPort.exe analyze -f .
実行結果として以下のようなレポートが出ています。
このレポートを見て、問題なさそうだと思ったら警告が出ないように VS2019 のプロパティウィンドウで警告抑止が出来ます。
.NET Core 3.0 は、まだプレビューですがとりあえず今すぐ試すことが出来るようになっています。 そして、自分たちのアプリケーションが .NET Core 3.0 だとどうなんだろう?というのを確認出来ます。
.NET Core 3.0 は、一般的に .NET Framework よりも性能がいいというメリットや、配布でも自己完結型の展開(SCD)を使うことで、アプリに .NET Core 3.0 を含めた形で展開することが出来ます。つまり、配布先の OS が最新のフレームワークを入れてなくてもフォルダーをコピーすれば動くようにパッケージング出来ます。(OS 管理者にランタイムのパッチ適用や更新をお願いするのではなく、アプリ開発者側が面倒みるというスタンス)
実際に、最初に作った WPF アプリを自己完結型として発行して zip 圧縮すると 40MB くらいになりました。そして、それを Azure 上に作成した仮想マシンにコピー(こっちには .NET Core 3.0 を入れてません)して実行するとばっちり動きました。
.NET Core 3.0 向けのパッケージ作るのがどれくらい大変なのか試すついでにやってみました。結果は思った以上にさくっと出来ました。
NuGet はこちら。
VS2019 向けの vsix は GitHub のリリースページからダウンロードできます。
使ってる画面はこんな感じです。
プレビューだけど、Azure Functions で Python のサポートが追加されました。
英語ですが、ドキュメントもちゃんとありますね!
ドキュメントにも記載がありますが現在サポートされている Python のバージョンは 3.6.x みたいです。3.7 ではないみたいですね。私のローカルには Python 3.6.2 が入ってたのですが折角なので最新の 3.6.7 を以下のサイトからダウンロードしてインストールしました。
C:\Users\ユーザー名\AppData\Local\Programs\Python\Python36
と C:\Users\ユーザー名\AppData\Local\Programs\Python\Python36\Scripts\
にパスを通して以下のコマンドで確認してみます。(インストール時にパス通すチェックボックスがあるみたいなのですが、さくっとインストールしたので見逃してました)
>>python --version Python 3.6.7
私の環境は Windows なのですが、Azure Functions で Python を動かすときは Linux がベースの OS みたいなので、実際に開発するときは Linux でしたほうが幸せな気がしてます。
Visual Studio Code に Azure Functions と Python の拡張機能を入れて準備完了です。func init
コマンドをうつか、Visual Studio Code でフォルダを開いて左側のアイコンの並びで Azure アイコンをクリックすると FUNCTIONS というものがあるので、Create new project をクリックして GUI でも可能です。
どちらの手順でも言語を選ぶようになるので Python を選びましょう。
プロジェクトが出来たら、続けて func new
か Visual Studio Code で Create Function をクリックして関数を作ります。
Http Trigger で、Echo という名前でさくっと作ってみました。Authorization level は Function (関数単位でアクセスするためのキーが出来る)を選びました。
そうすると、__init__.py
というファイル名で以下のようなコードが生成されます。
import logging import azure.functions as func defmain(req: func.HttpRequest) -> func.HttpResponse: logging.info('Python HTTP trigger function processed a request.') name = req.params.get('name') ifnot name: try: req_body = req.get_json() exceptValueError: passelse: name = req_body.get('name') if name: return func.HttpResponse(f"Hello {name}!") else: return func.HttpResponse( "Please pass a name on the query string or in the request body", status_code=400 )
Python にも Java でいう Annotation や C# でいう Attribute が無さそうなので、node と同じように function.json に関数のトリガーや入力や出力を定義する形みたいです。以下のような function.json が生成されていました。
{"scriptFile": "__init__.py", "bindings": [{"authLevel": "function", "type": "httpTrigger", "direction": "in", "name": "req", "methods": ["get", "post" ]}, {"type": "http", "direction": "out", "name": "$return" }]}
おもむろに func start
か F5
を押して実行してみます。Echo 関数のエンドポイントが表示されるので適当なツールなりブラウザで叩いてみます。今回は curl 使いました。実行するとちゃんと動いてますね!
> curl http://localhost:7071/api/Echo?name=okazuki StatusCode : 200 StatusDescription : OK Content : Hello okazuki!
init.py の main 関数が呼び出されるのがデフォルトなのですが、これは function.json でカスタマイズ出来ます。
scriptFile
と entoryPoint
で指定する感じです。ドキュメントのこの部分に書いてあります。
プロジェクトの下に SharedCode とかいうフォルダ(名前は任意)を作って、その中に logic.py とかいうファイルを作って以下のような関数を定義しました。
defgenerateGreetingMessage(name: str) -> str: return"Hello " + name
そして、__init__.py
を以下のように書き換えてみます。
import logging from ..ShreadCode.logic import generateGreetingMessage import azure.functions as func defmain(req: func.HttpRequest) -> func.HttpResponse: logging.info('Python HTTP trigger function processed a request.') name = req.params.get('name') ifnot name: try: req_body = req.get_json() exceptValueError: passelse: name = req_body.get('name') if name: return func.HttpResponse(generateGreetingMessage(name)) else: return func.HttpResponse( "Please pass a name on the query string or in the request body", status_code=400 )
実行同じように実行するとちゃんと動きました!
> curl http://localhost:7071/api/Echo?name=okazuki StatusCode : 200 StatusDescription : OK Content : Hello okazuki
例えば Azure Storage の QueueTrigger なんかを使おうとしたときには拡張機能を入れます。その時は以下のようなコマンドをうちます。
func extensions install --package Microsoft.Azure.WebJobs.Extensions.Storage --version 3.0.2
詳細はここらへんに書いてあります。
試しに QueueTrigger で関数を作ります。func new
か VS Code の Create Function
で。
適当に入力を進めていくと以下のような関数が作られます。
import logging import azure.functions as func defmain(msg: func.QueueMessage) -> None: logging.info('Python queue trigger function processed a queue item: %s', msg.get_body().decode('utf-8'))
function.json は以下のような感じ。
{"scriptFile": "__init__.py", "bindings": [{"name": "msg", "type": "queueTrigger", "direction": "in", "queueName": "python-queue-items", "connection": "AzureWebJobsStorage" }]}
とりあえず、Queue に投げ込まれたメッセージを出力するだけみたいですね。
local.settings.json を開くと AzureWebJobsStorage という項目があるので、そこの値を UseDevelopmentStorage=true
にして、エミュレーターにいくようにしておきます。
ローカルで実行して、Azure Storage Explorer あたりで適当に Queue にメッセージを投げ込んでみました。
ばっちり!
[12/11/2018 3:31:59 AM] Trigger Details: MessageId: c9f676c5-91d9-4a50-81a0-c4ca5d4531dc, DequeueCount: 1, InsertionTime: 12/11/2018 3:28:40 AM +00:00 [12/11/2018 3:31:59 AM] Python queue trigger function processed a queue item: { "message": "Hello world" } [12/11/2018 3:31:59 AM] Executed 'Functions.QueueFunc' (Succeeded, Id=0ac16f9a-6594-49ca-ae9b-d0d11528b416)
そのままデプロイしてみました。VS Code だと Deploy to Function App を押すだけ。 私は zip をパッケージから実行する方法でデプロイしてみました。
ポータルでもばっちり関数が見えてます。
ポータルから関数の URL を取得するとクリックすると関数の URL が認証キー付きでゲットできるので叩いてみましょう。
curl コマンドで叩いてみました。
> curl "https://pyfunckaota.azurewebsites.net/api/Echo?code=キーの値&name=MicrosoftAzure" StatusCode : 200 StatusDescription : OK Content : Hello MicrosoftAzure
いい感じですね。
Java と Python 早く GA しないかなぁ。
Durable Functions は、個人的に Azure で一番好きな機能なのですが、それが node でも使えるようになりました。 これまでもプレビューであったけど、今回は正式版ということで実践投入行ける感じですね。
これまで、Durable Functions を使おうと思ったら C# でしたが、これからは node でも OK。個人的には C# の方がなじむけど、好きな方が使える状態なのは大事ですね。
では行ってみましょう。適当なフォルダで func init
か VS Code で Create project して JavaScript を選びます。npm init -y
もしておきます。そして npm i durable-functions
でライブラリを入れておきます。
Durable Functions の拡張機能もコマンドで有効にしておきましょう。
func extensions install -p Microsoft.Azure.WebJobs.Extensions.DurableTask -v 1.7.0
続けて関数を作っていきます。Durable Functions を作るときは、オーケストレーターを起動するための関数(HTTPやQueueなど)と、色んな処理を呼び出したりフローを管理するオーケストレーター関数と、実際の処理を行うアクティビティ関数の最低 3 つをよく作ります。アクティビティ関数は、やりたい処理の数だけできるので実際にはもっと沢山の関数を定義します。
Durable Functions のいいところは、これらの沢山の関数をサーバーレスアーキテクチャーらしい呼び出し方をしてくれてるのに、書き味は普通のプログラムと変わらないというところが素晴らしいところです。 例えば、処理を 3 つシーケンシャルに呼ぶときには、サーバーレスとかだと Queue を間に挟んで 1 つ 1 つを細かくわけてやることが多いです。まぁ本当に小さい処理なら以下のように
funcA(); funcB(); funcC();
とすればいいのですが、そこそこ大きな処理になってくると長い実行時間だと色々困ることがあるサーバーレスのプラットフォーム上で動かすときは
Client(処理のお願いを QueueA に投げる) -> QueueA -> funcA(QueueA をトリガーに処理をして結果を QueueB に投げる) -> QueueB -> funcB(QueueB をトリガーに処理をして結果を QueueC に投げる) -> QueueC -> funcC(QueueC をトリガーに処理をして結果を QueueD に投げる) -> QueueD -> Client(QueueD を監視して結果を受け取る)
ツライ。この辛さを解消してくれて、さらに有り余るメリットを与えてくれるのが Durable Functions だと思ってます。
脱線しましたが、気を取り直して関数を作成します。まずはトリガーとなる関数ですね。今回は HttpTrigger で作成します。
func new
か VS Code で New Function
を選んで作ります。
function.json
に追記して Durable Functions の機能を使えるようにします。
{"disabled": false, "bindings": [{"authLevel": "anonymous", "type": "httpTrigger", "direction": "in", "name": "req", "methods": ["get", "post" ]}, {"type": "http", "direction": "out", "name": "$return" }, {"name": "starter", "type": "orchestrationClient", "direction": "in" }]}
type が orchestrationClient の定義がそれにあたります。index.js
ではオーケストレーターを呼び出す処理を書きます。
const df = require('durable-functions'); module.exports = async function (context, req) {const client = df.getClient(context); const instanceId = await client.startNew("OrchestrationFunction", undefined, req.body); return client.createCheckStatusResponse(req, instanceId); };
戻り値は、状態を確認したりするための情報をクライアントに返すようにていします。後で使いますが、これも便利。OrchestrationFunction には HTTP のリクエストのボディを入力として渡しています。
では、オーケストレーターの関数を作りましょう。HttpTrigger の関数を OrchesrationFunction という名前で作ります。そして function.json を以下のように編集します。
{"disabled": false, "bindings": [{"name": "context", "type": "orchestrationTrigger", "direction": "in"}]}
そして、処理を書いていきます。このオーケストレーターの関数は、実は何回も呼び出されて裏で実行履歴と突き合わせをして動く特殊な関数なので、ちょっと書くときにお作法がいります。要は何回動かしても同じ結果になるようなものしか使えません。(乱数とかはダメ、そういうのがしたい場合は Durable Functions が提供してる代替関数を使う)
const df = require('durable-functions'); module.exports = df.orchestrator(function* (context) { context.df.setCustomStatus({ message: 'OrchestrationFunction started'}); const output = []; output.push(yield context.df.callActivity('SayHello', context.df.getInput().first)); context.df.setCustomStatus({ message: 'first activity is completed'}); output.push(yield context.df.callActivity('SayHello', context.df.getInput().second)); context.df.setCustomStatus({ message: 'second activity is completed'}); output.push(yield context.df.callActivity('SayHello', context.df.getInput().third)); context.df.setCustomStatus({ message: `third activity is completed, waiting accept event: ${JSON.stringify(output)}`}); const accept = context.df.waitForExternalEvent('accept'); const reject = context.df.waitForExternalEvent('reject'); constevent = yield context.df.Task.any([accept, reject]); if (event === accept) {return output; }else{return[]; }});
さて、見慣れない関数の連続ですが、やってることはこの関数に渡された入力の first, second, third を、SayHello というアクティビティに渡して結果を output に追加していっています。
途中経過を連絡するためにカスタムステータスを適時設定していってます。
最後に、外部からの accept か reject という名前のイベントを待って accept されたら実行結果を返して、reject されたら空の配列を返しています。
最後に SayHello 関数を作ります。これも HttpTrigger で作ったあとに function.json を書き換えます。(そのうち func new とかにテンプレートが追加されると思いますが今はない)
{"disabled": false, "bindings": [{"name": "name", "type": "activityTrigger", "direction": "in"}]}
index.js は以下のような感じ。
function sleep10s() {returnnew Promise(resolve => setTimeout(resolve, 10 * 1000)); } module.exports = async function (context, name) { await sleep10s(); return `Hello ${name}`; };
10 秒待った後に Hello xxxx という文字を返します。
ということで、まとめると30秒くらい処理に時間がかかって、さらに外部からのイベントが来たら結果を返すという感じですね。SayHello は 1 つ 1 つが 10 秒なので、どれくらいの時間がかかるか読めますが外部からのイベントは Durable Functions が提供してくれる REST API を叩かないと発行出来ないので、どれくらい時間がかかるのかさっぱりわかりません。
そんな処理も Durable Functions でさくっと書けるのが素敵。
local.settings.json
で Azure Storage の接続文字列を入れます。macOS や Linux では公式では Azure にストレージアカウントを作って、その接続文字列を入れてって書かれてます。Windows の場合はエミュレーターでもいいので UseDevelopmentStorage=true
でも OK です。
あと、Azure の Storage Emulator のバージョンによっては AzureWebJobsSecretStorageType を追加しないといけないみたいです。
{"IsEncrypted": false, "Values": {"AzureWebJobsStorage": "UseDevelopmentStorage=true", "FUNCTIONS_WORKER_RUNTIME": "node", "AzureWebJobsSecretStorageType": "files"}}
Issue はこちら。
このブログから行きつきました。感謝。
さ・ら・に。ローカルで動かすときは以下の issue もあります。
ということで以下のように WEBSITE_HOSTNAME
環境変数に localhost:port番号
を追加します。デフォルトだとポート番号は 7071 なので以下のような感じになると思います。
では、func start
で実行して HttpTrigger を叩きます。
実行結果に、statusQueryGetUri というのがあります。これを叩くと関数の状態をチェックできます。
カスタムステータスがイベントを待ってるようなので、イベントを送りたいと思います。このときも最初に HttpTrigger を叩いた結果に sendEventPostUri
というのがあります。
今回の場合はこんな URL です。
http://localhost:7071/runtime/webhooks/durabletask/instances/98fbec63c5124414ae363518bb7d27bc/raiseEvent/{eventName}?taskHub=DurableFunctionsHub&connection=Storage&code=mF4BzJbHO9xX51OE6JK76feSO8A4zsUewXm53e9lSUxo5LCmwaYe6w==
{eventName} というところに今回は accept か reject を指定して POST でボディが空の JSON のリクエストを送ると OK です。
こんな感じ。
この API を叩いたあとに結果を取得するとこの通りステータスが Completed になって output に結果が入っています。
書きながら作ったソースは以下に置いてます。
何もしないと最新を使うので普段は何もしなくていい。 でも、今自分は .NET Core 3.0 Preview 1 を入れてるので出来れば使われたくないようなケースもある!
ということで、その場合にどうするかです。
今のマシンに入ってる SDK は以下のコマンドで見れるのでチェック
> dotnet --list-sdks 2.1.202 [C:\Program Files\dotnet\sdk] 2.1.500 [C:\Program Files\dotnet\sdk] 2.1.502 [C:\Program Files\dotnet\sdk] 2.1.600-preview-009426 [C:\Program Files\dotnet\sdk] 2.2.100 [C:\Program Files\dotnet\sdk] 2.2.200-preview-009648 [C:\Program Files\dotnet\sdk] 3.0.100-preview-009812 [C:\Program Files\dotnet\sdk]
なんかいっぱい入ってますね…
今回は 2.2.100 を使う用に指定したいと思います。
global.json というファイルで指定します。スキーマとしてはシンプルで以下のような感じです。
{"sdk": {"version": "2.1.300" }}
コマンドで生成できます。以下のコマンドを SDK のバージョンを指定したいプロジェクトのフォルダで実行します。
dotnet new globaljson --sdk-version 2.2.100
これで .NET Core 3.0 じゃなくて .NET Core 2.2.100 の SDK が使われるようになりました。 めでたしめでたし。
System.Reactive のバージョン上げました。 あと、不要な依存関係があったのを削除したのと、ドキュメントコメントが全部の public メソッドに追加されてます。
詳しいことは GitHub のリリースに書いています。
Release v5.3.1 · runceel/ReactiveProperty · GitHub
NuGet に上げてるのでインストールと更新はこちらからどうぞ。