🎢

Azure Functions v3 to v4 (.NET 7.0)

2023/03/13に公開

バージョン移行記事です。
v3 から v4 (.NET 7) に移行するときに行った作業メモを残しておきます。
さらに、isolated 化もするので、そこのめんどくさい部分もまとめようと思います。

function ランタイム回りの移行記事のつもりで書いています。dotnet バージョンの移行で発生する細かい修正は書く気ないのでご了承くださいな。

作業概要

ざっくりまとめると、v3 から v4 (.NET 7.0) の移行作業は、次の2つの手順で終わりです。

  1. 基本的な移行作業
  2. その他の移行作業
    • Docs に書かれている作業だけで、まともに動くようになるプロジェクトは少ないと思います。例えば、Function Startup を作っていた場合など、 Docs に書かれている内容では不十分な場合もあります。(書いてあったらどうしよ)

基本的な移行作業

docs を参照。

*.csproj の書き換えとプロジェクトの再読み込み

PropertyGroup 内を、移行先のバージョンに書き換えます。
今回はisolated function にするので <OutputType>Exe</OutputType> を加えます。

<PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <AzureFunctionsVersion>v4</AzureFunctionsVersion>
    <OutputType>Exe</OutputType>
</PropertyGroup>

次は、Microsoft.Azure.WebJobs 系から Microsoft.Azure.Functions.Worker 系に書き換え。
v4 isolated からは Microsoft.Azure.Functions.Worker系を使います。

<ItemGroup>
  <PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="x.x.x" />
  <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="x.x.x" />
  <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Storage" Version="x.x.x" />
  <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Timer" Version="x.x.x" />
  ...
</ItemGroup>

<ItemGroup>
  <Using Include="System.Threading.ExecutionContext" Alias="ExecutionContext" />
</ItemGroup>
<ItemGroup>
  <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="x.x.x" />
  <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="x.x.x" />
</ItemGroup>

↑の例では使いそうなパッケージを列挙しましたが、プロジェクトに合わせて追加したりして下さい。

Docs には超シンプルな Program.cs の追加が書いてありますが、これは Startup.cs がなかった前提だと思います。
ほとんどの場合、DIを利用したかったり、設定を AzureAppSettings や KeyVault からとってきたりするために Startup.cs があり、Docs に書いてあるほどシンプルな new HostBuilder()... にはならないと思うので、ここでは書きません。
v3 funciton で FunctionStartup を利用している場合の移行方法はここに書いてあります。

local.setting.jsonの書き換え

後忘れがちなのが、local.setting.jsonの書き換えです。
"FUNCTIONS_WORKER_RUNTIME": "dotnet"の記述を"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated"に書き換えます。

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated"
  }
}

他にも気が向いたら抜粋して書き出し。

その他の移行作業

ドキュメントを読みながら作業した後ビルドが通らなかったり、実行時エラーになったりして、「あ、ここもか~~~」と想像できなかった部分です。
多分全然困ってない部類だと思います。
今後同じような作業で、躓いた部分は随時追記していくつもりです。

FunctionName → Function attribute の切り替え

Microsoft.Azure.WebJobs ではFunctionNameattribute をつけるとそのメソッドがAzure Functionとして認識されますが、
Microsoft.Azure.Functions.Worker では名前が変わってFunctionになっています。

移行前 (Microsoft.Azure.WebJobs)

[FunctionName("Functionのなまえ")]
public async Task Run([TimerTrigger(...)]TimerInfo myTimer){....}

移行した後 (Microsoft.Azure.Functions.Worker)

[Function("Functionのなまえ")]
public async Task Run([TimerTrigger(...)]TimerInfo myTimer){....}

まぁこれはビルドエラーで弾かれるのでエラー見ればわかります。

TimerTrigger や QueueTrigger などの attribute がない

そんなことはない。よく見るんだ。
Nugetパッケージが足りてないです。

Httpトリガの場合は以下のように、Microsoft.Azure.Functions.Worker.Extensions.Httpを含めます。

同様に、Queueトリガの場合はMicrosoft.Azure.Functions.Worker.Extensions.Storage
Timer トリガの場合はMicrosoft.Azure.Functions.Worker.Extensions.Timerのパッケージをインストールしないと、参照できません。

Funciton Startup を HostBuilder.ConfigureServices への移植

isolated function にするにあたって、自分でワーカーを作ることになり、Function Startup を作って設定をしていた場合、少し書き換えて HostBuilder.ConfigureServices に移植する必要があります。

バージョンアップ前によくありそうな Funciton Startup

using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

[assembly: FunctionsStartup(typeof(FunctionSample))]

namespace FunctionSample
{
    public class Startup : FunctionsStartup
    {
        private IConfigurationRoot _configuration;

        public override void Configure(IFunctionsHostBuilder builder)
        {
            _configuration = new ConfigurationBuilder()
                .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
                .AddEnvironmentVariables()
                .Build();

            builder.Services.AddScoped(prv =>
            {
                return new HogeHogeServiceClient(_configuration["HogeHogeConnection"]);
            });
            .......
        }
    }
}

Funciton Startup から移植して書いた Program.cs

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

