しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

Azure Functions Proxies で実現出来ること

前にハッカソンを行った時に Azure Functions Proxies 周りの挙動が気になったので、一通り確認して気になったことを軽くメモとして残します。

まずは公式ドキュメントと牛尾さんの Qiita を読めば大体は理解できるはずです。

Azure Functions Proxies は名前の通り、入ってきた HTTP リクエストを別のバックエンドに流す機能がありますが、もうちょっと面白く使えるのではないかと思っています。

環境変数を使う

ARM Template や Deployment Slot と組み合わせて便利に使えそうなのが環境変数の参照ですね。

サンプルなので適当に App Settings に値を追加して試してますが、Slot Setting にチェックを入れると Swap を行った後でも値が変わらないので便利です。

f:id:shiba-yan:20190115154420p:plain

公式のドキュメントには App Settings で設定した値を参照できるとありますが、実際には環境変数を参照しているので App Settings 以外にも App Service が内部的に設定した値も参照できます。

プロキシの定義は以下のようにしました。%% で囲むと環境変数で置換されるという分かりやすい挙動です。

{
  "$schema": "http://json.schemastore.org/proxies",
  "proxies": {
    "buchizo": {
      "matchCondition": {
        "route": "/buchizo"
      },
      "backendUri": "https://%BLOG_AZURE_MOE%/"
    }
  }
}

f:id:shiba-yan:20190115154254p:plain

作成した Proxy URL にアクセスすると、ちゃんと App Settings で指定した URL へのリクエストが行われたことが確認出来ます。パスをキャプチャすれば、もっと柔軟に利用できます。

f:id:shiba-yan:20190115170046p:plain

フロントとバックエンドを複数用意した場合などで、その向き先を App Settings で指定できるので、Run From Package やマルチリージョンへのデプロイと相性が良いと思います。

ループバックアドレスを使う

Azure Functions Proxies を使って自分自身の Functions へのルーティングを行う場合にはループバックアドレスが使えます。つまり URL Rewrite でいうところの Rewrite に該当する挙動になります。

この場合はバックエンドへの HTTP 通信が発生しないため、余計なオーバーヘッドが発生しません。

もしこの挙動を変えたい場合には AZURE_FUNCTION_PROXY_DISABLE_LOCAL_CALL に true を設定します。その場合では localhost だと上手く動かなくなります。

しかし、パッと見は本当にバックエンドへのリクエストが発生していないか確認出来ないので、以下のような定義でトレースを有効にして確認しておきました。

{
  "$schema": "http://json.schemastore.org/proxies",
  "proxies": {
    "Internal Routing": {
      "debug": true,
      "matchCondition": {
        "route": "/internal"
      },
      "backendUri": "https://localhost/api/HttpTrigger1"
    }
  }
}

f:id:shiba-yan:20190115154242p:plain

ログが長いので割愛しますが forward-request を確認すれば、バックエンドへの HTTP ヘッダー設定などが含まれていないので、直接実行されたことが分かります。別のエンドポイントを指定した場合には、バックエンドへ送信する HTTP ヘッダーなどの情報がログに書き出されます。

ドキュメントには localhost を使えと書いてありましたが WEBSITE_HOSTNAME などを使って自分自身のホスト名を設定した場合でも、直接 Functions が実行されるようでした。

{
  "$schema": "http://json.schemastore.org/proxies",
  "proxies": {
    "Internal Routing": {
      "matchCondition": {
        "route": "/internal"
      },
      "backendUri": "https://%WEBSITE_HOSTNAME%/api/HttpTrigger1"
    }
  }
}

なので上の定義は先ほどの定義と全く同じ挙動となります。分かりやすさでは localhost のが良いですね。

リダイレクトを行う

Azure Functions Proxies では backendUri はオプションになっているので、別に指定しなくても適切に responseOverrides を使って値を設定すれば動作します。一般的にはモックの作成に使われていますね。

使う場面があるかわからないですが、302 と Location ヘッダーを返せばリダイレクトも出来ます。

{
  "$schema": "http://json.schemastore.org/proxies",
  "proxies": {
    "Redirect": {
      "matchCondition": {
        "route": "/redirect"
      },
      "responseOverrides": {
        "response.statusCode": "302",
        "response.headers.Location": "https://%BLOG_AZURE_MOE%/"
      }
    }
  }
}

f:id:shiba-yan:20190115175853p:plain

勿論ちゃんと動作します。Azure Functions では URL Rewrite が使えないので、簡単な処理でも Functions として実装しないといけないですが、多少のことなら Proxies で実現できます。

カスタム HTTP ヘッダーを追加する

普通の ASP.NET アプリケーションの場合は Web.config を使って HTTP ヘッダーを追加できますが、Azure Functions では Web.config を Function Runtime が持っているので基本的には弄れません。

なので Functions Proxies を使って同じような定義を書けば良いです。

{
  "$schema": "http://json.schemastore.org/proxies",
  "proxies": {
    "AddHeader": {
      "matchCondition": {
        "route": "/{*path}"
      },
      "backendUri": "https://localhost/{path}",
      "responseOverrides": {
        "response.headers.X-Kazuakix-Age": "50"
      }
    }
  }
}

上の定義を実行すると、ちゃんと HTTP ヘッダーが付与されて返ってきました。

f:id:shiba-yan:20190115174932p:plain

割と URL Rewrite と ARR の組み合わせに近いことが出来ますが、今のところはマッチする条件を全く指定できないので、せめて HTTP ヘッダーぐらいは条件として扱いたいですね。

パフォーマンス周りは調べてないですが、アーキテクチャ的に Application Gateway よりは悪いのではないかと思っています。しかし、Azure Functions Proxies は Consumption かつスケーリングが素早いので、デプロイに時間がかかる Application Gateway よりはシンプルに使えます。

