しばやん雑記

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

ASP.NET Core と Azure でアプリケーション設定が破綻しないように管理したい

最近は App Configuration と Key Vault を使っていい感じにアプリケーションの設定を扱う方法をいろいろと考えていましたが、App Configuration を使う必要が本当にあるのかと思い始めたので書き出して整理します。

今回実現したい内容は以下の通りになります。Infrastructure as a Code のアプリケーション設定版という感じです。Configuration as a Code とか言うのかも知れません(未確認)

  • 設定値のバージョン管理は必須
  • 設定の反映抜けや漏れを無くす仕組み
    • Azure Portal からの設定はさせたくない(絶対事故る)
  • 複数のアプリケーションで設定を共有したい

App Service には WEBSITE_* という Prefix から始まる設定値がいくつか存在していますが、これはインフラ側の設定になるので ARM Template や Terraform で管理します。

今回は ASP.NET Core でいうところの appsettings.json 部分をいい感じに管理する方法を考えます。

CI + App Settings を使う

Azure Pipelines を使うと App Service にある App Settings をデプロイと同じタイミングで設定が出来ます。

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

一応 YAML ベースでアプリケーション設定を管理出来ますが、App Service の App Settings は ASP.NET Core / Azure Functions ではインフラ側設定と接続文字列の管理にのみ使う方が良さそうです。

特に Azure Functions の Trigger が使う接続文字列は Connection Strings じゃなくて App Settings が必須なので Key Vault に格納しにくいです、ARM Template や Terraform で管理するのが鉄板な気がします。

接続文字列系は Key Vault Reference を使うことも出来そうですが、割と使い勝手悪いです。なので ARM Template や Terraform などで設定するようにする方が良いという結論です。

appsettings.json を使う

ASP.NET Core のプロジェクトを作成すると、デフォルトで appsettings.jsonappsettings.Development.json の 2 つが付いてくるので、実行中の再読み込みが必要ない場合は素直にこれを利用するのが手っ取り早いです。

テンプレートでは Development 向けのファイルのみ作られますが、App Service の App Settings を使わない場合は Production 向けのファイルを追加した方が扱いやすいです。

  • appsettings.json
    • appsettings.Development.json
    • appsettings.Production.json ← 新しく作成する

問題となるのが Azure Functions ですが、ランタイムに関係ない部分のアプリケーション設定に関しては ConfigurationBuilder を使うことで appsettings.json に設定を書けるようになります。

この方法だと必ずアプリケーションと設定がセットでデプロイされるのと、履歴も Git で同時に管理出来ます。複数のアプリケーションで共通化する場合には csproj を弄ってしまうのが手っ取り早いかと思います。

以下のような csproj を書くと、別ディレクトリのファイルをプロジェクトにリンク追加出来ます。

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

  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Content Include="..\Shared\appsettings.Production.json" Link="appsettings.Production.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Content>
  </ItemGroup>

</Project>

編集後、ソリューションエクスプローラーからはこれまで通りプロジェクト内にあるように見えます。プロジェクトの発行時にはちゃんと成果物に含まれるので、特に意識せずに扱えます。

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

更に手っ取り早い方法としては、シンボリックリンクでファイルを共通化する方法もありますが、Windows 上だと扱いが少し面倒な部分もあるので悩ましいところです。

今のところは共通化する部分は開発環境向け以外の設定で良いかなと思っています。

App Configuration を使う

Key Vault のようにアプリケーション設定を管理するサービスが App Configuration ですが、深く使おうとすると中々癖のあるサービスだと気が付いてきました。

これまでに何回か試してブログに書いてきましたが、基本的な考え方として開発環境を含めた全てを App Configuration にまとめることで、環境やアプリでの設定を一元管理してきました。

理想的な構成ではありますが、現実的には開発環境で設定を追加する際に Visual Studio で完結しないのはかなり手間がかかる作業です。接続文字列はともかく、アプリケーション設定を追加する場合は毎回 Azure Portal や Azure CLI での操作が必要になります。

一人やかなり小さいチームなら回るかもしれないですが、それ以上になると混乱が目に見えます。

それらのデメリットを相殺できるのが、実行時に再起動なしに再読み込みする機能だったり、Feature Management の活用だったりするわけですが、裏を返せばこれらの機能を使わない限りメリットはかなり少ないな、というのが触ってみた感想です。

とはいえ、JSON を使って定義する場合とは異なり、柔軟にアプリケーション設定を組み合わせ出来ることは確かです。以下のようにサイト名で Label を定義しておけば、サイト単位での設定を定義できます。

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

App Configuration は初期化時にどのラベルを使うかを定義できるので、以下のように書くとちゃんとサイト名の Label を付けた設定値が読み込まれます。

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

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.ConfigureAppConfiguration(config =>
                          {
                              if (!context.HostingEnvironment.IsDevelopment())
                              {
                                  var builtConfig = config.Build();

                                  config.AddAzureAppConfiguration(options =>
                                      options.ConnectWithManagedIdentity(builtConfig["AppConfig:Endpoint"])
                                             .Use(KeyFilter.Any)
                                             .Use(KeyFilter.Any, Environment.GetEnvironmentVariable("WEBSITE_SITE_NAME")));

                                  config.AddAzureKeyVault(builtConfig["KeyVault:Endpoint"]);
                              }
                          })
                          .UseStartup<Startup>();
            });
}

適当に App Service にデプロイすると、設定した値が読み込まれていることが確認できます。

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

App Service のスロット毎にしたい場合は WEBSITE_SLOT_NAME を、ASP.NET Core の環境毎にしたい場合は IWebHostEnvironment.EnvironmentName を参照するようにすれば良いです。

Azure CLI を使うと JSON からのインポートが行えるので、リポジトリで JSON を管理しつつ Azure Pipelines を使ってのデプロイ可能ですが、設定とアプリケーションのデプロイタイミングがバラバラになるのは、順番が変わると事故りそうです。

Feature Management を組み合わせるのが一番メリットが大きそうです。新しい機能のリリースはデプロイで行うより、Feature Management を使った方が素早く再起動なく行えるのが便利です。

App Service の Deployment Slot を使って Swap でリリースも出来ますが、ウォームアップに時間がかかったりもするのでメリデメ考えて使いたいところです。

どの方法を選択するのか

とりあえず色々と書き出してみましたが、まとめると以下のような内容になるかなと思います。最初は全て App Configuration で良いかなと思ってましたが、実際はそんなことはなかったです。

  • 共通となる考え方
    • App Service の App Settings は ARM Template / Terraform などの IaC で管理する
  • アプリケーション設定は起動時に 1 度読み込まれれば良い場合
    • appsettings.json を使って環境毎に管理する
  • アプリケーション設定の実行時変更、Feature Management が必要な場合
    • App Configuration を使って管理する
    • 設定値自体は別の方法で管理する必要がある(Azure Portal からの手動設定は無し)

App Configuration を使うと、今度はそこの管理をどうするかという問題が出てきます。変更履歴は持っているので問題ないですが、開発中に設定値を変更した場合のオペレーションが手間になる未来が見えます。

だからといって JSON と Git で管理して CI で反映という形にすると、わざわざ App Configuration を使う必要があるのかという話になってくるので、個人的には大半のケースで appsettings.json に統一で良いかなと考えています。もちろんシークレットなデータは Key Vault を使いますが。