ROMANCE DAWN for the new world

Microsoft Azure を中心とした技術情報を書いています。

Azure Functions におけるシークレットの管理方法を改めて考えてみた

昨年 .NET 6 に対応した Azure Functions v4 がリリースされたので、Azure Functions におけるシークレットの管理方法を改めて考えてみました。
データベースの接続文字列や API Key などのセンシティブな情報は、アプリケーションのリポジトリ内では管理したくありません。
アプリケーションとセンシティブな情報を分離させる構成を実現する方法にはいつくかありますが、現時点では次の方針がシンプルでいいのではと考えています。

  • Azure の本番環境では、Azure Key Vault を使って Managed Service Identity(MSI)で認証する
  • ローカルの開発環境では、ASP.NET Core の Secret Manger で管理する
  • アプリケーションに関する設定は、appsettings.json で管理する
  • Function のバインディング用の接続文字列は、local.settings.json で管理する

上記の方針に従って、実際に構成を作って試してみます。

Azure Functions に appsettings.json を追加する

Azure Functions のプロジェクトには local.settings.json が用意されていますが、JSON の配列を定義できないなど ASP.NET Core の appsettings.json と異なる部分が使いづらいので、Azure Functions においても appsettings.json を導入します。
まずは、前準備として Azure Functions のプロジェクトに拡張の NuGet パッケージをインストールし、Startup クラスを実装します。

PM> Install-Package Microsoft.Azure.Functions.Extensions -Version 1.1.0
[assembly: FunctionsStartup(typeof(FunctionApp.Startup))]

namespace FunctionApp
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
        }
    }
}

appsettings.json と appsettings.Development.json のファイルを追加し、.csproj ファイルと Startup クラスに構成と読み取りを実装します。

<ItemGroup>
    <None Update="appsettings.json">
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="appsettings.Development.json">
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
        <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </None>
</ItemGroup>
public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
{
    var context = builder.GetContext();

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

appsettings.json では、接続する Key Vault の URL を定義しておきます。

{
  "Function": {
    "KeyVaultUrl": "https://xxx.vault.azure.net/"
  }
}

Azure Key Vault にデータベースの接続文字列を登録する

Azure Functions からアクセスする SQL Database の接続文字列を登録するシナリオを例に説明していきます。
Key Vault の Secret に Function--SqlConnection という名前で登録します。

f:id:TonyTonyKun:20220114152545p:plain

名前に Function のプレフィックスを付けた理由は、1つの Key Vault で複数の Azure Functions を構成を管理する想定のためです。
JSON の階層構造で管理したいので -- で区切っています。Key Vault の名前には : を使うことができないで注意してください。

Azure Functions で Azure Key Vault を参照する

Azure Key Vault を参照するための NuGet パッケージをインストールします。

PM> Install-Package Azure.Identity -Version 1.5.0
PM> Install-Package Azure.Extensions.AspNetCore.Configuration.Secrets -Version 1.2.1

開発環境以外では MSI を使って Key Vault に接続するように構成します。

public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
{
    var context = builder.GetContext();

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

    if (context.EnvironmentName != "Development")
    {
        var config = builder.ConfigurationBuilder.Build();
        builder.ConfigurationBuilder
            .AddAzureKeyVault(new Uri(config["Function:KeyVaultUrl"]), new DefaultAzureCredential());
    }
}

アプリケーションで SQL Database の接続文字列を扱いやすくするため、POCO クラスにマッピングします。

public class MySettings
{
    public string SqlConnection { get; set; }
}

Key Vault の Secret から自分のアプリケーション情報だけを取得したいので、Function のプレフィックスを指定しています。

public override void Configure(IFunctionsHostBuilder builder)
{
    var context = builder.GetContext();
    builder.Services.Configure<MySettings>(context.Configuration.GetSection("Function"));
}

Function では、このような実装で SQL Database の接続文字列を取得します。

public class HttpFunction
{
    private readonly MySettings _settings;

    public HttpFunction(IOptions<MySettings> optionsAccessor)
    {
        _settings = optionsAccessor.Value;
    }

    [FunctionName(nameof(HttpFunction))]
    public IActionResult Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
        ILogger log)
    {
        log.LogInformation("C# HTTP trigger function processed a request.");

        var responseMessage = $"SqlConnection : {_settings.SqlConnection}";

        return new OkObjectResult(responseMessage);
    }
}

Azure 本番環境

Azure Functions の MSI を有効化し、Key Vault の Access policies を設定します。

$ az functionapp identity assign --name "<Function Apps Name>" --resource-group "<Resource Group Name>"
{
  "principalId": "xxx",
  "tenantId": "yyy",
  "type": "SystemAssigned",
  "userAssignedIdentities": null
}

$ az keyvault set-policy --name "<Key Vault Name>" --object-id "xxx" --secret-permissions get list

これで Azure Functions は、MSI で Key Vault に認証して SQL Database の接続文字列を取得できます。

ローカル開発環境

Visual Studio の機能でローカル開発環境から Key Vault に接続することはできますが、環境によって上手く認証できなかったり、デバッグ実行が遅くなったりするので、今回は使いません。
ASP.NET Core の Secret Manger を使って、プログラマーが開発用の構成情報を管理してコードを書いていくようにします。
docs.microsoft.com

Visual Studio のプロジェクトを右クリックしたユーザーシークレットの管理から secrets.json ファイルを追加し、このように SQL Database の接続文字列を定義します。

{
  "Function": {
    "SqlConnection": "SQL Connection for Secret Manager."
  }
}

これでローカル環境では、secrets.json から取得した SQL Database の接続文字列を使って開発できます。

local.settings.json でバインディングの接続文字列を管理する

Storage や Cosmos DB のバインディングを使う Function の場合、バインディング用の接続文字列を設定する必要があります。
バインディング用の接続文字列を Key Vault や Secret Manager で管理すると、Functions のスケーラーが値を読み取れません。ローカルのデバッグ実行でも警告が発生しますし、Azure Functions にデプロイするとエラーが発生します。

The Functions scale controller may not scale the following functions correctly because some configuration values were modified in an external startup class.

f:id:TonyTonyKun:20220114182646p:plain

そのため、バインディング用の接続文字列は local.settings.json で管理します。

{
    "IsEncrypted": false,
    "Values": {
      "AzureWebJobsStorage": "UseDevelopmentStorage=true",
      "FUNCTIONS_WORKER_RUNTIME": "dotnet",
      "StorageBindingConnection": "UseDevelopmentStorage=true"
    }
}
public class QueueFunction
{
    [FunctionName(nameof(QueueFunction))]
    public void Run([QueueTrigger("myqueue-items", Connection = "StorageBindingConnection")] string myQueueItem, ILogger log)
    {
        log.LogInformation($"C# Queue trigger function processed: {myQueueItem}");
    }
}

Azure 本番環境では、Application settings に接続文字列をベタ書きではなく Key Vault 参照を使うことで、シークレットを Key Vault で一元管理するようにします。

@Microsoft.KeyVault(SecretUri=https://xxx.vault.azure.net/secrets/Function--StorageConnection/)

docs.microsoft.com

まとめ

Azure Functions におけるシークレットの管理方法を改めて考えてみました。
セキュリティという観点では、Key Vault 自体を閉域網構成で管理したい話もあるので、そちらは追加で検討が必要です。

ASP.NET Core アプリケーションについては、こちらの記事を参照してください。
gooner.hateblo.jp