Azure Functions SignalR 出力バインド「ServiceEndpoints is empty」エラーの原因と解決策(dotnet-isolated)

Azure Functions(.NET アイソレーテッド / dotnet-isolated)で Azure SignalR Service の出力バインドを使ったとき、ローカルでも本番でも「ServiceEndpoints is empty」という例外が出て送信できないことがあります。多くの場合、原因はコードで指定した接続文字列のキー名と、アプリ設定(local.settings.json / Function App 構成)に置いたキー名の不一致です。この記事では、仕組みから最短の直し方、再発防止のチェックまでまとめます。

目次

起きていること:ServiceEndpoints is empty とは何か

SignalR の出力バインドは、関数実行時に「どの SignalR Service に接続してメッセージを配信するか」を 接続文字列から判断します。ところが接続文字列が見つからない(null / 空文字 / 空白)場合、配信先のエンドポイント一覧(ServiceEndpoints)が作れず、次のような例外で止まります。

ServiceEndpoints is empty. ConnectionString is null, empty, or consists only of white-space.

ここで重要なのは、SignalR の HubName や Group 名が間違っているケースよりも前に、接続文字列の解決で失敗している点です。つまり「SignalR 自体は動いているのに送れない」のではなく、「そもそも接続先を特定できていない」状態です。

よくある再現パターン

典型的には、属性で接続文字列キーを指定しているのに、設定ファイル側のキー名が別になっていると発生します。質問の状況を整理すると次のイメージです。

場所設定 / コード意味
関数コード(出力バインド属性)ConnectionStringSetting = "SignalRConnection"ランタイムは SignalRConnection という名前のアプリ設定を探す
local.settings.json(または Function App の構成)"AzureSignalRConnectionString": "..."接続文字列は AzureSignalRConnectionString という別名で置かれている
結果不一致SignalRConnection が空扱いになり、例外になる

コード例(問題が起きる書き方の一例):

[Function("SendToGroup")]
[SignalROutput(HubName = "dttelemetry", ConnectionStringSetting = "SignalRConnection")]
public static SignalRMessageAction SendToGroup(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req)
{
    // ...
}

設定例(キー名が一致していない):

{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "AzureWebJobsStorage": "<My AzureWebJobsStorage>",
    "AzureSignalRConnectionString": "<My SignalR Connection string>"
  }
}

原因:ConnectionStringSetting が「参照するキー名」を決める

SignalR 出力バインドの ConnectionStringSetting は、「接続文字列そのもの」を書く場所ではありません。接続文字列を取得するために参照するアプリ設定キー名を指定するためのプロパティです。

つまり、次のルールで動きます。

  • [SignalROutput(..., ConnectionStringSetting = "SignalRConnection")] と書いたら、設定側に SignalRConnection というキーが必須
  • ConnectionStringSetting を省略した場合は、拡張の既定キー(多くのケースで AzureSignalRConnectionString)が使われる

エラーメッセージは「ConnectionString が空」と言っていますが、裏側では「指定されたキー名で探したのに値が取れなかった」ことが原因です。キーが存在しても値が空だったり、スペースだけだったりしても同じ例外になります。

解決策:キー名を揃える(どちらか一方に統一すればOK)

対処はシンプルで、コードと設定のキー名を一致させるだけです。現場で採用しやすいパターンを2つ紹介します。

パターンA:設定ファイルをコードに合わせる(最短で直る)

属性で SignalRConnection を指定しているなら、local.settings.json と本番のアプリ設定も SignalRConnection に統一します。

{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "AzureWebJobsStorage": "<My AzureWebJobsStorage>",
    "SignalRConnection": "<My SignalR Connection string>"
  }
}

本番(Azure ポータル)側も同じキー名にするのがポイントです。Function App の「構成(Configuration)」で、アプリケーション設定に SignalRConnection を追加し、値に接続文字列を入れます。

パターンB:コードを既定のキー名に合わせる(設定を変えたくない場合)

すでに AzureSignalRConnectionString というキー名で運用しているなら、コード側で ConnectionStringSetting を削除して既定のキーを使う方法もあります。

