しばやん雑記

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

ASP.NET Core / Azure Functions で App Configuration と Key Vault を使って設定を一元化する

アプリケーションが 1 つとかの場合は App Service の App Settings や Connection Strings を使って設定すれば良いのですが、数が多くなったり環境が増えてくると大体管理しきれなくなって破綻する傾向にあります。

Infrastructure as a Code の考えで ARM Template や Terraform を使って App Settings を管理するのも良いですが、設定値はインフラというよりアプリ寄りなのでちょっと違うかなという気もします。なので Azure App Configuration と Key Vault を使います。

基本的な方針は App Service 側には必要最低限かつ最初以外ほぼ変更されないものだけを残します。今回であれば App Configuration と Key Vault のエンドポイントや、App Service 自体の設定値などです。

App Service には出来るだけキーなどを設定したくないので、App Configuration と Key Vault には Managed Identity を使ってアクセスします。これで App Service はエンドポイントだけ保持するようになります。

アクセスキーや接続文字列などの重要な情報は Key Vault を使い、それ以外のアプリケーションの動作に関わる設定は App Configuration に保存します。Feature Management と組み合わせるのも便利ですね。

App Configuration は AAD 認証に対応していないので細かなアクセス制御は行えませんが、重要な情報は Key Vault に保存すれば良いので特に問題はないでしょう。

既に何回か書いている気がしますが、以下のライブラリ 2 つをインストールするだけで実現できます。

  • Microsoft.Extensions.Configuration.AzureAppConfiguration
  • Microsoft.Extensions.Configuration.AzureKeyVault

両方とも Microsoft.Azure.Services.AppAuthentication を含んでいますがバージョンが古いので、新しいバージョンをプロジェクトに追加しておくと、使ってくれるようになります。割とバグ修正が多いです。

予め App Configuration と Key Vault は作成とキーの追加を行っておきました。ドキュメントに従って Managed Identity の設定もしておきます。

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

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

今回は使っていないですが、App Configuration は既存の App Service から設定のインポートと、App Service へのエクスポートまで出来るようになっていたのでかなり便利です。時間のある時に App Configuration を使った設計パターンをいくつか書いておきたいです。

タイトルの通り ASP.NET Core と Azure Functions のそれぞれで使ってみます。割と普通に出来てしまうので面白みはないですが、Azure Functions はやっと自分の中で答えを見つけられた気がします。

ASP.NET Core

特に新しいことはないですが、これまで通り ConfigureAppConfiguration を使って App Configuration と Key Vault の Configuration Provider を追加します。

これまで通り ConfigurationBuilder は後勝ちなので、追加する順番は重要になってきます。

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

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
               .ConfigureAppConfiguration(config =>
               {
                   var builtConfig = config.Build();

                   config.AddAzureAppConfiguration(options =>
                       options.ConnectWithManagedIdentity(builtConfig["AppConfig:Endpoint"]));

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

ドキュメントでは Key Vault 周りが KeyVaultClient を渡す方法になってますが、エンドポイントだけ渡せば勝手に Managed Identity とデフォルトの設定を使ってくれるのでシンプルです。

それぞれのエンドポイントは appsettings.Development.json や User secret に追加しておけば良いです。

{
  "AppConfig": {
    "Endpoint": "https://***.azconfig.io"
  },
  "KeyVault": {
    "Endpoint": "https://***.vault.azure.net"
  } 
}

これで App Configuration と Key Vault から値を取ってきてくれるので、使う側は特に意識する必要ありません。これまで通り IConfiguration を使って接続文字列を取ったり、オプションを組み立てるだけです。

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContextPool<MyDbContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("DefaultSqlConnection")));

        services.Configure<SampleOptions>(Configuration);

        services.AddMvc()
                .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    }
}

あとは適当にコントローラを用意して、値が正しく取れているのかを確認しました。

たまに IConfiguration をそのまま DI で受け取って使っているケースがありますが、メンテナンス性が下がるので IOptions<T> パターンを使った方が良いです。

public class HomeController : Controller
{
    public HomeController(IOptions<SampleOptions> options, MyDbContext myDbContext)
    {
        _options = options.Value;
        _myDbContext = myDbContext;
    }

    private readonly SampleOptions _options;
    private readonly MyDbContext _myDbContext;

