しばやん雑記

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

あえて 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 を作成して、デプロイして切り替えを行うしかないでしょう。

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

Azure App Service が .NET Framework 4.7.2 に対応

予定では 10 月中旬にデプロイされる予定だった App Service の .NET Framework 4.7.2 対応ですが、今日にようやくデプロイが行われたようです。

手元にある Japan East と Central US の Scale unit で確認していますが、全ての Scale unit にデプロイされたかは公式のアナウンスを待つ必要があります。*1

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

そろそろ appservice.info も複数リージョンにデプロイして、情報を集約出来るようにしたいです。

Visual Studio 2017 でもちょっと前に Installer から .NET 4.7.2 SDK がインストール出来るようになっているので、これで新しい機能を使う準備が整った形ですね。異常なほど時間かかりましたけど。

.NET Framework 4.7.2 での更新内容は以下のページを参照してください。

What's new in the .NET Framework | Microsoft Docs

大きな機能追加とかはないですが、バグ修正やパフォーマンス改善など細々としたものが多いです。信頼性の向上としては重要です。セキュリティ周りとコレクション、そして ASP.NET を見ておけば良いです。

今回のバージョンは暗号化周りで API が結構追加されています。個人的には Rfc2898DeriveBytes で HMAC-SHA256 が選べるようになったのは良い感あります。対応が遅すぎるという気もしないこともないですが、BCL レベルで入ったのは良いです。

大きく挙動が変わるものは無さそうなので、.NET 4.7.2 に再ターゲットしつつ検証すればよいかと思います。

おまけ : ASP.NET Web Forms の DI 対応

.NET Framework 4.7.2 での割と大きめの機能追加が ASP.NET Web Forms 向けの DI です。

こういうことを書くと今更 Web Forms かとか言われそうですが、Web Forms から ASP.NET Core MVC への移行を考えた場合には、適切に Web Forms のコードビハインドから処理が分離されていることが重要なので、まずは DI を使って処理を別のプロジェクトに分けるのは必要だと思ってます。

特に ASP.NET Core は全てが DI で解決されるので、そのスタイルに持って行った方がスムーズです。

4.7.2 がリリースされた当初はイマイチ情報が少なくて面倒でしたが、最近は Unity DI ベースの NuGet パッケージがリリースされていたので、これを使うとサクッと組み込むことが出来ます。

Unity DI は最近アクティブに開発されているので良い感じです。

実際に ASP.NET Web Forms のプロジェクトを作成して確認してみました。

ポイントとしては Application_Start で DI の設定を行うぐらいで、後はコンストラクタインジェクションでインスタンスが受け取れます。

public class Global : System.Web.HttpApplication
{
    protected void Application_Start(object sender, EventArgs e)
    {
        var container = this.AddUnity();

        container.RegisterType<TestService>();
    }
}

型の登録時に LifetimeManager の設定も出来ますが、ASP.NET の場合はどれを使うべきか多少迷いますね。昔は PerRequestLifetimeManager を作ってましたが、デフォルトでまだ入っていないんですね。*2

登録してしまえば後はコンストラクタインジェクションで受け取れます。Page や UserControl 以外にも大体の部分で使えるみたいですが、基本的にはこの二つで使えれば問題ないでしょう。

public partial class Default : System.Web.UI.Page
{
    public Default(TestService testService)
    {
        _testService = testService;
    }

    private readonly TestService _testService;

    protected void Page_Load(object sender, EventArgs e)
    {
        label.Text = _testService.SayHello("buchizo");
    }
}

流石に IHttpModule とかで使うのはアンチパターンだと思いますし、使いたいケースもほぼないでしょう。

デバッガーを使って実際にインジェクションされるか確認しておきました。

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

ちゃんと指定したクラスのインスタンスが渡されていることが確認出来ますね。

ちなみに HttpRuntime.WebObjectActivator を使って実装されてますが、必要な IServiceProvider の実装は少し面倒なので、用意されているパッケージのものを使うのが良いです。

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

最後に App Service にデプロイして確認しておきました。ちゃんと動いてますね。

