Azure Logic AppsでBlob Storageの動的パス読み取りが404になる原因と対処法|Event Grid・タイムアウトPT10M・再試行設計

Azure Logic Apps で Blob Storage に5分間隔で出力されるログ(Log Analytics のエクスポート)を取り込もうとすると、404 Not Found や InvalidResourceName が発生しやすい――そんな現場のつまずきを、原因と解決策に絞って体系化しました。深いディレクトリ構造・動的パス・Event Grid 連携という“壊れやすい三点セット”を、安全に運用へ乗せるための実装レシピと設計指針をまとめます。

目次

問題の概要(要約)

Log Analytics ワークスペースから Storage アカウントへ 5 分粒度でエクスポートされるファイル(実体は *.json)を、Logic Apps(標準/従来いずれも想定)で Event Grid の BlobCreated をトリガーに読み取ろうとしたところ、以下のエラーが発生:

  • 404 Specified resource ... not found.(対象の Blob が見つからない)
  • InvalidResourceName(ファイル名/パスの指定が無効)

対象の Blob は、次のような深い階層で生成されます。

/am-atcexpressroutecircuitipfix/
  WorkspaceResourceId=/subscriptions/<...>/workspaces/<...>/
    y=2025/m=08/d=28/h=07/m=45/PT5M.json

トリガーでは subjectBeginsWith と subjectEndsWith=.json のフィルターを設定。アクション側で List blobs (V2) や Get blob content (V2) を用いたが、404 が継続。最終的には アクションのタイムアウトを PT10M に延長 することで解消。

なぜ 404 が起こるのか(原因の切り分け)

原因カテゴリ典型的な兆候補足説明
イベントと実体のタイミングずれBlobCreated の直後に 404イベント受信時点では、対象ファイルがまだリスト/取得可能状態になっていないことがある(大規模出力・階層作成・インデックス反映)。
パスの組み立て誤りInvalidResourceName、あるいは存在しない minute フォルダを参照式を文字列としてエンコードしてしまう/UTC 時刻から m=mm を算出してズレる/拡張子や末尾スラッシュの有無相違。
URL エンコードの混乱%252F のような二重エンコードが見えるLogic Apps デザイナーは内部でエンコードを扱うため、過剰な手動エンコードが逆効果になる場合がある。
不適切なコネクタ設定接続パラメータセットが keyBasedAuth 固定即時性や権限の問題ではないが、運用では Managed Identity を推奨。権限不足時は 403/401 になる。

結論(再発防止に直結する最小解)

  • アクションのタイムアウトを十分に延長:Get blob content (V2) などファイル取得系は Settings → Timeout を PT10M(10 分)以上に。
  • 再試行ポリシーを有効化:Retry policy を Exponential、最大試行回数・間隔を適切に設定。
  • トリガーのペイロードから「実際の URL」や「subject」を機械的に分解して、現在時刻から推測せずにパスを組み立てる。
  • 「Delay」「Until」パターンで存在確認ループ:Get blob metadata (V2)/Get properties で存在確定まで待つ。

実装の核心:イベントから確実にパスを作る

Event Grid の BlobCreated には、次の 2 つの信頼できる情報源があります。

  1. data.url:https://<account>.blob.core.windows.net/<container>/<blob-path>
  2. subject:/blobServices/default/containers/<container>/blobs/<blob-path>

このどちらかから コンテナー名 と Blob パス を抽出すれば、utcNow() に頼る必要がなくなり、分単位フォルダーのズレ起因の 404 を潰せます。

推奨の式(シンプル & 頑健)

コンテナー名(data.url から):

@{split(triggerBody()?['data']?['url'],'/')[3]}

Blob パス(data.url から):

@{uriComponentToString(join(skip(split(triggerBody()?['data']?['url'],'/'),4), '/'))}

ポイント:

  • split(...,'/') の 0~3 は https:, 空要素, ホスト, コンテナー名。
  • 4 以降が Blob の相対パス。その配列を join(...,'/') で再構築。
  • uriComponentToString() で URL デコード(%2F 等を復元)。

「Get blob content (V2)」の指定例

Azure Blob コネクタの「パス指定」欄に、コンテナー名 + スラッシュ + Blob パス を渡します。

@{concat(
  split(triggerBody()?['data']?['url'],'/')[3],'/',
  uriComponentToString(join(skip(split(triggerBody()?['data']?['url'],'/'),4), '/'))
)}

デザイナーが内部で適切にエンコードするため、ここでさらに手動エンコードを噛ませる必要はありません。二重エンコード(%252F 等)が見えたら、どこかで文字列化→再エンコードしている可能性を疑います。

