Azure Storage Queue をトリガーにした Azure Functions は非同期処理に最適ですが、「同じ処理を外部向け HTTP API としても呼び出したい」と悩むことがあります。この記事では“1関数=1トリガー”の制約を前提に、Durable Functions なし/ありで現実的に実現する設計パターンと、運用で詰まりやすいポイントを具体例つきで整理します。
前提整理:なぜ「Queue トリガー関数をそのまま API 化」できないのか
Azure Functions の基本ルールとして、1つの Function(=1つの function.json / エントリポイント)は、1種類のトリガーに紐づきます。つまり、Queue トリガー(キュー到着)と HTTP トリガー(API 呼び出し)を同じ関数として二重に公開することはできません。
ここで重要なのは「同じコードを再利用したい」こと自体は可能で、トリガーは分けて、ビジネスロジックを共通化するのが王道です。以降は、その王道を“Durable Functions を使わない場合”と“Durable Functions を使う場合”で分けて説明します。
「Azure Queue」の読み替え(Storage Queue / Service Bus)
質問文の「Azure Queue」は、現場では次の2系統を指すことが多いです。
| 種類 | Functions のトリガー | 向いている用途 | 読み替えポイント |
|---|---|---|---|
| Azure Storage Queue(Queue Storage) | QueueTrigger | 軽量な非同期処理、シンプルなキューイング | 本記事のサンプルは主にこちら前提 |
| Azure Service Bus Queue | ServiceBusTrigger | 高度なメッセージング(重複検出、セッション、DLQ など) | 構成パターンは同じで、トリガー/クライアントが置き換わる |
どちらでも「トリガーと API を同一関数にまとめられない」点は同じです。以降は Storage Queue を例にしつつ、Service Bus の場合に強い選択肢も補足します。
Durable Functions を使わないベストプラクティス
Durable を使わない場合の結論はシンプルで、HTTP と Queue を別 Function に分ける、そして“処理の本体”を共通化して呼び出すです。ここを外すと、運用負債(タイムアウト、再実行、二重処理、調査不能な障害)が一気に増えます。
全体像:代表的な2パターン
| パターン | 何を実現するか | 向いているケース | 注意点 |
|---|---|---|---|
| パターンA:HTTP でキューに積む + Queue で処理 | 外部 API から非同期処理を起動 | API は即応答(202)で良い/バックエンドが重い | 結果参照をどうするか(別途設計が必要) |
| パターンB:Queue で処理→結果を保存→HTTP で参照 | 非同期処理の結果を後から API で取得 | ジョブ結果・進捗・履歴が必要 | 結果ストア設計(TTL、検索キー、整合性)が重要 |
パターンA:キューにメッセージを積む API + キュー処理関数
外部公開したいのが「処理そのもの」だとしても、HTTP は処理を直接実行するのではなく“キューに積む”役に徹するのが、スケールと障害耐性の面で最も安定します。
アーキテクチャ
クライアント
↓ HTTP POST
HTTP トリガー関数(公開 API)
↓ enqueue
Azure Storage Queue
↓ dequeue
Queue トリガー関数(バックエンド処理)
API 設計の勘所(同期に見せない)
このパターンは「非同期ジョブ起動 API」として設計すると破綻しません。おすすめは次の形です。
- HTTP は 202 Accepted を返す(処理完了を待たない)
- レスポンスに jobId(相関ID) を返す
- 必要なら Location ヘッダで「結果取得 URL(パターンB)」を返す
メッセージ設計(後で必ず効いてくる)
キュー処理を安定運用するため、メッセージには最低限この4つを入れると強いです。
| 項目 | 例 | 目的 |
|---|---|---|
| jobId | UUID | 追跡・再実行・結果参照のキー |
| requestedAt | ISO 8601 | 遅延検知、TTL、監査 |
| operation | GenerateReport | 1つのキューで複数用途を扱う場合の分岐 |
| payload | 必要最小限の JSON | 実処理に必要な入力 |
重要:Azure Storage Queue はメッセージサイズに上限があります。大きな入力(例:数百KBのJSON、画像、CSV本体など)をそのまま積むのではなく、Blob に本体を置いて参照(URL/キー)だけをキューに入れる設計が定番です。これだけで失敗率とコストが下がり、再処理も簡単になります。
実装例(C#:HTTP で enqueue、Queue で処理)
以下は「HTTP と Queue を分け、処理本体を共通化する」最小例です(概念を掴む用のサンプル)。
HTTP トリガー(公開 API):受け取ってキューに積む
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
public static class EnqueueJob
{
[FunctionName("EnqueueJob")]
public static async Task Run(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "jobs")] HttpRequest req,
[Queue("jobs", Connection = "AzureWebJobsStorage")] IAsyncCollector queue,
ILogger log)
{
var body = await new StreamReader(req.Body).ReadToEndAsync();
// 例:最低限のバリデーション(本番ではスキーマ検証や認可も実施)
if (string.IsNullOrWhiteSpace(body))
{
return new BadRequestObjectResult(new { message = "Request body is required." });
}
var jobId = Guid.NewGuid().ToString("N");
var message = JsonSerializer.Serialize(new
{
jobId,
requestedAt = DateTimeOffset.UtcNow,
operation = "ProcessSomething",
payload = JsonSerializer.Deserialize<object>(body)
});
await queue.AddAsync(message);
// 非同期ジョブ起動の形で返す
return new AcceptedObjectResult(new
{
jobId,
status = "accepted",
// パターンBを採用するなら、ここに結果取得URLを入れる
resultUrl = $"/api/jobs/{jobId}"
});
}
}
Queue トリガー(バックエンド処理):共通ロジックを呼ぶ
using System.Text.Json;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
public static class ProcessJobFromQueue
{
[FunctionName("ProcessJobFromQueue")]
public static async Task Run(
[QueueTrigger("jobs", Connection = "AzureWebJobsStorage")] string queueMessage,
ILogger log)
{
var envelope = JsonSerializer.Deserialize(queueMessage);
if (envelope == null)
{
throw new InvalidOperationException("Invalid queue message.");
}
// 重要:再実行されても壊れないように(冪等性)を意識
await JobProcessor.ProcessAsync(envelope, log);
}
private record JobEnvelope(string jobId, DateTimeOffset requestedAt, string operation, object payload);
}
共通処理(ビジネスロジック):HTTP/Queue どちらからも呼べる形に
using Microsoft.Extensions.Logging;
public static class JobProcessor
{
public static async Task ProcessAsync(object envelope, ILogger log)
{
// 実処理をここに集約(DB更新、外部API呼び出し、ファイル生成など)
// 例:重い処理でも、Queue ならタイムアウトや再試行設計がしやすい
await Task.Delay(100); // ダミー
log.LogInformation("Processed job.");
}
}
運用で差がつくポイント(Durable なしでも“壊れない”ために)
少なくとも一度は起きる「二重実行」への備え(冪等性)
Queue ベースの処理は原則として「少なくとも1回実行(at-least-once)」になりやすく、障害やタイミング次第で同じメッセージが再処理される可能性があります。したがって次のどれかを必ず入れてください。
- jobId をキーに重複処理を防ぐ(例:結果ストアに “処理済み” フラグ、または Upsert を活用)
- 外部 API 呼び出しは 冪等キー(Idempotency-Key)を使えるなら使う
- DB 書き込みは 一意制約 + 再試行で安全にする
Poison Queue(毒メッセージ)を前提にする
Azure Functions は、同じメッセージが一定回数失敗すると Poison Queue(例:jobs-poison)へ移動する構成を取りやすいです。ここを前提に、次を決めておくと事故が減ります。
- Poison に落ちたメッセージの再投入手順(手動でも自動でも)
- 失敗理由を追えるようにログ相関(jobId)を徹底
- 入力不正(バリデーション違反)と一時障害(リトライすべき)を例外設計で分ける
host.json のキュー設定で“詰まり”を避ける
負荷が上がると「処理が遅い」「急にリトライが増える」「キューが減らない」などが起きがちです。キュー拡張の設定は最初に触っておく価値があります。
{
"version": "2.0",
"extensions": {
"queues": {
"batchSize": 16,
"newBatchThreshold": 8,
"maxDequeueCount": 5,
"visibilityTimeout": "00:00:30"
}
}
}
数値の最適解はワークロード次第ですが、「処理時間」と「visibilityTimeout」のバランスは特に重要です。処理が30秒を超えるのに visibilityTimeout が30秒だと、同じメッセージが“見える”状態に戻り、二重処理の温床になります。
パターンB:キュー処理結果を API で取得したい場合(ストレージ経由)
「処理結果をクライアントが後で取りに来たい」「進捗も見たい」という要件では、結果をどこかに保存し、HTTP API はそれを参照する形が最も保守しやすいです。Queue トリガー関数が結果を書き込み、HTTP トリガー関数が読み取ります。
アーキテクチャ
Queue
↓
Queue トリガー関数(処理)
↓ write
結果ストア(Table / Blob / Cosmos DB / SQL など)
↑ read
HTTP トリガー関数(結果参照 API)
↑
クライアント
結果ストアの選び方(実務目線)
| 候補 | 向いているケース | 強み | 気をつける点 |
|---|---|---|---|
| Azure Table Storage | ジョブ状態(Accepted/Running/Succeeded/Failed)管理 | 安価、キー検索が速い | 複雑な検索には向かない |
| Blob Storage | 成果物(ファイル)を返したい | 大容量、配信に強い | メタ情報は別途持つと楽 |
| Cosmos DB | 高スループット、柔軟な検索、API での参照が多い | スケールしやすい | コスト最適化に設計が要る |
| SQL Database | トランザクションや参照整合性を重視 | 運用ノウハウが多い | スケール/コストを見ながら |
“状態管理”を先に決めると API が綺麗になる
結果参照 API を作るなら、最低限この状態モデルを持つと運用が安定します。
| 状態 | 意味 | API の返し方例 |
|---|---|---|
| Accepted | 受け付けたが未処理 | 202 + status |
| Running | 処理中 | 200 + progress(任意) |
| Succeeded | 成功 | 200 + result / downloadUrl |
| Failed | 失敗(再試行要/不要も区別できると良い) | 200 または 500 相当を payload で表現 |
実装の要点:Queue 側が「結果ストア更新の責務」を持つ
- Queue トリガー関数は、処理開始時に Running を保存し、成功で Succeeded、例外で Failed を保存する
- HTTP 参照 API は読み取り専用に寄せる(副作用を持たせない)
- 一定期間で削除するなら TTL(有効期限)を設計し、クリーンアップの責務を決める
ここまで作ると、最初の「Queue トリガーの処理を API としても公開したい」という要件は、“実行”は enqueue、 “参照”は GETに分離でき、API と非同期処理が綺麗に噛み合います。
Durable Functions を使う場合の実現方法
Durable Functions を使うと、非同期処理の状態管理・チェックポイント・再試行・オーケストレーションをフレームワークが面倒見てくれるため、要件が増えやすいシステム(進捗、並列処理、多段処理、長時間処理)で特に強力です。
Durable を採用すべき判断基準
| 要件 | Durable なし | Durable あり |
|---|---|---|
| 単発の非同期処理(シンプル) | ◎(最小構成で速い) | ○(過剰な場合も) |
| 複数ステップ(A→B→C) | △(状態管理を自前実装しがち) | ◎(オーケストレーターで自然) |
| 並列処理(ファンアウト/ファンイン) | △(実装/監視が難しくなる) | ◎(パターンとして確立) |
| 進捗照会(ステータス API) | △(結果ストアを自分で作る) | ◎(標準のステータス参照が使える) |
| 長時間実行(分〜時間) | △(制約と運用設計が重い) | ◎(再開/チェックポイントが強い) |
重要:Durable でも「1関数2トリガー」はできない
Durable を使っても、トリガー制約は変わりません。代わりに、次の形で“同じ処理(オーケストレーション)を複数の入口から起動”します。
- HTTP スターター関数(HTTP トリガー)
- Queue スターター関数(Queue トリガー)
- 両者が同じオーケストレーター関数を開始する
- 実処理はアクティビティ関数に集約する
アーキテクチャ
(入口1)クライアント → HTTP スターター関数
↓ start orchestration
オーケストレーター関数
↓ call activity
アクティビティ関数(実処理)
↓
ストレージ/外部サービス
(入口2)Queue → Queue スターター関数
↓ start orchestration
(同じ)オーケストレーター関数へ
実装例(C#:HTTP スターター + オーケストレーター + アクティビティ)
HTTP スターター:オーケストレーション開始 + ステータス URL を返す
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
public static class HttpStart
{
[FunctionName("HttpStart")]
public static async Task Run(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "jobs/durable")] HttpRequest req,
[DurableClient] IDurableOrchestrationClient starter)
{
var body = await new StreamReader(req.Body).ReadToEndAsync();
var instanceId = await starter.StartNewAsync("OrchestrateJob", body);
// Durable の定番:ステータス照会URLを返す(非同期HTTP APIパターン)
return starter.CreateCheckStatusResponse(req, instanceId);
}
}
Queue スターター:キュー到着でも同じオーケストレーションを開始
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
public static class QueueStart
{
[FunctionName("QueueStart")]
public static async Task Run(
[QueueTrigger("jobs", Connection = "AzureWebJobsStorage")] string queueMessage,
[DurableClient] IDurableOrchestrationClient starter)
{
// 同じオーケストレーターを起動できる
await starter.StartNewAsync("OrchestrateJob", queueMessage);
}
}
オーケストレーター:ワークフロー(状態管理は Durable が担当)
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
public static class Orchestrator
{
[FunctionName("OrchestrateJob")]
public static async Task Run(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var input = context.GetInput();
// 例:前処理→本処理→後処理 のように段階化できる
var result = await context.CallActivityAsync<string>("DoWork", input);
return result;
}
}
アクティビティ:実処理(ここにビジネスロジックを集約)
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Microsoft.Extensions.Logging;
public static class Activities
{
[FunctionName("DoWork")]
public static async Task DoWork(
[ActivityTrigger] string input,
ILogger log)
{
// 実処理(DB、外部API、ファイル生成など)
await Task.Delay(100); // ダミー
log.LogInformation("Work completed.");
return "Succeeded";
}
}
Durable 採用時の注意点(導入して終わり、になりがちな落とし穴)
- オーケストレーター関数は“決定論的”に書く(DateTime.Now や乱数、外部呼び出しは原則NG。必要なら context の API やアクティビティに寄せる)
- アクティビティは冪等に(Durable でも再実行は起こり得るため、結局ここが安全性の要)
- ステータス URL を外部公開するなら、認可(トークン/署名/ゲートウェイ)を設計する
- 既存のキュー連携がある場合は、Queue スターターで Durable に寄せると移行しやすい
「同じ処理を Queue と HTTP の両方から呼びたい」を綺麗に満たす設計
ここまでの内容を、要件に対して最短で満たす形にまとめると次の結論になります。
結論:入口は分ける、処理本体は1つに寄せる
| やりたいこと | おすすめ構成 | 理由 |
|---|---|---|
| Queue 到着で処理 | Queue トリガー関数 | 既存の非同期基盤を活かせる |
| 外部から API で同じ処理を起動 | HTTP トリガー関数(enqueue または Durable start) | 公開面(認可/レート制限/入力検証)を分離できる |
| 処理の中身は共通にしたい | 共通ライブラリ or Activity 関数に集約 | 重複実装を防ぎ、テストしやすい |
オリジナル実務Tips:API を「処理関数の別名」にしない
現場でよくある失敗は、HTTP API を「Queue トリガー処理の別名」として作り、HTTP 側でも同じ重い処理を同期実行してしまうことです。これをやると次の問題が同時に起きます。
- HTTP タイムアウトに引っ張られて不安定になる
- ピーク時にスケールせず詰まる
- Queue 経由と HTTP 直叩きで挙動がズレて調査が困難になる
したがって、外部公開 API は基本的に“起動(enqueue/start)”に寄せる、結果が必要なら“参照(GET)”を別に用意する、という分離が最も事故りません。
セキュリティと運用:公開 API にした瞬間に必要になること
認可・入口制御(最低ライン)
- 外部公開するなら、Function のキーだけに依存せず、Azure API Management や認証基盤(Entra ID など)で保護する
- 入力サイズ制限、レート制限、IP 制限/WAF を検討する
- Queue への接続情報はManaged Identityや Key Vault 連携で安全に扱う
監視・トラブルシュート(jobId を中心に考える)
- HTTP 受付ログと Queue 処理ログを jobId で相関できるようにする
- 成功率/失敗率、処理時間、キュー滞留(バックログ)をメトリクス化する
- Poison Queue の監視アラートを最初から入れる(気づいたときには詰まっているため)
まとめ
Azure Functions では、Queue トリガーの関数をそのまま HTTP エンドポイントとして兼用することはできません。最適解は「入口(トリガー)を分離し、処理本体を共通化する」ことです。
- Durable Functions を使わないなら、HTTP→Queue→Queue処理(必要なら結果はストレージ経由で参照)
- ワークフローが複雑・進捗や状態管理が必要なら、Durable Functions(HTTP/Queue から同じ Orchestrator を起動)
どちらを選んでも、冪等性・ログ相関・Poison 対応・入口の認可を押さえると、公開 API と非同期処理が無理なく両立します。

コメント