Azure IoT Hub のファイルアップロードを C のデバイスエージェントから開始し、実際の大容量アップロードは別プロセスに任せたい——そんな構成で詰まりやすいのが 403006(アクティブなファイルアップロード要求の上限超過)です。本記事では IoTHubClient_Auth_Get_SasToken の扱いと、確実に枠を解放する通知設計を整理します。
結論: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)に登場する点です。ここを分離して考えると実装が一気に安定します。
| 要素 | 中身 | 主な用途 | どこに渡すべきか | よくある勘違い |
|---|---|---|---|---|
| azureBlobSasUri | Azure 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 で行う |
| エージェントがクラッシュ/再起動 | 復帰後に枠が残って 403006 | correlationId が失われ通知できない | correlationId と対象ファイルを永続化(ジャーナル)し、再起動後に失敗通知を送る |
| 並列で Initialize を投げすぎる | 短時間で上限到達 | 枠の消費が通知より速い | キュー/セマフォで同時要求数を制御し、外部アップロードの進捗に合わせて流量制御 |
推奨フロー:外部プロセスに渡すのは azureBlobSasUri だけ、通知は SDK で必ず行う
推奨構成はシンプルです。外部プロセスは「Storage にアップロードする係」に徹し、IoT Hub への通知は認証情報を持つデバイスエージェントが行います。
- デバイスエージェントが
IoTHubDeviceClient_LL_AzureStorageInitializeBlobUploadを呼び、azureBlobSasUriとcorrelationIdを取得 - 外部プロセスへ渡すのは azureBlobSasUri(必要なら correlationId とローカルのジョブ ID) のみ
- 外部プロセスが Storage へアップロード(成功/失敗/タイムアウトを結果として返す)
- デバイスエージェントが
IoTHubDeviceClient_LL_AzureStorageNotifyBlobUploadCompletionを呼び、correlationIdで結果を通知
この設計だと、外部プロセスにデバイスキーや接続文字列を一切渡さずに済みます。信頼境界が崩れないため、セキュリティレビューや監査にも強い構成になります。
外部プロセス連携で「何を渡して良いか」早見表
| 渡すもの | 推奨 | 理由 | 注意点 |
|---|---|---|---|
| azureBlobSasUri | ◎ | Storage へアップロードするために必要 | クエリ文字列を含むため秘密情報。ログに出すならマスキング |
| correlationId | ○ | ジョブの突合に便利(通知はエージェント側でも保持) | 改ざんされると別ジョブを誤通知しうるため、IPC を保護する |
| デバイス接続文字列 | × | IoT Hub への完全な認証情報。漏えい時の被害が大きい | 外部プロセスの権限範囲が一気に広がる |
| デバイスキー | × | REST 通知の SAS を作れる=IoT Hub への操作が可能 | やむを得ずなら、署名専用の最小サービスで隔離 |
| iothubowner の SAS | × | ハブ全権。最悪の事故パターン | 「動く」だけで採用すると後で必ず問題になる |
実装で差が出るポイント:finally、永続化、並列度制御
通知は「成功したときだけ」ではなく「必ず」
403006 を根絶する最短ルートは、通知を例外安全にすることです。成功時だけでなく、失敗・タイムアウト・外部プロセス異常終了など、あらゆる分岐で通知が走るようにします。
- 外部アップロード開始前に「このジョブは通知が必要」という状態を保存する
- 外部アップロード結果が不明なら「失敗」として通知して枠を返す
- 通知自体が失敗した場合はリトライ(ただし無限ループではなくバックオフ)
correlationId を失うと復旧が難しいので、ジャーナルを持つ
意外と多いのが、エージェントが再起動した瞬間に correlationId が消えて、枠を解放できず詰むケースです。以下のような最小限のジャーナル(永続化)を持つと、復旧が現実的になります。
| 項目 | 保存例 | 用途 |
|---|---|---|
| correlationId | 文字列 | Notify に必須 |
| ローカルファイルパス | /var/spool/iot/upload/xxx.bin | 再送・調査のため |
| 作成日時/期限 | ISO8601 | SAS 期限切れ時の判断 |
| 状態 | 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"
}'
通知パラメータの決め方(おすすめ)
| シーン | isSuccess | statusCode 例 | statusDescription 例 |
|---|---|---|---|
| アップロード成功 | true | 200 | File uploaded successfully |
| Storage 側 403(SAS 期限切れ等) | false | 403 | Storage authorization failed (SAS expired) |
| ネットワークタイムアウト | false | 504 | Upload timed out |
| 外部プロセス異常終了 | false | 500 | External uploader crashed (exit code=…) |
| ローカルファイル不存在 | false | 404 | Local 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 が必要で鍵の取り扱いリスクが上がる。鍵を広げない設計(通知だけ代理実行)が現実的

コメント