🦦

Azure Functionsでappsettings.jsonを使う:インプロセスと分離ワーカープロセス(isolated)で構成をいじる

2024/02/15に公開

この記事でわかること

  • Azure Functoinsで任意の構成ファイル(appsettings.json)を読み込む方法
    • インプロセスと分離ワーカープロセス(isolated)での違い
  • Azure Functoinsの構成に複数の値を設定せずに済む方法
    • 値を一つだけ設定すれば任意の環境のアプリケーション設定を読みに行ってくれるようになる(シークレットの扱いは対象外)

構成周りに注目してみるとインプロセスと分離ワーカープロセスの違いも理解しやすくなります。

Azure App ServiceでASP.NET CoreのWebアプリケーションを動かす場合の構成についてはこちら記事にまとめていますが、先にこちらを理解していることを前提に進めていきます。

https://zenn.dev/yuma_prog/articles/aspdotnetcore-config

はじめに

Azure Functions開発時、いつもこのような流れで検証をしています。

  1. ローカルで動作確認
  2. Visual StudioからAzure環境に発行
  3. Azure Functionsの構成をポータルから更新

この3番目「ポータルから更新する」をやりたくない!複数の値を手動で設定するとか嫌だ!
ということで、一つの値をAzure Functionsの構成に追加するだけで環境ごとに構成情報を切り替える方法を検証しました。

ASP.NET Coreの場合と同じような書き心地にしたかったので、appsettings.jsonを使えるようにする方向で行きました。
appsettings.jsonというファイルを構成として読み込む方法をまとめていますが、ファイル名は何でもOKです。
インプロセスと分離ワーカープロセス(isolated)で構成の取得方法が異なるので、それぞれ検証した内容をまとめていきます。

インプロセスと分離ワーカープロセスについて気になる方は詳細はこちらのドキュメントを参照してください。古い方がインプロセス、新しい方が分離ワーカープロセスです。

https://learn.microsoft.com/ja-jp/azure/azure-functions/dotnet-isolated-process-guide

https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-dotnet-class-library

経緯(飛ばしてOK)

https://azure-waigaya.connpass.com/event/309575/

こちらのイベントで検証した内容をまとめました。
その他イベント内で知ったVisual Studioの便利機能については別に記事を書こうと思います。

つよつよナビゲーターに囲まれて検証すると、副次的に色々な知見を得られるので最高です。
Azureわいがや会は突発でイベントを立てて1週間以内に開催しがちなので、connpassでメンバーになっておいていただけるとイベントを見逃しにくくなるかもしれません(宣伝)。

検証の概要

HTTPトリガーのAzure Functionsで、構成から取得した値を返すアプリケーションをサンプルとして作成して検証します。

前提

  • Visual Studio 2022
  • インプロセスのFunctionsは.NET 6
    • 執筆時点で.NET 8のインプロセスはまだ使えなかったため
  • 分離ワーカープロセス(isolated)のFunctionsは.NET 8
  • Azure Functions(Windows)の消費プラン

Azure Functionsのプロジェクトテンプレートを使ってサンプルアプリを作成しています。

インプロセス検証用のプロジェクト設定

インプロセス検証用のプロジェクト設定

分離ワーカープロセスのプロジェクト設定

分離ワーカープロセスのプロジェクト設定

インプロセスでappsettings.jsonを使うための下ごしらえ

はじめに

インプロセスのAzure Functionsではappsettings.jsonをそのまま使うことができません /(^o^)\ナンテコッタイ

ASP.NET Coreであれば構成の一つとしてappsettings.jsonを読み込んでくれました。
しかしAzure Functionsでappsettings.jsonを使いたい場合は依存性の注入(DI)を使えるように自分でもろもろ設定する必要があります。
ちょっと手間ですが、この設定をしておけばappsettings.jsonの読み込み以外でもDIを使えますしASP.NET Coreに慣れている人は似たような書き心地になるので、最初に設定しておくと便利です。

概要

次のドキュメントにあるNugetパッケージを入れて、自分でStartup.csを追加することでDIを使えるようになります。
なお、この機能はAzure Functions 2.x 以降で利用可能です。

