🦦

ASP.NET CoreとAzure App Serviceの構成を理解する〜検証時に一番楽な構成切り替え方法を追い求める〜

2024/02/13に公開

この記事でわかること

  • ASP.NET Coreの構成の読み込み順とカスタマイズ方法
  • Azure Web Appsの構成に複数のアプリケーション設定を追加せずに済む方法
    • 値を一つだけ設定すれば任意の環境のアプリケーション設定を読みに行ってくれるようになる(シークレットの扱いは対象外)
  • Azure Web Appsの構成の楽な更新方法

はじめに

Azure App Serviceで動かすWebアプリケーションは、いつもこのような流れで検証をしています。

  1. ローカルで動作確認
  2. Azure Web AppsにVisual Studioから発行
  3. Azure Web Appsの構成をポータルから更新(ローカルと異なるアプリケーション設定が必要な場合)

この3番目「ポータルで手動で値を更新する」をやりたくない!複数の値を手動で設定するとか嫌だ!
ということで、できる限り手軽にアプリケーション設定を環境ごとに切り替える方法を検証しました。

結論としてはappsettings.{ASPNETCORE_ENVIRONMENT}.jsonです。
appsettings.{ASPNETCORE_ENVIRONMENT}.jsonに複数のアプリケーション設定をまとめておいて、一つの値(ASPNETCORE_ENVIRONMENT)をWebAppの構成に追加するのです。
そうすることで環境ごとに一気にアプリケーション設定を切り替えることができます。
※シークレットは含めない場合かつ検証段階での個人的最適解です。

この記事では構成の読み込み方などを含め検証した内容をまとめます。

背景(飛ばしてもOK)

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

こちらのイベントで検証した内容をまとめました。
Azure Functonsでの構成の扱いと、その他イベント内で教えていただいたVisual Studioの便利機能については別に記事を書こうと思います。

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

検証内容

構成から取得したAzure FunctionsのURLを表示するASP.NET CoreのWebアプリケーションを使って検証します。

前提

  • Visual Studio 2022
  • ASP.NET Core 8.0
  • WindowsのAzure Web Apps

ASP.NET Core Webアプリのプロジェクトテンプレートを元にしてサンプルアプリを作成しています。

appsettngs.jsonから値を取得する

例として、ASP.NET CoreのWebアプリケーションからAzure Functionsの関数を呼び出す構成を想定します。
Azure FunctionsのURLはアプリケーション設定に保存しておきたいです。
これを実現するためにappsettings.jsonを下記のように設定します。

appsettings.json
{
  "FunctionsURL": "http://localhost:7777/api"
}

この状態で今回サンプルで作ったアプリケーションを実行すると、appsettings.jsonを読み込んでくれているのがわかります。

どのようにappsettings.jsonを読み込んでいるか

ASP.NET Core WebアプリのプロジェクトテンプレートではデフォルトでProgram.csにこのような記述があります。

Program.cs
var builder = WebApplication.CreateBuilder(args);

ここでappsettings.jsonも含んだ構成を読み込んでくれています。
構成として読み込まれるものは色々ありますが、WebApplication.CreateBuilder(args)を呼び出したときにどんな順序で構成を読み込んでいるかはこちらのドキュメントに詳しく書かれています。

https://learn.microsoft.com/ja-jp/aspnet/core/fundamentals/configuration/?view=aspnetcore-8.0#default-application-configuration-sources

優先度:高↑

  1. コマンドライン構成プロバイダーを使用するコマンドライン引数。
  2. 接頭辞なしの環境変数構成プロバイダーを使用した接頭辞なしの環境変数。
  3. Development 環境でアプリが実行される場合に使用されるユーザー シークレット。
  4. JSON 構成プロバイダーを使用する appsettings.{Environment}.json。 たとえば、appsettings.Production.json とappsettings.Development.json です。
  5. JSON 構成プロバイダーを使用する appsettings.json。
  6. 次のセクションで説明するホスト構成へのフォールバック。

優先度:低↓

コマンド実行時に引数として渡された値が一番優先されます。
appsettings.jsonは上から5番目の優先度です。

読み込み順はこのあたりのコードで定義されているようです。

https://github.com/aspnet/MetaPackages/blob/rel/2.0.0/src/Microsoft.AspNetCore/WebHost.cs#L157-L179

この箇所のコードはこちらでわかりやすく解説されていますので、興味がある方は見てみてください。

https://devblogs.microsoft.com/premier-developer/order-of-precedence-when-configuring-asp-net-core/

余談ですが3番目に優先度が高いユーザーシークレットについてはくさばさん(@tomo_kusaba)の記事が分かりやすいです。

https://techblog.sakurug.co.jp/article/aspdotnetcore-usersecrets/

どのようにappsettings.jsonの値を表示しているか

WebApplication.CreateBuilder(args)で読み込まれた構成はIndex.cshtml.csでこのように取得しました。
構成をどう取得するかはもろもろ方法があるのでお好きなやり方で大丈夫です。

Index.cshtml.cs
public class IndexModel : PageModel
{
    private readonly IConfiguration _config;
    internal readonly string FunctionsURL;

