paho-mqttでAzure IoT Hubへ送ったテレメトリが、メッセージルーティングの$body条件に一致しない。さらにTLS証明書移行で接続できなくなる。現場で詰まりやすい2点を、MQTTトピックの$.ct/$.ce指定とルート証明書更新の実装例で解決します。
paho-mqttで送信したメッセージがIoT Hubのルーティングで「JSON本文」として扱われない原因
Pythonのpaho-mqttクライアントからMQTTでAzure IoT Hubへ送信しているのに、IoT Hubのメッセージルーティングで$body.levelのようなJSON本文条件が効かず、想定したエンドポイントに振り分けられない――この症状は「MQTTのどこにプロパティを載せるべきか」を取り違えると高確率で起きます。
結論から言うと、IoT Hubは一般的なフル機能MQTTブローカーとは違い、デバイス向けMQTTはMQTT v3.1.1を前提に、決め打ちのトピック形式でメッセージを受け取ります。そのため「MQTT v5のUser Propertiesを付ければContent-Typeが伝わるはず」という発想が、そのままIoT Hubには当てはまりません。
最初に押さえるべき前提:本文ルーティングはcontentType/contentEncodingが鍵
IoT Hubのメッセージルーティングでは、メッセージ本文がJSONとして解釈できる状態でないと、$bodyを使った条件式は評価されません。具体的には、メッセージ本文がJSONで、エンコードがUTF系であることに加え、システムプロパティとしてcontentType(application/JSON)とcontentEncoding(UTF-8/16/32のいずれか)が指定されている必要があります。指定がない場合、IoT Hubは本文に対する条件式を評価しません。
また、Blob Storageなど一部のエンドポイントでは、contentType/contentEncodingの指定が不十分だと「JSONとして保存したいのに、Base64エンコードされたバイナリ扱いになる」形で気付くことがあります。ルーティングが当たらないだけでなく、保存形式にも影響する点が落とし穴です。
| やりたいこと | IoT Hub側で必要な情報 | MQTTでの指定場所(IoT Hub仕様) | ルーティング条件の例 |
|---|---|---|---|
| 本文をJSONとして扱い、$bodyで絞り込みたい | contentType / contentEncoding | トピック末尾のプロパティバッグ($.ct / $.ce) | $body.level > 700 |
| StorageにJSONで保存したい | contentType / contentEncoding(特にcontentEncoding) | トピック末尾のプロパティバッグ($.ct / $.ce) | $contentType = 'application/json' |
| 独自のアプリプロパティで振り分けたい | 任意のアプリプロパティ | トピック末尾のプロパティバッグ(key=value) | processingPath = 'hot' |
IoT HubのMQTTは「トピック末尾」にプロパティを付ける
IoT HubへMQTTでテレメトリ(Device-to-Cloud)を送る場合、基本トピックは次の形式です。
devices/{device-id}/messages/events/
そして、IoT Hubの仕様では、追加プロパティはMQTTパケットのヘッダーに載せるのではなく、トピック名の末尾に「プロパティバッグ」として付加します。プロパティバッグはクエリ文字列に近い形式で、&区切り・URLエンコード(RFC 2396相当)を使います。
JSONとして扱わせたい場合は、システムプロパティに対応するキーとして$.ct(contentType)と$.ce(contentEncoding)を付けます。たとえば、Blob StorageエンドポイントでJSONエンコーディングを使うケースでは、$.ct=application%2Fjson&$.ce=utf-8のように指定します。
なお、IoT Hubのメッセージングはプロパティ名の扱いが大文字小文字を区別しないため、同名プロパティを大小違い(例:Levelとlevel)で送ると、後段で「片方しか見えない」ように見えることがあります。アプリプロパティは命名を統一し、重複しないキーに絞るのが安全です。
さらに、本文ルーティング($body)を確実に成立させるためのシンプルな指定として、$.ctにcharset込みでapplication/json;charset=utf-8を設定する方法がドキュメント例として示されています。
実装の要点:payloadは必ずJSON文字列(json.dumps)で送る
paho-mqttのpublish()にPythonのdictをそのまま渡すと、Pythonの辞書表現(シングルクォートなど)で文字列化され、厳密なJSONではなくなることがあります。IoT Hubのルーティングで「本文がJSON」として扱わせるなら、必ずjson.dumps()でJSON文字列にして送るのが安全です。
もう1点、ルーティング条件で数値比較をするなら、JSONの型も重要です。"level": 750(数値)なら$body.level > 700の評価が期待どおりに動きますが、"level": "750"(文字列)だと一致しない原因になります。
Python(paho-mqtt)での正しいトピック組み立て例
ポイントは2つです。
- トピック末尾に
$.ct(必要なら$.ce)を付ける $.ctの値(例:application/json;charset=utf-8)はURLエンコードする
import json
from urllib.parse import quote
device_id = "<device-id>"
base_topic = f"devices/{device_id}/messages/events/"
# 本文ルーティングを狙うなら、まずはctにcharset込みで指定するのが分かりやすい
ct_value = "application/json;charset=utf-8"
ct_encoded = quote(ct_value, safe="") # / ; = なども含めてエンコード
# 必要に応じてcontentEncodingも明示(StorageのJSONエンコーディング等で役立つ)
topic = base_topic + f"$.ct={ct_encoded}&$.ce=utf-8"
payload_obj = {
"datetime": "2023-03-30 06:20:30",
"RaR": 123,
"level": 750,
"id": "1234"
}
payload_json = json.dumps(payload_obj, ensure_ascii=False)
client.publish(topic, payload_json, qos=1)
URLエンコードで迷うポイント:何をエンコードするのか
IoT Hub向けのMQTTでは、$.ct/$.ceというキーはそのまま使い、値側をエンコードするのが分かりやすいです。特にapplication/json;charset=utf-8のように/や;を含む値は、エンコードしないとトピックとして解釈が崩れます。
| 元の文字 | 意味 | エンコード例 |
|---|---|---|
| / | MIMEタイプ区切り | %2F |
| ; | パラメータ区切り(charsetなど) | %3B |
| = | キーと値の区切り | %3D |
| 半角スペース | 値に含まれると崩れやすい | %20 |
プロパティバッグを関数化して「付け忘れ」を防ぐ
現場運用では「特定ルートだけJSON本文ルーティングしたい」「アプリプロパティも混ぜたい」など、トピック組み立てが分岐しがちです。組み立てロジックを関数にしておくと、付け忘れ・エンコード漏れが減ります。
from urllib.parse import quote
def build_iothub_topic(device_id, *, ct=None, ce=None, app_props=None):
base = f"devices/{device_id}/messages/events/"
bag = []
if ct:
bag.append(f"$.ct={quote(ct, safe='')}")
if ce:
bag.append(f"$.ce={quote(ce, safe='')}")
# アプリプロパティ(ルーティング条件で processingPath='hot' のように参照する想定)
if app_props:
for k, v in app_props.items():
k_enc = quote(str(k), safe="")
v_enc = quote(str(v), safe="")
bag.append(f"{k_enc}={v_enc}")
return base + "&".join(bag) if bag else base
topic = build_iothub_topic(
"my-device",
ct="application/json;charset=utf-8",
ce="utf-8",
app_props={"processingPath": "hot", "schema": "v1"}
)
ルーティング条件の具体例(本文+プロパティ)
contentType/contentEncodingは大文字小文字を区別しないため、送信側はapplication/jsonやUTF-8など表記ゆれがあっても動くことが多いです。ただし、運用で迷わないために「値の表記を固定する」ことをおすすめします(例:contentTypeはapplication/json、contentEncodingはUTF-8)。
本文がJSONとして扱える状態になったら、ルーティングクエリで次のような条件が書けます。
$body.level > 700
「JSON以外が混ざる可能性がある」「将来別フォーマットも来る」など、運用上の混在を想定するなら、システムプロパティも一緒に条件に入れると事故が減ります。
$contentType = 'application/json' AND $contentEncoding = 'UTF-8' AND $body.level > 700
よくある失敗と原因切り分け(最短で直すための表)
| 症状 | ありがちな原因 | まず試す修正 |
|---|---|---|
| $body条件が一切効かない | contentType/contentEncoding未指定 | トピックに$.ct(必要なら$.ce)を追加 |
| 本文がJSONのはずなのに当たらない | dict直送でJSONではない/文字列型になっている | json.dumps()で送信、数値は数値で送る |
| StorageにJSONで保存されない | contentEncoding不足でBase64扱い | $.ce=utf-8を追加 |
| 接続できるがすぐ切断される | QoS 2でpublish/subscribeしている | QoS 0/1に変更(IoT HubはQoS2非対応) |
| 同じdeviceIdで2台目が繋ぐと1台目が落ちる | 同一deviceIdで複数接続 | 同時接続を避ける(IoT Hubは1デバイス1接続が基本) |
プロパティの確認方法:表示されないツールに注意
「トピックに$.ct/$.ceを付けたのに、監視ツールでcontent-type/content-encodingが見えない」ことがあります。表示系の都合でシステムプロパティが省略されるケースがあるため、検証時はルーティング先(StorageやEvent Hubs互換エンドポイントなど)で確認するのが確実です。
IoT Hub MQTT固有の制限も知っておくとトラブルが減る
「プロパティの話は直ったのに、なぜか安定しない」という場合は、IoT HubのMQTT仕様上の制限に引っかかっていることがあります。代表例は次のとおりです。
- QoS 2はサポートされず、QoS2でpublishすると接続が閉じられる
- Retainメッセージはブローカーのように保持されない
- 同一deviceIdでアクティブ接続は1つ(新規接続で既存が落ちる)
MQTT v5のUser Propertiesだけでは解決できない理由
paho-mqttでUserPropertyを付けたくなる場面は多いのですが、IoT Hubのデバイス向けMQTTはMQTT v3.1.1が前提であり、MQTT v5のUser Propertiesを「contentType/contentEncodingの指定」としては扱いません。IoT Hubで本文ルーティングを成立させる目的なら、User Propertiesではなく、トピック末尾の$.ct/$.ceを優先してください。
なお、一般的なMQTTブローカー(MQTT v5対応)向けにUser Propertiesを使う場合は、paho-mqtt側もMQTT v5で接続し、PUBLISHパケット用のPropertiesを使う必要があります。IoT Hub向けとは考え方が異なる点に注意してください。
Azure IoT Hubのサーバー証明書移行とルート証明書ファイルの更新ポイント
MQTTでIoT Hubへ直接接続する場合、通信はTLS必須です。実装例ではtls_set()でルート証明書ファイル(例:digicert.cer)を指定しますが、過去にBaltimore CyberTrust Rootを同梱していた実装は、証明書移行のタイミングで接続断のリスクがあります。
移行はいつ起きたのか:2024年9月30日までに完了
Azure IoT Hub(およびDPSなど)は、Baltimore CyberTrust Root(2025年に失効)からDigiCert Global Root G2へ段階的に移行してきました。公式ドキュメントでは、2024年9月30日時点で移行が完了しており、以降はAzure IoTの対象リソースでBaltimoreルートは使われていません。
つまり、2025年以降の現場運用としては「旧ルートと新ルートが両方有効な猶予期間に頼る」よりも、すみやかにDigiCert Global Root G2を信頼ストアへ追加(または差し替え)することが現実的な対策になります。
結論:paho-mqttのコードは概ねそのまま、CAファイルの中身を更新
MQTT接続ロジックやSASトークン生成ロジック自体は、証明書移行で変わりません。変更点は、TLS検証に使うCA(ルート証明書)です。paho-mqttのtls_set()で指定しているca_certsは「ファイルパス」なので、パスを変えずにファイルの中身(PEM)だけ更新する運用が取りやすいです。
path_to_root_cert = "<local path to root CA bundle>"
client.tls_set(
ca_certs=path_to_root_cert,
certfile=None,
keyfile=None,
cert_reqs=ssl.CERT_REQUIRED,
tls_version=ssl.PROTOCOL_TLSv1_2,
ciphers=None
)
推奨:DigiCert Global Root G2+Microsoft RSA Root CA 2017の2本をバンドル
公式ガイダンスでは、DigiCert Global Root G2に加えて、将来の予期しないルート更新に備える目的でMicrosoft RSA Root Certificate Authority 2017も信頼することが推奨されています。組み込み機器やコンテナなど「OSの証明書ストアを頼れない」構成では、2本をまとめたCAバンドル(PEMを連結したファイル)を用意して配布するのが実務的です。
| デバイス/実装形態 | 影響の出やすさ | 推奨対応 |
|---|---|---|
| Windows/LinuxなどOSの証明書ストアを使用 | 低(OS更新で追従しやすい) | OSのCA更新を優先。証明書ピン留めは避ける |
| コンテナ(最小イメージ) | 中(ca-certificates未導入など) | CAバンドルを含める/ca-certificatesを導入 |
| 組み込み機器(ルート証明書をファームに同梱) | 高(更新しないと接続不可) | DigiCert Global Root G2+Microsoft RSA Root 2017を同梱 |
証明書のピン留めを避ける(中間CA/リーフ固定は危険)
移行ドキュメントでは、公開ルート(Public Root)で検証し、中間証明書やリーフ証明書にピン留めしないことが推奨されています。中間CAはローテーションされることがあり、ピン留めしていると突然接続できなくなるためです。
「.cerが読み込めない」問題を避ける:PEM形式を意識する
paho-mqtt(OpenSSL)は一般にPEM形式(-----BEGIN CERTIFICATE-----~)を想定します。Windowsからエクスポートした.cerがDER形式の場合、そのままでは読み込めず接続に失敗することがあります。現場での事故を避けるには、配布するCAファイルの形式(PEM/DER)を統一し、必要ならOpenSSLでPEMへ変換する運用を用意しておくと安全です。
更新後の動作確認:TLSハンドシェイクとAzure側ログを見る
証明書を更新したら、まずは端末上でTLSハンドシェイクが成立するかを確認します。OpenSSLが使える環境なら、次のようなコマンドでサーバー証明書チェーンを確認できます(証明書チェーンが更新されているか、想定ルートに到達できるかをチェック)。
openssl s_client -connect <hubname>.azure-devices.net:8883 -servername <hubname>.azure-devices.net -showcerts
また、Azure Monitor(接続ログ)で「切断→再接続」が大量に発生していないか、接続に失敗したデバイスが残っていないかを併せて確認すると、現場の取りこぼしが減ります。
将来の接続断を減らすための追加チェック:TLS 1.2と強い暗号スイート
証明書だけでなく、TLSのバージョンや暗号スイートも将来の互換性に直結します。IoT HubではTLS 1.0/1.1のサポート終了と、弱い暗号スイートの廃止が2025年8月31日にアナウンスされています。古いTLSスタックの機器は、証明書を更新しても接続できない可能性があるため、証明書更新とセットでTLS 1.2互換性の検証を行ってください。
実務向けまとめチェックリスト
- IoT Hubで本文ルーティング($body)を使うなら、本文はJSONで送る(dict直渡しではなくjson.dumps)
- MQTTでは、contentType/contentEncoding相当は「トピック末尾のプロパティバッグ」で指定する($.ct / $.ce)
- $.ctの値はURLエンコードする(application/json;charset=utf-8など)
- StorageでJSON保存する場合はcontentEncoding不足でBase64扱いになり得るので、$.ceも付ける
- 接続が不安定なときは、QoS2や同一deviceId複数接続などIoT Hub特有の制限も疑う
- 証明書はDigiCert Global Root G2へ移行済み(2024/09/30完了)。Baltimore前提の機器は更新必須
- 推奨ルートCAはDigiCert Global Root G2+Microsoft RSA Root CA 2017(CAバンドル化が運用しやすい)
- TLS 1.2と強い暗号スイートに対応しているかを、証明書更新と同時に検証する

コメント