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 つの信頼できる情報源があります。
data.url:https://<account>.blob.core.windows.net/<container>/<blob-path>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 つの処方箋
- アクションのタイムアウト延長(最優先)
取得アクションのSettings→TimeoutをPT10M(10 分)へ。これで大半の 404 は解消します(実際の現場でもこれで収束)。 - 再試行ポリシーの設定
Retry Policyを Exponential にし、初期待機 10~30 秒、最大 5~10 回を基準に。瞬間的な 404 を自動吸収します。 - 存在確認ループ(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の地獄に。
改善設計:最小フロー構成
- トリガー:
When a resource event occurs (Event Grid)
Filter:subjectBeginsWith=/blobServices/default/containers/<container>/,subjectEndsWith=.json - Compose(任意):
BlobUrl = triggerBody().data.url - Blob パス抽出(式):前述の
split/join/uriComponentToString - Until ループ(推奨):
Get blob metadata (V2)が成功するまでDelay(10~30 秒)。 - Get blob content (V2):Timeout=PT10M、Retry Exponential。
- 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 回 |
| 待機ループ | 存在確認 Until | Get 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 点セットを組み込んだ、待てる実装が正解です。
導入チェックの実用テンプレ
新規導入時は、以下をコピペして順に検証すると早いです。
- トリガーの
subjectBeginsWith/subjectEndsWithで対象を絞る。 Composeでdata.urlを出力(ログ)。- 上記式で コンテナー名 と Blob パス を抽出、さらに 完全パス を作って出力(ログ)。
Get blob metadataを実行し、200 になるまでDelayループ。Get blob contentをTimeout=PT10M・Retry 有効で実行。- 下流(HTTP 送信など)へ連携。失敗分岐を通知へ接続。
最後に
難しいテクニックは不要です。イベントの事実を使い、待つべきところで待つ。この 2 点を徹底するだけで、Logic Apps と Blob の取り込みは驚くほど安定します。本稿の式と設定をそのまま適用し、まずは 404 を完全に沈めてから、最適化や高機能化に進んでください。

コメント