Azure IoT Hubファイルアップロードの403006を回避する方法|NotifyBlobUploadCompletionとSAS設計

Azure IoT Hub のファイルアップロードを C のデバイスエージェントから開始し、実際の大容量アップロードは別プロセスに任せたい——そんな構成で詰まりやすいのが 403006(アクティブなファイルアップロード要求の上限超過)です。本記事では IoTHubClient_Auth_Get_SasToken の扱いと、確実に枠を解放する通知設計を整理します。

日程Fit。無料・登録不要。「いつ空いてる?」を、ひとつのリンクで。リンクを送って、○△×でかんたん日程調整。無料で日程を作る。
目次

結論:IoTHubClient_Auth_Get_SasToken で「今の SAS トークンを抜く」は前提として成立しない

IoTHubClient_Auth_Get_SasToken は Azure IoT C SDK の内部ヘルパーであり、公開 API としての互換性が保証されません。つまり「デバイスクライアントから現在利用中の SAS を取り出して外部プロセスに渡す」という発想は、長期運用の設計としては成立しません。

やるべきことは SDK の契約どおりに、Initialize →(外部でアップロード)→ Notify のフローを守ることです。これだけで、403006 の主要因である「アクティブ枠の解放漏れ」を潰せます。

まず押さえる:IoT Hub の「ファイルアップロード」は 3 つの別物でできている

混乱の根本は、似た言葉が 2 種類の認証(Storage と IoT Hub)に登場する点です。ここを分離して考えると実装が一気に安定します。

要素中身主な用途どこに渡すべきかよくある勘違い
azureBlobSasUriAzure Blob Storage への一時アクセス URL(SAS 付き)実ファイルを Blob に PUT/UPLOAD する外部プロセスへ渡してよい(ただし秘密情報として扱う)「これで IoT Hub の通知もできる」→ できない
correlationIdアップロード要求と通知を結びつける相関 ID完了通知で IoT Hub に「どの要求の結果か」を伝える外部プロセスへ渡してよい(ただし改ざん防止)「Storage のパスに関係する」→ 直接は関係しない
IoT Hub への SAS(デバイススコープ)resourceUri に対する署名付きトークン/files/notifications を REST で叩くときの認証原則、デバイスエージェント内に閉じ込める「iothubowner を使えば楽」→ 権限過大で NG

重要:azureBlobSasUri は Storage 用の入口であって、IoT Hub へ「完了しました」を伝える鍵ではありません。IoT Hub 側の管理(アクティブ枠の解放、監査、後続処理)は、IoTHubDeviceClient_LL_AzureStorageNotifyBlobUploadCompletion(または同等の REST 通知)でのみ完結します。

403006 が起きるメカニズム:通知しないと「アクティブ枠」が残り続ける

IoT Hub は、デバイスあたり同時に保持できるファイルアップロード要求(Initialize 済みで完了通知待ちの状態)に上限を持ちます。IoTHubDeviceClient_LL_AzureStorageInitializeBlobUpload はその枠を 1 つ消費し、NotifyBlobUploadCompletion(SDK/REST)が来たタイミングで枠が戻ります。

外部プロセスでのアップロードに成功しても、失敗しても、通知しなければ枠が戻りません。その結果、次の Initialize が 403006 で弾かれます。

よくある状況現象原因現場で効く対策
外部プロセスが成功したが通知を書き忘れしばらく動いた後に 403006 が連発枠が解放されず蓄積成功/失敗に関係なく finally 相当で必ず通知
外部プロセスがタイムアウト/強制終了アップロード結果が不明で詰む通知が来ないタイムアウト時は失敗として通知し、再送は新規 Initialize で行う
エージェントがクラッシュ/再起動復帰後に枠が残って 403006correlationId が失われ通知できないcorrelationId と対象ファイルを永続化(ジャーナル)し、再起動後に失敗通知を送る
並列で Initialize を投げすぎる短時間で上限到達枠の消費が通知より速いキュー/セマフォで同時要求数を制御し、外部アップロードの進捗に合わせて流量制御

推奨フロー:外部プロセスに渡すのは azureBlobSasUri だけ、通知は SDK で必ず行う

推奨構成はシンプルです。外部プロセスは「Storage にアップロードする係」に徹し、IoT Hub への通知は認証情報を持つデバイスエージェントが行います。

  1. デバイスエージェントが IoTHubDeviceClient_LL_AzureStorageInitializeBlobUpload を呼び、azureBlobSasUricorrelationId を取得
  2. 外部プロセスへ渡すのは azureBlobSasUri(必要なら correlationId とローカルのジョブ ID) のみ
  3. 外部プロセスが Storage へアップロード(成功/失敗/タイムアウトを結果として返す)
  4. デバイスエージェントが IoTHubDeviceClient_LL_AzureStorageNotifyBlobUploadCompletion を呼び、correlationId で結果を通知

