しばやん雑記

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

Azure Functions でも appsettings.json と User Secrets を使った設定とシークレットの管理を行う

前に設定周りを全て App Configuration と Key Vault に一元化する方法を紹介しましたが、ローカル環境ではデバッグの度に Access Token と値の取り直しが必要になるので、そこそこオーバーヘッドが大きいです。

ちなみにデプロイすると起動のタイミングで 1 回だけ取りに行くので、オーバーヘッドは無視できます。

ASP.NET Core ではローカル環境では appsettings.json と User Secrets を使い、設定と接続文字列などのシークレットを管理するのが一般的です。それに対して Azure Functions では local.settings.json を使うのと、フォーマットが異なっているので少し扱いが違います。

設定周りはドキュメントにまとまっているので、既に読んでいる人は多いと思います。かなり便利です。

新しい IConfiguration はネストした JSON を扱えますが、local.settings.json は特殊な実装になっていて JSON 内でネスト表記出来ないのも結構不便です。

なので Azure Functions でも ASP.NET Core と同じ形で設定とシークレットを扱えるようにしました。

まずは準備として Azure Functions のプロジェクトを作成し、必要なパッケージのインストールと csproj にいくつかのプロパティを追加します。重要なのは UserSecretsId と appsettings.json 周りの設定です。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp2.2</TargetFramework>
    <AzureFunctionsVersion>v2</AzureFunctionsVersion>
    <UserSecretsId>xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</UserSecretsId>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.2.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="2.2.0" />
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.29" />
  </ItemGroup>
  <ItemGroup>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </None>
    <None Update="appsettings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="appsettings.*.json" DependentUpon="appsettings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>
</Project>

UserSecretsId は適当に GUID を生成して設定すれば良いです。appsettings.json 周りはビルド・デプロイ時に出力ディレクトリにコピーする設定です。

プロジェクトを修正後、ソリューションエクスプローラーでは以下のように表示されるはずです。

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

local.settings.json は必須なので消さないように注意してください。特に Consumption / Premium Plan を使う場合には接続文字列などの Scale Controller が必要とする情報を、local.settings.json や App Service 側の App Settings に追加しないと正常に動作しなくなります。

User Secrets に必要な設定を追加すると、Visual Studio から管理できるようになるので便利です。

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

設定は完了したので後はコードから利用するだけですが、ファイルパスと環境の扱いについては少し注意が必要です。特にカレントディレクトリがローカル環境と Azure 上で異なっているのがはまりポイントです。

この辺りの違いを吸収するために、以下のように簡単なクラスを用意しておきました。

public static class FunctionsEnvironment
{
    private const string Development = "Development";
    private const string Staging = "Staging";
    private const string Production = "Production";

    public static string EnvironmentName =>
        Environment.GetEnvironmentVariable("AZURE_FUNCTIONS_ENVIRONMENT");

    public static string RootDirectory =>
        Environment.GetEnvironmentVariable("WEBSITE_SITE_NAME") != null ? Environment.ExpandEnvironmentVariables("%HOME%/site/wwwroot") : Environment.CurrentDirectory;

    public static bool IsDevelopment => IsEnvironment(Development);

    public static bool IsStaging => IsEnvironment(Staging);

    public static bool IsProduction => IsEnvironment(Production);

    public static bool IsEnvironment(string environmentName) =>
        string.Equals(EnvironmentName, environmentName, StringComparison.OrdinalIgnoreCase);
}

DI を使って IHostingEnvironment などを取得しても良いですが、IServiceProvider を作るのを避けたかったので環境変数を使って解決することにしました。

最後に FunctionsStartup の実装で JSON や User Secrets から設定を読み込んでしまえば完了です。User Secrets に関しては Development の場合だけ読み込むようにします。

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

        config.SetBasePath(FunctionsEnvironment.RootDirectory)
              .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
              .AddJsonFile($"appsettings.{FunctionsEnvironment.EnvironmentName}.json", optional: true, reloadOnChange: true);

        if (FunctionsEnvironment.IsDevelopment)
        {
            config.AddUserSecrets<Startup>();
        }

        config.AddEnvironmentVariables();

        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);
    }
}

Function の実装は App Configuration / Key Vault の時と同じものを使いました。ローカル環境で実行してみると、正しく appsettings.Development.json と User Secrets から値を取れていることが確認できます。

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

次は実際に Azure にデプロイして確認してみます。SQL Database の接続文字列は Azure Functions の Connection Strings から直接追加してあります。

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

実行してみると本番では appsettings.Development.json の値が読み込まれないので、ローカル環境とは値が異なっています。接続文字列は同じものを使っているので、データは同じものが返っています。

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

個人的にはこれで Azure Functions で設定とシークレットを扱う方法に悩まなくなりました。

いつかは正式に Azure Functions でサポートされるようになると思いますが、実装の方向性は大きく変わらないはずなので移行も簡単でしょう。