Azure Functions には、バインディング ( bindings ) という仕組みがあります。これを使うことでいちいち細かいコードを書かなくても設定と少量のコードで、Queue トリガーを指定したり、Cosmos DB からデータを取得したり登録したりできます。
bindings のタイプは3種類あります。
- Trigger: Azure Functions の起動トリガーになる
- Input (入力): 起動のタイミングで値を取得して、Functions の引数にセットしてくれる
- output (出力): Functions の中で値をセットすることで、Functions の終了時にバインダーとして紐づいているリソース(Blob や Cosmos DBなど)登録してくれる
bindings は以下のドキュメントにあるよう、MS でたくさん用意してくれています。
Triggers and bindings in Azure Functions | Microsoft Docs
そして、今回はこれを自作するってお話です。
- 今回つくるものの構成
- 1. Teams で アプリ "Incoming Webhook" を追加
- 2. Adaptive card の作成
- 3. Custom bindings の作成
- 4. Azure Functions で動作確認
- 終わりに
作るものは、Http Trigger の Function App で3つのデータ(タイトル、本文、参考リンク)を受け取ったら Teams の特定のチャネルに呟くっていう binding です。
ユースケース的な話だと、GitHub のようなサービスで Issue が更新されたら Teams に通知する感じです。また、プレーンなテキストじゃなくていい感のものにしたいです。
( ↓ これを作ります。)
今回つくるものの構成
全く同じことを Logic Apps でコーディングレスででそうですが、C# でやる理由はシンプルです。
単にコーディングしたい、 Azure Functions 使いたいし Custom Bindings でなんか作りたいのです♪
別に Logic Apps 嫌いじゃないし、アンチじゃないです。
今回やることは以下です。
- Teams で アプリ "Incoming Webhook" を追加
- Adaptive card の作成
- Custom bindings の作成
- Azure Functions で動作確認
1. Teams で アプリ "Incoming Webhook" を追加
まず MS Teams で Webhook を受ける口を作ります。
Teams を起動して、左側のメニューの チーム > 通知を受けたいチーム(今回は Teams-Yokohama ) の 「・・・」をクリック > チームを管理 をクリックします。
アプリ タブをクリック > その他のアプリ をクリックします。
アプリの検索に「webhook」と入力すると、Incoming Webhook が表示されますのでクリックします。
チームに追加をクリックします。
追加したいチャネルを選びます。デフォルトで入ってる内容を消してチーム名から入力するといい感じに検索できます。
今回は、一般 を選びました。
名前を適当に付けます。ロゴもかわいいキャラを用意しておくと気分が上がると思うのでガチの時は用意しましょう。作成ボタンをクリックすると、 webhook の URL が表示されます。これはコピーしてどっかにメモしておきましょう。
これで Teams 側の準備完了。
2. Adaptive card の作成
Teams にメッセージを送信するのにはテキストだけだとブログが書くモチベーションにならなかったので、サクッと Adaptive Card で実装をすることにしました。
Card についてよくわからなくても、ここら辺に適当なサンプルが落ちてますし、複雑なことやるわけでもないし、Json で決まったフォーマットを書くだけなので気軽に作れます。ちなみに今回はこんな Json です。
これをmessage card playgroundにはりつけると、プレビュー表示できます。簡単ですね。
ここまでくると、json で動的となるタイトルやメッセージやリンクの URL の値をうけとり、HTTP の POST で良い感じに json を送っちゃえばおしまいにできますが.... 本題の Custom Bindings を作ります。
ちなみにちょっと複雑なカードを作ろうとすると Teams ではきれいに表示できないこともあるので、Postman とかで送信して Teams で表示を確認・調整した方がよいです。
3. Custom bindings の作成
コードの全体は GitHub に上げています。
できてしまえば簡単ですが、ドキュメント読んでもいまいちわからず悩んだ要素がそれなりにあったので、コーディングしたプロセスをメモってみました。
ということで、
ようやく本題の Azure Functions の Custom Bindings です。.NET Core 3系が登場した時期で色々変更が走ったタイミングでこのブログを書くのも微妙で震えますね。
今回の前提は、
- Azure Functions V2
- Net Standard 2.0 のクラスライブラリ
で作ります。 Custom Bindings は以下の流れで作りました。
- Attribute の作成
- Collector と Context の作成
- Binding rules の定義と Converter の作成
- Azure Functions の DI
プロジェクトの作成
.NET Standard のクラスライブラリを作るだけなので手順は省きます。CustomBindingsSamples.TeamsBinding というプロジェクト名で作成しました。
プロジェクトを作成したら、以下のの Nuget package をインストールします。
- Microsoft.Azure.Functions.Extensions: v1.0.0
- Microsoft.Azure.WebJobs: v3.0.13
- Microsoft.Extensions.Http: v2.2.0
注意: Microsoft.Extensions.Httpを現時点最新の v3.0.0で動かしたら Exception 吐いて動きませんが、細かく見てません。.NET Core 3.0 絡みでなんかあるかもですねー。気が向いたら調べたい気もしてます。
ついでに Teams のメッセージ用の Model を用意します。今回は、以下の3つが欲しいだけです。
- タイトル
- テキスト(本文)
- 参照用のリンク
シンプルな POCO のクラスでよいし、前述した Adaptive Card の json に埋め込めばいいので string format でもすればよいです。こんなやつ。
動的な内容の Adaptive Card 使うときはコード化した方がよいですが、今回のように固定の Adaptive Card を使うなら上記のように string format でやった方が無難でしょう。
と思いつつも Json 出力に必要なものを class 化するあえてクソ面倒くさいものを作りました。(理由はコード書きたかっただけで特にないです...)
public に公開しているプロパティは入力値としてほしい3つ(タイトル・テキスト・参照用のリンク)のみですが、ToJson
メソッドで必要な Json の文字列を出力します。余談として、17行目は json を作るうえでは public
である必要がないのですが、Functions のモデルバインドをしたいので public
にしています。
Attribute の作成
Azure Functions 使ってると Queue trigger や Http trigger とか使ってると思いますが、あれの自作版です。
今回は Teams へ投稿する先の Webhook の URL を環境変数から取得したいので、こんなクラスを作成しました。
シンプルですね。
[AutoResolve]
が気になるとこだと思います。Functions のメソッドで値を受け取る際、特定フォーマットで書いてくれれば環境変数からとるよーとかそんなノリの話です。一番最後に出てくる使うときのコードを見ればわかりますが、今回なら Azure Functions のメソッドで Teams の Attribute を書くとき [Teams("%TeamsWebhookUri%")]
って書くと、環境変数の TeamsWebhookUri ってキーの値をとってきてくれます。他にバインドされた値も使えますが、詳しくはこちらに書かれてます。
Collector と Context の作成
今回は、Functions で取得した値を Teams にメッセージを送るための bindings を作りたいので、 Collector を実装します。Collector にも何種類かあります(詳しくはこちら)が、今回は IAsyncCollector<T>
のタイプを実装します。
IAsyncCollector<T>
インターフェースを実装することで、やりたいこと(Teams へのメッセージ送信)を AddAsync
メソッドに実装すればよいです。メソッドの引数の型は、 IAsyncCollector<T>
の Generics で定義します。
で、個人的にはここにメソッドを書くのもなんかあれなので、Context 側に実際に Teams へ送信するメソッドを書くことにします。
というのも、例えばDBへ接続するなら DbContext 的なものが必要になりますし、そのインスタンスの初期化で Attribute の値が必要だったり、各種設定でコードがごちゃごちゃしだすのが目に見えますね。
それは Context や Context の初期化する Factory を作って隠蔽した方がテストしやすそうで良さそうって理由です。
ただ、結局のところ実装全体のバランスによると思うのでケースバイケースか。。。
Context の作成
今回は TeamsBindingContext ってクラスを作りました。
今回は REST でメッセージ送るだけなので、HttpClient
が欲しいだけということでこのように実装しました。HttpClient
のインスタンスだけもらえばいいかなと思っていましたが今回はまーどっちでもいいかって感じですかね。
Collector の作成
このクラスが Azure Function のメソッドの引数をして渡されて、Azure Function のメソッドの中で AddAsync
メソッドが呼ばれるイメージです。
Context に処理をまとめたのでシンプルです。今回くらいシンプルな全体構成なら、ここは Unit Test 書く必要ない方針で作ってます。
Binding rules の定義と Converter の作成
ここら辺からお作法のようなものになってきますが...2つのクラスを作ります。
Binding rules を定義: ExtensionConfigProvider の作成
Binding rules を定義するって話はこちらに記載があります。なるほど具体的なことがよくわからん空気に包まれますが、ようは IExtensionConfigProvider
インターフェースを実装したクラスを作ります。
44行目で TeamsBindingOpenType
という class が定義されていますが、こちらに何なのか書かれています。
Converter の作成
Converter は、Functions で IAsyncCollector
のインスタンスが必要になったときにインスタンスを生成して渡すためのものです。コードはこんな感じ。
ドキュメントはこちらになりますが、たいした内容ではないにしろ、私の中では理解度低いです。
Azure Functions の DI
今回必要なものを Functions 起動時に DI する設定を書きます。Functions の DI は2019/5頃に正式サポートされ、ブログに書いたので記憶が鮮明!
なんとなくプロジェクト直下に Startup
っていうフォルダをつくり、その中にこの class を作りました。
後はお決まりのこの class を作成。 ↑の DI の内容をそのままこちらに書いても変わらんですが、テストしにくいしとかで個人的にはこのスタイルにしてるので2つに分けました。
後は Functions のプロジェクト作って動作確認ですね。
4. Azure Functions で動作確認
とりあえずの動作確認なので、先ほど作った CustomBindingsSamples.TeamsBinding プロジェクトのソリューションと同じとこに Azure Functions のプロジェクトを作り、以下のコードを書きました。
Nuget: Microsoft.NET.Sdk.Functions は現時点最新の v1.0.29 を使っています。クラスやメソッドは static
ではありません。
14行目で、Teams の Attribute 使ってます。 %TeamsWebhookUri%
を環境変数として定義していますので、雑に local.settings.json
にキーと値をセットしましょう。値は、最初の方で Teams の Incoming Webhook を設定した時に取得した URL です。
13行目のようにモデルバインドしたことで、 17行目のメソッド1行で実装がおしまいです。
デバッグして Postman でこんな感じで送信することで正常に動かすことができます。
また、Azure 上にデプロイして試してみます。AppSettings に TeamsWebhookUri
って環境変数の登録は忘れないようにしましょう。
デバッグ時同様に Postman で送信すると正常に動作することが確認できました。
終わりに
あとは Nuget パッケージにでもまとめれば完成ですね。
Teams の bindings を作りたかったってよりも DB 関連の bindings 作るための予習だったので、その妄想からコードの構成が多くなりましたが、この流れで Database 用のも作れますねー(connection pool 関連を考える必要はあるだろうけど)。