Azure Functions でも HttpTrigger でモデルバインディングを使って楽をしたい

ASP.NET Core MVC だと普通に使っているモデルバインディングですが、Azure Functions の HttpTrigger でも近い形で使えるようになってます。テンプレートでは HttpRequest から自前でパースしてますが、ランタイムに任せることが出来ます。

使い方は Core MVC と同様に、単純に受け取りたいクラスを Function の引数として追加するだけです。

public static class Function1
{
    [FunctionName("Function1")]
    public static IActionResult Run(
        [HttpTrigger(AuthorizationLevel.Function, "post")] DemoModel model,
        ILogger log)
    {
        return new OkObjectResult($"Hello, {model.Name}");
    }
}

public class DemoModel
{
    public string Name { get; set; }
    public int Age { get; set; }
}

この HttpTrigger に対して JSON を POST で投げると、値がバインドされたインスタンスを受け取れます。

f:id:shiba-yan:20181227022951p:plain

これで毎回 HttpRequest から Body を読み取ってデシリアライズするという手間が省けて、実装がスッキリとします。POST の Body だけではなくクエリパラメータの値もバインドしてくれるのも便利。

上の例のように単純な型であれば問題ないのですが、残念ながら対応していない型があります。今のところ確認しているのは配列と列挙型です。

モデルを少し書き換えて配列のプロパティを追加してみます。

public class DemoModel
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string[] Friends { get; set; }
}

Core MVC なら問題なくバインドしてくれるのですが、Azure Functions では null になります。

f:id:shiba-yan:20181227023002p:plain

そして更に列挙型のプロパティを追加して同じように試してみます。

public class DemoModel
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string[] Friends { get; set; }
    public JobType Job { get; set; }
}

public enum JobType
{
    社畜
}

こっちは配列よりも酷く、以下のようなキャスト失敗の例外を吐いて 500 を返します。

System.Private.CoreLib: Exception while executing function: Function1. Microsoft.Azure.WebJobs.Host: Exception binding parameter 'model'. System.Private.CoreLib: Invalid cast from 'System.String' to 'FunctionApp31.JobType'.

当然ながら Core MVC では問題なくバインドできるようなケースです。HttpTrigger の場合は Core MVC では扱えていた型でも、バインドできなかったりそもそもエラーになってしまうという面倒な問題があります。

回避策と原因

そして回避策ですが、HttpTrigger の実装を調べたところ Core MVC から追加された InputFormatter を使うコードになっていたのに、肝心の Formatter が DI に登録されていなかったので動いてませんでした。*1

以下のように IWebJobsStartup を実装して JsonInputFormatter を追加すると動くようになります。

[assembly: WebJobsStartup(typeof(FunctionApp31.JsonWebJobsStartup))]

namespace FunctionApp31
{
    public class JsonWebJobsStartup : IWebJobsStartup
    {
        public void Configure(IWebJobsBuilder builder)
        {
            builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, MvcJsonMvcOptionsSetup>());
        }
    }
}

更に残念なことに .NET Core 2.1 向けになった Azure Functions v2 では、MetadataGenerator の問題により IWebJobsStartup が起動時に実行されないという不具合があります。問題は認識しているようですが、今のところは TFM を netstandard2.0 に変更するしか回避策がありません。

これでやっと配列や列挙型も Core MVC の InputFormatter を使ってバインドされるようになります。

f:id:shiba-yan:20181227023013p:plain

このように残念な結果になった原因ですが、そもそも Azure Functions の HttpTrigger がバインディングに Core MVC の機能を使わずに自前で実装していることにあります。

何で独自に実装しているのか分からないですが、とにかくその実装が中途半端で配列と列挙型を考慮していないため、意図したとおりの挙動にならないのでした。

おまけ : モデルバリデーション

モデルバインディングを行えば、次はバリデーションを追加したくなるはずです。牛尾さんが Data Annotations を使った方法を既に書いてくれているので参照してください。

HttpTrigger が公式にバリデーションを提供してくれると嬉しいのですが、難しいでしょうね。

*1:該当リポジトリに Issue と Pull Request は投げてあります

Azure App Service 上での ASP.NET Core 2.2 のパフォーマンス

ASP.NET Core 2.2 と同時にリリースされた ANCM v2 が +400% パフォーマンス改善したと書かれていたので、実際どのくらいまで頑張れるのか調べたくなりました。

ブログにあった +400% という数値は恐らく物理マシンを使っているので、実際に Azure 上の一般的な構成で動かした時にはそんな数値は出ないと思っていますが、IIS と Kestrel 間でのオーバーヘッドは無視出来ないものなので、App Service を使って実際の値を調べました。

はじめに

テストの条件は以下の通りです。インスタンスは 1 台で Windows は複数のサイトを 1 Service Plan にホストしているので、ANCM のバージョンとホスティングモデル以外は全く同じになっているはずです。

  • 同じプロジェクトを使用 (Core MVC テンプレそのまま)
  • Premium V2 の Medium (P2V2)
  • West US 2
  • Windows は Run From Package / Linux は Docker
  • Load Test は West US / Warmup 設定あり

Run From Package は便利だから使っていますが、ストレージに足引っ張られるのも嫌だったという理由もあります。Linux では使えないので Docker を使っていますが、テストを行う前に 10 秒間のウォームアップ設定を入れているので、そこまで大きな差は出ないはず。

今回は以下の視点でテストを行っています。インスタンスサイズでの比較は面倒だったのでしませんでした。

  • ANCM v1 と v2 での違い
  • OutOfProcess と InProcess での違い
  • Windows と Linux での違い

個人的には Windows と Linux での違いが気になっていたので追加で試しました。

