yotiky Tech Blog

とあるエンジニアの備忘録

Azure Functions で Startup クラスを定義して DI を利用する

Azure Functions は、v2 で DI を正式にサポート。

Startup クラスを(自分で)定義し、 DI を設定することで Azure Functions にインジェクションすることができるようになる。

目次

検証環境

  • Azure Functions v3
  • Microsoft.Azure.Functions.Extensions v1.1.0
  • Microsoft.Extensions.Http v3.1.11
    • v5.0.0 だとMicrosoft .Extentions.DependencyInjection のライブラリがバージョン不一致を起こす
  • Azure.Extensions.AspNetCore.Configuration.Secrets v1.0.2
  • Microsoft.Extensions.Configuration.AzureKeyVault v3.1.11
  • Microsoft.Azure.Services.AppAuthentication v1.6.0
  • Microsoft.Azure.KeyVault v3.0.5
  • Microsoft.Extensions.Azure v1.0.0

インストール

Nuget で Microsoft.Azure.Functions.Extensions をインストールする。

使い方

基本的な使い方

今回は DI するために適当なクラスを用意する。

public interface IMyService { }
public class MyServiceA : IMyService { }
public class MyServiceB : IMyService { }
public class MyServiceC : IMyService { }

まず FunctionsStartup を継承した Statup クラスを新規作成する。

using Microsoft.Azure.Functions.Extensions.DependencyInjection;

[assembly: FunctionsStartup(typeof(FunctionApp1.Startup))]

namespace FunctionApp1
{
    class Startup : FunctionsStartup
    {
        public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
        {
            base.ConfigureAppConfiguration(builder);
        }
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddTransient<MyServiceA>();
            builder.Services.AddTransient<IMyService, MyServiceA>();

            builder.Services.AddScoped<MyServiceB>();
            builder.Services.AddScoped<IMyService, MyServiceB>();

            builder.Services.AddSingleton<MyServiceC>();
            builder.Services.AddSingleton<IMyService, MyServiceC>();
        }
    }
}

アセンブリ属性FunctionsStartupAttributeStartup クラスを指定する。

IFunctionsHostBuilder を使ってサービスを登録する。登録メソッドは登録するサービスの有効期間毎に別れており、各インスタンスの有効期間は以下の3通りになる。

  • 一時的(Transient)
  • スコープ(Scoped)
    • Function(関数)実行ごとに1回生成される
  • シングルトン(Singleton)
    • ホストの有効期間と一致する
    • DocumentClientHttpClient など接続やクライアントに推奨されている

なお IFunctionsConfigurationBuilderFunctionsHostBuilderContext を取得すると以下の3つの値が取得できる。

    var context = builder.GetContext();
    var rootPath = context.ApplicationRootPath;
    var configuration = context.Configuration;
    var environment = context.EnvironmentName;      


関数側は、 Azure Functions のクラスとメソッドの static を外し、コンストラクタを実装してインジェクションするための引数を追加する。

public class Function1
{
    private readonly MyServiceA _serviceA;
    private readonly MyServiceB _serviceB;
    private readonly MyServiceC _serviceC;
    private readonly IMyService _service;

    public Function1(
        MyServiceA serviceA,
        MyServiceB serviceB,
        MyServiceC serviceC,
        IMyService service)
    {
        _serviceA = serviceA;
        _serviceB = serviceB;
        _serviceC = serviceC;
        // 3つの有効期間で同じインターフェイスを登録してると Scoped のインスタンスが入ってくるらしい
        _service = service;
    }

    [FunctionName("Function1")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
        ILogger log)
    {
        return new OkObjectResult("This HTTP triggered function executed successfully.");
    }
}

参考:

環境変数を設定する

IConfigurationStartup クラスの中で何もしなくてもインジェクションされる。ローカル実行時も ConfigurationBuilder と違い明示的に local.settings.json を追加しなくて良い。

    public class Function1
    {
        private readonly IConfiguration _configuration;
        public Function1(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        [FunctionName("Function1")]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation(_configuration["AzureWebJobsStorage"]);
            log.LogInformation(_configuration["APPLICTION_SETTINGS_VALUE"]);
            log.LogInformation(_configuration.GetConnectionString("DB_CONNECTION_STRING"));

            return new OkObjectResult("This HTTP triggered function executed successfully.");
        }
    }

f:id:yotiky:20210128210604p:plain

f:id:yotiky:20210129211630p:plain

HttpClient を利用する

Nuget で Microsoft.Extensions.Http をインストールする。
v5.0.0 だとMicrosoft .Extentions.DependencyInjection のライブラリがバージョン不一致を起こすので注意。

サービスに HttpClient を登録する。(この1行なくてもインジェクションされるけどどうなんだろうか)

    class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddHttpClient();
        }
    }