https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-dotnet-dependency-injection

このドキュメントを参照しつつ今回やった設定をまとめていきます。

1. NuGetパッケージをインストール

次の NuGet パッケージをインストールします。デフォルトで入っているものもありますが、バージョンが古くないか確認しておきます。

Microsoft.Azure.Functions.Extensions
Microsoft.NET.Sdk.Functions パッケージ バージョン 1.0.28 以降
Microsoft.Extensions.DependencyInjection (現在のところ、バージョン 2.x 以降のみサポートされています)

私の環境では下記2つをインストールしました。

  • Microsoft.Azure.Functions.Extensions.DependencyInjectionの検証時点の最新版(1.1.0)
  • Microsoft.Extensions.DependencyInjectionの検証時点の最新版(8.0.0)

2. Startup.csでappsettings.jsonを読み込む

プロジェクトにStartup.csファイルを追加し、次のように書きます。

using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;

[assembly: FunctionsStartup(typeof(InProcessFuncConfigSample.Startup))]
namespace InProcessFuncConfigSample;

	public class Startup : FunctionsStartup
	{
		public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
		{
			FunctionsHostBuilderContext context = builder.GetContext();

			builder.ConfigurationBuilder
				.SetBasePath(context.ApplicationRootPath)
				.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
				.AddJsonFile($"appsettings.{context.EnvironmentName}.json", optional: true, reloadOnChange: true)
				.AddEnvironmentVariables();
		}

		public override void Configure(IFunctionsHostBuilder builder)
		{
		}
	}

FunctionsStartupクラスを継承したStartupクラスを作成し、ConfigureAppConfigurationメソッドをオーバーライドすることでappsettings.jsonを読み込むようにFunctionsの構成をいじっています。
ASP.NET Coreの場合と同じようにappsettings.jsonの後にappsettings.{EnvironmentName}.jsonを読み込むようにしています。

AddJsonFileoptional: trueにしているのでファイルが見つからなくてもエラーになりません。

重要なのがSetBasePath(builder.GetContext().ApplicationRootPath)です。
これを指定しないとappsettings.jsonを見つけてくれませんでした。

ここではASP.NET Coreと同じようにappsettings.jsonという名前のファイルを使えるようにしていますが、好きなファイル名でConfigurationBuilderにAddできます。
最後にAddされたものが最強なので、好きな順に構成の優先度を設定できます。

3. Functionsクラスで構成を取得する

ここまでくればほぼASP.NET Coreと同じです。
Functionsクラスでコンストラクタを追加し、IConfigurationを受け取るようにします。

テンプレートからプロジェクトを作成した場合はFunctionsクラスはstaticになっているので、インスタンス化してDIを使えるようにstaticを外しておきます。
デフォルトだとRunメソッドもstaticになっているので、それも外しておきます。
using Microsoft.Extensions.Configuration;も追加が必要です。

HttpTrigger.cs
public class HttpTrigger
{
	private readonly IConfiguration _configuration;

	public HttpTrigger(IConfiguration configuration)
	{
		_configuration = configuration;
	}