一応は Kestrel で直接受けた場合には、Windows の方が Linux より速いというベンチマーク結果が Power BI で公開されていますが、App Service で使う場合にはどのくらい差があるのか疑問でした。

Azure DevOps の Load Test を使っていろんなパターンの負荷をかけて、503 エラーが出る前ぐらいの数値を拾ってきました。あくまでも P2V2 かつテンプレそのままの数値と考えてください。

ANCM v1 OutOfProcess vs ANCM v2 OutOfProcess

これまで通り dotnet のプロセスが w3wp とは別に立ち上がって、Kestrel で通信を受けるパターンですが、v1 と v2 で特に差はないようです。誤差ぐらいの違いしか出ていないです。

f:id:shiba-yan:20181225170948p:plain

ASP.NET Core MVC 2.2 は P2V2 で大体 1k RPS ぐらいまで単純なページなら返せるのは、結構頑張っている感じがあります。RPS のグラフがギザギザになってるのは少し疑問です。

特に ANCM v1 はスループットが不安定ですし、v2 が出た今は使う必要はないでしょう。

ANCM v2 OutOfProcess vs ANCM v2 InProcess

気になっていた In-Process Hosting のパフォーマンスです。これまでの Out-Of-Process と比較して良好なパフォーマンスが出ています。安定したスループットが出せているのも良いです。

f:id:shiba-yan:20181225170959p:plain

IIS と Kestrel 間で発生していた HTTP のオーバーヘッドが無くなった効果が出ているということでしょう。*1

これまでは IIS が受け取ったパース済みリクエストを、再度 HTTP に組み立てて後ろに流していたので当然とは言えますが…。とりあえず InProcess がデフォルトなのでそのまま使えば良いです。

Linux Docker vs ANCM v2 InProcess

最後は実質的に Linux と Windows の比較になるやつですが、実際は App Service のアーキテクチャ的に Linux より Windows の方がわずかに有利になっているので、どのくらいの差が付くのかという確認です。

f:id:shiba-yan:20181225171011p:plain

結果としては ANCM の OutOfProcess よりは良い数値が出ました。スループットも安定しています。

ANCM v2 で InProcess がリリースされていなければ、ASP.NET Core は App Service の Windows より Linux を使った方がパフォーマンスが良いという現実だったようです。

とはいえ、普通に 1.3k RPS を 1 台の P2V2 で捌けているのは優秀ですね。もし Web App for Containers が TCP ではなく Unix Domain Socket に対応すれば、また結果は大きく変わる気がします。

*1:ネイティブ <=> C# でのマーシャリングコストは発生しているはず。

Azure App Service で ASP.NET Core 2.2 と ANCM v2 が使えるようになった

.NET Core 2.2 リリース時には 12 月末までの対応が宣言されていた Azure App Service での .NET Core 2.2 対応ですが、全リージョンへのデプロイが完了したようです。

デプロイのタイミングによっては .NET Core 2.2 はインストールされていても、ASP.NET Core Module (ANCM) v2 がインストールされていないという状態に一部なっていたみたいです。

今は ANCM v2 のインストールまで完了した状態になっているので、csproj を弄って修正する必要なく、そのまま App Service で動かすことが出来るようになってます。

ちなみに ASP.NET Core 2.2 からはプロジェクトを Visual Studio で作成すると、デフォルトで ANCM v2 と In-Process Hosting が有効化されています。2.1 からの移行時には少し気を付けたい部分です。

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.2</TargetFramework>
    <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
  </PropertyGroup>


  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" />
  </ItemGroup>

</Project>

AspNetCoreHostingModel の値を InProcess から OutOfProcess に変更すると、w3wp の下に dotnet プロセスが立ち上がるこれまでと同じ仕組みに戻ります。

In-Process Hosting を有効化すると w3wp が直接 .NET Core をホスティングする形になるので、以下のように Kudu などで w3wp が .NET のアセンブリを読み込んでいることが確認できます。

f:id:shiba-yan:20181223222658p:plain

子プロセスとして dotnet が立ち上がっていないことも同時に確認できます。開発チームはエラーハンドリングとパフォーマンス面で有利だと言ってます。

ANCM での In-Process に関しては .NET Core 2.1 の時に少し書いたので参考にしてください。

この変更により Visual Studio の Cloud Explorer からリモートデバッグを行おうとすると、In-Process Hosting の場合は以下のようなエラーが出てデバッガをアタッチ出来なくなります。

f:id:shiba-yan:20181223224659p:plain

ぶちぞう RD 曰く、前は ASP.NET Core でエラーになっていたらしいので、Cloud Explorer のアップデートで解決しそうですが、リモートデバッグが必要になった場合には OutOfProcess に切り替える必要があります。

実際に切り替えれば、以下のようにすんなりと Cloud Explorer からリモートデバッグが行えます。

f:id:shiba-yan:20181223224924p:plain

スナップショットデバッガーは動作を確認してないですが、こっちはクラッシュした時にダンプを取る仕組みなので、In-Process Hosting でも動作しそうな感じはします。

In-Process Hosting の効果が本当にどのくらいあるのか気になったので、パフォーマンスを Azure DevOps を使って確認しています。長くなりそうなので、それは別のエントリにまとめることにします。

少なくとも言えることは ASP.NET Core 2.2 の場合は In-Process の方がパフォーマンスが良いです。

Azure Key Vault を使って App Service の証明書を管理する

今や必須となっている https 対応のために App Service に SSL 証明書をバインドする場合、ほとんどの場合は PFX を作成してアップロードしていると思いますが、割とこの辺りの手順は面倒ですよね。アップロードしたはずの証明書が思ったように出てこない場合もあります。

調べた限りでは App Service にアップロードした証明書の挙動は以下のようになっています。

  • App Service の証明書は Webspace に関連付く
  • 証明書と App Service が同じ Webspace 上にある場合のみ利用可能