この設計だと、外部プロセスにデバイスキーや接続文字列を一切渡さずに済みます。信頼境界が崩れないため、セキュリティレビューや監査にも強い構成になります。

外部プロセス連携で「何を渡して良いか」早見表

渡すもの推奨理由注意点
azureBlobSasUriStorage へアップロードするために必要クエリ文字列を含むため秘密情報。ログに出すならマスキング
correlationIdジョブの突合に便利(通知はエージェント側でも保持)改ざんされると別ジョブを誤通知しうるため、IPC を保護する
デバイス接続文字列×IoT Hub への完全な認証情報。漏えい時の被害が大きい外部プロセスの権限範囲が一気に広がる
デバイスキー×REST 通知の SAS を作れる=IoT Hub への操作が可能やむを得ずなら、署名専用の最小サービスで隔離
iothubowner の SAS×ハブ全権。最悪の事故パターン「動く」だけで採用すると後で必ず問題になる

実装で差が出るポイント:finally、永続化、並列度制御

通知は「成功したときだけ」ではなく「必ず」

403006 を根絶する最短ルートは、通知を例外安全にすることです。成功時だけでなく、失敗・タイムアウト・外部プロセス異常終了など、あらゆる分岐で通知が走るようにします。

  • 外部アップロード開始前に「このジョブは通知が必要」という状態を保存する
  • 外部アップロード結果が不明なら「失敗」として通知して枠を返す
  • 通知自体が失敗した場合はリトライ(ただし無限ループではなくバックオフ)

correlationId を失うと復旧が難しいので、ジャーナルを持つ

意外と多いのが、エージェントが再起動した瞬間に correlationId が消えて、枠を解放できず詰むケースです。以下のような最小限のジャーナル(永続化)を持つと、復旧が現実的になります。

項目保存例用途
correlationId文字列Notify に必須
ローカルファイルパス/var/spool/iot/upload/xxx.bin再送・調査のため
作成日時/期限ISO8601SAS 期限切れ時の判断
状態initialized / uploading / notified再起動後の再開ポイント
最終エラーHTTP 403 / timeout などstatusDescription の材料

再起動時は「initialized なのに notified になっていない」レコードを拾い、外部アップロードが完了していないなら失敗通知を送って枠だけでも返す、という運用が可能になります。

並列度制御は「Initialize の数」ではなく「未通知の数」で行う

外部アップロードを高速化したいと、つい Initialize を先に大量発行しがちです。しかし IoT Hub の上限にぶつかるのはここです。おすすめは、「未通知のジョブ数」をセマフォで制御し、枠が開くペースに合わせて初期化する方式です。

  • 未通知ジョブ数が上限近辺なら新規 Initialize を止める
  • 外部アップロードが遅いなら並列数を下げる(回線やディスクが詰まっている可能性)
  • 逆にアップロードが速いなら、Notify 呼び出しの失敗率(スロットリング等)も監視する

参考実装(C 風の擬似コード):外部アップロードの成否に関わらず通知する

質問の意図に沿って、最小構成のイメージです。実際には戻り値、メモリ解放、リトライ、ジャーナル永続化などを追加してください。

IOTHUB_CLIENT_RESULT r =
  IoTHubDeviceClient_LL_AzureStorageInitializeBlobUpload(
    device_ll_handle,
    path,
    &correlationId,
    &azureBlobSasUri);

if (r != IOTHUB_CLIENT_OK) {
  // ログ + リトライ方針(バックオフ)など
  return;
}

// 外部プロセスに渡す(推奨:azureBlobSasUri のみ)
ExternalUploaderJob job = {
  .sasUri = azureBlobSasUri,
  .localPath = path
};

// 例:待ち合わせ(同期でも非同期でもよい)
ExternalUploadResult res = ExternalUploader_Run(job);

// 成功/失敗に関係なく、必ず通知して「アクティブ枠」を戻す
bool ok = (res.ok == true);

IoTHubDeviceClient_LL_AzureStorageNotifyBlobUploadCompletion(
  device_ll_handle,
  correlationId,
  ok,                              // isSuccess
  ok ? 200 : res.httpStatus,        // statusCode(例)
  ok ? "File uploaded successfully" // statusDescription
     : res.message
);

// ※azureBlobSasUri / correlationId の解放は SDK の仕様に従って実装

外部プロセスでのアップロード実務:SAS の扱いで事故らない

azureBlobSasUri は「短命だから雑に扱って良い」わけではありません。期限内であれば第三者もアクセスできるため、パスワード同等として扱います。

  • ログ出力する場合はクエリ文字列(sig など)をマスキングする
  • プロセス間通信で渡すなら、平文ファイルではなく OS の保護機構(名前付きパイプ、UNIX ドメインソケット、共有メモリ + ACL など)を使う
  • アップロードツールに渡す場合も、コマンドライン引数は監視ツールに残りやすいので注意(可能なら環境変数や一時ファイルで渡し、すぐ削除)
  • 外部プロセスがクラッシュして URL が残る設計は避け、ジョブ終了時に確実に破棄する