namespace FunctionSample
{
    public class Program
    {
        public static async Task Main()
        {
            var host = new HostBuilder()
                .ConfigureFunctionsWorkerDefaults()
                .ConfigureServices((context, services) =>
                {
                    var configuration = context.Configuration;

                    services.AddScoped(prv =>
                    {
                        return new HogeHogeServiceClient(configuration["HogeHogeConnection"]);
                    });
                    .......
                })
                .Build();

            await host.RunAsync();
        }
    }
}

今までのDI設定や設定を取得するために、 IServiceCollection と、IConfiguration を持っているインスタンスをHostから引っ張れれば移植できると思うので、その例を書きました。
ほかに何か使いたい場合ってあったっけ?

logger 刺してくれない

なんか刺してくれるようにする方法あるんでしょうが、よくわからなかったので DI コンテナに注入してもらうようにします。

以下のように適当に Logger の DI を追加しました。

private readonly ILogger _logger;
public FunctionSample(ILoggerFactory loggerFactory)
{
	_logger = loggerFactory.CreateLogger<FunctionSample>();
}

[Function(nameof(FunctionSample))]
public async Task Run([QueueTrigger(......))
{
	_logger.LogInformation("あああああああああ");
}

xin9le さんにお聞きしたのですが、FunctionContext だけは引数で渡してくれるそうで、以下のように書き換えることもできそうです。
好みで書き分ける感じですね。

[Function(nameof(FunctionSample))]
public async Task Run([QueueTrigger(......), FunctionContext context)
{
	var log = context.GetLogger<FunctionSample>();
	log.LogInformation("あああああああああ");
}

個人的には、前者の方がスタンダードな気がします。Docs も、前者のパターンになっていますし。

Disable attribute 無い件

Disable がないみたいなので、local.setting.json や、Azure functions の環境設定に無効の設定を書き込みます。

local.setting.json の例

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "AzureWebJobs.SampleFunction.Disabled":  "true"
  }
}

設定の移行が必要なので、リリース前に前のdisable設定を引き継いで新しい設定を書き加えておきましょう。

BlobやQueueの出力バインドが無い件

マジか…
これがあるから funciton 使ってた節はあると思うんだが。。。消えるのか…

近いうちに実装されるかもとの噂も聞きますが・・・
しばらくは自前で実装するしかなさそうです。

BlobやQueueのServiceクラスを実装して、ConnecitonString等を指定できるFactoryクラスをDIしてもらうようにするなどして対応します。(意味不明)

Factory

public enum StorageServiceConnectionType
{
	// レコメンドサービス向けの書き出し先とか
	XXXRecommendServiceConnection = 0,
	XXXCRMServiceConnection = 1,
	...
}
public class StorageServiceFactory
{
	// (接続の種類, 接続文字列)
	private readonly Dictionary<StorageServiceConnectionType, string> _connections;
	
	public StorageServiceFactory(Dictionary<StorageServiceConnectionType, string> connections)
	{
		_connections = connections
	}
	
	public BlobClient CreateBlobClient(StorageServiceConnectionType type)
	{
		// blobClient 作る
		return clent;
	}
	
	public QueueClient CreateQueueClient(StorageServiceConnectionType type)
	{
		// queueClient 作る
		return clent;
	}
}

Service

public class BlobClient
{
	// なんか基本的な読み書きを提供するメソッドとか
}

Singleton attribute どうすんの問題

なるほど。

https://blog.iwate.me/41041/azure-functions-isolated-singleton

iwate さんのsingleton ライブラリを使ってみよう。
preview 版なので、自己責任で、自前で検証してから導入。

プレスリリースを含めるにチェックを入れてから、Iwate.AzureFunctions.Middlewares.Singletonを検索

LockService の DI が解決できないといわれるので、

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults(app => {
        app.UseMiddleware<Iwate.AzureFunctions.Middlewares.Singleton.SingletonMiddleware>();
    })
    .ConfigureServices((context, services) =>
    {
        var configuration = context.Configuration;
        services.AddSingleton(new Iwate.AzureFunctions.Middlewares.Singleton.LockService(context.Configuration));
    })
    .Build();

ConfigureServices で追加。

その他

移行作業後、dotnet-isolated にならない・functionが見つからない

いろいろ考えられますが、私がハマったのはlocal.setting.jsonのプロパティで、出力ディレクトリにコピーの項目がコピーしないになっていたり、
新しい場合はコピーするだが挙動がおかしい状態だったことが原因でした。
うまくビルド時に最新の設定が当たらずに、 "FUNCTIONS_WORKER_RUNTIME": "dotnet"のまま動かそうとして、WebJobが起動してしまい、function が起動しませんでした。

常にコピーするにしてとりあえず設定を最新にして解消しました。(本当は新しい場合はコピーするでも問題ないはずなんだけどなぁ)
ここ自分でも納得いってないですなんで?

DurableFuncitons のバージョンアップ

長くなるので別の記事に。

Docs 適当すぎん?

context.Configuration から接続文字列が読めん

<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="x.x.x" />

csproj に Microsoft.Azure.Functions.Worker.Sdk がないとlocal.settings.json の内容を読めませんでした。

Discussion