Azure Functions 10 のアンチパターン

[Top 10 Azure Functions Anti-Patterns](https://tsuyoshiushio.medium.com/690ccff04a58) を日本語に翻訳してみました。Azure Functions をお使いの皆さんは是非自分のアプリケーションが下記の１０個のアンチパターンに該当していないか確認してみてください。

![functions.jfif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470/830e66f8-c258-53a8-4651-66c6e270ba17.jpeg)

私はよくAzure Functionsに関連した問題を引き起こす可能性の高い実装や設定に直面します。その情報を整理しやすくするために、Azure Functionsのトップ10アンチパターンをまとめました。これらの一般的な過ちに光を当てることで、より優れたAzure Functionsを設計し、システムの停止やアプリケーションの不整合な動作を防ぐ手助けとなることを願っています。

# 1. Azure Functionsホスト機能のオーバーライド (C#)
[Azure Functionsホストの機能をオーバーライドすることはサポートされていません](https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-dotnet-dependency-injection#overriding-host-services)。多くの開発者がこのアンチパターンの影響を知らないかもしれません。以下の`Startup.cs`の例をご覧ください：このコードは、[Azure Functionsの基本的な能力を実質的にオーバーライド](https://github.com/Azure/azure-functions-host/blob/d6a572bcdcdefce5c10ac41c8cda7e8456d273be/src/WebJobs.Script.WebHost/WebHostServiceCollectionExtensions.cs#L52)しています。これにより、意図しない動作が生じる可能性があります。

## 例1: 認証ロジックのオーバーライド
以下は、`Startup.cs`からのコード断片で、Azureのデフォルトの[認証ロジックをオーバーライド](https://github.com/Azure/azure-functions-host/blob/d6a572bcdcdefce5c10ac41c8cda7e8456d273be/src/WebJobs.Script.WebHost/WebHostServiceCollectionExtensions.cs#L52)しようとしています：

```csharp:BAD CODE
// 良くないコード
public override void Configure(IFunctionsHostBuilder builder) {
    :
  builder.Services
    .AddAuthentication()  // これは行わないでください！
    :
}
```

このようなアプローチを採用することで、Azure Functionsの核心的な機能を損なうリスクがあります。詳しく言うと、このアクションは、Azureが内部のマイクロサービスからのリクエストを検証するための認証ロジックを妨げます。

## 例2: IConfigurationのオーバーライド
IConfigurationをオーバーライドすると、Azure Functionsホストの機能を阻害します。以下のコードはこのアンチパターンを示しています：

```csharp:BAD CODE
// 良くないコード
public class Startup : FunctionsStartup {
    public override void Configure(IFunctionsHostBuilder builder) {
        // デフォルトのIConfigurationをオーバーライド。
        // AddSingleton<IConfiguration>を呼ぶことは絶対にしないでください！
        builder.Services.AddSingleton<IConfiguration>( 
            new ConfigurationBuilder()
            .AddEnvironmentVariables()
            .Build()
        );
    }
}
```

逆に、IConfigurationのカスタマイズ方法については、[Azure Functionsのドキュメント](https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-dotnet-dependency-injection#customizing-configuration-sources)を参照してください。IConfigurationを更新する正しい方法は以下の通りです：

```csharp:GOOD CODE
// 良いコード
public class Startup : FunctionsStartup {
    // IConfigurationの更新のためにConfigureAppConfigurationを使用する
    public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder) {
        FunctionsHostBuilderContext 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();
    }

    public override void Configure(IFunctionsHostBuilder builder) {
    }
}
```

## Isolated Workerのオーバーライド
dotnet-isolatedで似たような問題が発生することもあります。問題のある例を以下に示します：

```csharp:BAD CODE
// 良くないコード
var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureServices(services => {
        // AddSingleton<IConfiguration>()を呼ぶことは絶対にしないでください！
        services.AddSingleton<IConfiguration>(
            new ConfigurationBuilder()
                .AddEnvironmentVariables()
                .Build());
    })
    .Build();

host.Run();
```

このコードは、孤立したプロセスを終了させる例外を頻繁に引き起こすことがあり、診断が難しくなることがあります。

## Key Vaultの不適切な設定
dontnet-isolatedを使用している場合、Key Vaultの設定はホスト側で管理する必要があります。Key Vaultを誤用すると、設定されたしきい値を超える過度なリクエストが発生する可能性があります。避けるべき事例は以下の通りです：

```csharp:BAD CODE
// 良くないコード
var host = new HostBuilder()
            .ConfigureFunctionsWorkerDefaults()
            .ConfigureAppConfiguration(config =>{
                 var azureKeyVaultURL = Environment.GetEnvironmentVariable("AzureKeyVaultURL");
                var azureKeyVaultADAppID = Environment.GetEnvironmentVariable("AzureKeyVaultMIAppID");

                config
                   .SetBasePath(Environment.CurrentDirectory)
                   .AddAzureKeyVault(new Uri(azureKeyVaultURL), new ManagedIdentityCredential(azureKeyVaultADAppID))
                   .AddEnvironmentVariables()
                .Build();
            })
.Build();
```

Key Vaultのベストプラクティスとしては、常に[Key Vault Reference](https://learn.microsoft.com/ja-jp/azure/app-service/app-service-key-vault-references?tabs=azure-cli)を使用することをおすすめします。

指定されたガイドラインや推奨される実践方法を遵守することは不可欠です。これらのガイドラインから外れると、予期しない動作が発生し、問題の診断が難しくなる可能性があります。

* **参考:** [Expose configuration from worker processes #418](https://github.com/Azure/azure-functions-dotnet-worker/issues/418)

# 2. VNETを使用してRuntime Scale Monitoringを有効にしないこと
Azure FunctionsでVNETを利用する場合、[Runtime Scale Monitoringを有効](https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-networking-options?tabs=azure-cli#premium-plan-with-virtual-network-triggers)にすることが非常に重要です。なぜなら：

非HTTPトリガーは、スケーリングのために"Scale Controller"という内部コンポーネントに依存しています。Azure FunctionsがVNETと組み合わされると、このScale Controllerは、VNET経由でのみアクセス可能な、Storage AccountsやEvent Hubsなどの顧客のイベントソースと通信するのが難しくなります。

その結果、スケーリング機能が制約を受けることとなります。実際にはスケーリングしているように感じるかもしれませんが、それはHTTPトリガーが異なるコンポーネントをスケーリングの仕組みとして使用しているためです。これにより、効果的なスケーリングの錯覚が生じますが、実際にはダイナミックスケーリングのフルパワーを活用しているわけではありません。

解決策は何か？ Runtime Scale Monitoringを有効にします。この機能がアクティブになると、スケーリングの決定は、顧客のVNET内に位置するAzure Functionsホスト側で処理されます。このセットアップにより、Scale Controllerは顧客のイベントソースと直接対話することなく、スケーリングの決定を理解することができます。

それを有効にする方法は？ Azure Portalに移動して、`Configuration > Functions runtime settings`にナビゲートし、"Runtime Scale Monitoring"オプションをオンにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470/23a420b1-b288-1b66-8f22-4aa7af538556.png)


# 3. HTTPやその他のネットワーククライアントを繰り返しインスタンス化することを避ける

Azure Functionsにおいて、コード内でのネットワーククライアントの繰り返しインスタンス化という一般的なミスが観察されます。これはC#に限らず、他のプログラミング言語にも当てはまります。ここでの主な懸念は、新しいアウトバウンド接続の不必要な生成と、それらを再利用しないことです。

例えば、以下のC#のコードを考えてみましょう：

```csharp: BAD CODE
// 良くないコード
using var client = new HttpClient();
```

このパターンでは、毎回新しい接続が生成され、この振る舞いがチェックされずに続くと、最終的にはSocket Exceptionが発生する可能性があります。特にAzure Functionsのようなサーバーレス環境では、ネットワークリソースがしばしば複数の顧客間で共有されるため、この問題は特に問題となります。この問題はHttpClientだけに限られているわけではありません。他のネットワーククライアント、例えばAzure SDKを使用している場合、各リクエストごとに新しいクライアントを作成することは避けるべきです。

解決策は？
クライアントをstaticにするか、さらに良い方法として、ベストプラクティスとしてDependency Injection（DI）を採用することを検討してください。これによりリソースの最適化が保証され、潜在的なネットワーク関連の例外を避けるのに役立ちます。この解決策の実装についての詳しい情報は、次を参照してください：[.NET Azure Functions で依存関係の挿入を使用する | Microsoft Learn](https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-dotnet-dependency-injection)。

詳細については：
「[不適切なインスタンス化アンチパターン - Azureアーキテクチャセンター | Microsoft Learn](https://learn.microsoft.com/ja-jp/azure/architecture/antipatterns/improper-instantiation/#how-to-fix-improper-instantiation-antipattern)」
「[Azure App Service での断続的な送信接続エラーのトラブルシューティング - Azure App Service | Microsoft Learn](https://learn.microsoft.com/ja-jp/azure/app-service/troubleshoot-intermittent-outbound-connection-errors)」


# 4. C#でasync/awaitを活用しないこと
ネットワーククライアントや他の非同期的な性質を持つ操作を扱う際、`async/await`パターンの使用は極めて重要です。シンプルなアプリケーションでは問題ないかもしれませんが、ファイルI/Oや外部システムへの呼び出しなどの操作が一般的な本番環境では、非同期パターンを無視すると問題が生じることがあります。

具体的には、`async/await`パターンを使用しないと、外部呼び出し中に関数がスレッドをブロックし、スレッドが枯渇する原因となります。大切な注意点として、Azure Functionsで`Thread.Sleep()`を使用することは絶対に避けるべきです。代わりに、`async/await`と`await Task.Delay()`を組み合わせることで、スレッドをブロックしないより効率的な方法を採用できます。

非同期を使用しない簡単な例を以下に示します：

```csharp:BAD CODE
// BAD CODE (外部呼び出しやIO操作がある場合)

public static class SimpleExample
{
    [FunctionName("QueueTrigger")]
    public static void Run(
        [QueueTrigger("myqueue-items")] string myQueueItem, 
        ILogger log)
    {
        log.LogInformation($"C# function processed: {myQueueItem}");
    }
}
```

しかし、async/awaitへの移行は簡単です。メソッドのシグネチャを変更し、awaitを使用して非同期メソッドを利用するだけです。

async/awaitを使用した改訂された例を以下に示します：

```csharp:GOOD CODE
// GOOD CODE

public static class AsyncExample
{
    [FunctionName("BlobCopy")]
    public static async Task RunAsync(
        [BlobTrigger("sample-images/{blobName}")] Stream blobInput,
        [Blob("sample-images-copies/{blobName}", FileAccess.Write)] Stream blobOutput,
        CancellationToken token,
        ILogger log)
    {
        log.LogInformation($"BlobCopy function processed.");
        await blobInput.CopyToAsync(blobOutput, 4096, token); // external call
    }
}
```

このアプローチを採用することで、Azure Functionsは特に高負荷時や外部リソースを扱う際に、効率的に動作することが確保されます。

# 5. ホスト名を32文字以上の長さで使用することを避ける

Azure Functionsは、そのホスト名の最大長として32文字を規定しています。
[リソース名の規則 - Azure Resource Manager | Microsoft Learn](https://learn.microsoft.com/ja-jp/azure/azure-resource-manager/management/resource-name-rules#microsoftweb)
システムがホストIDを生成する際、機能アプリ名はこの32文字の制限内に収まるように切り詰められます。共有ストレージアカウントが使用されている場合、特にホストIDの衝突が発生すると、スケーリングやリース管理などの基本的なAzure Functionsの機能が妨げられる潜在的な問題が生じる可能性があります。
2023年10月15日現在、Azure Portalはこの制限を強制していません。そのため、AzureCLI、PowerShell、ArmTemplate、Bicep、またはTerraformなどのツールを使用して関数をデプロイする場合、この命名制約に特に注意が必要です。
より包括的な理解と潜在的な解決策については、[ホスト ID に関する考慮事項](https://learn.microsoft.com/ja-jp/azure/azure-functions/storage-considerations?tabs=azure-cli#host-id-considerations)を参照してください。

# 6. 古いホストおよび Extension バージョンの使用を避ける
最新のバージョンを維持することは、最適なパフォーマンスとセキュリティのために極めて重要です。特にAzure Functions Runtimeに関しては、最新バージョンへの更新により、多くの既知の問題が解消されます。
[Azure Functions ランタイム バージョンの概要](https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-versions?tabs=isolated-process%2Cv4&pivots=programming-language-csharp)
定期的にライブラリを更新することが推奨されます。例えば、以下の.NET Isolatedのサンプル.csprojファイルを考えてみましょう。これは、さまざまなNuGetパッケージを示しています。dotnet以外の言語を使用している場合、拡張バンドルに焦点を当てる必要があります。ホストバージョンを最新に保つだけでなく、これらのSDKや拡張ライブラリを定期的に更新することも重要です。

```xml
  <ItemGroup>
    <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.19.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.14.1" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.0.13" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Storage.Queues" Version="5.2.0" />
  </ItemGroup>
```

更新は基本的な「ハウスキーピング」と思われるかもしれませんが、多くのユーザーがダウンタイム中にのみ更新を考慮するため、問題に直面することが驚くほど多いです。この遅延の背後にはさまざまな理由があります。時には、サードパーティのベンダーがシステムを開発しており、コードを変更できない運用スタッフしか残っていないこともあります。他の場合、バージョンの更新が機能を誤って壊す可能性があり、ダウンタイムにつながることがあります。本番環境でのインシデント中に古いバージョンの潜在的な影響を過小評価しないでください。そのため、定期的な更新が強く推奨されます。
更新プロセスをさらに効率的かつ安全にするために、特定のセキュリティ機能を有効にすることを検討してみてください。例えば、GitHubのDependabotの使用は非常に有効です。このボットは、古いライブラリを更新するだけでなく、潜在的な脅威に対するシステムの防御も強化します。
Dependabotのセットアップ手順については、[Dependabot Quickstart Guide - github Docs](https://docs.github.com/en/code-security/getting-started/dependabot-quickstart-guide)を参照してください。

# 7. ストレージアカウントの共有時に注意を払う

一般的に、Azure Functionsは専用のストレージアカウントを必要としますが、いくつかのユーザーの中には、複数の関数で単一のアカウントを共有する傾向があります。これは実行可能ですが、このアプローチはお勧めできません。

ストレージアカウントは、Azure Functionsに特有のさまざまなメタデータを保持します。例としては、Azure Functionsのメタデータなどが挙げられます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470/fda0765e-9e44-3750-0ba2-b2873fd2474c.png)

複数のFunctionAppsで1つのストレージアカウントを共有すると、混乱が生じる可能性があります。ホスト名の32文字制限のような制約と組み合わせると、予期しない問題が生じることがあります。

さらに、同じストレージアカウントを使用してFunctionAppを削除してから再作成しようとすると、問題が生じる可能性があります。ストレージアカウント内の不正確なメタデータや設定が問題を引き起こす可能性があります。既存のストレージアカウントに関連付けられたFunctionAppを削除してから再作成する必要がある場合、新しいストレージアカウントを設定するか、既存のものを入念にクリーンアップすることが賢明です。すべてのBLOB、キュー、テーブル、ファイルが適切に管理されていることを確認してください。

# 8. 機密情報のログに記録を避ける

私たちがユーザーでよく見かける見落としの一つは、`Connection String`や`トークン`のような秘密情報を不注意にもログや表示に記録してしまうことです。こうした機密情報がログに記録されないようにすることは非常に重要です。

この罠を避ける効果的な方法の一つは、Managed Identitiesを利用することです。Managed Identityを使用すれば、接続文字列やSASトークンを取り扱う必要がなくなり、偶発的な露出のリスクが最小限になります。
[Azure Functionsでのアイデンティティベースの接続の使用 - Microsoft Learn](https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-identity-based-connections-tutorial-2)

別の前向きなアプローチとして、サニタイザーを使用することが考えられます。これにより、ログに記録される前に機密データをマスクまたは削除することができます。この点についてさらに詳しく知りたい場合は、Azureがfunction hostでサニタイゼーションをどのように取り扱っているかを調査することができます：
[Azure Functions Hostでのサニタイザー - GitHub](https://github.com/Azure/azure-functions-host/blob/d6a572bcdcdefce5c10ac41c8cda7e8456d273be/src/WebJobs.Script/Sanitizer.cs#L12)

# 9. アプリ内で複数のトリガーを使用する際の注意

特にAzureのFunction Appのようなサーバーレスアーキテクチャでアプリケーションを構築する際、複数のトリガーを組み込むことは魅力的に思えます。しかし、このプラットフォームではそれが許容されているものの、注意が必要です。これは特に、Consumption PlanやElastic Premium Planのような動的SKUに特に当てはまります。その理由を説明します：

**スケーリング決定のあいまいさ：** 複数のトリガーが存在する場合、システムは各トリガーのスケーリングニーズを集約して、スケーリングの決定を下します。この集約により、スケーリングパターンの明瞭さが失われる可能性があります。各トリガーが自らのスケーリングニーズを指定するのではなく、それらの集約した要求が予測不可能なスケーリングの結果をもたらす可能性があります。

**リソースのオーバーヘッド：** 様々なトリガーを組み込むことで、必要以上の計算リソース（スレッド、CPU、メモリ）が使用される状況が生じる可能性があります。例として、HTTPトリガーとEventHubトリガーを組み合わせるシナリオを考えてみましょう。EventHubトリガーは最大で32のインスタンスにスケーリングするかもしれませんが、HTTPトリガーは、例えば100のインスタンスにスケーリングする可能性があります。このセットアップでは、100のインスタンスすべてがEventHubリスナーを実行しますが、そのうちの32のインスタンスのみが効果的に使用されます。同様に、キューボリュームが少ないために1つのワーカーのみが必要なキュートリガーを導入した場合、100のインスタンスすべてがまだキューをリスニングまたはポーリングしていることになります。これは不必要なシステムリソースが消費されていることを意味し、パフォーマンスやコストに影響を与える可能性があります。

**推奨：**トリガーを、それぞれの特定の要件や特性に基づいて分離することが賢明です。特定のインスタンスやアプリケーションを個々のトリガーに専念させることで、より予測可能なスケーリングと効率的なリソースの利用を実現できます。

クラウドコンピューティングの世界では、どの機能を活用できるかだけでなく、それらをどれだけ賢明に使用するかも重要です。Function Appsのトリガーに関しては、少しの注意が大きな違いを生むことがあります。

# 10. 一つのプランを過度に使用するリスク

`AppService Plan`の中で一つのプランに複数のFunction Appsをデプロイすることは効率的に思えますが、それにはいくつかの課題が伴います：

**リソース競合：** 多数のFunction Appsが1つのサーバーファームを共有する場合、同じCPUとメモリを巡って競合します。1つのアプリでの急増は他のアプリのリソースを奪い、パフォーマンスに影響を及ぼし、障害を引き起こす可能性があります。

**トラブルシューティングの複雑さ：** 複数のアプリが共有リソースにアクセスすると、パフォーマンスの問題の原因を特定するのが難しくなります。

**推奨：** 共有プラン上で複数のアプリをホスティングする際は、リソースの利用状況を監視してください。一貫したパフォーマンスと問題の簡単な解決のために、リソースを多く使用するアプリや重要なアプリを専用のプランに割り当てることを検討してください。決定を下す前に、アプリの合計のニーズと成長予測を常に評価してください。

# 結論：正しいツールを使ってアンチパターンをナビゲートする

本議論を通じて、アプリケーションの開発やデプロイメントで現れるさまざまなアンチパターンに深く触れてきました。これらの落とし穴は困難かもしれませんが、希望の光があります。これらのアンチパターンの一部は、「Diagnose and Solve Problems」ツールを使用して識別できます。

問題を目的もなくナビゲートするのではなく、このツールは問題の根本原因を直接示してくれます。そして、それだけで終わることはありません。問題の診断と同時に、それを修正するための具体的な推奨手順も提供されます。この二重の能力 - 問題を識別し、対策を指南する - は、欠かせない財産となります。私はその効果を個人的に保証します。特にインシデントに対処する際に、私たちは常にこのツールに頼っています。

Azure Functionsを使用する際には、ベストプラクティスを守ることが重要です。これにより、関数の効率と信頼性が向上するだけでなく、一般的なアンチパターンに関連する潜在的な落とし穴も防ぐことができます。

[Microsoft LearnでAzure Functionsのベストプラクティスをさらに探る](https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-best-practices?tabs=csharp)

最後に、潜在的な落とし穴を注意深く考慮することが重要である一方、同様に重要なのは、あなたの手元にある適切なツールを知っていることです。「Diagnose and Solve Problems」は、アプリがスムーズかつ効率的に動作することを確実にする強力な味方の一つです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470/0d58c4a6-651b-72c5-2534-41ae4a7e25ad.png)