	[FunctionName("HttpTrigger")]
	public async Task<IActionResult> Run(
		[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
		ILogger log)
	{
		// 省略
        return new OkObjectResult(_configuration["Test"]);		
	}
}

これで構成で設定したTestというキーの値をレスポンスとして返すことができます。

4. appsettings.jsonを追加

プロジェクトにappsettings.jsonを追加し、次のように設定します。

appsettings.json
{
  "Test": "Default"
}

appsettings.jsonファイルのプロパティで「出力ディレクトリにコピー」>「新しい場合はコピーする」に設定しておきます(「常にコピーする」でもOK)。

これを忘れるとappsettings.jsonがビルド後のディレクトリにコピーされないので値が反映されず「なんで!?」となります。(なりました)

ちゃんと公式ドキュメントにも書いてありました…。

https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-dotnet-dependency-injection#customizing-configuration-sources

5. ローカルで検証

ローカルで実行してみます。

検証:インプロセス&ローカルの場合

ASP.NET Coreの場合とほぼ変わりませんが、若干設定箇所が異なります。

appsettings.jsonの値を取得

上記下ごしらえが終わったらローカルで実行して、コンソールに表示されたURLをGETで叩いてみます。
ポート番号は各自の環境に合わせていただき、Postmanやcurlなどお好きな方法でGETリクエストを送信してみてください。

curl http://localhost:7777/api/Inprocess

今回はAzureわいがや会で教えていただいたエンドポイントエクスプローラーを使って試しました(わいがや会で教えていただいた他のVisual Studioの便利機能については別に記事を書こうと思います)。

準備したHTTPトリガーの関数を呼び出すと、Defaultという値が返ってきました。
しっかりとappsettings.jsonの値が取得できていることが確認できました。

appsettings.Development.jsonを追加してみる

プロジェクトにappsettings.Development.jsonを追加し、次のように書きます。

appsettings.Development.json
{
  "Test": "Development"
}

このファイルも忘れずにプロパティで「出力ディレクトリにコピー」を「新しい場合はコピーする」に設定しておきます。
この設定は忘れがちなのでプロジェクトファイルを次のように書いておくとappsettings.*.jsonというファイル名の場合は必ず「新しい場合はコピーする」になるので楽です。

InProcessFuncConfigSample.csproj
<Project Sdk="Microsoft.NET.Sdk">
(省略)
  <ItemGroup>
    <None Update="appsettings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="appsettings.*.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </None>
  </ItemGroup>
</Project>

これで再度Functionsをローカルで実行してみます。
すると何が返ってくるでしょうか。

Developmentという値が返ってきました。
appsettings.Development.jsonを追加する以外の設定はいじっていません。

下記ドキュメントに書いてありますが、Functionsをローカルで実行する際に使っている Azure Functions Core Tools では、ローカル コンピューターでの実行時に AZURE_FUNCTIONS_ENVIRONMENTDevelopment に設定します。

https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-app-settings#azure_functions_environment

その結果Startup.csの{context.EnvironmentName}の値が Developmentになり、appsettings.Development.jsonの値がappssettings.jsonの値を上書きしました。

では、このAZURE_FUNCTIONS_ENVIRONMENTをローカル実行時に任意の値に変えたい場合はどうすればいいでしょうか。

appsettings.{EnvironmentName}.jsonを追加してみる

{EnvironmentName}を任意の名前(今回はCustom)にしたい場合を考えます。

まずはappsettings.Custom.jsonを追加します。
プロジェクトファイルでappsettings.*.jsonの設定をしていない場合はここでも忘れずにプロパティで「出力ディレクトリにコピー」を「新しい場合はコピーする」に設定しておきます。

appsettings.Custom.json
{
  "Test": "Custom"
}

次に、launchSettings.jsonを編集し、AZURE_FUNCTIONS_ENVIRONMENTCustomに設定します。

launchSettings.json
{
  "profiles": {
    "InProcessFuncConfigSample": {
		(省略)
        "environmentVariables": {
            "AZURE_FUNCTIONS_ENVIRONMENT": "Custom"
        }
    }
  }
}

これで再度Functionsをローカルで実行してリクエストを送ってみるとどうなるでしょう。

期待通りCustomという値が返ってきました!
このようにlaunchSettings.jsonAZURE_FUNCTIONS_ENVIRONMENTを設定することで、Functionsでも好きな環境のappsettingsを読み込むことができるようになりました。

ただ、launchSettings.jsonはローカルでしか考慮されません。
Azure上で実行する場合はどうなるでしょうか。

検証:インプロセス&Azure上の場合

この辺りはASP.NET CoreをAzure App Serviceで動かす場合とほぼ同じです。

まずはサンプルアプリケーションをVisual StudioからAzure Functionsに発行します。
発行手順はこちらのドキュメントをご覧ください。

https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-create-your-first-function-visual-studio#publish-the-project-to-azure

appsettings.jsonの値を取得

発行した後、Azure FunctionsのポータルからGETリクエストを送信してみます。関数の「コードとテスト」からGETリクエストを送信できます。
何が返ってくるでしょうか。

Defaultという値が返ってきています。
appsettings.jsonの値が取得されていて、appsettings.Development.jsonやappsettings.Custom.jsonの値は無視されています。

補足:ポータルから関数を実行しようとするとCORSのエラーが出る場合

ポータルから関数を実行しようとするとCORSのエラーが出る場合

次のどちらかの方法で解決できます。