これまで .NET Framework 4.7.2 が使える PaaS はほぼ無かったので、今回のデプロイでようやく新機能を使うことが出来るようになって嬉しいです。

*1:数日中にアナウンスがあるとは思いますけど

*2:代替があるのかも知れないけど未確認

Azure Functions v2 は .NET Standard から .NET Core へ

ちょっと前に Azure Functions v2 で netcoreapp2.1 が使えるようになったと書きましたが、気が付いたらテンプレートもアップデートされ、デフォルトが netcoreapp2.1 になっていました。

当時はデバッグに問題がありましたが、最新のツールへアップデートすることで解消されました。

これで Azure Functions v2 は晴れて .NET Core 2.1 対応と言うことが出来そうです。使う前に確認は必要なので、メモとして確認のポイントを残しておきます。

実際に使う前に Visual Studio にインストールされているツールが最新版か確認しておきます。10/17 にリリースされた 15.10.2046.0 以降なら、デバッグ時に謎エラーが発生することなく実行できます。

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

ツールのアップデート後は適当に新しく Azure Functions プロジェクトを作成するダイアログを表示します。このタイミングで新しいランタイムとテンプレートがある場合にはアップデートが行われます。

左下でぐるぐるが回っている場合には、アップデートが行われてるので少し待ちます。

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

アップデートの完了後、最新の情報に更新すると新しいテンプレートが使われるようになります。

適当に v2 を選んで Azure Functions プロジェクトを作成すると、最新の Azure Functions SDK と .NET Core 2.1 SDK が使われていることがソリューションエクスプローラーから確認できます。

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

netcoreapp2.1 なので当然ながら .NET Standard のライブラリはインストールして使えますし、Span<T> や Memory<T> などの新しいクラスも利用可能です。

プロジェクトの設定からデバッグ周りの設定を弄る必要もありません。

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

Azure Functions のランタイム側も少し更新されたのか、Http Functions が許可するメソッドが確認出来るようになっていました。多分、最近のアップデートで追加されたものだと思います。

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

netcoreapp2.1 にしたとしても、何の違和感もなく .NET Standard 2.0 の時と同じようにデバッグが出来ますし、互換性面で問題になる要素はないはずです。

元々 v2 へのアップデートが推奨されていることもあるので、同時に netcoreapp2.1 にしてしまうのはありだと思います。サイズも小さくなり、使える API も増えるのでアップデートするメリットは割と大きいです。

Azure Functions v2 で netcoreapp2.1 が使えるようになった

.NET Standard 2.0 で書く必要があった Azure Functions v2 ですが、最新版の SDK を使うと .NET Core 2.1 向けにビルド出来るようになりました。正確には netcoreapp2.1 をターゲットに設定出来ます。

先に SDK をリリースして、問題なければテンプレートも netcoreapp2.1 になりそうですね。

これでようやく Span<T> などの新しい API が使えるようになりそうです。流石に .NET Standard 2.0 はアプリのコードを書くには API が少なすぎました。

対応している SDK は 1.0.23 からです。このバージョンからは UseNETCoreGenerator の設定も不要になっているので、単純にターゲットを変更するだけでビルドが通るので楽です。

テンプレートから作ると netstandard2.0 になっているので、netcoreapp2.1 に変更します。

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

後はリビルドを実行するとパッケージの復元から走るので、ビルドに成功するはずです。手元の環境ではリビルドを実行しないと、古いファイルが残っていて失敗することが多かったです。

これで netcoreapp2.1 が使えるようになっていろいろ捗りそうですが、今はそのままの状態だと Visual Studio からデバッグ実行が出来ないので、デバッグ周りの設定を変更をして対処します。

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

デバッグ時の起動対象を実行可能ファイルに変更して、Azure Functions Tools を直接使うように設定します。

パスは現時点では %localappdata%\AzureFunctionsTools\Releases\2.8.1\cli\func.exe を指定すれば良いですが、バージョンが上がった時には追従させた方が良いでしょう。

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

設定が終われば、F5 でデバッグ実行出来るようになります。この不具合は今後修正されるはずです。