    public IndexModel(IConfiguration configuration)
    {
        _config = configuration;
        FunctionsURL = _config["FunctionsURL"] ?? string.Empty;
    }
(以下略)

ここで取得したFunctionsURLをIndex.cshtml@Model.FunctionsURLの形で取得して表示しています。

appsettings.{Environment}.jsonから値を取得する

構成の優先度で4番目、appsettings.jsonよりも優先されるappsettings.{Environment}.jsonを使ってみます。

appsettings.jsonと同じ階層にappsettings.Development.jsonを作成して、FunctionsURLの値を設定します。
わかりやすく末尾にDevelopmentをつけています。

appsettings.Development.json
{
  "FunctionsURL": "http://localhost:7777/api/Development"
}

これでアプリケーションを実行するとどうなるでしょう。appsettings.Development.jsonを追加した以外、コードに変更はありません。

appsettings.Development.jsonの値が優先されていますね。
appsettings.Development.jsonを読み込むよう指定したわけではないのになぜちゃんと読み込まれているのでしょうか。

appsettings.{Environment}.json{Environment}はどこから? ~ローカル編~

appsettings.{Environment}.json{Environment}は環境変数ASPNETCORE_ENVIRONMENTの値で指定できます。

ローカルで実行する場合はlaunchSettings.jsonで設定できます。
IIS Expressでデバッグ実行している場合は次のような階層にあるASPNETCORE_ENVIRONMENTの値を参照しています。

launchSettings.json
{
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
(以下略)

ASP.NET Core WebアプリのプロジェクトテンプレートではデフォルトでASPNETCORE_ENVIRONMENTの値がDevelopmentで設定されています。
なのでappsettings.Development.jsonの値が読み込まれていました。

UIでデバッグ起動プロファイルを見てみる(おまけ)

Visual Studioではデバッグ時に読み取られるこれらの内容をUIで見ることもできます。
任意のプロジェクトを右クリック>プロパティ>デバッグ>デバッグ起動プロファイルUIを開く

このUIから変更を行うとlaunchSettings.jsonにも反映されます。

ASPNETCORE_ENVIRONMENTを別の値にしてみる

試しに次のように書き換えるとどうなるでしょう。

launchSettings.json
{
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Test"
(以下略)

appsettings.jsonの値が表示されています。
この場合はappsettings.Test.jsonがあればそれが優先されますが、今回は作成していないのでappsettings.jsonから値を取得しています。
もちろんappsettings.Development.jsonは無視されています。

WebAppへのデプロイ後にappsettings.{Environment}.jsonから値を取得したい

WebAppへのデプロイ後、特にASPNETCORE_ENVIRONMENTの指定がなければappsettings.jsonから値を読み込まれます。
appsettings.{Environment}.jsonを使いたい場合はどうすればいいでしょうか。

Visual Studioでデプロイ

ひとまずWebAppにデプロイします。
Visual StudioからWebAppへの発行方法は下記ドキュメントを参考にしてください。

https://learn.microsoft.com/ja-jp/azure/app-service/quickstart-dotnetcore?tabs=net70&pivots=development-environment-vs&view=vs-2022#publish-your-web-app

発行完了時点でポータルからWebAppの構成を確認してみると、appsettings.jsonで設定したFunctionsURLも内容もlaunchSettings.jsonASPNETCORE_ENVIRONMENTも反映されていないはずです。
WEBSITE_NODE_DEFAULT_VERSIONのみが設定されている状態でした。

この状態でWebAppを実行するとappsettings.jsonで設定した値が表示されます。
これは、ASPNETCORE_ENVIRONMENTが指定されていないためです。

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

appsettings.Production.jsonを下記の値で追加します。WebAppで読みたい内容なのでlocalhostではなくそれっぽいURLにしています。

appsettings.Production.json
{
    "FunctionsURL": "https://xxxx.azurewebsites.net/api/Production"
}

この状態で発行してWebAppを実行してみましょう。

appsettings.Production.jsonの値が表示されていますね。
ASPNETCORE_ENVIRONMENTが指定されていない場合、appsettings.jsonの後にappsettings.Production.jsonを読みにいくためです。
これはローカルでも同じです。

WebAppで指定した環境のappsettings.{Environment}.jsonを使いたい

WebAppでASPNETCORE_ENVIRONMENTを指定してappsettings.{Environment}.jsonを使いたい場合はどうすればいいでしょうか。

まずプロジェクトにappsettings.Custom.jsonを追加します。
{Environment}の部分は後から自由に指定できるためなんでも大丈夫です。今回はCustomとしました。

appsettings.Custom.json
{
    "FunctionsURL": "https://xxx.azurewebsites.net/api/Custom"
}

わかりやすく末尾にCustomとつけ、この状態で発行します。
この状態ではWebAppはまだappsettings.Production.jsonの値を読みに行っています。

appsettings.Custom.jsonを優先させるには、WebAppの構成にASPNETCORE_ENVIRONMENTを追加する必要があります。

ポータルからぽちぽちやる、Visual Studio を使う、Azure CLIを使うなどで設定できます。
構成設定方法はこちらのドキュメントでご確認ください。
Azure CLIかVisual Studioを使って設定するのが楽かなと思っているので、この記事ではその2つの方法だけ紹介します。

https://learn.microsoft.com/ja-jp/azure/app-service/configure-common?tabs=portal

Azure CLIで設定する場合

このコマンドでWebAppの構成を設定できます。
Azure CLIのインストールや使い方など、詳細は上記ドキュメントでご確認ください。

az webapp config appsettings set --name <WebApp名> --resource-group <リソースグループ名> --settings ASPNETCORE_ENVIRONMENT=Custom

Visual Studioで設定する場合

発行の設定画面で「Azure App Serviceの設定を管理する」

設定の追加

設定名と値を入力してOK

すると発行ボタンを押さなくてもWebAppの構成が更新されます。

構成の優先度を変更する

デフォルトの優先度はすでに書きましたが、このような順です。

優先度:高↑

  1. コマンドライン構成プロバイダーを使用するコマンドライン引数。
  2. 接頭辞なしの環境変数構成プロバイダーを使用した接頭辞なしの環境変数。
  3. Development 環境でアプリが実行される場合に使用されるユーザー シークレット。
  4. JSON 構成プロバイダーを使用する appsettings.{Environment}.json。 たとえば、appsettings.Production.json とappsettings.Development.json です。
  5. JSON 構成プロバイダーを使用する appsettings.json。
  6. 次のセクションで説明するホスト構成へのフォールバック。

優先度:低↓

WebAppでは構成で設定する値が最強で、appsettings.{Environment}.json内の値は構成より弱いです。

appssettings.{Environment}.jsonの値を構成で上書きする

まずは検証してみましょう。

appsettings.Custom.jsonにFunctionsURLを設定します。

appsettings.Custom.json
{
    "FunctionsURL": "https://xxx.azurewebsites.net/api/Custom"
}

WebAppの構成にASPNETCORE_ENVIRONMENTFunctionsURLを設定します。

この状態でデプロイされたWebAppを実行すると、構成の内容で表示されています。

ASPNETCORE_ENVIRONMENTが設定されているので、まずappsettings.Custom.jsonが読み込まれていますが、appsettings.Custom.jsonで設定したFunctionsURLが構成にも設定されているため、構成側の内容で上書きされています。

これが基本の挙動です。

appssettings.{Environment}.jsonを最強にする

appsettings.{Environment}.jsonを最強にするには、構成の優先度を変更する必要があります。
Program.csでその定義を書くことができます。
序盤でご紹介したvar builder = WebApplication.CreateBuilder(args);の下に次のコードを追加します。

Program.cs
builder.Configuration
	.AddEnvironmentVariables()
	.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true);

AddEnvironmentVariables()で構成を読み込んでいます。
そのあとにAddJsonFileappsettings.{Environment}.jsonを読み込むように書いたため、これをデプロイすると先ほど構成に設定したFunctionsURLではなく、appsettings.Custom.jsonのFunctionsURLの値が優先されます。
Addの順番が考慮されてくれるので、とにかく最強にしたい構成要素を最後にAddしてあげれば望みの結果になります。

AddEnvironmentVariables()は書かなくても同じ挙動になりますが、どの順で読み込まれているかを明示したかったので書いています。

もし構成よりもappsettings.{Environment}.jsonを優先させたい場合はこのように明示してあげるとわかりやすいかもしれません。が、デフォルトの構成の読み込み順をいじると混乱の原因になるのでできる限り避けた方がいいです。
構成にちゃんと設定を書いたのに反映されない!ってなっちゃう。

おわりに

今回はシークレットを含まないアプリケーション設定について扱いました。
もしシークレットを含む場合はAzure Key Vaultを使うのが良いかと思います。

また、今回のものはあくまで検証段階でのやり方です。

運用環境では、Azure上の別リソースを参照するような値であれば個人的にはTerraformを使いたいです。
例えばAzure FunctionsのURLは今回appsettings.{Environment}.jsonにべた書きしていますが、参照するFunctionsが変わる可能性もあります。
この場合はappsettings.{Environment}.jsonを使っているとアプリケーション自体をデプロイしなおすことになります。

一方Terraformであれば新しいFunctionsをリソースとして参照してべた書きせずにURLを取得できる上に、WebAppの構成を更新するだけで済みます。

しかしどちらの場合も若干のダウンタイムは発生します。
ダウンタイムを気にする場合はスロットを利用して…とかやっているとどちらでも結局対応工数が変わらない感じになるので好みの問題かもしれません。
スロットを利用するのであれば、アプリケーションが最新の状態になっていないといけないので結局再デプロイする場合が多いでしょうし、むしろappsettings.{Environment}.jsonにべた書きした方がTerraformで構成まで更新する必要がなくなって楽かも?
私はAzureのリソースとして取得できるFunctionsのURL(ホスト名)がべた書きで管理されているのが嫌なのでTerraformを使いたいかな…。

ひとまず検証段階で簡単に構成を切り替える術と、構成の読み込み順序についてはすっきりしました!
Azure Functionsの場合はまた異なるので別記事にします。良きPaaSライフを!

Discussion