Azure Container Apps Jobsで非同期ジョブ型アプリケーションをデプロイする
サイバーアゴットは、LLM の長時間処理を伴う非同期ジョブ型アプリケーションを Azure Container Apps Jobs で構築し、Managed Identity を活用した認証一元化のアーキテクチャを公開した。
キーポイント
非同期ジョブによる UX 改善
LLM の長時間処理によりブラウザが応答待ちになる従来構成に対し、Azure Container Apps Jobs を導入して非同期実行を実現し、ユーザーは即座に別の作業へ移れるようにした。
Azure 環境での認証一元化
Managed Identity を採用することで、DB アクセスから LLM 呼び出しまでパスワードや API キーを環境変数に埋め込むことなく安全かつ一元的な認証管理を実現した。
長時間処理への対応要件
LLM の生成やワークフロー全体で数十分〜数時間かかるケースに対応するため、タイムアウトなく実行可能なジョブ基盤の選定が最重要課題となった。
Azure Container Apps Jobs の非同期実行特性
HTTP接続を保持せずジョブとして非同期に実行できるため、長時間処理に適しており、フロントのContainer Appと同じ環境で運用可能です。
Durable Functions との比較と選定理由
既存ワークフローをステップ単位に書き直す必要がある Durable Functions に比べ、コンテナベースのジョブ実行として柔軟性が高い点が評価されました。
App Service や Foundry の不適合性
App Service は HTTP サーバー前提で長時間処理に向かず、Foundry は動的なエージェント挙動が主用途のため、決定的な LLM 逐次実行のジョブ化には旨味が薄いとしています。
Container Apps Jobs の採用理由
LLM 監視の自由度(Langfuse等の組み込み)、ゼロスケールによるコスト削減、マネージドなジョブ基盤(リトライ・ログ機能)を理由に Durable Functions を選択せずこちらを採用しました。
重要な引用
本来であれば、ユーザーは LLM の処理を開始した後にタブを閉じて別の作業を行い、完了後に結果を確認できる状態が理想です。
Managed Identity を利用することで、DB アクセスから LLM 呼び出しまで、パスワードや API キーを環境変数に埋め込むことなく認証を行えます。
HTTP接続を保持せず、ジョブとして非同期に実行できる
動的なエージェント挙動が主用途のため、決定的な LLM 逐次実行のジョブ化には旨味が薄い
「ブラウザ側のポーリングは処理中の間だけ動き、完了すれば自動で止まるようにしています」
「この設計のおかげで、フロント・Worker・DB を疎結合に保つことが可能となりました」
影響分析・編集コメントを表示
影響分析
本記事は、生成 AI アプリケーション開発において頻発する「長時間処理によるユーザー体験の悪化」という課題に対し、Azure の具体的な機能(Container Apps Jobs)とベストプラクティス(Managed Identity)を組み合わせた実用的な解決策を提示しています。特に、大規模な LLM ワークフローを運用する現場におけるアーキテクチャ設計の参考となり、セキュリティと UX の両立を実現する手法として即座に適用可能な知見を提供します。
編集コメント
LLM アプリケーション開発において、長時間処理を伴うワークフローの設計は必須課題であり、本記事はその具体的な実装パターンとセキュリティ対策を示す貴重な事例です。特に Managed Identity を活用した認証管理の記述は、大規模システム構築におけるセキュリティリスク低減に直結する重要な知見と言えます。
はじめに
こんにちは。MIU AI戦略本部でAIエンジニアをしている田中宏樹です。
LLMを活用したアプリケーションでは、長時間の思考を必要とするタスクが複数組み合わさることで、処理時間が伸びてしまうケースが多くあります。
本記事では、こうした長時間処理を必要とするアプリケーションをAzure上で構築した際に採用したアーキテクチャや工夫点をご紹介します。
対象読者
- 長い処理時間を要するアプリケーションを作りたい人
- 非同期ジョブ型アプリケーションをAzureでデプロイしたい人
- 非同期ジョブの実行に適しているAzureサービスが何かを知りたい人
背景
LLMの長いレスポンスをタブを開いたまま待ちたくない
私が所属するチームではAIの技術調査の一環として、超長尺の文章作成を行うWebアプリケーションを開発しています。
imageLLM 完了までブラウザが応答待ちになる従来構成
このアプリケーションはNext.jsで構築されており、ブラウザからServer Actionを呼び出し、その内部でLLMによる文章生成が行われる構成となっていました。
しかし、生成結果を後続の入力として利用するというアプリケーションの特性上、LLMの実行を並列化するのが難しく、ユーザーがタブを開いたまま実行を待たなくてはいけないという課題がありました。
本来であれば、ユーザーはLLMの処理を開始した後にタブを閉じて別の作業を行い、完了後に結果を確認できる状態が理想です。
そこで、LLMの処理をジョブ化して非同期実行する仕組みが必要になりました。
今回のアプリケーションでは、Azure OpenAI Service経由でLLMを利用していました。そのため、アプリケーションのデプロイ先もAzureに統一することとしました。
これにより、Azureリソース間の認証をManaged Identityで行えるようになり、アプリケーション全体の認証を一元化することが可能になりました。
このようなジョブ形式の構成では、実行主体が増えることで認証情報の管理が複雑になりがちです。
しかしManaged Identityを利用することで、DBアクセスからLLM呼び出しまで、パスワードやAPIキーを環境変数に埋め込むことなく認証を行えます。
Managed Identityの詳細については、技術選定のセクションで詳しく紹介します。
技術選定
フロント + ジョブ実行基盤の選定
本章では、ジョブ化を実現するためのAzureリソースについて比較検討した結果を示します。
ジョブ実行をクラウド上で実現するためには、フロントエンドに即座にレスポンスを返し、それをトリガーとして非同期でLLMを実行する仕組みが必要でした。
その上で、最優先とした条件は「長時間処理との相性」です。
LLM単体の文章生成には数分〜数十分かかる場合があり、それらを組み合わせたワークフロー全体では数十分〜数時間ほどかかっていました。
そのため、それらのジョブをタイムアウトなく実行できることをリソースの必須条件として比較検討しました。
なお、以下は2026年4月時点の情報に基づく比較です。各サービスの仕様は変更される可能性があるため、最新情報は各公式ドキュメントを参照してください。
リソース
長時間処理との相性
**特徴と評価**
○
- 直列・並列・リトライといったワークフロー制御が標準機能として揃っている
- 既存ワークフローをステップ単位の関数に書き直す必要がある
○
- HTTP接続を保持せず、ジョブとして非同期に実行できる
- フロントのContainer Appと同じ環境で動かせる
△
- エージェントホスティング基盤として優秀。LLMトレーシングがOpenTelemetryベースで組み込み済み
- 動的なエージェント挙動が主用途のため、決定的な LLM 逐次実行のジョブ化には旨味が薄い
✕
- 安定したWebホスティング基盤で短時間応答のフロントに向いている
- HTTPサーバー前提のため、今回のジョブ実行用途には向いていない
比較の結果、候補に上がったのはDurable FunctionsとContainer Apps Jobsの2つでした。
Azure Durable Functions
Azure Durable Functionsは、Azure Functionsをベースにしたワークフロー実行基盤で、複数の処理を状態付きでオーケストレーションできるサービスです。
オーケストレーター関数を中心に複数の処理を順序立てて実行できるとともに、内部的に状態を永続化する仕組みを持っているため、長時間にわたる処理でも途中状態を保持したまま再開可能という特徴もあります。
また、外部イベントの待機やリトライ、分岐、並列実行といった制御フローをコードで表現できるため、複雑な非同期ワークフローの構築に適しています。
Durable Functions のドキュメント – Azure Durable | Microsoft Learn
Container Apps Jobs
Azure Container Apps Jobsは、コンテナを単位としてバッチ処理や非同期ジョブを実行できるマネージドサービスです。
任意のコンテナを実行できるため、言語や実行環境に制約がなく、既存の処理をそのままジョブとして実行できます。
ジョブはトリガーによって起動され、処理が完了すると終了するシンプルな実行モデルであるため、長時間にわたる処理をそのまま1つのジョブとして扱いやすいです。
さらに、CPU やメモリの割り当ても柔軟に設定できるため、LLM のような計算負荷の高い処理とも相性が良いです。
Azure Container Apps のジョブ | Microsoft Learn
今回採用したサービス
検討した結果、我々のチームでは以下の理由で Container Apps Jobs を採用しました。
- LLM 監視の自由度: コンテナ上に任意のトレーシングツール(Langfuse 等)を組み込み、LLM 入出力を詳細に監視できる
- ゼロスケールによるコスト低減: ジョブ非実行時はレプリカ数がゼロになり課金が発生しない
- ジョブ基盤のマネージド化: リトライ・タイムアウト・ログ収集がビルトインで、自前実装が不要
- 環境の共有: フロントの Container App と同じ Container Apps Environment で動くため、ネットワーク・ログ・シークレット基盤を共通化できる
Durable Functions はワークフロー制御が標準機能として揃っている一方で、既存ワークフローをステップ単位の関数に書き直す必要があり、移行コストが見合いませんでした。
Managed Identity によるパスワードレス認証
続いて、「Azure を採用したことによるメリット」の章で触れた認証の一元化について、具体的な構成を紹介します。
本アプリケーションでは、リソース間の認証に Microsoft Entra ID(旧 Azure AD)の Managed Identity を採用しました。
Managed Identity は、Azure の仮想マシンやアプリホスティング基盤に割り当てることができる ID であり、その ID に対してロールベースでアクセス権限を設定できます。これにより、従来のようにパスワードや API キーといった長期的な認証情報を管理する必要がなくなります。
今回の構成では、PostgreSQL Flexible Server において Entra ID 認証を有効化し、パスワード認証を無効化しています。
また、Key Vault(シークレット管理)・Azure Container Registry(ACR)(コンテナイメージの管理)・Storage(生成結果などのデータ保存)へのアクセスもすべて Managed Identity に統一しました。これにより、アプリケーションコードや環境変数にシークレットを埋め込むことなく、安全に各リソースへアクセスできるようになっています。
さらに、CI/CD においても、GitHub Actions の OIDC Federated Credentials を利用することで、Azure 側にシークレットを保存せずに認証を行う構成としました。これにより、リポジトリに長期的な認証情報を保持する必要がなくなり、セキュリティリスクの低減と運用負荷の軽減を両立しています。
このように Managed Identity を中心に据えた構成にすることで、Azure リソース間の認証においては長期的な認証情報を一切持たずに運用できるようになりました。
アーキテクチャの全体像
今回採用したアーキテクチャの全体像を示します。
image Container Apps Jobs に LLM 処理を切り離した採用構成
ブラウザからのリクエストは Container Apps(Next.js Web App)で受け、生成タスクを PostgreSQL に記録したのちに、Container Apps Jobs(Worker)を起動します。
Worker を起動したら、その完了を待たずに即座にレスポンスを返すので、ブラウザは LLM の実行完了を待つ必要がありません。
Worker は Container Apps Jobs を Manual Trigger で動かしていて、Container Apps(Next.js Web App)から Azure SDK 経由で直接起動しています。
さらに、ブラウザと Worker は直接接続せずに、PostgreSQL だけを介してやり取りする形にしました。
WebSocket や SSE で進捗を双方向に流す選択肢もありましたが、「ブラウザを閉じても処理を続けたい」「結果も進捗も永続化したい」という要件から、Worker は DB に書くだけ、ブラウザは DB を読むだけ、というシンプルな構造にしています。
そのため、ブラウザ側のポーリングは処理中の間だけ動き、完了すれば自動で止まるようにしています。
この設計のおかげで、フロント・Worker・DB を疎結合に保つことが可能となりました。
また、ユーザーがタブを閉じて翌日戻ってきても結果がそのまま残っているという体験も実現できました。
工夫点
ここでは、Azure へのデプロイや実装周りで工夫した点についてお話ししていきます。
ポーリング + visibilitychange での結果確認
ジョブをバックエンドに切り離して非同期で実行する場合、フロントエンドから Worker の完了をどう検知するかが課題になります。
今回はシンプルなポーリング方式を採用しました。
useEffect(() => {
if (!status || !POLLING_STATUSES.has(status)) return;
let isFetching = false;
const fetchLatest = async () => {
if (isFetching) return;
isFetching = true;
try {
const data = await getDetail(id);
if (!data) return;
setData(parseData(data));
} finally {
isFetching = false;
}
};
const interval = setInterval(fetchLatest, POLLING_INTERVAL_MS);
const onVisible = () => {
if (document.visibilityState === "visible") fetchLatest();
};
document.addEventListener("visibilitychange", onVisible);
return () => {
clearInterval(interval);
document.removeEventListener("visibilitychange", onVisible);
};
}, [status, id]);
ポイントは2つです。
- ステータスが処理中の間だけポーリングする: 完了後は無駄なリクエストを発生させない
- visibilitychange イベントでタブ復帰時に即時フェッチ: タブに戻ったときに最大ポーリング間隔分の待ちが発生するため、visibilitychange で即座にフェッチし、この待ち時間を解消している
LLMワークフローの生成時間が数分〜数十分単位なので、WebSocketのようなリアルタイム通知は不要と判断し、シンプルなポーリングを採用しました。ポーリング間隔は、サーバー負荷とUXのバランスで決めています。
パスワードレス認証下でのDBマイグレーション
本プロジェクトではORMにPrismaを採用しており、アプリ実行時は、PrismaのDriver Adapter機能を使用しています。
これは、Prismaに pg などのNode.jsライブラリを組み込める拡張ポイントです。
pg には「接続時に動的にパスワードを取得するコールバック機能」があり、ここにEntra IDトークン取得処理を組み込むことで、接続のたびに新しいトークンでPostgreSQLに接続しています。
一方、DBマイグレーション(スキーマ変更)はCDパイプラインの中で prisma migrate deploy コマンドを実行する形で行っています。
ここで問題になるのが、このCLIコマンドは独立したバイナリとして動作するため、アプリ内で設定したDriver Adapterを認識できない点です。そのため動的なトークン取得ができず、DATABASE_URL にトークンを自分で埋め込んで渡す必要があります。
そこで、マイグレーション実行前にEntra IDトークンを取得し、DATABASE_URL のパスワード部分に注入するラッパースクリプトを作成しました。
// scripts/migrate-deploy.ts
async function main() {
if (process.env.AZURE_CLIENT_ID && process.env.DATABASE_URL) {
const credential = new DefaultAzureCredential({
managedIdentityClientId: process.env.AZURE_CLIENT_ID,
});
const tokenResponse = await credential.getToken(
"https://ossrdbms-aad.database.windows.net/.default",
);
const url = new URL(process.env.DATABASE_URL);
url.password = tokenResponse.token;
process.env.DATABASE_URL = url.toString();
}
}
execSync("npx prisma migrate deploy", {
stdio: "inherit",
env: process.env,
});
}
Managed Identity で取得した短命の Entra ID トークンをパスワードとして DATABASE_URL に埋め込むことで、prisma migrate deploy のような CLI ツールでもパスワードレスで動かせます。
PostgreSQL だけでなく、Entra ID 認証に対応した他の Azure リソースでも同じ手法が使えます。
まとめ
LLM ワークフローのように処理時間が長いアプリケーションでは、「ブラウザから処理を切り離す」ことが重要です。
本記事で紹介した Azure Container Apps Jobs + Managed Identity の構成により、以下を実現できました。
- タブを閉じても処理が継続する:Server Action で即座にレスポンスを返し、Container Apps Jobs でバックエンド処理を非同期実行
- アプリケーション全体の認証を統一:Managed Identity + Entra ID で、DB アクセスから LLM 呼び出しまでキーレスで実現
同じような課題に取り組む方の参考になれば幸いです。
参考
- Jobs in Azure Container Apps
- Managed identities in Azure Container Apps
- Microsoft Entra authentication for Azure Database for PostgreSQL
- Configuring OpenID Connect in Azure (GitHub Actions)
- Page Visibility API – MDN
原文を表示
はじめに
こんにちは。MIU AI戦略本部でAIエンジニアをしている田中宏樹です。
LLMを活用したアプリケーションでは、長時間の思考を必要とするタスクが複数組み合わさることで、処理時間が伸びてしまうケースが多くあります。
本記事では、こうした長時間処理を必要とするアプリケーションをAzure上で構築した際に採用したアーキテクチャや工夫点をご紹介します。
対象読者
- 長い処理時間を要するアプリケーションを作りたい人
- 非同期ジョブ型アプリケーションをAzureでデプロイしたい人
- 非同期ジョブの実行に適しているAzureサービスが何かを知りたい人
背景
LLMの長いレスポンスをタブを開いたまま待ちたくない
私が所属するチームではAIの技術調査の一環として、超長尺の文章作成を行うWebアプリケーションを開発しています。

