ASP.NET CoreとAzure App Serviceの構成を理解する〜検証時に一番楽な構成切り替え方法を追い求める〜
この記事でわかること
- ASP.NET Coreの構成の読み込み順とカスタマイズ方法
- Azure Web Appsの構成に複数のアプリケーション設定を追加せずに済む方法
- 値を一つだけ設定すれば任意の環境のアプリケーション設定を読みに行ってくれるようになる(シークレットの扱いは対象外)
- Azure Web Appsの構成の楽な更新方法
はじめに
Azure App Serviceで動かすWebアプリケーションは、いつもこのような流れで検証をしています。
- ローカルで動作確認
- Azure Web AppsにVisual Studioから発行
- Azure Web Appsの構成をポータルから更新(ローカルと異なるアプリケーション設定が必要な場合)
この3番目「ポータルで手動で値を更新する」をやりたくない!複数の値を手動で設定するとか嫌だ!
ということで、できる限り手軽にアプリケーション設定を環境ごとに切り替える方法を検証しました。
結論としてはappsettings.{ASPNETCORE_ENVIRONMENT}.json
です。
appsettings.{ASPNETCORE_ENVIRONMENT}.json
に複数のアプリケーション設定をまとめておいて、一つの値(ASPNETCORE_ENVIRONMENT
)をWebAppの構成に追加するのです。
そうすることで環境ごとに一気にアプリケーション設定を切り替えることができます。
※シークレットは含めない場合かつ検証段階での個人的最適解です。
この記事では構成の読み込み方などを含め検証した内容をまとめます。
背景(飛ばしてもOK)
こちらのイベントで検証した内容をまとめました。
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
を下記のように設定します。
{
"FunctionsURL": "http://localhost:7777/api"
}
この状態で今回サンプルで作ったアプリケーションを実行すると、appsettings.jsonを読み込んでくれているのがわかります。
appsettings.json
を読み込んでいるか
どのようにASP.NET Core WebアプリのプロジェクトテンプレートではデフォルトでProgram.csにこのような記述があります。
var builder = WebApplication.CreateBuilder(args);
ここでappsettings.json
も含んだ構成を読み込んでくれています。
構成として読み込まれるものは色々ありますが、WebApplication.CreateBuilder(args)
を呼び出したときにどんな順序で構成を読み込んでいるかはこちらのドキュメントに詳しく書かれています。
優先度:高↑
- コマンドライン構成プロバイダーを使用するコマンドライン引数。
- 接頭辞なしの環境変数構成プロバイダーを使用した接頭辞なしの環境変数。
- Development 環境でアプリが実行される場合に使用されるユーザー シークレット。
- JSON 構成プロバイダーを使用する appsettings.{Environment}.json。 たとえば、appsettings.Production.json とappsettings.Development.json です。
- JSON 構成プロバイダーを使用する appsettings.json。
- 次のセクションで説明するホスト構成へのフォールバック。
優先度:低↓
コマンド実行時に引数として渡された値が一番優先されます。
appsettings.json
は上から5番目の優先度です。
読み込み順はこのあたりのコードで定義されているようです。
この箇所のコードはこちらでわかりやすく解説されていますので、興味がある方は見てみてください。
余談ですが3番目に優先度が高いユーザーシークレットについてはくさばさん(@tomo_kusaba)の記事が分かりやすいです。
appsettings.json
の値を表示しているか
どのようにWebApplication.CreateBuilder(args)
で読み込まれた構成は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
をつけています。
{
"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
の値を参照しています。
{
"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を別の値にしてみる
試しに次のように書き換えるとどうなるでしょう。
{
"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への発行方法は下記ドキュメントを参考にしてください。
発行完了時点でポータルからWebAppの構成を確認してみると、appsettings.json
で設定したFunctionsURL
も内容もlaunchSettings.json
のASPNETCORE_ENVIRONMENT
も反映されていないはずです。
WEBSITE_NODE_DEFAULT_VERSION
のみが設定されている状態でした。
この状態でWebAppを実行するとappsettings.json
で設定した値が表示されます。
これは、ASPNETCORE_ENVIRONMENT
が指定されていないためです。
appsettings.Production.jsonを追加してみる
appsettings.Production.json
を下記の値で追加します。WebAppで読みたい内容なのでlocalhostではなくそれっぽいURLにしています。
{
"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としました。
{
"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つの方法だけ紹介します。
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の構成が更新されます。
構成の優先度を変更する
デフォルトの優先度はすでに書きましたが、このような順です。
優先度:高↑
- コマンドライン構成プロバイダーを使用するコマンドライン引数。
- 接頭辞なしの環境変数構成プロバイダーを使用した接頭辞なしの環境変数。
- Development 環境でアプリが実行される場合に使用されるユーザー シークレット。
- JSON 構成プロバイダーを使用する appsettings.{Environment}.json。 たとえば、appsettings.Production.json とappsettings.Development.json です。
- JSON 構成プロバイダーを使用する appsettings.json。
- 次のセクションで説明するホスト構成へのフォールバック。
優先度:低↓
WebAppでは構成で設定する値が最強で、appsettings.{Environment}.json内の値は構成より弱いです。
appssettings.{Environment}.jsonの値を構成で上書きする
まずは検証してみましょう。
appsettings.Custom.jsonにFunctionsURLを設定します。
{
"FunctionsURL": "https://xxx.azurewebsites.net/api/Custom"
}
WebAppの構成にASPNETCORE_ENVIRONMENT
とFunctionsURL
を設定します。
この状態でデプロイされたWebAppを実行すると、構成の内容で表示されています。
ASPNETCORE_ENVIRONMENT
が設定されているので、まずappsettings.Custom.jsonが読み込まれていますが、appsettings.Custom.jsonで設定したFunctionsURL
が構成にも設定されているため、構成側の内容で上書きされています。
これが基本の挙動です。
appssettings.{Environment}.jsonを最強にする
appsettings.{Environment}.jsonを最強にするには、構成の優先度を変更する必要があります。
Program.csでその定義を書くことができます。
序盤でご紹介したvar builder = WebApplication.CreateBuilder(args);
の下に次のコードを追加します。
builder.Configuration
.AddEnvironmentVariables()
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true);
AddEnvironmentVariables()
で構成を読み込んでいます。
そのあとにAddJsonFile
でappsettings.{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