こういった条件があるため複数リージョンに Traffic Manager などで分散している場合には、それぞれの App Service に対して証明書のアップロードを行う必要があるので、正直なところ凄く手間がかかります。

なので後から Key Vault に保存されている証明書を使えるような機能が実装されましたが、Portal との統合は行われておらず ARM の REST API を直接実行する必要があるので、まあまあ手間ですが証明書の管理を 1 つの Key Vault に任せられるのがメリットです。

予め Key Vault を作成しつつ、アクセスポリシーに App Service を追加します。

App Service Team のブログでは ARMClient が使われていますが、Resource Explorer でも大丈夫です。以下のような JSON を用意して、certificates に対して実行すれば証明書が新しく作られます。

{
  "location": "Japan East",
  "properties": {
    "keyVaultId": "Key Vault Resource Id",
    "keyVaultSecretName": "daruyanagi-com",
    "serverFarmId": "App Service Plan Resource Id"
  }
}

とても残念なことに Front Door や対応予定されている Application Gateway v2 とは異なり Key Vault 側を更新しても自動で反映してくれません。

新しい証明書を反映させるためには、もう一度 ARM の REST API を実行する必要があるので、割といまいちな実装です。Azure App Service は歴史的な経緯から Azure Key Vault との統合が弱めです。*1

今はポータルから Sync が行えるようになっていますが、手動なのであまり意味がないですね。Key Vault の証明書が変更されたら Sync するような Azure Functions を書くしかなさそうです。

ちなみに App Service Certificate を使って証明書を発行すると Key Vault が作られ、そこに証明書が安全に格納されるようになっています。こっちは自動的に新しい証明書が発行された場合には反映されます。

補足 : Webspace の話

App Service の制約は割と Webspace に関係しているようなので、気になった部分を確認しておきました。Scale unit と同じスコープかと思っていましたが、厳密には異なっているようです。

帝国兵殿の言う通り、最近のデプロイでは Webspace は以下のルールで作られます。ちなみに大昔に作った App Service の場合はリソースグループ名が含まれていませんでした。

リソースグループ名 + "-" + リージョン名 + webspace

このような命名ルールとなっているので、同じ Webspace に新しい App Service をデプロイ出来ます。

公式ドキュメントでは App Service が動作している Service Plan を変更する場合には、同じ Webspace に Service Plan をデプロイする必要があると書いてあります。おそらく Webspace に関して言及しているのは、このドキュメントだけです。

先述した SSL 証明書も内部的には Webspace に関連付いているので、同じ Webspace にデプロイされている App Service にはバインドが可能です。Webspace は表に出てこないので意外に分かりにくい。

そして盲点となりやすいのが Premium V2 が使える Service Plan と使えない Service Plan は同じ Webspace にデプロイ出来ないことです。なので、別のリージョンを指定して Webspace を変えることで、Premium V2 が使える Service Plan と使えない Service Plan を同時にデプロイすることが可能です。

*1:単純に App Service がリリースされた時に Key Vault が無かっただけ

あえて Durable Functions でオーケストレーターの完了まで待つ Web API を作る

通常 HttpTrigger で起動する Durable Functions を作成すると CreateCheckStatusResponse を使って、インスタンス管理用のエンドポイントを含んだレスポンスを返すと思います。

テンプレートから作成したコードも以下のようになっているはずです。

[FunctionName("Function1_HttpStart")]
public static async Task<HttpResponseMessage> HttpStart(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")]HttpRequestMessage req,
    [OrchestrationClient]DurableOrchestrationClient starter,
    ILogger log)
{
    // Function input comes from the request content.
    string instanceId = await starter.StartNewAsync("Function1", null);

    log.LogInformation($"Started orchestration with ID = '{instanceId}'.");

    return starter.CreateCheckStatusResponse(req, instanceId);
}

実際にこの API を実行すると 202 Accepted が返ってきて、後は statusQueryGetUri を使って完了したか、それとも失敗したかを確認します。

f:id:shiba-yan:20181209161147p:plain

すぐにレスポンスが返ってきて、その後は非同期で処理を待てば良いのでスケーリングで有利な面が多いですが、これがブラウザから実行しようとする場合には正直面倒です。

Long-running なオーケストレーターの場合は厳しいですが、数秒で完了するようなものは API のレスポンスとしてオーケストレーターの実行結果を返せた方が便利ですよね。ちゃんとその為のメソッドが Durable Functions には用意されていました。

コードの変更自体は非常に簡単で、これまで呼び出されていた CreateCheckStatusResponse の代わりに WaitForCompletionOrCreateCheckStatusResponseAsync を実行するようにすると、引数で指定したインスタンスの完了を待つようになります。

処理としては定期的にインスタンスの状態をチェックしているみたいなので、用意されているオーバーロードのメソッドを使って調整する必要はあると思います。

[FunctionName("Function1_HttpStart")]
public static async Task<HttpResponseMessage> HttpStart(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")]HttpRequestMessage req,
    [OrchestrationClient]DurableOrchestrationClient starter,
    ILogger log)
{
    // Function input comes from the request content.
    string instanceId = await starter.StartNewAsync("Function1", null);

    log.LogInformation($"Started orchestration with ID = '{instanceId}'.");

    return await starter.WaitForCompletionOrCreateCheckStatusResponseAsync(req, instanceId);
}

このように書き換えて実行すると、オーケストレーターの実行結果がレスポンスとして返ってきます。

f:id:shiba-yan:20181209161200p:plain

これで挙動としては極めて一般的な Web API と同じようになりました。結果としては同期 API と同じ挙動となりますが、ちゃんとオーケストレーターやアクティビティは非同期で動作し続けます。

そしてオーケストレーターが失敗した場合には以下のようなエラーレスポンスが返ります。