タイミング問題に効く 3 つの処方箋

  1. アクションのタイムアウト延長(最優先)
    取得アクションの Settings → Timeout を PT10M(10 分)へ。これで大半の 404 は解消します(実際の現場でもこれで収束)。
  2. 再試行ポリシーの設定
    Retry Policy を Exponential にし、初期待機 10~30 秒、最大 5~10 回を基準に。瞬間的な 404 を自動吸収します。
  3. 存在確認ループ(Delay→Exists→Until)
    Get blob metadata (V2) を 404 は再試行 とみなしてループ。exists == true を確認してから Get blob content に進む。

やってはいけない実装(404/400の温床)

  • utcNow() からフォルダー(y/m/d/h/m)を算出:5 分粒度の帳尻が合わず、存在しない minute を指しやすい。
  • 式を文字列としてパスへ埋め込む:(split(...)) のような生文字列をさらにエンコードしてしまうと InvalidResourceName。
  • 手動の二重エンコード:デザイナーがやるべきエンコードを人力で重ねると %252F の地獄に。

改善設計:最小フロー構成

  1. トリガー:When a resource event occurs (Event Grid)
    Filter:subjectBeginsWith=/blobServices/default/containers/<container>/, subjectEndsWith=.json
  2. Compose(任意):BlobUrl = triggerBody().data.url
  3. Blob パス抽出(式):前述の split/join/uriComponentToString
  4. Until ループ(推奨):Get blob metadata (V2) が成功するまで Delay(10~30 秒)。
  5. Get blob content (V2):Timeout=PT10M、Retry Exponential。
  6. HTTP 送信(例:Splunk HEC)
    Chunked 送信やサイズ制限に注意(大きい場合は分割 or イベント単位で送る)。

コード断片(パス抽出~取得~送信)

{
  "actions": {
    "Compose_BlobUrl": {
      "type": "Compose",
      "inputs": "@{triggerBody()?['data']?['url']}"
    },
    "Compose_Container": {
      "type": "Compose",
      "inputs": "@{split(triggerBody()?['data']?['url'],'/')[3]}"
    },
    "Compose_BlobPath": {
      "type": "Compose",
      "inputs": "@{uriComponentToString(join(skip(split(triggerBody()?['data']?['url'],'/'),4), '/'))}"
    },
    "Until_Exists": {
      "type": "Until",
      "expression": "@equals(outputs('Get_metadata')?['statusCode'], 200)",
      "actions": {
        "Get_metadata": {
          "type": "ApiConnection",
          "inputs": {
            "method": "get",
            "path": "/v2/datasets/@{encodeUriComponent(encodeUriComponent('ertrafficcollectorsa'))}/files/@{encodeUriComponent(encodeUriComponent(concat(outputs('Compose_Container'),'/',outputs('Compose_BlobPath'))))}"
          }
        },
        "Delay": { "type": "Wait", "inputs": { "interval": { "count": 20, "unit": "Second" } } }
      }
    },
    "Get_blob_content_V2": {
      "type": "ApiConnection",
      "limit": { "timeout": "PT10M" },
      "retryPolicy": { "type": "exponential", "count": 8, "interval": "PT20S", "minimumInterval": "PT5S", "maximumInterval": "PT1M" },
      "inputs": {
        "method": "get",
        "path": "/v2/datasets/@{encodeUriComponent(encodeUriComponent('ertrafficcollectorsa'))}/files/@{encodeUriComponent(encodeUriComponent(concat(outputs('Compose_Container'),'/',outputs('Compose_BlobPath'))))}/content",
        "queries": { "inferContentType": true }
      }
    }
  }
}

上記は考え方の骨子です。実際にはコネクタ名・接続名は環境に合わせて読み替えてください。

ファイル粒度が 5 分のときの落とし穴と回避策

落とし穴原因回避策
分フォルダ(m=mm)のズレトリガー時刻 ≠ ファイル名の分。丸め誤差や遅延でズレる。必ず data.url から実体のパスを抽出。計算で作らない。
拡張子・末尾スラッシュの相違フォルダをファイル扱い、または .json の抜け。subjectEndsWith=.json で絞込み、抽出後に目視ログ(Compose)で確認。
大規模書き込み直後の 404コンテナーの整合性・インデックス反映待ち。Timeout=PT10M、Retry、Delay/Until の三点セット。
InvalidResourceName式を文字列化してパスに埋めた/括弧など不正文字が先頭に現れた。式は @{...} に入れる。文字列化した式をさらにエンコードしない。

ベストプラクティス(運用で効く小技)

  • Observability:Compose で「コンテナー名」「Blob パス」「完全パス」を必ずログに残す。失敗時の原因特定が一気に早くなる。
  • 死活監視:Run After で 失敗状態→アラート通知 を確実に配線。DLQ/リカバリキューを用意できるならなお良い。
  • セキュリティ:本番は Managed Identity で接続し、最小権限(Blob Data Reader 等)を割り当てる。Key ベースからの移行を計画。
  • 大容量対策:Get blob content はサイズが大きいとタイムアウトの常習犯。可能なら パイプライン(Azure Functions / Data Factory) に任せるか、分割送信。
  • 冪等性:同一 Blob の重複処理を避けるため、Blob の ETag / Last-Modified を処理記録(Cosmos DB 等)に保存し、二重取り込みを抑止。
  • テストモード:本番の巨大ツリーに対しては、まず限定プレフィックス(専用コンテナーやtest/階層)で検証し、式とパス解決を固めてから展開。