作成した Function は Azure にデプロイしても、何の問題もなく動作します。

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

netstandard2.0 から netcoreapp2.1 に変更することで、デプロイに必要となるファイル数が減るので zip 後のサイズもかなり減りました。パフォーマンス面でのメリットもありそうです。

正式にテンプレートレベルで netcoreapp2.1 に対応されたら、既存の移行が捗りそうな気配があります。

.NET Core における Activity と Application Insights による分散トレーシング

.NET Core 2.0 から追加された Activity というクラスがありますが、トレース用の ID を管理する重要な機能を持っています。特に Application Insights と組み合わせることで分散トレーシングが容易になります。

.NET Framework 4.5 以降なら System.Diagnostics.DiagnosticSource を NuGet からインストールすれば使えます。.NET 4.6 以降の場合は AsyncLocal<T> で実装されてます。

Activity Class (System.Diagnostics) | Microsoft Docs

新しく ASP.NET Core 2.1 のプロジェクトを作成すると、エラーページにユニークな ID を表示するコードとして、既に Activity を使うようになっています。

Activity.Current は null の可能性があるので HttpContext.TraceIdentifier も参照しています。

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
    return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}

とはいえ、基本的には Activity が自動的に開始されているので、変なことをしない限りは値は取れます。

実装としては全く別物なので、それぞれの ID フォーマットは異なります。

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

これだけだと利用価値がいまいちですが、HttpClient を使うと自動的に ID を HTTP ヘッダーに付与してくれるので、受け取り側が ASP.NET Core の場合は ID が継承されて Activity が開始されます。

実際に以下のようなコードで HTTP リクエストを投げてみます。httpbin.org を使うとリクエストのデータを JSON で返してくれるので簡単に確認出来て便利です。

class Program
{
    private static readonly HttpClient _httpClient = new HttpClient();

    static async Task Main(string[] args)
    {
        var activity = new Activity("Main").Start();
        
        activity.AddBaggage("name", "kazuakix");

        var response = await _httpClient.GetStringAsync("https://httpbin.org/get");

        activity.Stop();
    }
}

デフォルトだと ID だけ付与されますが AddBaggage を使うと追加のデータとして付与されます。

実行した結果は以下の通りです。Request-IdCorrelation-Context というヘッダーが何もしなくても付与されていることが確認出来ます。

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

このヘッダーに関しては CoreFx のリポジトリにてドキュメントが公開されています。標準化に向けた作業中という話ですが、ちょっと進捗は怪しいですね。

Request-Id の値は親子関係を持っているので、受け取った側が更に別のサービスに処理を投げたとしても、ちゃんとその呼び出し関係は保持されるので、単純な ID よりも情報量が多いです。

Application Insights で分散トレーシング

Activity によって分散トレーシングに必要な値が準備されるので、後は Application Insights に送信してあげるだけです。既に SDK レベルで対応されているので、アプリケーションに Application Insights を追加すればすぐに使える状態になっています。

ドキュメントも用意されていますが、大体は Id の仕様についての話です。

Application Insights 的にはユニークな ID が送信されたら、それを関連付けて扱うだけなので、実際のプロトコルには依存していません。なので別にどんなプロトコルでも使えるはずです。

挙動を確認するために ASP.NET Core なアプリケーションを作って、HttpClient を使って別のアクションを呼び出してみました。E2E Transaction を確認すると分かりやすいです。

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

特に専用のコードを書いているわけではないですが、透過的に HttpClient に Request-Id が引き継がれているので、一連の処理として Application Insights では扱われています。

これが別の App Service になると、Application Map での見栄えが良くなります。

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

ちゃんと App Service 間のリクエストが可視化されて表示されています。1 つや 2 つぐらいだと大したことはないですが、関係のあるアプリケーションが増えていくと非常に有用だと思われます。

もちろん AIQL を使って Request-Id 単位でのクエリ実行も可能です。

ASP.NET での対応

ASP.NET Core はデフォルトで Request-Id の値を見るようになっていますが、ASP.NET 向けにも以下の Http Module をインストールすると、同じように処理されるようになります。