Functions 側はコンストラクタで受け取る。

    private readonly HttpClient _httpClient;

    public Function1(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

ロガーを利用する

host.jsonlogLevel を追加する。 FunctionApp1の部分は出力対象とする名前空間やクラス名を指定する。

{
  "version": "2.0",
  "logging": {
    "applicationInsights": {
      "samplingExcludedTypes": "Request",
      "samplingSettings": {
        "isEnabled": true
      }
    },
    "logLevel": {
      "FunctionApp1": "Information"
    }
  }
}

Functions で利用する場合は、コンストラクタで受け取るだけ。 デフォルトで付いてくるメソッド引数の log は削除して問題ない。

    private readonly ILogger<Function1> _logger;

    public Function1(ILogger<Function1> logger)
    {
        _logger = logger;
    }

    [FunctionName("Function1")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req)
    {
        _logger.LogInformation("ILogger<Functions1> _logger");

        return new OkObjectResult("This HTTP triggered function executed successfully.");
    }

Functions 以外のクラスで利用する場合は、対象のクラスに ILogger をインジェクションしてもらうために、Startup クラスの Configure メソッドでサービスに登録する。 こうすることでロガーをクラス間でたらい回しにしなくて済む。

public class LogWriter
{
    private readonly ILogger<LogWriter> _logger;
    public LogWriter(ILogger<LogWriter> logger)
    {
        _logger = logger;
    }

    public void Write()
    {
        _logger.LogInformation("ILogger<LogWriter> _logger");
    }
}
class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        builder.Services.AddTransient<LogWriter>();
    }
}

Functions 側では目的のクラスをインジェクションしてもらいそれを利用する。

    private readonly ILogger<Function2> _logger;
    private readonly LogWriter _logWriter;

    public Function2(
        ILogger<Function2> logger,
        LogWriter logWriter)
    {
        _logger = logger;
        _logWriter = logWriter;
    }

    [FunctionName("Function2")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req)
    {
        _logger.LogInformation("ILogger<Functions2> _logger");
        _logWriter.Write();

        return new OkObjectResult("This HTTP triggered function executed successfully.");
    }

f:id:yotiky:20210129234436p:plain

参考:Azure Functions におけるロガーの扱いとフィルタの注意点 - しばやん雑記

appsettings.json を読み込む

ASP.NET Core でよく使われる appsettings.json を読み込む。local.settings.json では配列や特殊なオブジェクトなどを定義できない。

プロジェクトに appsettings.json を追加して、出力ディレクトリに「常にコピーする」ように設定する。実行環境毎の設定は appsettings.Development.json などを追加する。

f:id:yotiky:20210130011947p:plain:w300

{
  "SampleAppSettings": {
    "Name": "SampleAppSettings",
    "Items": [
      {
        "Key": "Key1",
        "Message": "Message1"
      },
      {
        "Key": "Key2",
        "Message": "Message2"
      }
    ]
  }
}

設定を受け取るクラスを定義する。

public class SampleAppSettings
{
    public string Name { get; set; }
    public Item[] Items { get; set; }
}

public class Item
{
    public string Key { get; set; }
    public string Message { get; set; }
}

Startup クラスで appsettings.json を読み込んで、設定を受け取るクラスを登録する。

public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
{
    base.ConfigureAppConfiguration(builder);

    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();
}
public override void Configure(IFunctionsHostBuilder builder)
{
    builder.Services.Configure<SampleAppSettings>(context.Configuration.GetSection("SampleAppSettings"));
}

参考:構成ソースのカスタマイズ - .NET Azure Functions で依存関係の挿入を使用する

Functions 側は IOption<SampleAppSettings> で受け取る。

public Function1(IOptions<SampleAppSettings> sampleAppSettings)
{
}

参考:Azure Functions V2 の Startup.cs で appsettings.json を読み込む(2019年5月バージョン) - BEACHSIDE BLOG


設定にオブジェクト単位ではなく、個別のパラメータを定義してバインドすることもできる。 local.settings.jsonMyOptions:Value1MyOptions:Value2 を追加する。

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "MyOptionsSettings:Value1": "local.settings.json-Value1",
    "MyOptionsSettings:Value2": "local.settings.json-Value2"
  }
}

設定を受け取るクラスを定義する。

public class MyOptions
{
    public string Value1 { get; set; }
    public string Value2 { get; set; }
}

Startup クラスで設定を読み込んで、MyOptions クラスを MyOptionSettings セクションにバインドする。

public override void Configure(IFunctionsHostBuilder builder)
{
    builder.Services.AddOptions<MyOptions>()
        .Configure<IConfiguration>((target, configuration) =>
        {
            configuration.GetSection("MyOptionsSettings").Bind(target);
            // もしくは
            //configuration.Bind("MyOptionsSettings", target);
        });
}

Functions 側は IOptions<MyOptions> で受け取る。

public Function1(IOptions<MyOptions> options)
{
}

f:id:yotiky:20210130204011p:plain

参考:Azure Functions で設定情報を使いたい - Qiita