f:id:shiba-yan:20181209233316p:plain

デフォルトのタイムアウト時間である 10 秒を過ぎると、通常と同じようなインスタンス管理用のエンドポイントが返るようですが、ちゃんと動いていない気もしました。

処理時間が大体わかっている場合はタイムアウトを長くしても問題ないでしょう。

今回はブラウザから実行する必要があった API だったので、Durable Functions の特徴と既存のアクティビティを活かしながら、シンプルな Web API を素早く用意出来ました。

ASP.NET Core 向けに Azure Table Storage を利用する Configuration Provider を書いた

タイトルの通りですが、ASP.NET Core と一緒に追加された Configuration 周りは Provider を実装すると簡単に拡張できるようになっているので、実際に Azure Table Storage 向けの実装を書いて試してみました。

流れとしては以下の 2 つを継承したクラスを実装するだけなので簡単です。

  • IConfigurationSource
  • ConfigurationProvider

Configuration 周りは同期処理として実行されるので、Task ベースの非同期とか使ってる場合には、デッドロックしないように ConfigureAwait(false) を付けたりと工夫が必要です。

サンプルに近い実装ですが、とりあえず GitHub に一式を公開しておきました。

デフォルトでは Key Vault 向け Provider が入っているのでそれを使えば良いという感じはしますが、Key Vault では ":" での区切りではなく "--" になったり、接続文字列以外のセキュリティが求められないパラメータ的なものまで入れるのは無駄な感じもします。

何より Azure Table は安くて容量も大きいので、使いどころはあるのではないかなと思います。

Provider 周りの実装は GitHub を見てもらうとして、残りはちょっとだけ設定周りについて書いておくことにします。この Provider は Azure Storage の接続文字列が必要になるので、Program.cs にある IWebHostBuilder のメソッド内で接続文字列を config から取得するようにします。

public class Program
{
    public static void Main(string[] args)
    {
        CreateWebHostBuilder(args).Build().Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
               .ConfigureAppConfiguration((context, config) =>
               {
                   var builtConfig = config.Build();

                   config.AddAzureTableStorage(builtConfig.GetConnectionString("StorageConnection"), builtConfig["TableName"], builtConfig["PartitionKey"]);
               })
               .UseStartup<Startup>();
}

デフォルトのファイルや環境変数からの設定後の値が ConfigureAppConfiguration のタイミングで取れるので、それを利用して Azure Storage の Provider を追加しています。

appsettings.json には以下のように書いていますが、TableName と PartitionKey はセクションにしても良いかもしれません。これらの設定は App Service の場合は App Settings からオーバーライド出来るので便利です。

{
  "TableName": "Config",
  "PartitionKey": "Primary",
  "ConnectionStrings": {
    "StorageConnection": "STORAGE_CONNECTION_STRING"
  }
}

この辺りの考え方は Key Vault の時と同じです。というかドキュメントに書いてあります。

テーブル名やプライマリキーは設定から読んでも良いし、固定値にしても良いと思います。Table の構造としては RowKey を Configration 内で参照するためのキーとして扱うようになっています。

なので Table には以下のような形でデータを投入することになります。

f:id:shiba-yan:20181206103012p:plain

後は実際にConfiguration から値を読み込むのですが、今回は Configure を使ってオプション値として扱ってみました。以下のようなコードを書くとセクション内の値をバインドしてくれます。

services.Configure<CustomModel>(Configuration.GetSection("Custom"));

コンストラクタインジェクションを使って受け取ったクラスには、ちゃんと Table Storage に書き込んだ値が入っていることが確認出来ます。

f:id:shiba-yan:20181206102816p:plain

Table Storage はツールが充実しているので、Azure Portal からポチポチ設定するより楽かもしれませんね。

フレームワーク側にデフォルトの実装がある程度用意されているので、カスタマイズは思ったよりも簡単でした。是非はともかく SQL Server や Cosmos DB などをソースとする Provider も実装も出来るはずです。

デフォルトの実装は非常に参考になるので、カスタマイズの際には参考にするとよい感じです。

Azure の場合は Key Vault で大体解決すると思いますが、AWS Secrets Manager や HashiCorp Vault などに対応した Provider は価値がありそうです。*1

*1:既にありそうな気もしますが調べてないです

Azure Artifacts を使って自前 NuGet サーバー運用を捨てる

最近 Azure DevOps 周りを弄ってますが、あまり話として Azure Artifacts について聞いた覚えがないので軽く確認しておきました。単なるプライベート NuGet リポジトリではないのは確かです。

とりあえずドキュメントは参照しておきますが、思ったより機能は多くないです。

NuGet 以外にも npm / Maven / Gradle / pip にも対応しています。Azure Artifacts にフィードを 1 つ作れば、対応している全てのパッケージマネージャに対応できるのが利点です。

雑に機能と特徴をまとめてみると以下の通りでしょうか。