このアプリケーションはNext.jsで構築されており、ブラウザからServer Actionを呼び出し、その内部でLLMによる文章生成が行われる構成となっていました。
しかし、生成結果を後続の入力として利用するというアプリケーションの特性上、LLMの実行を並列化するのが難しく、ユーザーがタブを開いたまま実行を待たなくてはいけないという課題がありました。
本来であれば、ユーザーはLLMの処理を開始した後にタブを閉じて別の作業を行い、完了後に結果を確認できる状態が理想です。
そこで、LLMの処理をジョブ化して非同期実行する仕組みが必要になりました。
今回のアプリケーションでは、Azure OpenAI Service経由でLLMを利用していました。そのため、アプリケーションのデプロイ先もAzureに統一することとしました。
これにより、Azureリソース間の認証をManaged Identityで行えるようになり、アプリケーション全体の認証を一元化することが可能になりました。
このようなジョブ形式の構成では、実行主体が増えることで認証情報の管理が複雑になりがちです。
しかしManaged Identityを利用することで、DBアクセスからLLM呼び出しまで、パスワードやAPIキーを環境変数に埋め込むことなく認証を行えます。
Managed Identityの詳細については、技術選定のセクションで詳しく紹介します。
技術選定
フロント + ジョブ実行基盤の選定
本章では、ジョブ化を実現するためのAzureリソースについて比較検討した結果を示します。
ジョブ実行をクラウド上で実現するためには、フロントエンドに即座にレスポンスを返し、それをトリガーとして非同期でLLMを実行する仕組みが必要でした。
その上で、最優先とした条件は 「長時間処理との相性」 です。
LLM単体の文章生成には数分〜数十分かかる場合があり、それらを組み合わせたワークフロー全体では数十分〜数時間ほどかかっていました。
そのため、それらのジョブをタイムアウトなく実行できることをリソースの必須条件として比較検討しました。
なお、以下は2026年4月時点の情報に基づく比較です。各サービスの仕様は変更される可能性があるため、最新情報は各公式ドキュメントを参照してください。
リソース
長時間処理との相性
**特徴と評価**
○
- 直列・並列・リトライといったワークフロー制御が標準機能として揃っている
- 既存ワークフローをステップ単位の関数に書き直す必要がある
○
- HTTP接続を保持せず、ジョブとして非同期に実行できる
- フロントのContainer Appと同じ環境で動かせる
△
- エージェントホスティング基盤として優秀。LLMトレーシングがOpenTelemetryベースで組み込み済み
- 動的なエージェント挙動が主用途のため、決定的な LLM 逐次実行のジョブ化には旨味が薄い
✕
- 安定したWebホスティング基盤で短時間応答のフロントに向いている
- HTTPサーバー前提のため、今回のジョブ実行用途には向いていない
比較の結果、候補に上がったのはDurable FunctionsとContainer Apps Jobsの2つでした。
Azure Durable Functions
Azure Durable Functionsは、Azure Functionsをベースにしたワークフロー実行基盤で、複数の処理を状態付きでオーケストレーションできるサービスです。
オーケストレーター関数を中心に複数の処理を順序立てて実行できるとともに、内部的に状態を永続化する仕組みを持っているため、長時間にわたる処理でも途中状態を保持したまま再開可能という特徴もあります。
また、外部イベントの待機やリトライ、分岐、並列実行といった制御フローをコードで表現できるため、複雑な非同期ワークフローの構築に適しています。
Durable Functions のドキュメント – Azure Durable | Microsoft Learn
Container Apps Jobs
Azure Container Apps Jobsは、コンテナを単位としてバッチ処理や非同期ジョブを実行できるマネージドサービスです。
任意のコンテナを実行できるため、言語や実行環境に制約がなく、既存の処理をそのままジョブとして実行できます。
ジョブはトリガーによって起動され、処理が完了すると終了するシンプルな実行モデルであるため、長時間にわたる処理をそのまま1つのジョブとして扱いやすいです。
さらに、CPUやメモリの割り当ても柔軟に設定できるため、LLMのような計算負荷の高い処理とも相性が良いです。
Azure Container Apps のジョブ | Microsoft Learn
今回採用したサービス
検討した結果、我々のチームでは以下の理由でContainer Apps Jobsを採用しました。
- LLM監視の自由度: コンテナ上に任意のトレーシングツール(Langfuse等)を組み込み、LLM入出力を詳細に監視できる
- ゼロスケールによるコスト低減: ジョブ非実行時はレプリカ数がゼロになり課金が発生しない
- ジョブ基盤のマネージド化: リトライ・タイムアウト・ログ収集がビルトインで、自前実装が不要
- 環境の共有: フロントのContainer Appと同じContainer Apps Environmentで動くため、ネットワーク・ログ・シークレット基盤を共通化できる
Durable Functionsはワークフロー制御が標準機能として揃っている一方で、既存ワークフローをステップ単位の関数に書き直す必要があり、移行コストが見合いませんでした。
Managed Identityによるパスワードレス認証
続いて、「Azureを採用したことによるメリット」の章で触れた認証の一元化について、具体的な構成を紹介します。
本アプリケーションでは、リソース間の認証にMicrosoft Entra ID(旧Azure AD)のManaged Identityを採用しました。
Managed Identityは、Azureの仮想マシンやアプリホスティング基盤に割り当てることができるIDであり、そのIDに対してロールベースでアクセス権限を設定できます。これにより、従来のようにパスワードやAPIキーといった長期的な認証情報を管理する必要がなくなります。
今回の構成では、PostgreSQL Flexible ServerにおいてEntra ID認証を有効化し、パスワード認証を無効化しています。
また、Key Vault(シークレット管理)・Azure Container Registry(ACR)(コンテナイメージの管理)・Storage(生成結果などのデータ保存)へのアクセスもすべてManaged Identityに統一しました。これにより、アプリケーションコードや環境変数にシークレットを埋め込むことなく、安全に各リソースへアクセスできるようになっています。
さらに、CI/CDにおいても、GitHub ActionsのOIDC Federated Credentialsを利用することで、Azure側にシークレットを保存せずに認証を行う構成としました。これにより、リポジトリに長期的な認証情報を保持する必要がなくなり、セキュリティリスクの低減と運用負荷の軽減を両立しています。
このようにManaged Identityを中心に据えた構成にすることで、Azureリソース間の認証においては長期的な認証情報を一切持たずに運用できるようになりました。
アーキテクチャの全体像
今回採用したアーキテクチャの全体像を示します。