設計オプション:時刻に基づくパス組み立てが必要な場合

原則は「イベントから受け取る URL を使う」です。ただし要件上、現在時刻から 5 分刻みのパスを算出する必要があるなら、丸め を忘れないでください。

@{padLeft(string(mul(div(int(formatDateTime(utcNow(),'mm')),5),5)), 2, '0')}

上の式は「現在の分を 5 で割って切り捨て→×5→2 桁ゼロ埋め」で m=00,05,10,...,55 を生成します。ただし、これでもデータ側の遅延には勝てません。やはり Timeout/Retry/Delay の三点は必須です。

チェックリスト(導入〜安定化)

項目確認ポイント推奨設定
Event Grid フィルターsubjectBeginsWith/subjectEndsWith対象コンテナー+.json
パス抽出data.url から分解split+join+uriComponentToString
Timeout取得系アクションの設定PT10M 以上
Retry指数バックオフ初期待機 20s、最大 8〜10 回
待機ループ存在確認 UntilGet metadata 200 になるまで
ロギング抽出値の記録Compose で可視化

トラブルと対処の早見表

エラー/症状一次切り分け即効性のある処置恒久対策
404 Not Foundイベント直後/多発Timeout=PT10M、Retry 有効化Until ループ + パスをイベントから抽出
InvalidResourceName式を文字列でエンコード式を @{} で評価させる手動エンコードの排除、デザイナー任せ
稀に 403/401資格情報の期限切れ接続再作成Managed Identity + RBAC
Splunk 側で受信失敗ペイロード過大/整形不備チャンク送信/分割関数経由で整形・圧縮

セキュリティと運用上の注意

  • 認可:Storage へのアクセスは Storage Blob Data Reader 等の最小権限。Managed Identity を用いて権限をローテーション不要に。
  • 可用性:重大アクション(取得/送信)には Run After で失敗分岐を用意し、通知・リトライ・隔離キューを設ける。
  • 監査:受信イベント ID・Blob ETag・送信ステータスコードを Tracked Properties で埋め込み、KQL/Log Analytics で追跡可能に。

まとめ

「深い階層 × 5 分粒度 × Event Grid」の取り込みは、パスの推測とタイミング依存が 404 の主因です。最短で安定化する鍵は、(1)イベントの data.url から実体パスを抽出する、(2)取得アクションの Timeout=PT10M と再試行を徹底、(3)必要に応じて Until で存在確認してから読む——この三点に尽きます。ここまで押さえれば、実運用でも 404 は実質消滅し、Log→Blob→Logic Apps→外部 SIEM という一連のデータパイプが堅牢に動きます。

付録:よく使う式スニペット集

  • コンテナー名:@{split(triggerBody()?['data']?['url'],'/')[3]}
  • Blob パス:@{uriComponentToString(join(skip(split(triggerBody()?['data']?['url'],'/'),4), '/'))}
  • 完全パス(コンテナー/Blob):@{concat(split(triggerBody()?['data']?['url'],'/')[3],'/',uriComponentToString(join(skip(split(triggerBody()?['data']?['url'],'/'),4), '/')))}
  • 5 分丸め(mm):@{padLeft(string(mul(div(int(formatDateTime(utcNow(),'mm')),5),5)), 2, '0')}

今回の QA から読み取れる本質

最終的な解決は「アクションのタイムアウトを PT10M に延長」であり、これは「イベント通知の到着」と「ストレージでの完全可視化」の間に 時間差があり得る ことを示しています。イベントは「生成された事実」を教えてくれますが、「今すぐ読める」は保証しません。だからこそ、Timeout/Retry/Delay/Until の 4 点セットを組み込んだ、待てる実装が正解です。


導入チェックの実用テンプレ

新規導入時は、以下をコピペして順に検証すると早いです。

  1. トリガーの subjectBeginsWith/subjectEndsWith で対象を絞る。
  2. Compose で data.url を出力(ログ)。
  3. 上記式で コンテナー名 と Blob パス を抽出、さらに 完全パス を作って出力(ログ)。
  4. Get blob metadata を実行し、200 になるまで Delay ループ。
  5. Get blob content を Timeout=PT10M・Retry 有効で実行。
  6. 下流(HTTP 送信など)へ連携。失敗分岐を通知へ接続。

最後に

難しいテクニックは不要です。イベントの事実を使い、待つべきところで待つ。この 2 点を徹底するだけで、Logic Apps と Blob の取り込みは驚くほど安定します。本稿の式と設定をそのまま適用し、まずは 404 を完全に沈めてから、最適化や高機能化に進んでください。

この記事を書いた人

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

コメント

コメントする

目次