    public async Task<IActionResult> Index()
    {
        ViewBag.Value = _options.TestKey;

        var users = await _myDbContext.Users.ToArrayAsync();

        return View(users);
    }
}

開発環境で実行すると App Configuration に追加した値と、Key Vault に保存した接続文字列を使って DB からデータを取得できているのが確認できます。

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

Azure サービス認証のおかげでエンドポイントだけでサクッと扱えました。App Service にデプロイする際もエンドポイントと Managed Identity の設定をすると、全く同じコードで動きます。

Azure Functions

ASP.NET Core 向けに Configuration Builder が設計されているので簡単に使えるのは当たり前なのですが、地味に悩むのが Azure Functions です。正式に Configuration Builder を弄る方法が無いので、暫くは ASP.NET Core のようには設定できないでしょう。

他にも appsettings.json を公式にサポートして欲しいという意見がかなり出てますが、簡単にはいかなさそうな雰囲気です。v2 での解決は難しいかも知れませんね。

なので、Azure Functions は App Configuration と Key Vault を使って設定を管理した方が楽なケースが割とありそうです。環境ごとの設定も App Configuration で一元管理できるようになります。

現在 Azure Functions では DI が使えますが、ASP.NET Core とは異なり IConfiguration が提供されないので IServiceProvider が取れるメソッドを使うか、別で ConfigurationBuilder を使う必要があります。

出来るだけ ASP.NET Core の書き方に寄せたかったので、今回は以下のような Startup を用意しました。

public class Startup : FunctionsStartup
{
    public Startup()
    {
        var config = new ConfigurationBuilder()
                     .AddEnvironmentVariables();

        var builtConfig = config.Build();

        config.AddAzureAppConfiguration(options =>
            options.ConnectWithManagedIdentity(builtConfig["AppConfig:Endpoint"]));

        config.AddAzureKeyVault(builtConfig["KeyVault:Endpoint"]);

        Configuration = config.Build();
    }

    public IConfiguration Configuration { get; }

    public override void Configure(IFunctionsHostBuilder builder)
    {
        builder.Services.AddDbContextPool<MyDbContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("DefaultSqlConnection")));

        builder.Services.Configure<SampleOptions>(Configuration);
    }
}

Azure Functions Runtime が管理している IConfiguration は使わずに ConfigurationBuilder を作成して、App Configuration と Key Vault を追加するようにしました。Startup クラス内では IConfiguration を触れるので、接続文字列やオプションなども簡単に設定できます。

Azure Functions 独自の挙動ですが local.settings.json の内容は Runtime 起動時に環境変数に設定されるので、以下のような local.settings.json を用意すると問題なくエンドポイントが参照できます。

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "AppConfig:Endpoint": "https://***.azconfig.io",
    "KeyVault:Endpoint": "https://***.vault.azure.net"
  }
}

Azure 上で動かす場合には App Settings に設定を追加すれば、環境変数に設定されるので同様に動作します。

使う場合は ASP.NET Core とほぼ同じです。IOptions<T> を使ってオプションを受け取れます。

public class Function1
{
    public Function1(IOptions<SampleOptions> options, MyDbContext myDbContext)
    {
        _options = options.Value;
        _myDbContext = myDbContext;
    }

    private readonly SampleOptions _options;
    private readonly MyDbContext _myDbContext;

    [FunctionName("Function1")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req,
        ILogger log)
    {
        var result = await _myDbContext.Users.ToArrayAsync();

        return new OkObjectResult(new { Value = _options.TestKey, Users = result });
    }
}

適当に上の HttpTrigger を実行してみると、ちゃんと App Configuration と Key Vault の設定が使われていることが確認できます。DI サポートのおかげで大分楽になりました。

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

Azure App Configuration と Key Vault、そして Managed Identity と Azure サービス認証を組み合わせることで設定の一元化が出来るようになりましたが、これで課題が全て解決したわけではないです。

開発中から App Configuration と Key Vault を使うと、アプリケーションの起動に毎回オーバーヘッドが乗って来ますし、設定項目を追加したい時に Azure Portal に行ったりするのは非常に手間です。

この辺りは開発時のフローを決めたり、Git で設定を管理しつつ CI/CD で App Configuration / Key Vault に反映するといった仕組み作りが必要になって来そうです。