Azure Functions の Custom Bindings を開発する

Azure Functions では、V2からCustom Bindings の作成がサポートされている。Custom Bindings を作成すると、自分の好きなバインディングを作れるだけではなく、Azure Functions の Bindings の挙動を理解することもできるので、遊ぶにはとてもお勧めだ。

![bindings.PNG](https://qiita-image-store.s3.amazonaws.com/0/3470/61db4139-19c7-2143-00a9-99886217cff6.png)


今回は ARM テンプレートをデプロイする Output bindings を作成してみたので、それを元に Output bindings を作成する方法を整理しておきたい。

NOTE: Azure Functions V2 の [breaking change](https://github.com/Azure/app-service-announcements/issues/129) により、書き方が若干変わりました。その対応の[ブログ](https://medium.com/@tsuyoshiushio/azure-functions-v2-custom-bindings-for-breaking-change-3fae9984e9c1)を書いておきましたので、ご参照ください。2018/9/4

# Custom Trigger のサポート状況

尚、Custom Trigger も作成できるのだが、Custom Trigger は、Consumption Plan ではサポートされていない。App Service Plan なら良い。Consumption plan だと、Scale Controller にその Trigger のサポートのロジックが必要になるが、残念ながら Scale Controller のコードは現在のところ公開されていないので、現時点では、Custom Bindings のみが正式サポートだ。もしご興味があれば、Custom Trigger の記事とサンプルも公開しているので、参考になれたい。

# Custom Bindings の構成要素

Custom Bindings を作成するためには次の４つの要素が必要になる。

* Attribute クラス
* Context クラス
* Configuration クラス
* Collector クラス

それぞれを、下記のサンプルプロジェクトを基に解説していきたい。

 * [GitHub: Resource Deployment bindings](https://github.com/TsuyoshiUshio/azure-functions-resource-extension)

# 事前準備

Bindings を作成するためには、`.NET Standard Class library` のプロジェクトを作成しよう。その後、Nuget パッケージとして、`Microsoft.NET.Sdk.Functions` をプロジェクトに追加しよう。私の環境だと、`v1.0.13` を使用した。基本的にそれだけだ。今回はリソースマネージャのデプロイメントがあるので、`Microsoft.Azure.Management.Fluent` ライブラリも追加しているが、それは、リソースマネージャのデプロイの方に使われるので`Bindings` 自体には関係ない。

## Attribute の作成

Attribute は クラスやパラメータに付加できるメタ情報のことだ。例えば今回のプロジェクトだと、今回作った Custom Bindings を使ったサンプルのファンクションは次のようなメソッドシグネチャになる。この、`HttpTrigger` だったり、`ResourceDeployment` の部分だ。この部分はクラスになっている。

Azure Functions では、メソッド定義の部分で使われて、ここに定義した値が Bindings に引き渡される。

実体を見てみよう。

```csharp
        public static async Task<IActionResult> RunAsync([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]HttpRequest req,
            [ResourceDeployment("CLIENT_ID", "CLIENT_PASS", "TENANT_ID")] IAsyncCollector<ResourceDeploymentContext> collector,
            TraceWriter log)
```

クラスの定義の実態は次のとおり。なんてことはない。単なるクラスで、ただ、Attribute を継承しており、`AttributeUsage` という Attribute で、どのようにこのクラスが使えるかが定義してあり、`Binding` という Attribute でこれは Azure Functions にこれは、`Binding` で使われることを示すためのものだ。上記のメソッド定義と比較しても、単に属性をAttribute からセットできるようにしているに過ぎない。ここにセットした値が、Bindings に引き渡すことができるようになる。Attribute 名は、クラス名 - `Attribute` になる。

```csharp
    [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue)]
    [Binding]
    public class ResourceDeploymentAttribute : Attribute
    {
        public string ClientID { get; set; }
        public string ClientPassword { get; set; }
        public string TenantId { get; set; }
        public bool DeferMode { get; set; }

        /// <summary>
        /// Constructs a new instance. Set the Application attribute with ServicePrincipal.
        /// </summary>
        /// <param name="clientId">Application Id</param>
        /// <param name="clientPassword">Application Password</param>
        /// <param name="tenantId">Tenant Id</param>
        public ResourceDeploymentAttribute(string clientId, string clientPassword, string tenantId)
        {
            ClientID = clientId;
            ClientPassword = clientPassword;
            TenantId = tenantId;
            DeferMode = false;
        }
    }
```
## Context クラス

Context クラスは、Bindings の出力につかわれる。出力したい値を格納するためのクラスだ。中身を見ても単なるクラスに過ぎない。これは、`IAsyncCollector` 等に Add することが可能なオブジェクトとなる。今回は ARM のテンプレートをデプロイするために必要な情報を持つようにしている。例えば皆さんが仮に、Slack の Output bindings をつくりたかったら、Slack に出力するために必要な情報を持たせるようにするとよい。

```csharp
    public class ResourceDeploymentContext
    {
        public string Name { get; set; }
        public string TemplateUrl { get; set; }
        public string TemplateVersion { get; set; }
        public string Parameter { get; set; }
        public string ResourceGroup { get; set; }
        public string Location { get; set; }
        public Microsoft.Azure.Management.ResourceManager.Fluent.Models.DeploymentMode DeploymentMode { get; set; }

        public ResourceDeploymentContext(string name, string templateUrl, string parameter, string resourceGroup, string location)
        {
            this.Name = name;
            this.TemplateUrl = templateUrl;
            this.Parameter = parameter;
            this.ResourceGroup = resourceGroup;
            this.Location = location;
            SetDefault();
        }

        private void SetDefault()
        {
            this.TemplateVersion = "1.0.0.0";
            this.DeploymentMode = Microsoft.Azure.Management.ResourceManager.Fluent.Models.DeploymentMode.Incremental;
        }
    }
``` 

## Configuration 

このクラスは Bindings の起点となるクラスだ。ポイントは２つ。`IExtensionConfigProvider`を継承していること。また、`public void Initialize(ExtensionConfigContext context)` というメソッドが実装されていることだ。Azure Functions の ScriptHost は、`IExtensionConfigProvider` を継承したクラスをスキャンして、bindings を登録し、そのクラスの`Initialize` を実行して、バインディングルールを設定している。ここでは、Output bindings のみなので、`BindToCollector` というメソッドのみ実装していて、`ResourceDeploymentAttribute` を Attribute として使用していることと、`ResourceDeploymentCollector` がOutput bindings 用の ResourceDeploymentCollectorであることをバインディングしている。

```csharp
    public class ResourceDeploymentConfiguration : IExtensionConfigProvider
    {
           :
        public void Initialize(ExtensionConfigContext context)
        {
            context.AddBindingRule<ResourceDeploymentAttribute>().BindToCollector(attr => new ResourceDeploymentCollector(attr, this));
        }
```

他のサンプルに関しては、CosmosDB のものがいろいろサポートしていてわかりやすいと思う。該当部分を下記にリンクしておく。

* [azure-webjobs-sdk-extensions/src/WebJobs.Extensions.CosmosDB/Config/CosmosDBConfiguration.cs](https://github.com/Azure/azure-webjobs-sdk-extensions/blob/dev/src/WebJobs.Extensions.CosmosDB/Config/CosmosDBConfiguration.cs#L50-L87)

## Collector 

Collector クラスは、多くの場合ロジックの実行を行うクラスだ。Output bindings のパラメータとして引き渡される。先の例を再度引用すると、`IAsyncCollector` で引き渡される実体のクラスを実装する。

```csharp
        public static async Task<IActionResult> RunAsync([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]HttpRequest req,
            [ResourceDeployment("CLIENT_ID", "CLIENT_PASS", "TENANT_ID")] IAsyncCollector<ResourceDeploymentContext> collector,
            TraceWriter log)
``` 

該当のクラスを見てみよう。ARM テンプレートの具体的なロジックは一旦置いておいて、Azure Functions の Bindings の構造として必須のものを見てみよう。本質的な部分は次の箇所のみだ。

`IAsyncCollector` インターフェイスを実装する。その時テンプレートクラスは、`ResourceDeploymentContext` クラスを指定しておく。このようにすると、`(1) コンストラクタ` の箇所で先に作成した `Attribute` と `Config` クラスが引き渡されている。どのような値を引き渡すかは、本来は `Config` クラスの `Initialize` の部分で決定されている。ただ、Attribute 等は最低限渡しておくとよい。`(2)` の部分は実際にユーザが functions の中で呼び出す部分だ。大抵の場合は、ここで、`Context` クラスを内部のコレクションに格納しておく。最終的に `(3) FlushAsync` の部分が、Functions の実行後に呼ばれて実行される。

```csharp
 public class ResourceDeploymentCollector : IAsyncCollector<ResourceDeploymentContext>
    {

        public ResourceDeploymentCollector(ResourceDeploymentAttribute attribute, ResourceDeploymentConfiguration config) // (1) コンストラクタ
        {
            :
        }

        public async Task AddAsync(ResourceDeploymentContext item, CancellationToken cancellationToken = default(CancellationToken)) // (2) AddAsync メソッド
        {
             :
        }

        public async Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken))   // (3) FlushAsync メソッド
        {
             : 
        }


```

是非本番のコードがどうなっているか見てほしい。

* [ResourceDeploymentCollector.cs](https://github.com/TsuyoshiUshio/azure-functions-resource-extension/blob/master/WebJobs.Extensions.ResourceDeployment/ResourceDeploymentCollector.cs)

# Tips

この４つの要素で Bindings が出来ている。皆さんが Bindings のコードを読む際も、`Config` クラスを見つけてそこから読み始めるのがよいと気づくかもしれない。私の Bindings のサンプルで、いくつか 工夫をしたところがあるのでそこだけ共有しておきたい。

## IAzure client をキャッシュ

de:code 2018 の私のsessionを見ていただいた人はわかるかもしれないが、Azure Functions のコードを書くときに重要なこととして、Outbound の通信をするときのクライアントは、static にしておくなり、DI でインジェクションするなりして、毎回 New されないようにするのが必要だ。今回の私の Bindings では、`cliendId` はユニークになるはずなので、`clientId` 毎に１クライアントを作成するようにしている。これにより、他のサブスクリプションにもデプロイしたい場合にも問題がないだろう。実体は `Config` クラスの中で記述した。ここは１度しか呼ばれないからだ。

```csharp
        internal readonly ConcurrentDictionary<string, IAzure> ClientCache = new ConcurrentDictionary<string, IAzure>(); // Client のキャッシュ

        public void Initialize(ExtensionConfigContext context)
        {
            context.AddBindingRule<ResourceDeploymentAttribute>().BindToCollector(attr => new ResourceDeploymentCollector(attr, this));
        }

        internal IAzure GetClient(string clientId, string clientPassword, string tenantId)
        {
            return ClientCache.GetOrAdd(clientId, (c) => {
                var credentials = SdkContext.AzureCredentialsFactory.FromServicePrincipal(c, clientPassword, tenantId, AzureEnvironment.AzureGlobalCloud);
                var client = Azure
                    .Configure()
                    .WithLogLevel(HttpLoggingDelegatingHandler.Level.Basic)
                    .Authenticate(credentials)
                    .WithDefaultSubscription(); // Client の生成。インスタンスがあればそれを使う
                return client;
            });

        }
```

## DeferMode の実装

サンプルのファンクションを見てみると次のようになる。ポイントは、`Collector` クラスの `AddAsync` メソッドだが、通常の Azure Functions のバインディングの実装だと、この時点では内部のコレクションに足されるだけで、最終的に Functions の実行が完了してから、`FlushAsync` が呼ばれた時点で目的のロジックを実行する。

この実装だと、問題が一つあって、`FlushAsync` が実行されるのは、functions の実行の後なので、エラーが発生した場合にデバッグがやりにくくなる。この `Bindings` ではデフォルトで、`AddAsync` の時にコードが実行されて、ARM テンプレートがデプロイされるようになっている。もし問題があれば、エラーメッセージを見れてデバッグがしやすい。ただし、この実装にもトレードオフがあって、この実装にすると、C#, F# 以外の実装だと、実行がされないようになってしまう。あくまで bindings は C# で書かれており、node の output bindings はあくまで functions 実行後にしか有効にならない。このようなケースだと、`Attribute` の部分で `DeferMode = true` にしておくと、通常の`functions`のように実行が `FlushAsync` で実行されるので、nodeでも問題なく実行できるようになる。

```csharp
            var deployment = new ResourceDeploymentContext("testResource5", "https://raw.githubusercontent.com/azure/azure-quickstart-templates/master/101-function-app-create-dynamic/azuredeploy.json", parameter, "testResource5", "westus");

            await collector.AddAsync(deployment); // This execute deployment
```

# おわりに

Azure Functions のインターナルを含めて、Custom Bindings の書き方を解説してみた。たった４つのクラスをみなさんにもカスタムバインディングスが作れる。ぜひ楽しんでみてほしい。

尚、Custom Trigger に興味がある方は、私が過去にブログを書いているので参考にされたい。

* [Azure Functions の Custom Trigger を書いて理解してみた](https://qiita.com/TsuyoshiUshio@github/items/4d74ea1a4e48139f7426)

# リソース

* [Creating custom input and output bindings](https://github.com/Azure/azure-webjobs-sdk/wiki/Creating-custom-input-and-output-bindings)