[Function("SendToGroup")]
[SignalROutput(HubName = "dttelemetry")]
public static SignalRMessageAction SendToGroup(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req)
{
    // ...
}

この場合、設定側はそのまま AzureSignalRConnectionString を使えます(ローカル・本番とも同じ)。

どちらを選ぶべきか:運用で迷わないための比較表

観点パターンA(SignalRConnection に統一)パターンB(既定キーを使う)
直しやすさ設定を1か所変えるだけで即反映されやすいコード変更が必要(デプロイが伴う)
わかりやすさ「この関数はこのキーを見る」が明確既定挙動を知っている前提になりやすい
複数環境(Dev/Stg/Prod)環境ごとに同じキー名で値だけ変えられる同左
将来の拡張別 Hub / 別 SignalR を使い分けたいときにキーを増やしやすい既定キーだと追加設計が必要になることがある

結論としては、チーム開発や長期運用では「キー名が明示される」パターンAが事故を減らしやすいです。一方、既定キーで統一している組織ルールがあるならパターンBも合理的です。

ローカルと本番で同じ名前を使う(ここがズレると片方だけ失敗する)

今回のようなエラーは「ローカルでは動いたのに本番で落ちる」「本番は動くのにローカルだけ落ちる」という形でも出ます。その多くは、環境ごとにキー名がズレているか、値が空になっていることが原因です。

チェック項目ローカル(local.settings.json)本番(Function App 構成)
キー名SignalRConnection か AzureSignalRConnectionString のどちらかに統一ローカルと同じキー名
配置場所Values の直下(JSON の階層に注意)アプリケーション設定(App settings)に追加
値空・スペースのみになっていない空・スペースのみになっていない
大文字小文字できるだけ本番と同じ表記に合わせるLinux 環境では環境変数名の大小文字差が事故要因になりやすい

すぐに切り分けたいときのデバッグ方法(秘密は出さない)

「本当にキーが読めていないのか?」を最短で確認するには、接続文字列そのものをログに出さず、存在チェックだけを出します。dotnet-isolated なら Environment.GetEnvironmentVariable で確認できます。

var key = "SignalRConnection"; // または "AzureSignalRConnectionString"
var value = Environment.GetEnvironmentVariable(key);

// 絶対に value をそのままログに出さない(秘密情報)
var isSet = !string.IsNullOrWhiteSpace(value);

logger.LogInformation("SignalR connection setting '{Key}' is {State}",
    key, isSet ? "SET" : "EMPTY");

ログで EMPTY になるなら、コードが参照しているキー名と、設定に置いたキー名が一致していないか、値が空になっています。ここまで確認できれば、あとは「キー名を揃える」だけで解決できます。

落とし穴:local.settings.json の書き方と、Function App 側の配置

初心者がハマりやすいポイントもまとめておきます。

  • local.settings.json は必ず Values 配下に置く(ルート直下に書くと読み込まれません)
  • スペースや改行だけの値でも「空」と判定される(コピペ時の事故が多い)
  • 環境ごとにキー名が微妙に違う(例:ローカルは AzureSignalRConnectionString、本番は SignalRConnection)
  • Windows では動くのに Linux で落ちる(環境変数名の大小文字、起動方法の違いが影響することがあります)

また、Azure ポータルの「構成」には「接続文字列(Connection strings)」という欄もありますが、Functions の拡張がどこから読むかは機能ごとに差があります。迷ったら、まずは アプリケーション設定(App settings)に置くのが無難です。

補足:User Secrets / App Configuration を使いたい場合の考え方

「local.settings.json に接続文字列を置きたくない(コミット事故が怖い)」「開発環境では User Secrets、クラウドでは App Configuration / Key Vault を使いたい」という要望はよくあります。ここで重要なのは、SignalR バインドが見ているのは 最終的な構成に載っているキー名だけだという点です。

つまり、最終的に次のどちらかのキーに値が入っていれば動きます。

  • SignalRConnection(属性で明示している場合)
  • AzureSignalRConnectionString(既定キーを使う場合)