  1. Function key入りのURLを取得してcurlで実行する

ポータルからの関数呼び出しではなくなるのでCORSエラーを回避できます。

  1. CORSのAllowed Originsにhttps://portal.azure.comを追加


https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-how-to-use-azure-function-app-settings?tabs=portal#cors

Azure FunctionsのCORS設定でポータルのURLを許可することでポータルからの関数呼び出しが可能になります。

appsettings.Production.jsonを追加してみる

プロジェクトにappsettings.Production.jsonを追加し、次のように設定します。

appsettings.Production.json
{
  "Test": "Production"
}

この状態でAzure Functionsに再度発行し、ポータルからGETリクエストを送信してみるとどうなるでしょうか。

Productionという値が返っているため、appsettings.Production.jsonの値が取得されていることが確認できました。
こちらのドキュメントにもあるように、AZURE_FUNCTIONS_ENVIRONMENTが設定されていない場合Azure上での規定値Productionになるため、appsettings.Production.jsonの値が優先されています。

https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-app-settings#azure_functions_environment

appsettings.Custom.jsonの値を取得する

Azure Functionsの構成にAZURE_FUNCTIONS_ENVIRONMENTというキーで、値はCustomで設定します。
ポータルから設定してもOKですが、Azure CLIで設定する場合は次のようになります。

az functionapp config appsettings set --name <functionapp-name> --resource-group <resource-group-name> --settings AZURE_FUNCTIONS_ENVIRONMENT=Custom

Visual Studioの発行画面からも設定できますが、その方法は以前のASP.NET Coreの記事を参照してください。

https://zenn.dev/yuma_prog/articles/aspdotnetcore-config

これで再度ポータルからGETリクエストを送信してみるとどうなるでしょうか。

Customという値が返っているため、appsettings.Custom.jsonの値が取得されていることが確認できました。
もしProductionという値が返ってくる場合はAzure Functionsを再起動してみてください。

ということで、Azure Functionsの構成にAZURE_FUNCTIONS_ENVIRONMENTというキーを設定しておけば、appsettings.{EnvironmentName}.jsonの値を取得することができるようになり、一つの値を設定するだけで複数のアプリケーション設定を読み込ませることができることがわかりました!

構成の優先度のカスタマイズ

お察しかと思いますが、構成読み込みの優先度のカスタマイズもできます。この辺りもASP.NET Coreと同じです。
Startup.csのConfigureAppConfigurationメソッドで構成の優先度を変えることができます。

Startup.cs
builder.ConfigurationBuilder
	.SetBasePath(context.ApplicationRootPath)
	.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
	.AddJsonFile($"appsettings.{context.EnvironmentName}.json", optional: true, reloadOnChange: true)
	.AddEnvironmentVariables();

例えばこの状態でappsettings.Custom.jsonに書いている値をAzure Functionsの構成に書いた場合はどうなるでしょうか。
TestというキーをOverwriteという値で設定してみます。

az functionapp config appsettings set --name <functionapp-name> --resource-group <resource-group-name> --settings Test=Overwrite

この状態で関数を実行すると、Overwriteという値が返ってきます。
構成にはAZURE_FUNCTIONS_ENVIRONMENT=Customという値が設定されているのでappsettings.Custom.jsonが読み込まれてはいるのですが、appsettings.Custom.jsonに設定されているTestというキーが構成にも設定されていることによって上書きされてしまっています。
AddEnvironmentVariables()が最後に実行されているため、Azure Functionsの構成が最強です。

Azure環境における構成よりもappsettings.{EnvironmentName}.jsonの値を優先させたい場合は、Startup.csを次のようにいじります。

builder.ConfigurationBuilder
	.SetBasePath(context.ApplicationRootPath)
	.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
	.AddEnvironmentVariables()
	.AddJsonFile($"appsettings.{context.EnvironmentName}.json", optional: true, reloadOnChange: true);

この状態で再度発行するとどうなるでしょうか。

Customという値が返っているため、構成よりもappsettings.Custom.jsonの値が取得されていることが確認できました。
AddEnvironmentVariables()の後にappsettings.{context.EnvironmentName}.jsonを読み込むようにしているためです。

このようにして好きな順に構成の優先度を変えられますが、一般的ではない構成の読み込み順は混乱の原因になるので控えた方がいいと思います。
特にAzure Functionsの構成が最強にならない設定は後の人にキレられる気がします。

分離ワーカープロセス(Isolated)の場合

こちらはデフォルトでDIを使えるのでかなり楽です!
Nugetパッケージを入れてファイルを作って…といった工程は不要で、ほぼASP.NET Coreと同じ感覚です。

プロジェクトを作成

Visual Studio で、Azure Functions のプロジェクトテンプレートを使って.NET 8 isolatedでプロジェクトを作成します。

これで出来上がったProgram.csを見てみると、ASP.NET Coreで見たような感じになっています。

Program.cs
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var host = new HostBuilder()
	.ConfigureFunctionsWebApplication()
	.ConfigureServices(services =>
	{
		services.AddApplicationInsightsTelemetryWorkerService();
		services.ConfigureFunctionsApplicationInsights();
	})
	.Build();

host.Run();

この部分で構成の読み込み設定をいじれます。

appsettings.jsonを構成として読み込む

appsettings.jsonを使えるようにしてみます。
参考までに、分離ワーカープロセスでの構成のドキュメントはこちらです。

https://learn.microsoft.com/ja-JP/azure/azure-functions/dotnet-isolated-process-guide?tabs=windows#configuration

ConfigureAppConfiguration メソッドを HostBuilder で呼び出すことで構成をいじることができます。
using Microsoft.Extensions.Configuration;を追加して、Program.csを次のように書き換えます。

Program.cs
var host = new HostBuilder()
	.ConfigureFunctionsWorkerDefaults()
	.ConfigureAppConfiguration((context, builder) =>
	 {
		   string environmentName = context.HostingEnvironment.EnvironmentName;

		   builder
			  .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
			   .AddJsonFile($"appsettings.{environmentName}.json", optional: true,reloadOnChange: true)
			   .AddEnvironmentVariables()
			   .AddCommandLine(args);
	 })
	.ConfigureServices(services =>
	(省略)

このように書けばASP.NET Coreと同じような優先度で構成を読み込めます。(参考:ASP.NET Coreの構成の優先度)
ユーザーシークレットを使いたい場合もここで設定可能です。
インプロセスと異なり、アプリケーションパスを設定しなくてもappsettings.jsonを読み込んでくれていました。

ここまで書けば構成読み込みの挙動はインプロセスと同じなので検証は割愛します。
構成を関数で取得する方法も変わりません。
相違点は、デフォルトで作成されている関数がstaticクラスではなくなっているためstaticを外す作業がなくなることくらいです。

おわりに

https://techcommunity.microsoft.com/t5/apps-on-azure-blog/net-on-azure-functions-august-2023-roadmap-update/ba-p/3910098

上記公式ブログではこのように書かれています。

  • .NET 8 を Azure Functions でインプロセスモデルのサポートを受ける最後の LTS リリースにする予定
  • 2024 年初頭に.NET 8 のインプロセスモデルをリリース予定

ということで、インプロセスモデルはまだしばらく使えそうですが、ゆくゆくは分離ワーカープロセスを使うことになります。

構成のカスタマイズを考えると圧倒的に分離ワーカープロセスの方が楽で、ASP.NET Coreと考え方が同じ感じでとっつきやすかったです。
インプロセスでやってから分離ワーカープロセスの検証をしたので、「こっちの方が簡単じゃん!」と感動しました。

しかし分離ワーカープロセスはまだ品質について微妙な話を聞くので、もう少しインプロセスモデルを使って様子見するのがいいのかもしれないなと個人的に思っています。(もちろんサポートはされているので、使っていて何かあっても何とかはなるのでしょうが…)

個人的な検証の範囲では、DIを使うのが楽な分離ワーカープロセスを使っていこうと思いました。

Discussion