こんにちは、サイオステクノロジーの佐藤 陽です。
今回は ManagedIdentity を使って ServiceBus に接続する ServiceBusTrigger の AzureFunctions を、Local で実行する方法をご紹介します。
また記事のポイントとしては、VisualStudioでログインしているユーザーのADテナントと、ServiceBusが存在するテナントが異なるケースを扱っている点です。
少しでも気になった方は最後まで読んでくださいね。
目次
はじめに
冒頭の文章の横文字が渋滞していますが、今回ポイントとなるワードとしては
- Azure Functions
- Service Bus Trigger
- Managed Identity
- Local 実行
- Visual Studio
- .NET
- Cross Tenant
くらいですね。
まず、ServiceBus への Enqueue をトリガーとした AzureFunction(.NET)を実装します。
そして、ServiceBus と AzureFunctions の接続に関しては、ManagedIdentity を利用します。
デプロイ後の接続に関しては、公式のドキュメントにも記載があります。
また、こちらのページにも丁寧に解説がありました。
今回はこれに加えて、
- ローカルの実行時にも実際に Service Bus に接続したい
- ローカルの実行時には、VisualStudio でログインしている AzureAD のアカウント情報を利用したい
- VisualStudio でログインしているアカウントのホームテナントの TenantID と、ServiceBus の存在するテナントの TenantID が異なっている(CrossTenant)
という時のお話をしたいと思います。
特に 3 個目がポイントです。
実装
今回 ServiceBusTrigger の AzureFunctions を以下のように実装します。
public static class ServiceBusTriggerFunction
{
[FunctionName("ServiceBusFunction")]
public static void ServiceBusFunction(
[ServiceBusTrigger("{queue-name}", Connection = "ServiceBusConnection")]
string message,
ILogger log)
{
log.LogInformation($"C# ServiceBus queue trigger function processed message: {message}");
}
}
この時、ローカルでデバッグする際は、local.settings.json の value に以下の内容を追加します。
{
"IsEncrypted": false,
"Values": {
"ServiceBusConnection__fullyQualifiedNamespace": "{servicebus-namespace}.servicebus.windows.net"
}
}
また、ServiceBus の RBAC の設定に、自分の AD アカウントをAzure Service Bus Data Receiverのロールで追加しておきましょう。
こうすれば、ドキュメントにも以下のような記載があるように、Visual Studio 上でログインしている ID が利用され、RBAC の設定が正しく行われていれば ServiceBus に接続できるはずです。
ローカル開発などの他のコンテキストで実行する場合は、代わりに開発者 ID が使用されますが、カスタマイズすることもできます。 ID ベースの接続によるローカル開発に関するページをご覧ください。
ただし、上の方で書いたような
「VisualStudio でログインしているアカウントのホームテナントの TenantID と、ServiceBus の存在するテナントの TenantID が異なっている」
場合には、接続時に大量のエラーが発生します。
一つピックアップすると、以下のような内容です。
System.Private.CoreLib: Put token failed. status-code: 401, status-description: InvalidIssuer: Token issuer is invalid. TrackingId:a63e4325-096c-4257-bd0e-7a389831450d_G19, SystemTracker:NoSystemTracker, Timestamp:2023-05-18T06:30:35.
これを読むと、「ServiceBus のリソースがあるテナント」と「VisualStudio でログインしているアカウントのホームテナント」の不一致により認証が失敗していると予想されます。
解決策
色々ググっていると、こちらの会話の中に書いてありました。
解決策としては、「local.settings.json の value にAZURE_TENANT_IDのプロパティを追加し、ServiceBus のある TenantID を設定する」です。
{
"IsEncrypted": false,
"Values": {
"AZURE_TENANT_ID": "{tenant-id}",
"ServiceBusConnection__fullyQualifiedNamespace": "{servicebus-namespace}.servicebus.windows.net"
}
}
こうすると、指定した TenantID で認証が行われ、ローカルでも実行することができます。
これで、ローカルでもデプロイ後も同じソースコードで実装できるようになりますね。
検証
正しく動作はしたのですが、何が起きているのか気になったので調べてみました。
とはいっても、先ほど載せた GitHub の Issue の会話を見て、公式ドキュメントを合わせ読みしたくらいです。
IssueもMicrosoft の社員らしき方が答えているので、信頼性はそれなりにあるのではと思ってますが
※保証はできないので、そのあたりは自己責任でお願いします。
Azure Functions の Trigger における認証の流れとしては、以下のような感じです。
- Identity を使った Functions のトリガーにおいては、tenantId, clientId, clientSecret を与えることで Credential が取得できる
- Credential 情報が不足している場合は DefaultAzureCredential の認証方法に返される
- DefaultAzureCredential 内で、local.settings.json に記載した tenantID を利用して VisualStudioCredential にて認証を行う
それぞれ、もう少し深堀りしてみます。
Credential 生成
「1.Identity を使った Functions のトリガーにおいては、tenantId, clientId, clientSecret を与えることで Credential が取得できる」
に関しては、Issueに以下のような言及がありました。
Setting only a tenantId is not a supported configuration for the Functions triggers when using Identity.
この時、Settingといっている対象が、おそらくここに書かれている以下 3 つの事ではないかと予想しています。
- <CONNECTION_NAME_PREFIX>__tenantId
- <CONNECTION_NAME_PREFIX>__clientId
- <CONNECTION_NAME_PREFIX>__clientSecret
この 3 つを Functions のトリガーとして与えることで、Credential を取得することができるようです。
ただ、今回はManagedIdentityを使っていることからも、Secretが存在していないかと思うので、今回はスルーします。
DefaultAzureCredential への Fall back
「2.Credential 情報が不足している場合は DefaultAzureCredential の認証方法に返される」
に関しては、Issueに以下のような言及がありました。
If you only set the tenantId, the trigger will end up falling back to the DefaultAzureCredential.
つまり先ほど述べた、Credential 取得に必要な情報 3 つのうち、1つでもかけている場合は、DefaultAzureCredential を利用するようになるとのことです。
DefaultAzureCredential での認証
最後に、
「3.DefaultAzureCredential 内で、local.settings.json に記載した tenantID を利用して VisualStudioCredential にて認証を行う」
についてです。
今回、DefaultAzureCredential に関する詳細な説明は省きますが、
ManagedIdentity を使った接続によく使われる Credential 取得方法になります。
そして [Environment Credential] -> [VisualStudio Credential] -> [hogehogeCredential] といったように、順々に既定された認証方法を試みるようになっています。
そこで、「今回成功しているケースでは、どの認証方法が利用されているのか?」を調査しました。
前提として
"AZURE_TENANT_ID": "{tenant-id}"
の設定が行われているものとして検証を行います。
また、local.settings.json の Value に以下のパラメータを追加し、詳細なログを表示させます。
"AzureFunctionsJobHost__logging__logLevel__Default": "Debug",
すると、関数実行時に以下のログが表示されました。(一部抜粋)
EnvironmentCredential.GetToken was unable to retrieve an access token. Scopes: [ https://servicebus.azure.net/.default ] ParentRequestId: Exception: Azure.Identity.CredentialUnavailableException (0x80131500):
VisualStudioCredential.GetToken succeeded
つまり、Environment Credential での認証 は失敗し、その後に行われる VisualStudio Credential によって Token が取得されるようです。
確かに、 Environment Credential に必要なパラメータは渡せていないので失敗するのは納得ですが、VisualStudio Credential で成功してしまうのは少し意外でした。
というのも、Visual Studio Credential において使用するテナントを特別指定していません。
AZURE_TENANT_IDにてテナントを指定していますが、この値は Environment Credential 時に用いられるパラメータであるため、VisualStudio Credential には使われないという理解です。
そのため結局、接続したいテナントには接続できずに401エラーが発生するものと思っていました。
しかし、Issue でも以下のように言及されており
Interestingly, if the AZURE_TENANT_ID environment variable is set when your code runs and it does not match the tenantId for the user with which you’re logged into Visual Studio, VisualStudioCredential will try to authenticate your stored user identity against this specified tenant.
どうやら、VisualStudio Credential の場合でも、環境変数であるAZURE_TENANT_IDで指定した TenantID にて認証を試みる振る舞いをするようです。
ただ公式のドキュメントを探しても、ここの部分に関して言及が見つからなかったため、急に仕様が変わる可能性もありそうです。
余談
少し話は逸れますが、本来 DefaultAzureCredential には、DefaultAureCredentialOptionという Option が設定可能であり、その中にTenantIdというパラメータが用意されています。
このTenantIDに、接続したいテナントのID を与えることで、明示的に接続するテナントを指定することができます。
ただ、今回の ServiceBus のケースにおいては、暗黙的に DefaultAzureCredential が利用されるため、DefaultAzureCredentialOption を利用できない状況でした。
まとめ
今回は ローカル環境における ServiceBus への接続方法を調べてみました。
とりあえず、ローカルでデバッグする際には、local.settings.json にAZURE_TENANT_IDを追加することで接続できるようになりました。
ただ、
- Issue を読んだだけの判断であり、あまり自分の手元で検証できていない
- Identity を使った認証方法に関して、理解が曖昧な部分もある
といった感じで、不安の残る結果になったため、もう少し調査してみたいと思います。
ではまた!