Activity の実装には AsyncLocal<T> が使われているので、実行コンテキストが変わった時に意図しない挙動になる可能性はありますが、ASP.NET / ASP.NET Core の両方ともリクエストスコープになるストレージも併用されているので、基本的には問題とならないはずです。

Azure Functions での対応

最近リリースされた Azure Functions v2 でも同じような仕組みを使ったトレースの仕組みが追加されています。基本的には Request-Id を引き継いでいく仕組みですが、いろんな Trigger で使えるようです。

ソースを確認した感じでは Service Bug Trigger の場合は UserProperties を使って、Request-Id を引き継いでいくみたいです。Storage の Trigger も一部は対応しているようでした。

念のために HttpTrigger でも試してみましたが、ちゃんと Request-Id を引き継いでくれませんでした。最新のランタイムでもまだ未対応のようです。

なお Request-Id を引き継ぐ必要があるので、メタデータが無い Storage Queue は難しそうです。

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

ちなみに Application Map での表示は Cloud Role Name でまとめられるので、Telemetry Initializer を使って上書きすることが出来ます。実際に Kubernetes 向け拡張ではコンテナ名で上書きされています。

大注目されている Durable Functions の場合は、Queue の値が隠蔽されているので比較的簡単に対応は行えそうですが、今のところは特に動きは無さそうです。Azure Functions の Consumption Plan の場合はスケーリングの状態を可視化出来るので、結構面白そうです。

Azure Functions の各 Trigger の動作が不安定な時にやること

最近、新しくデプロイした Azure Functions の TimerTrigger が何故か発火してくれない現象に悩んでいました。実際に Monitor で実行ログを確認すると、時間がめちゃくちゃになっています。

Consumption の場合は App Service Plan とは違う仕組みで動いているので、安定するのに時間がかかるのかとかタイムゾーンの設定を疑ったりしていましたが、結論から言うと違いました。

今は TimerTrigger を Let's Encrypt の証明書更新で使っているので、上手く動かない場合は割と致命的です。

いい加減になんとかせねばと真面目に調べたら、以下の Issue が引っ掛かりました。

どうやら Run From Package を使って外部 URL から zip をデプロイした場合に、正しく各 Trigger のメタデータが Scale Controller に同期されない問題があるらしいです。

コメントに書かれていた Workaround は以下になります。

  • 明示的に Azure Portal から Restart を行う
  • syncfunctiontriggers を実行する

Web Deploy を使っても解決するみたいですが、今更 Web Deploy には戻れません。修正が行われるまではどちらかの方法を実行する必要がありそうです。

手っ取り早いのは Restart ですが syncfunctiontriggers の方は Azure Functions のアーキテクチャ部分を垣間見ることが出来るので、調べておいて損はない感じです。

最近は API ドキュメントからすぐに試すことが出来るので便利です。

ちなみに Azure Resource Explorer から実行することも出来ます。実行には多少時間がかかります。

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

そして Cloud Shell などから Azure CLI を使っても実行することが出来ます。ARM Action として提供されているので、割と簡単に叩くことが出来て便利ですね。

az resource invoke-action --resource-group <ResourceGroupName> --action syncfunctiontriggers --name <SiteName> --resource-type Microsoft.Web/sites

そもそも何故メタデータの同期が必要かというと、Queue や Timer の場合は実行しないといけないタイミングになっても、Function 本体がそもそも立ち上がっていないことがあるからです。

Consumption では必要な時に必要な数立ち上げることになるので、インスタンスを管理する Scale Controller はそういった Trigger の情報を知っておく必要があるというわけです。

今回、私の場合は TimerTrigger で発生しましたが、原理上は HttpTrigger 以外でも発生するので Run From Package を使って外部 URL の zip デプロイする際には気を付けましょう。

後から CRON 式を変更したり、新しく Function を追加した場合にもメタデータが同期されないことがあるみたいなので、地味にはまります。早く直ってくれると良いのですが、少し時間がかかりそうです。

ちなみに syncfunctiontriggers を実行後は TimerTrigger が正しく動くことを確認しました。