Azure FunctionsのQueueトリガーをHTTP APIとして公開する設計パターン|Durable Functionsなし/ありで実装解説

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 QueueServiceBusTrigger高度なメッセージング(重複検出、セッション、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つを入れると強いです。

項目例目的
jobIdUUID追跡・再実行・結果参照のキー
requestedAtISO 8601遅延検知、TTL、監査
operationGenerateReport1つのキューで複数用途を扱う場合の分岐
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 と非同期処理が無理なく両立します。

この記事を書いた人

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

コメント

コメントする

目次