ブラウザからのリクエストはContainer Apps(Next.js Web App)で受け、生成タスクをPostgreSQLに記録したのちに、Container Apps Jobs(Worker)を起動します。
Workerを起動したら、その完了を待たずに即座にレスポンスを返すので、ブラウザはLLMの実行完了を待つ必要がありません。
WorkerはContainer Apps JobsをManual Triggerで動かしていて、Container Apps(Next.js Web App)からAzure SDK経由で直接起動しています。
さらに、ブラウザとWorkerは直接接続せずに、PostgreSQLだけを介してやり取りする形にしました。
WebSocketやSSEで進捗を双方向に流す選択肢もありましたが、「ブラウザを閉じても処理を続けたい」「結果も進捗も永続化したい」という要件から、WorkerはDBに書くだけ、ブラウザはDBを読むだけ、というシンプルな構造にしています。
そのため、ブラウザ側のポーリングは処理中の間だけ動き、完了すれば自動で止まるようにしています。
この設計のおかげで、フロント・Worker・DBを疎結合に保つことが可能となりました。
また、ユーザーがタブを閉じて翌日戻ってきても結果がそのまま残っているという体験も実現できました。
工夫点
ここでは、Azureへのデプロイや実装周りで工夫した点についてお話ししていきます。
ポーリング + visibilitychangeでの結果確認
ジョブをバックエンドに切り離して非同期で実行する場合、フロントエンドからWorkerの完了をどう検知するかが課題になります。
今回はシンプルなポーリング方式を採用しました。
useEffect(() => {
if (!status || !POLLING_STATUSES.has(status)) return;
let isFetching = false;
const fetchLatest = async () => {
if (isFetching) return;
isFetching = true;
try {
const data = await getDetail(id);
if (!data) return;
setData(parseData(data));
} finally {
isFetching = false;
}
};
const interval = setInterval(fetchLatest, POLLING_INTERVAL_MS);
const onVisible = () => {
if (document.visibilityState === "visible") fetchLatest();
};
document.addEventListener("visibilitychange", onVisible);
return () => {
clearInterval(interval);
document.removeEventListener("visibilitychange", onVisible);
};
}, [status, id]);ポイントは2つです。
- ステータスが処理中の間だけポーリングする: 完了後は無駄なリクエストを発生させない
- visibilitychange イベントでタブ復帰時に即時フェッチ: タブに戻ったときに最大ポーリング間隔分の待ちが発生するため、visibilitychange で即座にフェッチし、この待ち時間を解消している
LLMワークフローの生成時間が数分〜数十分単位なので、WebSocketのようなリアルタイム通知は不要と判断し、シンプルなポーリングを採用しました。ポーリング間隔は、サーバー負荷とUXのバランスで決めています。
パスワードレス認証下でのDBマイグレーション
本プロジェクトではORMにPrismaを採用しており、アプリ実行時は、PrismaのDriver Adapter機能を使用しています。
これは、Prismaに pg などのNode.jsライブラリを組み込める拡張ポイントです。
pg には「接続時に動的にパスワードを取得するコールバック機能」があり、ここにEntra IDトークン取得処理を組み込むことで、接続のたびに新しいトークンでPostgreSQLに接続しています。
一方、DBマイグレーション(スキーマ変更)はCDパイプラインの中で prisma migrate deploy コマンドを実行する形で行っています。
ここで問題になるのが、このCLIコマンドは独立したバイナリとして動作するため、アプリ内で設定したDriver Adapterを認識できない点です。そのため動的なトークン取得ができず、DATABASE_URL にトークンを自分で埋め込んで渡す必要があります。
そこで、マイグレーション実行前にEntra IDトークンを取得し、DATABASE_URL のパスワード部分に注入するラッパースクリプトを作成しました。
// scripts/migrate-deploy.ts
async function main() {
if (process.env.AZURE_CLIENT_ID && process.env.DATABASE_URL) {
const credential = new DefaultAzureCredential({
managedIdentityClientId: process.env.AZURE_CLIENT_ID,
});
const tokenResponse = await credential.getToken(
"https://ossrdbms-aad.database.windows.net/.default",
);
const url = new URL(process.env.DATABASE_URL);
url.password = tokenResponse.token;
process.env.DATABASE_URL = url.toString();
}
execSync("npx prisma migrate deploy", {
stdio: "inherit",
env: process.env,
});
}Managed Identityで取得した短命のEntra IDトークンをパスワードとして DATABASE_URL に埋め込むことで、prisma migrate deploy のようなCLIツールでもパスワードレスで動かせます。
PostgreSQLだけでなく、Entra ID認証に対応した他のAzureリソースでも同じ手法が使えます。
まとめ
LLMワークフローのように処理時間が長いアプリケーションでは、「ブラウザから処理を切り離す」ことが重要です。
本記事で紹介したAzure Container Apps Jobs + Managed Identityの構成により、以下を実現できました。
- タブを閉じても処理が継続する: Server Actionで即座にレスポンスを返し、Container Apps Jobsでバックエンド処理を非同期実行
- アプリケーション全体の認証を統一: Managed Identity + Entra IDで、DBアクセスからLLM呼び出しまでキーレスで実現
同じような課題に取り組む方の参考になれば幸いです。
参考
- Jobs in Azure Container Apps
- Managed identities in Azure Container Apps
- Microsoft Entra authentication for Azure Database for PostgreSQL
- Configuring OpenID Connect in Azure (GitHub Actions)
- Page Visibility API – MDN
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み