ただし、dotnet-isolated の起動順序や拡張機能の初期化タイミングによっては、ユーザー側で追加した構成プロバイダー(User Secrets など)が読み込みより後に適用され、起動時点で「空」と判定されるケースがあります。回避策は次のいずれかです。

やり方メリット注意点
local.settings.json / 環境変数に置く最も確実。拡張機能が必ず読めるファイル管理(Git 事故)に注意。local.settings.json は基本的にコミットしない
User Secrets を使うローカルの秘密情報をリポジトリ外で管理できる構成追加の順序が重要。できるだけ早い段階で追加する
本番は Key Vault 参照や App Configuration秘密情報を安全に集中管理できる最終的に Function App のアプリ設定として値(または参照)が解決される形にする

「バインドが読めるか不安」という場合は、まずは出力バインドではなく SDK を直接使う設計(送信処理をアプリ側で制御する)に切り替える選択肢もあります。運用要件(秘密情報の管理、送信失敗時の再試行、監査ログ)を考えると、バインドよりも SDK 直叩きの方が扱いやすいケースもあります。

それでも直らないとき:接続文字列そのものを再確認する

キー名を揃えても同じ例外が出る場合は、「キーは読めているが、値が正しい接続文字列になっていない」可能性があります。Azure SignalR Service の接続文字列は、少なくとも Endpoint と AccessKey を含む形式です。たとえば次のような形になります(値はダミーです)。

Endpoint=https://xxxxx.service.signalr.net;AccessKey=xxxxxxxxxxxxxxxx;Version=1.0;

よくある間違いは次のとおりです。

  • 「アクセスキー」だけを貼ってしまい、Endpoint=... が含まれていない
  • 余計なダブルクォートや改行が混ざり、実質的に空文字になっている
  • 別のリソース(別リージョン/別環境)の接続文字列を貼ってしまっている

迷ったら、Azure ポータルで SignalR Service の「キー」画面から 接続文字列(Connection string) をコピーし直すのが確実です。ローカルで試すときも、まずは最小構成(local.settings.json に 1 本だけ置く)で動く状態を作り、そこから秘密管理の仕組みに移行すると事故を減らせます。

CI/CD・スロット運用での実践ポイント

Function App をスロット(staging / production)で運用している場合、設定がスロット間で入れ替わるタイミングで「片方だけ空になっていた」事故が起きがちです。特に SignalR の接続文字列は、送信先が変わるとリアルタイム配信が突然途切れるため、デプロイ前後の確認が重要です。

シーンおすすめ理由
スロットを使う接続文字列の設定を「スロット設定」にするか、環境ごとに完全に分離するスワップで別環境の値が混ざると、意図せぬ配信先に送ってしまう
GitHub Actions / Azure DevOpsデプロイパイプラインで「設定キー名の存在チェック」を入れるコードは通っても設定ミスは実行時まで気づきにくい
複数サービスを併用SignalRConnection のような汎用名より、用途が分かるキー名にする運用担当が見たときに「どの SignalR か」判断できる

「設定キー名の不一致」は初歩的に見えますが、環境が増えるほど発生確率が上がるタイプの不具合です。最初に命名ルールを決め、コードと設定をテンプレ化しておくと、後から参加したメンバーでも迷いません。

再発防止:実装前に押さえるチェックリスト

  • 属性の ConnectionStringSetting と設定キー名が一致している
  • ローカルと本番でキー名が一致している
  • 値が空文字・スペースだけになっていない
  • 誤ってログに接続文字列を出していない
  • (Linux 運用の場合)キー名の大文字小文字を揃えている

まとめ:最初に見るべきは「属性で指定したキー名」

ServiceEndpoints is empty は、SignalR の配信処理以前に「接続文字列が解決できない」ことで起きる例外です。出力バインドを使う場合は、ConnectionStringSetting(または既定キー)と、local.settings.json / Function App 構成のキー名が一致しているかを最優先で確認してください。キー名さえ揃えば、ローカルでもクラウドでも同じ設定で安定して動かせます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次