  • メジャーなパッケージマネージャに対応
  • 複数のパッケージソースをまとめて管理できる
    • Azure Artifacts フィード自体もパッケージソースとして追加できる
  • Organization 内のプロジェクトで共有される
  • フィードとして公開するパッケージやバージョンを管理可能

あくまでも組織やプロジェクト内部で使うのが目的のようなので、MyGet みたいにパブリックなフィードを作ることは出来ないようです。なので置き換えるようなサービスではないです。

作成したフィードを実際に開発で使うためのコマンドは Connect to feed から確認出来ます。

f:id:shiba-yan:20181125201014p:plain

プレビュー扱いですが Universal Package としてどんなフォーマットでも管理できるようにもなっています。

実際に使うためには VSTS の CLI が必要みたいなので少し面倒ですが、Azure Pipelines などでの成果物を何らかのバージョン管理をしつつ扱いたい場合には有用でしょう。

この後は NuGet に関してのみ書いてますが、他のリポジトリでも大体は同じです。

パッケージのプッシュ / 復元

Azure Pipelines 内では Azure Artifacts のフィードを使ってパッケージのプッシュと復元が簡単に行えます。

新しく NuGet のタスクを追加して Command を push や restore にすると、よい感じにデフォルトの処理が追加されます。push の場合は *.nupkg を自動で探してプッシュしてくれます。

f:id:shiba-yan:20181125023037p:plain

プッシュ先はドロップダウンで組織内のフィードが一覧表示されるので選ぶだけです。

restore も同様にデフォルトの設定で良いですが、フィードの設定で Upstream を使うようになっている場合には、nuget.org を使わないように変更しておいても良さそうです。

f:id:shiba-yan:20181125022726p:plain

一番使うことが多いのは、この二つだと思います。なのでこれだけ覚えておけば大体 OK です。

Upstream の扱い

フィードの作成時に nuget.org や npmjs.com をパッケージソースとして使うように設定すると、Azure Artifacts がパッケージ取得のリクエストを透過的に処理して、パッケージ自体をフィードに保存するようになります。キャッシュに近い形で扱うことが出来ます。

デフォルトのソースは Feed の設定から追加や削除が行えます。複数のパブリックソースは npm しか行えないようですが、Azure Artifacts のフィードは問題なく追加できるようです。

f:id:shiba-yan:20181126004620p:plain

ビルドに必要なパッケージを Azure Artifacts に保存できるので、NuGet や npm に障害が発生した場合や必要なパッケージが削除された場合でも、バージョンが変わらない限りはビルドを壊すことはありません。

Azure Blog に Deep dive が公開されているので、見ておくと良さそうな感じです。

ネットワーク的に有利なはずなので、ビルドの速度改善に繋がるのではと期待しましたが、試した限りでは特に差は出なかったです。この辺りは少し残念でした。

Upstream パッケージの管理

Azure Artifacts が保存しているパッケージは分かりやすく一覧表示されます。パッケージソースで絞り込むことも出来るので、NuGet / npm を使っていても安心。

f:id:shiba-yan:20181125022604p:plain

保存されているバージョンも確認出来ます。NuGet Gallery よりも扱いやすい UI だと思います。

f:id:shiba-yan:20181125194825p:plain

パッケージやバージョンは簡単に削除やリストからの非表示も出来ます。ここから選択したバージョンを後述する View に追加することも出来ます。

面白そうなのが新しいバージョンが出た時に通知する機能です。

f:id:shiba-yan:20181126010856p:plain

実際にどういうタイミングで通知されるのか分からないですが、試してみたい感じです。

View の使いどころ

正直、いまいち良いユースケースが理解できないのですが、公式的にはちゃんとテストして互換性のあるバージョンだけを利用するために、Release や Prerelease といった View を使っていく方向らしいです。

f:id:shiba-yan:20181126013558p:plain

大きなチームでの開発時に好き勝手にパッケージを入れられたり、互換性を確認出来ていないバージョンにアップデートされることは防げますが、開発者を信用しなさすぎという気もして微妙な気持ちです。

むしろ最近はパッケージがアップデート全くされないことの方が多いと思うので。

Azure Pipelines から Run From Package としてデプロイする

そろそろ Azure Functions だけじゃなくて Web Apps でも Run From Package を使ったデプロイを行っていきたいので、ひとまず標準で Run From Package でのデプロイに対応している Azure Pipelines を使ってパイプラインを組んでみました。

Run From Package 自体は wwwroot 以下への書き込みが無ければ、問題なく Web Apps で使えます。該当の Issue はちょいちょい更新されてるので確認しておきます。

zip に Unicode のファイル名が含まれていると、マウント後にファイルがおかしくなることがあるようです。日本語を使っている場合は気を付けておいた方が良いでしょう。

アプリケーションパッケージをビルド

Run From Package は wwwroot に指定した zip を直接マウントする方式なので、zip のディレクトリ構造は wwwroot 以下に配置する時と同じ構造にしないとちゃんと動きません。

例えば ASP.NET や ASP.NET Core では zip の直下に bin や Web.config が必要だったりします。

ASP.NET Core や Azure Functions v2 の場合は Web Deploy を最初から使わない前提でタスクが作られていますが、ASP.NET の場合はそうはいかないので少し工夫が必要になります。もう少しそれぞれのビルドについて説明していくことにします。

ASP.NET の場合

これまでも ASP.NET アプリケーションを単一の zip にパッケージングすることは出来ましたが、Web Deploy 向けのフォーマットになっているので、今回の Run From Package では使えません。

なのでファイルシステムに対して書き出してから、zip にする処理を挟み込んで実現します。

ファイルシステムへの書き出しは pubxml を使うと簡単に設定できますが、MSBuild のオプションだけで済ませたかったので以下のようなオプションを用意して ASP.NET アプリケーションのビルドを行います。

/p:DeployOnBuild=true /p:DeployDefaultTarget=WebPublish /p:WebPublishMethod=FileSystem /p:publishUrl="$(Agent.TempDirectory)\publish\\" /p:PrecompileBeforePublish=true /p:EnableUpdateable=false

zip にすると読み込みのみになるので、更新不可能な形でビューのプリコンパイルを行っておきます。

基本的には Azure Pipelines にある ASP.NET 向けテンプレートを使えば良いですが、zip にする処理を挟む必要があるので、とりあえず参考までに YAML の定義を載せておきます。

queue:
  name: Hosted VS2017
  demands: 
  - msbuild
  - visualstudio
  - vstest

steps:
- task: NuGetToolInstaller@0
  displayName: 'Use NuGet 4.4.1'
  inputs:
    versionSpec: 4.4.1

- task: NuGetCommand@2
  displayName: 'NuGet restore'
  inputs:
    restoreSolution: '$(Parameters.solution)'

- task: VSBuild@1
  displayName: 'Build solution'
  inputs:
    solution: '$(Parameters.solution)'
    msbuildArgs: '/p:DeployOnBuild=true /p:DeployDefaultTarget=WebPublish /p:WebPublishMethod=FileSystem /p:publishUrl="$(Agent.TempDirectory)\publish\\" /p:PrecompileBeforePublish=true /p:EnableUpdateable=false'
    platform: '$(BuildPlatform)'
    configuration: '$(BuildConfiguration)'

- task: ArchiveFiles@2
  displayName: 'Archive Artifact'
  inputs:
    rootFolderOrFile: '$(Agent.TempDirectory)/publish'
    includeRootFolder: false

- task: VSTest@2
  displayName: 'Test Assemblies'
  inputs:
    testAssemblyVer2: |
     **\$(BuildConfiguration)\*test*.dll
     !**\obj\**
    platform: '$(BuildPlatform)'
    configuration: '$(BuildConfiguration)'

- task: PublishSymbols@2
  displayName: 'Publish symbols path'
  inputs:
    SearchPattern: '**\bin\**\*.pdb'
    PublishSymbols: false
  continueOnError: true

- task: PublishBuildArtifacts@1
  displayName: 'Publish Artifact'
  inputs:
    PathtoPublish: '$(build.artifactstagingdirectory)'
    ArtifactName: '$(Parameters.ArtifactName)'

このパイプラインを実行すると Web Deploy 形式ではない、シンプルな zip が生成されます。

f:id:shiba-yan:20181119163942p:plain

ここで生成した zip はこの後に作成する Release パイプラインから利用します。牛尾さんから Build と Release はちゃんと分けてパイプラインを作った方が良いと聞いていたので、ちゃんと分離しておきます。*1

ASP.NET Core の場合

先ほど少し書いたように、ASP.NET Core は Web Deploy に依存しない形でタスクが用意されているので、Azure Pipelines から簡単に必要なパッケージを作成することが出来ます。

基本的には ASP.NET Core 向けのテンプレートを利用するだけで良いです。

f:id:shiba-yan:20181119164048p:plain

.NET Framework で ASP.NET Core を書いてるケースは非常に少ないと思いますが、似たようなテンプレート名になっているので間違えないようにします。

作成されたパイプラインを見ると、publish で zip にするチェックが入っているはずなので確認します。

f:id:shiba-yan:20181119164130p:plain

このパイプラインを実行すると、同様にシンプルな zip が生成されるのでこれで終わりです。

Azure Functions v2 の場合

Azure Functions v2 は .NET Core を使って作られているので、ビルド自体は ASP.NET Core のテンプレートで作られるパイプラインとほぼ同じですが、Web プロジェクトではないので、設定を変更しておきます。

デフォルトでは Publish Web Projects にチェックが入っているので、それを外しておきます。

f:id:shiba-yan:20181119170424p:plain

これで ASP.NET Core の時と同様に zip が生成されますが、テンプレートではビルドするプロジェクトの指定が **/*.csproj になっているので、ビルドするリポジトリに複数のプロジェクトを含んでいる場合は、予めプロジェクト名を明示的に指定しておきます。

Run From Package としてデプロイ

ここまででビルドと zip の生成まで完了しているはずなので、後は Release 用のパイプラインを作成すれば終わりです。単純に App Service にデプロイする場合は 1 タスクだけで済むので非常に簡単です。

f:id:shiba-yan:20181119175146p:plain

Run From Package に対応しているのは 4.* のプレビュー版になるので、サクッと変更しておきます。

下の方にある Additional Deployment Options を開くと、デプロイ方法を指定出来るようになっているので、ドロップダウンから Run From Package を選べば完了です。

f:id:shiba-yan:20181119151829p:plain

ついでに Continuous deployment trigger の設定をしておくと、指定したブランチがビルドされた後に自動的に Release 用のパイプラインを実行出来ます。

他にも Pre-deployment conditions からスケジュールやユーザーの承認が必要などといった設定も出来ますが、今回の話とは関係ないので省略します。Release はいろいろと面白い使い方が出来そうです。*2

f:id:shiba-yan:20181119151820p:plain

実際にこれまで Kudu でビルドさせていたアプリを Azure Pipelines + Run From Package でのデプロイに変更してみました。ASP.NET Core なのでビルドパイプラインはデフォルトのままです。

https://appservice.info/

Azure Functions を使っている場合は Run From Package は必須で、それ以外の ASP.NET / ASP.NET Core でもデプロイでの一貫性を担保するためにも利用を検討した方が幸せになれると思います。

ロールバックもマウントしている zip を差し替えるだけなので Web Deploy よりも高速です。

*1:一部のテンプレートはデプロイまで行ってしまうものがある

*2:複数の App Service へのデプロイとか、事前にステージングにデプロイして承認後に本番デプロイとか

Azure Functions v1 から v2 への移行手順メモ

Azure Functions のランタイムは v1 がほぼ更新が無くなり、最近は v2 の更新ばかりなので、近いうちに v1 の実質 EOL のお知らせとかが出るのではないかなと思ってます。

v2 ランタイムは安定したものになっているので、早めに移行しておいた方が良いでしょう。移行手順はドキュメントが公開されてますが、実際に作業して確認しておくことにしました。

作業前に Azure Functions 向けの Visual Studio 拡張は最新版にアップデートしておいた方が良いです。

実際に行った手順は以下の通りです。基本的にはターゲットフレームワークの変更と NuGet パッケージを更新して、淡々とコンパイルエラーを取り除く作業といった感じです。

プロジェクトを .NET Core 2.1 向けに修正

Azure Functions v1 のプロジェクトはターゲットフレームワークが net461 になっているはずなので、そこを netcoreapp2.1 に変更します。そして同様に AzureFunctionsVersion も v2 に変更します。

Microsoft.NET.Sdk.Functions は現時点の最新となる 1.0.24 を指定すれば良いです。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
    <AzureFunctionsVersion>v2</AzureFunctionsVersion>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.24" />
  </ItemGroup>
  <ItemGroup>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </None>
  </ItemGroup>
</Project>

最低限必要なプロジェクトファイルはこのような形になるはずです。

最新のプロジェクトテンプレートを使って作成すると、SDK のバージョン指定は以下のようになっていることもあります。ビルド時点での最新が使われるので、こっちの方が楽かもしれません。

<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.*" />

勝手にアップデートされて問題が出る場合もあるので、この辺りは難しい部分です。個人的にはバージョンは明示的に指定する派です。

新しい Extension をインストール

Azure Functions v2 向けの Extensions は大体バージョンが 3.0.0 とかになっているので、そのバージョン以降で一番新しいものをインストールします。

時々違うバージョンもありますが、依存関係で WebJob SDK の 3.0.0 以上を要求しているものが正解です。

  • Cosmos DB (DocumentDB)
    • Microsoft.Azure.WebJobs.Extensions.CosmosDB
  • EventGrid
    • Microsoft.Azure.WebJobs.Extensions.EventGrid (2.0.0)
  • Event Hubs
    • Microsoft.Azure.WebJobs.Extensions.EventHubs
  • SendGrid
    • Microsoft.Azure.WebJobs.Extensions.SendGrid
  • Service Bus (Topic / Queue)
    • Microsoft.Azure.WebJobs.Extensions.ServiceBus
  • Storage (Blob / Table / Queue)
    • Microsoft.Azure.WebJobs.Extensions.Storage
  • Twilio
    • Microsoft.Azure.WebJobs.Extensions.Twilio

v1 から v2 への移行では特に Storage 周りの Trigger が別アセンブリになっているので、インストールが追加で必要となっています。DocumentDB は Cosmos DB に ID が変わっているのでそこも注意。

Durable Functions は最新版にアップデートしておけば良いので簡単です。

コードの修正

基本的にはコンパイルエラーになっている部分を直していけばよいです。プロパティが無くなっていたりしますが、大体は問題なく移行できるはずです。HttpTrigger 周りは変更が大きいので注意です。

TraceWriter を ILogger に変更

単純に TraceWriter を ILogger に置換すれば良いというわけでもなく、メソッドも変わっているので該当するログレベルのメソッドに置き換えていきます。

ログ周りに関しては三宅さんの記事も参考になります。

Verbose だけは該当するメソッドが無くなっているので、適当に Debug や Trace に置き換えると良いです。

HttpRequestMessage / HttpResponseMessage を修正

v1 までは ASP.NET Web API 的なインターフェースだったのが、v2 からは ASP.NET Core と同じインターフェースに変わっているので、HttpRequestMessageHttpResponseMessage はそれぞれ HttpRequestIActionResult を使うように変更していきます。

v2 では IActionResult を作るのが少し面倒です。多用する場合はヘルパーを用意しても良さそう。

// v1 の例
[FunctionName("Function1")]
public static HttpResponseMessage Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequestMessage req, TraceWriter log)
{
    log.Info("C# HTTP trigger function processed a request.");

    return req.CreateResponse(HttpStatusCode.OK, "Hello, world ");
}

// v2 の例
[FunctionName("Function1")]
public static IActionResult Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, ILogger log)
{
    log.LogInformation("C# HTTP trigger function processed a request.");

    return new OkObjectResult($"Hello, world");
}

v2 の方が Query や Post Data へのアクセスが行いやすいです。マルチパートデータの扱いも簡単なので、楽にアップロード周りのコードも書けるようになってます。

ConfigurationManager の削除

v2 というか .NET Core には ConfigurationManager が存在しないので、アプリ設定や接続文字列は環境変数から取るか、現時点では手動で IConfiguration を作成して取得します。

将来的には DI とかで取れるようになる気がします。最低でも標準でサポートされるはず。

config ファイルを修正

おまじない感ありますが local.settings.json に FUNCTIONS_WORKER_RUNTIME を追加しておきます。

{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "AzureWebJobsStorage": "",
    "AzureWebJobsDashboard": ""
  }
}

host.json には version を追加しておくとよい感じです。

空っぽでも良いみたいですが、テンプレートでは version が付いているので合わせておきましょう。

{
  "version": "2.0"
}

スキーマが大きく変わっているので、必要な設定を 2.0 向けに修正していきます。ドキュメントがあるので、それを確認しながら行えば問題ないはずです。

extensions 周りに互換性が無くなっているはずなので、何か書いている場合には注意。

新しいバージョンをリリース

既にデプロイまでの自動化を行っている場合、v2 へのコード側の対応をしたとしてもランタイムのバージョンが v1 のままなのでデプロイしても動作しません。

ちなみにポータルからはデプロイ済みの場合は変更できないようになっています。

f:id:shiba-yan:20181112210737p:plain

Visual Studio からデプロイする場合はバージョンを自動で確認して、必要であれば変更もしてくれるので楽ですが、この手はあまり使えないでしょう。

f:id:shiba-yan:20181112211236p:plain

割と致命的なのが既存のキーが引き継がれずに、新しく作成されることです。HttpTrigger で認証をキーで行っている場合、v2 に切り替えると動かなくなるので詰みます。新しく Function App を作成して、デプロイして切り替えを行うしかないでしょう。

コードの修正は正直大したことないので簡単でしたが、リリースに関しては少し面倒かなという印象です。既にプロダクション環境で動かしているものは移行計画を立てた方が良いでしょう。