マルチテナント SaaS のテナントコンテキストを Application Insights のテレメトリに設定してみた

2024.01.07

いわさです。

SaaS アーキテクチャーをマルチテナント(プール)型で、リソースを共用する形で構築すると、テナントごとにどれだけリソースを使用しているのか、ランニングコストはどの程度なのかを把握するのが難しくなりがちです。
シングルテナント(サイロ)型の場合はコスト管理用のタグなどで把握することが可能なのですが、マルチテナント型でこのあたりが把握出来ていないという相談をよく受けます。

AWS の場合だとこのあたりを簡単に解決出来るサービスが現状ないので、テナントごとに使用量を測定する仕組みを独自に実装して、全体のインフラコストから使用量を使って独自に計算することでテナントごとの使用量やインフラ原価コストを算出する形となります。

最近 Azure における SaaS アーキテクチャーの設計方法などについて調べてまして、Azure の場合どうなるのかな?と思って調べてみたのですが、どうやらこのあたりの考え方は AWS と同じようで、要はアプリケーション側でテナントを識別出来る情報(テナントコンテキスト)をログやメトリクスに付与することで、クラウドサービス側で分析出来るようにするアプローチになります。

具体的な実装方法としては Azure の場合は Application Insights という Azure Monitor の機能を使うことが出来ます。AWS でいうと X-Ray が近いかなと個人的に思ってます。

本日は Application Insights と親和性の高そうな .NET アプリケーションを使って、テナントコンテキストっぽいものをテレメトリのカスタムプロパティとして設定し、Log Analitics などでテナントごとの使用状況を見てみるというのをやってみました。

Application Insights を利用するにあたって

Azure アーキテクチャセンターの SaaS アーキテクチャーの章では具体的に Azure サービスごとの考慮事項などについて触れられていまして、これが大変参考になります。このあたりは設計・実装前に必ず見ておきましょう。

具体的には Application Insights のインスタンスもテナント間で共用するか分離するか、その際にどういった考慮が必要なのかなどについて触れられています。

今回は次のように全テナントを 1 つのインスタンスに集約する形を想定しました。(画像は上記ドキュメントから引用)

