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 構成のキー名が一致しているかを最優先で確認してください。キー名さえ揃えば、ローカルでもクラウドでも同じ設定で安定して動かせます。

コメント