オプション

環境変数を含むアプリ設定に定義されている値は IConfiguration で読み取ることできる。 さらに、ASP.NET Core ではオプションパターンを使って IConfiguration から任意のクラスに値を抽出することができる。 このパターンで関連する設定をグループ化する。

docs.microsoft.com

参考:

Key Valut を読み込む

Azure Key Vault のアクセスポリシーは「取得」と「一覧」を設定する。

f:id:yotiky:20210131004000p:plain:w200

Key Vault のエンドポイントをアプリケーション設定などに保存する。local.settings.json の例。

f:id:yotiky:20210131004546p:plain

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "KeyVaultEndpoint": "https://<KeyVaultName>.vault.azure.net/"
  }
}

Nuget で以下のパッケージをインストールする。

  • Azure.Extensions.AspNetCore.Configuration.Secrets

Startup で、Key Vault のエンドポイントを使って Configuration に追加する。

class Startup : FunctionsStartup
{
    public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
    {
            var builtConfig = builder.ConfigurationBuilder.Build();

            builder.ConfigurationBuilder.AddAzureKeyVault(new Uri(builtConfig["KeyVaultEndpoint"]), new DefaultAzureCredential());
    }
}

※Options を使う場合は、「appsettings.json を読み込む」の項目を参照。

Functions 側は インジェクションした IConfiguration からシークレットの名前で取得できる。

    public class Function1
    {
        private readonly IConfiguration _configuration;
        public Function1(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        [FunctionName("Function1")]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation(_configuration["keys-blobconnectionstring"]);

            return new OkObjectResult("This HTTP triggered function executed successfully.");
        }
    }

以下は、既に非推奨となっているライブラリであるが、実装例として残しておく。

Nuget で以下のパッケージをインストールする。

Startup で、Key Vault を参照して Configuration に追加する。

class Startup : FunctionsStartup
{
    public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
    {
        var builtConfig = builder.ConfigurationBuilder.Build();
        var tokenProvider = new AzureServiceTokenProvider();
        var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(tokenProvider.KeyVaultTokenCallback));
    
        builder.ConfigurationBuilder.AddAzureKeyVault(builtConfig["KeyVaultEndpoint"], keyVaultClient, new DefaultKeyVaultSecretManager());
    }
}

参考:

Blob Storage の Client を利用する

Nuget で以下のパッケージをインストールする。

  • Microsoft.Extensions.Azure
  • Azure.Storage.Blobs

Startup で、Blob Service Client を追加する。接続先が複数ある場合は名前を付けておく。

class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        var context = builder.GetContext();
        builder.Services.AddAzureClients(x =>
        {
            var connectionString = context.Configuration["AzureWebJobsStorage"];
            x.AddBlobServiceClient(connectionString);
            //x.AddBlobServiceClient(connectionString).WithName("BlobClientA");
        });
    }
}

Functions 側では、BlobServiceClient を受け取る。

public Function2(BlobServiceClient blobClient)
{
}

名前付きのクライアントを取得するには、IAzureClientFactory<BlobServiceClient> をインジェクションしてもらう。

public Function2(IAzureClientFactory<BlobServiceClient> clientFactory)
{
    var namedClient = clientFactory.CreateClient("BlobClientA");
}

参考:【.NET】BlobServiceClientFactoryはあったんだ! – 10bace LOG

Table Storage の Client を利用する

Nuget で以下のパッケージをインストールする。

Startup で、CloudTableClient を追加する。

class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        var context = builder.GetContext();
        builder.Services.AddSingleton(_ =>
        {
            var connectionsString = context.Configuration["AzureWebJobsStorage"];
            var storageAccount = CloudStorageAccount.Parse(connectionsString);
            return storageAccount.CreateCloudTableClient();
        });
    }
}

Functions 側では、CloudTableClient を受け取る。

public Function2(CloudTableClient tableClient)
{
}

昔の書き込みはスレッドセーフじゃないってあるが、最近はライフサイクルは Singleton で良さそう。

Queue Storage の Client を利用する

QueueClientインスタンスqueuName に結びつくため、キュー毎にラップしたクラスなどを用意して別々に生成する必要がある。

yotiky.hatenablog.com

App Configuration を読み込む

App Configuration のエンドポイントをアプリケーション設定などに保存する。

Nuget で以下のパッケージをインストールする。

  • Azure.Extensions.AspNetCore.Configuration.Secrets

Startup で、App Configuration のエンドポイントを使って Configuration に追加する。

class Startup : FunctionsStartup
{
    public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
    {
            var builtConfig = builder.ConfigurationBuilder.Build();

            builder.ConfigurationBuilder.AddAzureAppConfiguration(options =>
                options.Connect(new Uri(builtConfig["AppConfigEndpoint"]), new ManagedIdentityCredential()));
    }
}

設定や接続文字列を使うなど詳細は以下を参照。

yotiky.hatenablog.com

関連記事