どうしても REST で /devices/{deviceId}/files/notifications を叩きたい場合

「外部プロセスがすべて完結させたい」「言語や実行環境の都合で SDK が使えない」など、やむを得ない事情がある場合の代替案です。ただし、外部プロセスが IoT Hub に対して認証できるようにするため、どこかでデバイスキー(または同等の秘密情報)を扱う必要が出ます。

絶対に避けたい:iothubowner(全権ポリシー)を Authorization に使う

skn=iothubowner の SAS はハブ全体を操作でき、漏えい時の被害が「1 デバイス」では済みません。開発中の動作確認用途でもログや履歴に残りやすく危険です。

REST 通知で必要になるのは「デバイススコープ SAS」

/files/notifications はデバイススコープ API なので、resourceUri を {hub}.azure-devices.net/devices/{deviceId} として SAS を作ります。概念としては以下です。

項目ポイント
resourceUri (sr){hub}.azure-devices.net/devices/{deviceId}URL エンコードして署名文字列に使う
expiry (se)UNIX epoch 秒短め推奨。長くすると漏えい時の被害が伸びる
signature (sig)HMAC-SHA256鍵はデバイスキー(Base64 をデコードして使う)
skn(通常なし)デバイスキーで作る SAS ではポリシー名が不要なことが多い

署名に使う文字列は一般に urlEncode(resourceUri) + "\n" + expiry です。これを HMAC-SHA256 し Base64 化し、さらに URL エンコードして sig に入れます。

REST 通知の最小例(概念)

curl -X POST \
  "https://{hub}.azure-devices.net/devices/{deviceId}/files/notifications?api-version=2021-04-12" \
  -H "Authorization: SharedAccessSignature sr={hub}.azure-devices.net%2Fdevices%2F{deviceId}&sig=...&se=..." \
  -H "content-type: application/json" \
  -d '{
        "correlationId": "<Initialize で得た値>",
        "isSuccess": true,
        "statusCode": 200,
        "statusDescription": "File uploaded successfully"
      }'

通知パラメータの決め方(おすすめ)

シーンisSuccessstatusCode 例statusDescription 例
アップロード成功true200File uploaded successfully
Storage 側 403(SAS 期限切れ等)false403Storage authorization failed (SAS expired)
ネットワークタイムアウトfalse504Upload timed out
外部プロセス異常終了false500External uploader crashed (exit code=…)
ローカルファイル不存在false404Local file not found

「外部プロセスに REST を叩かせたい」場合でも鍵を広げない設計案

どうしても外部側で通知まで完結させたい場合、最大のリスクはデバイスキーの拡散です。現場で取り得る妥協案を、危険度の低い順に並べます。

概要メリットデメリット/注意
エージェントが通知だけ代理実行外部プロセス→IPC で結果送信→エージェントが SDK Notifyデバイスキーを外に出さない。推奨フローに近いIPC 実装が必要(ただし小さい)
署名専用のローカルサービス外部プロセスは SAS 生成をローカルに依頼し、通知は外部が実行鍵の保護を 1 箇所に集約できるサービスの認可・監査が必要。SAS の受け渡しも秘密情報
外部プロセスにデバイスキーを埋め込む外部が直接 SAS を作って REST実装は早い漏えい時の被害が大きい。更新も手間。基本的に避ける

403006 を二度と踏まないための実務チェックリスト

チェック項目狙い実装メモ
成功/失敗に関わらず必ず Notify するアクティブ枠の確実な解放finally 相当、クラッシュ時の復旧も考慮
外部アップロードのタイムアウトを設ける「永遠に枠を掴む」状態を回避タイムアウト時は失敗通知 + 再試行は新規 Initialize
correlationId をジャーナルに保存する再起動後に枠を返せるinitialized のまま残ったら失敗通知する運用が可能
未通知ジョブ数で並列度を制御する上限超過を構造的に防止キュー + セマフォ + バックプレッシャ
azureBlobSasUri を秘密情報として扱う情報漏えいの抑止ログマスキング、引数/環境変数の扱い、権限分離
iothubowner を使わない権限過大による事故防止REST を使うならデバイススコープ SAS

まとめ:Initialize → 外部アップロード → Notify を守れば 403006 は設計で潰せる

  • IoTHubClient_Auth_Get_SasToken は内部ヘルパーで、デバイスクライアントから「現在の SAS」を取り出す正式手段にはならない
  • azureBlobSasUri は Storage 用。IoT Hub への完了通知(枠解放)は NotifyBlobUploadCompletion(SDK/REST)が担当
  • 403006 の主因は「通知しない」こと。成功/失敗に関わらず必ず通知し、未通知数で並列度を制御する
  • REST での直接通知は可能だが、デバイススコープ SAS が必要で鍵の取り扱いリスクが上がる。鍵を広げない設計(通知だけ代理実行)が現実的

この記事を書いた人

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

コメント

コメントする

目次