Application Insigts を利用するにあたって考慮しておくべきことをざっくりまとめておきます。

  • 料金については取り込み量と保存期間に依存しているのでインスタンスを分ける分けないはあまり関係ない
  • インスタンスあたりのメトリクスとイベントの数に制限がある(Azure Monitor サービスの制限 - Azure Monitor | Microsoft Learn
  • サンプリングの機能があったり、あるいは上限によってスロットリングが発生しデータポイントがドロップされる場合があることを知っておく
    • 分析用途であればだいたい許容出来るはず
    • 使用量ベースでテナントへの料金を計算する必要がある場合などの用途の場合は、確実に記録出来るように別のデータストアへの登録処理などをしっかり実装するほうが良さそう

ASP.NET Core で Azure の Application Insights を使ってテナントコンテキストを設定してみる

今回はローカルホストで ASP.NET Core の Web API を実行し、そこから Azure 上の Applicatio Insights へテレメトリを送信してもらおうと思います。

アプリ作成

アプリはデフォルトのテンプレートを使いました。

% dotnet new webapi -n hoge0106webapi
The template "ASP.NET Core Web API" was created successfully.

Processing post-creation actions...
Restoring /Users/iwasa.takahito/work/hoge0107appinsights/hoge0106webapi/hoge0106webapi.csproj:
  Determining projects to restore...
  Restored /Users/iwasa.takahito/work/hoge0107appinsights/hoge0106webapi/hoge0106webapi.csproj (in 103 ms).
Restore succeeded.

% cd hoge0106webapi/
% dotnet run
Building...
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5285
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /Users/iwasa.takahito/work/hoge0107appinsights/hoge0106webapi
^[^[warn: Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware[3]
      Failed to determine the https port for redirect.

実行して、リクエストを送信します。

% curl http://localhost:5285/WeatherForecast | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   385    0   385    0     0  45931      0 --:--:-- --:--:-- --:--:--  187k
[
  {
    "date": "2024-01-08",
    "temperatureC": -18,
    "temperatureF": 0,
    "summary": "Cool"
  },
  {
    "date": "2024-01-09",
    "temperatureC": 27,
    "temperatureF": 80,
    "summary": "Scorching"
  },
  {
    "date": "2024-01-10",
    "temperatureC": -18,
    "temperatureF": 0,
    "summary": "Cool"
  },
  {
    "date": "2024-01-11",
    "temperatureC": -4,
    "temperatureF": 25,
    "summary": "Scorching"
  },
  {
    "date": "2024-01-12",
    "temperatureC": 31,
    "temperatureF": 87,
    "summary": "Hot"
  }
]

良いですね。ここに Application Insights を組み込みます。

Application Insights の導入

事前に Azure 上で Application Insights を作成しておきます。

リソースモードはクラシックとワークスペースベースがありました。注意書きがされており、クラシックについては非推奨で来月には廃止されるようです。今回はワークスペースベースを選択しました。

クラシック Application Insights は非推奨であり、2024 年 2 月に廃止されます。ワークスペースベースの Application Insights の使用を検討してください。

ワークスペースベースのリソースを使用すると、Application Insights からテレメトリが指定された Log Analytics ワークスペースに送信されるようになります。
これによってアプリケーション、インフラストラクチャ、プラットフォームのログを 1 つの統合された場所に保持出来るとのこと。

作成出来たら次の接続文字列を控えておきます。
こちらとは別途 API キーを発行して利用する機能もあるのですが、そちらも廃止がスケジュールされているので基本的には接続文字列を使うことになります。

アプリケーションへの実装

まずは先程のアプリに Application Insights のここから有効化していきます。
今回は Visual Studio Code + Mac 環境でアプリケーションをセットアップしています。
なお、詳細な手順は以下に記載されており、こちらに沿って実装しています。

アプリケーションへ NuGet パッケージ Microsoft.ApplicationInsights.AspNetCore をインストールします。

% dotnet add package Microsoft.ApplicationInsights.AspNetCore --version 2.22.0
  Determining projects to restore...
  Writing /var/folders/4d/nhd1bp3d161crsn900wjrprm0000gp/T/tmpVLp3yo.tmp
info : X.509 certificate chain validation will use the fallback certificate bundle at '/usr/local/share/dotnet/sdk/7.0.203/trustedroots/codesignctl.pem'.
info : X.509 certificate chain validation will use the fallback certificate bundle at '/usr/local/share/dotnet/sdk/7.0.203/trustedroots/timestampctl.pem'.
info : Adding PackageReference for package 'Microsoft.ApplicationInsights.AspNetCore' into project '/Users/iwasa.takahito/work/hoge0107appinsights/hoge0106webapi/hoge0106webapi.csproj'.
info : Restoring packages for /Users/iwasa.takahito/work/hoge0107appinsights/hoge0106webapi/hoge0106webapi.csproj...
info :   GET https://api.nuget.org/v3-flatcontainer/microsoft.applicationinsights.aspnetcore/index.json

:

インストール出来たらアプリケーションの初期化部分に以下を追記します。

Program.cs

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddApplicationInsightsTelemetry();

var app = builder.Build();

:

続いて先程 Azure ポータルから取得した接続文字列を設定します。

appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "ApplicationInsights": {
    "ConnectionString": "InstrumentationKey=1518b8e1-ae07-4857-91ca-f11285329241;IngestionEndpoint=https://japaneast-1.in.applicationinsights.azure.com/;LiveEndpoint=https://japaneast.livediagnostics.monitor.azure.com/"
  }
}

]

ここで正しく導入出来ているか一度確認しました。
アプリケーションを実行し、リクエストを何度か送信してみます。

Azure ポータルで Application Insights の対象アプリケーションを開き、ライブメトリック機能で観察してみると、リクエストを送信したタイミングで次のように確認が出来ました。余談ですがライブメトリック良いですね。思ってた以上にライブだった。

また、トランザクション情報を見てみるとリクエストの履歴を確認することも出来ました。

ただし、この時点ではまだどのリクエストがどのテナントかを識別することは出来ません。

テナントコンテキストをテレメトリーに設定してみる

いくつか方法があるようですが、ITelemetryInitializerを実装してミドルウェアを追加しておくと、テレメトリのトラッキングイベントが発生するたびにカスタム初期化処理が実行されるようで、テナントが混在している環境だとこのタイミングでテナントコンテキストを埋め込むのが良さそうです。

またリクエストについてはコンストラクタでIHttpContextAccessorを保持しておくと、リクエスト内容にアクセス出来るようになります。

実際には Authorization のトークンクレーンからテナントコンテキストを取得する形になると思いますが、今回は簡易的に適当にx-hoge-tenantというヘッダーでテナント ID を引き渡すようなリクエストを前提にしてみます。

HogeInitializer.cs

using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;

namespace hoge0106webapi;

public class HogeInitializer : ITelemetryInitializer
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    public HogeInitializer(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public void Initialize(ITelemetry telemetry)
    {
       var requestTelemetry = telemetry as RequestTelemetry;
       if (requestTelemetry == null) return;
       requestTelemetry.Properties["hoge-tenant"] = _httpContextAccessor.HttpContext?.Request.Headers["x-hoge-tenant"];
    }
}

Program.cs

:

builder.Services.AddSwaggerGen();

builder.Services.AddApplicationInsightsTelemetry();
builder.Services.AddSingleton<ITelemetryInitializer, HogeInitializer>();

var app = builder.Build();

:

デバッグ実行してみると、テナント ID は所得出来ているようなので、あとはテレメトリーのカスタムプロパティが送信できていれば良さそうですね。

テナント 1 ~ 3 まででランダムにリクエストを送信してみました。
その後 Application Insights 上でトランザクション検索を行うと、フィルタ条件にカスタムプロパティが指定出来るようになりましたね。特定のテナントを指定して時系列のリクエスト情報を拾うことが出来るようになりました。

また、Log Analytics 側で、試しにAppRequetsテーブルからテナント別のリクエスト状況を可視化してみました。
テナント 1 のリクエスト比率が多いということがわかりますね。

さいごに

本日はマルチテナント SaaS のテナントコンテキストを Application Insights のテレメトリに設定してみました。

確認出来た比率だけを以てコスト算出に紐付けるべきなのかどうかというのは、また別の検討が必要だと思います。
ただ、テナントが混在しているマルチテナントアーキテクチャーな環境でも、テナントコンテキストを送信することでテナントごとのオブザーバビリティを高めることが出来そうだということが今回確認出来ました。