Azure Functions(分離プロセス/isolated process)で OpenAPI を導入すると、実行時には /api/swagger.json で問題なく参照できるのに、ビルド時に Swagger ファイルを自動出力しようとしてつまずく――そんな相談をよく受けます。本記事は、.NET 9 の分離プロセス関数で Microsoft.Extensions.ApiDescription.Server を追加した結果「HostEndpoint が見つからない」などのビルドエラーに陥る理由と、CI/CD で安全かつ再現性高く swagger.json を取り出すベストプラクティスを、実装例・スクリプト込みで丁寧に解説します。
結論と要点
結論:Azure Functions(分離プロセス)では Microsoft.Extensions.ApiDescription.Server による「ビルド時生成」は前提を満たさないため使えません。代わりに、Microsoft.Azure.Functions.Worker.Extensions.OpenApi が提供するランタイム生成(/api/swagger.json)を利用し、ビルド後やパイプライン内で自動起動 → 取得 → 停止する方式に切り替えるのが、実務で最も堅牢かつサポートされている手法です。
背景と用語整理
- 分離プロセス(isolated process):Functions ホストとユーザーコードが別プロセスで動作。ASP.NET Core のミドルウェア/エンドポイントパイプラインを持たず、
Functions Workerがトリガーを仲介します。 - OpenAPI 拡張:
Microsoft.Azure.Functions.Worker.Extensions.OpenApi。関数に付与した属性やメタデータから/api/swagger.jsonと Swagger UI をランタイムで生成します。 - ApiDescription.Server:
Microsoft.Extensions.ApiDescription.Server。ASP.NET Core のApiExplorer/エンドポイントルーティングなどの内部構造を前提に「ビルド時に OpenAPI を固定ファイル化」する用途のパッケージです。
ASP.NET Core と Functions(分離)の違い(要点比較)
| 観点 | ASP.NET Core | Azure Functions(分離) |
|---|---|---|
| 実行ホスト | WebHost/Kestrel | Functions Host + Worker |
| ルーティング | IEndpointRouteBuilder/ApiExplorer | トリガー属性(HttpTrigger 等)を Worker が処理 |
| OpenAPI 生成 | Swashbuckle/NSwag などでビルド時も可能 | Worker.Extensions.OpenApi によるランタイム生成のみが公式ルート |
| ApiDescription.Server | ◯(内部前提が存在) | ✕(内部前提が無く、キー解決エラーに繋がる) |
現象:ビルド時に発生する代表的エラー
分離プロセスの Functions プロジェクトに Microsoft.Extensions.ApiDescription.Server を追加してビルドすると、次のようなメッセージで失敗します(例)。
error: Unable to resolve configuration key 'Functions:Worker:HostEndpoint' ...
error: The required ASP.NET Core hosting environment and ApiExplorer are not available ...
要は、ApiDescription.Server が想定する「ASP.NET Core の WebHost と構成キー(HostEndpoint など)」が存在せず、初期化に失敗している状態です。
原因:ApiDescription.Server は ASP.NET Core 専用の前提で動作する
ApiDescription.Server は、ASP.NET Core のエンドポイントルーティングと ApiExplorer に収集されたメタデータから OpenAPI を生成します。Functions(分離)はトリガー駆動で、HTTP パイプラインの形態が異なるため、同じ仕組みは使えません。結果、構成キー HostEndpoint や WebHost 関連のサービスが見つからず、ビルドが停止します。
内部前提のギャップ(もう少し詳しく)
| 内部機能 | ApiDescription.Server が期待 | Functions(分離)の実態 | 結果 |
|---|---|---|---|
| WebHost | Kestrel/Minimal API/Controllers など | Worker 経由のトリガー処理のみ | 起動/検出に失敗 |
| ApiExplorer | エンドポイント→メタデータ 収集 | 存在しない | ドキュメント生成源が無い |
| 構成キー | HostEndpoint 等の ASP.NET Core 設定 | Functions 独自の host.json/環境変数 | 解決不可→ビルド失敗 |
解決策・ベストプラクティス
1. ApiDescription.Server を除外する
プロジェクトから Microsoft.Extensions.ApiDescription.Server の参照を外してください。Functions(分離)では非対応です。
2. ランタイム生成(/api/swagger.json)を正しく使う
既に Microsoft.Azure.Functions.Worker.Extensions.OpenApi を導入済みで /api/swagger.json にアクセスできるなら、それが公式かつサポートされている方法です。以下は最小構成例です。
csproj(例)
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
</PropertyGroup>
関数コード(例)
using System.Net;
using System.Text.Json;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes;
using Microsoft.OpenApi.Models;
[assembly: OpenApiInfo(Title = “Sample Functions API”, Version = “v1”, Description = “Isolated Functions with OpenAPI”)]
public class HelloFunction { [Function(“Hello”)] [OpenApiOperation(operationId: “Hello_Get”, Summary = “挨拶を返します”, Description = “名前を受け取り、挨拶を返します。”)] [OpenApiParameter(name: “name”, In = ParameterLocation.Path, Required = true, Type = typeof(string), Summary = “名前”, Description = “挨拶される人の名前”)] [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: “application/json”, bodyType: typeof(HelloResponse), Summary = “OK”, Description = “挨拶結果”)] public async Task RunAsync( [HttpTrigger(AuthorizationLevel.Function, “get”, Route = “hello/{name}”)] HttpRequestData req, string name) { var res = req.CreateResponse(HttpStatusCode.OK); await res.WriteStringAsync(JsonSerializer.Serialize(new HelloResponse { Message = $”Hello, {name}!” })); return res; } } public record HelloResponse(string Message);
ランタイム取得
GET http://localhost:7071/api/swagger.json
このレスポンスを CI/CD で自動ダウンロードし、成果物に含めるのが推奨です。
3. ビルド後に自動抽出する(ローカル/CI 共通レシピ)
以下のスクリプトは「関数ホストを起動 → /api/swagger.json を取得 → 確実に停止」を行います。単純な一発コマンドよりも起動待ち・リトライ・後片付けを明示しておくと安定します。
Bash(macOS/Linux)
#!/usr/bin/env bash
set -euo pipefail
PORT="${1:-7071}"
LOG="func.log"
# バックグラウンドで起動
func start --port "${PORT}" --verbose > "${LOG}" 2>&1 &
FUNC_PID=$!
# 起動待ち(最大 90 回・2 秒間隔)
for i in $(seq 1 90); do
if curl -fsS "[http://127.0.0.1:${PORT}/api/swagger.json](http://127.0.0.1:${PORT}/api/swagger.json)" -o swagger.json; then
echo "swagger.json exported"
break
fi
sleep 2
done
# 終了処理
kill "${FUNC_PID}" >/dev/null 2>&1 || true
# 孤児プロセスを念のため終了(Linux)
pgrep -P "${FUNC_PID}" >/dev/null && pkill -TERM -P "${FUNC_PID}" || true
PowerShell(Windows)
param([int]$Port = 7071)
$proc = Start-Process "func.exe" "start --port $Port --verbose" -PassThru -WindowStyle Hidden
try {
$ok = $false
1..90 | ForEach-Object {
try {
Invoke-WebRequest -Uri "[http://127.0.0.1:$Port/api/swagger.json](http://127.0.0.1:$Port/api/swagger.json)" -OutFile "swagger.json" -UseBasicParsing -TimeoutSec 5
$ok = $true
Break
} catch {
Start-Sleep -Seconds 2
}
}
if (-not $ok) { throw "Export timed out." }
}
finally {
if ($proc -and -not $proc.HasExited) { Stop-Process -Id $proc.Id -Force }
}
MSBuild に埋め込む(AfterTargets=Build)
ビルド直後に上記スクリプトを呼び出し、生成した swagger.json を出力フォルダへコピーします。
<Target Name="ExportOpenApi" AfterTargets="Build">
<PropertyGroup>
<_IsWindows>$([MSBuild]::IsOSPlatform('Windows'))</_IsWindows>
</PropertyGroup>
PreserveNewest
GitHub Actions の例
name: build-and-export-swagger
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
# Core Tools の導入(バージョンはプロジェクトと統一)
- name: Install Azure Functions Core Tools
run: npm i -g azure-functions-core-tools@4 --unsafe-perm true
- name: Restore & Build
run: dotnet build -c Release
- name: Export swagger.json
run: |
bash scripts/export-swagger.sh 7071
mkdir -p artifacts
cp swagger.json artifacts/
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: swagger
path: artifacts/swagger.json
Azure DevOps Pipelines の例
trigger:
- main
pool:
vmImage: 'windows-latest'
steps:
* task: UseDotNet@2
inputs:
packageType: 'sdk'
version: '9.0.x'
* powershell: npm i -g azure-functions-core-tools@4 --unsafe-perm true
displayName: Install Core Tools
* script: dotnet build -c Release
displayName: Build
* powershell: |
./scripts/export-swagger.ps1 -Port 7071
New-Item -ItemType Directory -Force -Path artifacts | Out-Null
Copy-Item swagger.json artifacts/swagger.json -Force
displayName: Export swagger.json
* publish: artifacts/swagger.json
artifact: swagger
Docker を使う場合の例
コンテナでビルド・起動し、ホスト側から curl で取得する方法です。
# イメージを作成して起動(例)
docker build -t funcapp:latest .
docker run --rm -p 7071:7071 --name funcapp funcapp:latest &
CID=$!
# 取得
for i in $(seq 1 90); do
if curl -fsS [http://127.0.0.1:7071/api/swagger.json](http://127.0.0.1:7071/api/swagger.json) -o swagger.json; then break; fi
sleep 2
done
# 停止
docker stop funcapp >/dev/null 2>&1 || true
OpenAPI ドキュメントの品質を高める具体テクニック
- 属性でメタデータを出し切る:
OpenApiOperation(Summary/Description)、OpenApiParameter(必須・型・説明)、OpenApiRequestBody(スキーマ)、OpenApiResponseWithBody(ステータス/本文型)を丁寧に付与。 - DTO を明確化:匿名型や
dynamicを避け、レコード/クラスでリクエスト・レスポンスを定義。JsonPropertyName属性でプロパティ名を固定。 - ルートのプレースホルダとスキーマの整合:
Route = "items/{id}"と[OpenApiParameter(name: "id", In = Path, ...)]を必ず一致させる。 - ステータスコードを網羅:400/404/500 などのエラー応答も
OpenApiResponseWithBodyで明示し、クライアント生成の質を上げる。 - アセンブリレベルの
OpenApiInfo:Title/Version/Descriptionを設定し、ドキュメントのヘッダを整える。
属性を使った詳細例
using System.Net;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes;
using Microsoft.OpenApi.Models;
[assembly: OpenApiInfo(Title = “Orders API”, Version = “v2”, Description = “注文管理のサンプル API”)]
public class CreateOrderRequest { [Required] [JsonPropertyName(“productId”)] public string ProductId { get; set; } = default!; [Range(1, 100)] [JsonPropertyName(“quantity”)] public int Quantity { get; set; } } public class CreateOrderResponse { [JsonPropertyName(“orderId”)] public string OrderId { get; set; } = default!; } public class Orders { [Function(“CreateOrder”)] [OpenApiOperation(operationId: “Orders_Create”, Summary = “注文を作成”, Description = “新しい注文を作成します。”)] [OpenApiRequestBody(contentType: “application/json”, bodyType: typeof(CreateOrderRequest), Required = true, Description = “作成する注文”)] [OpenApiResponseWithBody(HttpStatusCode.Created, “application/json”, typeof(CreateOrderResponse), Summary = “作成成功”, Description = “注文番号を返します。”)] [OpenApiResponseWithoutBody(HttpStatusCode.BadRequest, Summary = “不正なリクエスト”)] public async Task CreateAsync( [HttpTrigger(AuthorizationLevel.Function, “post”, Route = “orders”)] HttpRequestData req) { var body = await System.Text.Json.JsonSerializer.DeserializeAsync(req.Body); if (body is null || string.IsNullOrWhiteSpace(body.ProductId) || body.Quantity <= 0) { var bad = req.CreateResponse(HttpStatusCode.BadRequest); return bad; } var created = req.CreateResponse(HttpStatusCode.Created); await created.WriteStringAsync(System.Text.Json.JsonSerializer.Serialize( new CreateOrderResponse { OrderId = Guid.NewGuid().ToString(“N”) })); return created; } }
運用のコツ・チェックリスト
- ツールのバージョン統一:Core Tools/拡張パッケージ/.NET SDK をチームで固定。バージョン差があると JSON の細部が変わり、差分ノイズの原因になります。
- ポート衝突の回避:パイプラインで並列ジョブがある場合は、ジョブごとに別ポート(例:
7071 + $(System.StageAttempt))を割り当てるか、スクリプト引数で指定。 - 安定化のためのヘルスチェック:
/api/swagger.json直接の取得でよいですが、長引く場合は先に/admin/host/statusを叩いて起動確認 → Swagger 取得、の順にする手もあります。 - 出力場所の固定:生成した
swagger.jsonをartifacts/や$(Build.ArtifactStagingDirectory)に集約して公開。CD パイプラインで API Management 等へ自動反映する基盤を作りやすくなります。 - 署名・リージョン情報などの機密を含めない:OpenAPI は公開前提で扱い、秘密情報をスキーマに載せない(例:内部ヘッダ名や内部エンドポイント名を避ける)。
- スキーマの互換性維持:既存クライアントがある場合は破壊的変更(必須化・型変更・パス変更)を避け、
versionを上げた別ドキュメントとして公開する。
「やってよいこと/やってはいけないこと」早見表
| 項目 | 推奨 | 非推奨 |
|---|---|---|
| OpenAPI 生成方式 | ランタイム生成 → スクリプトで抽出 | ApiDescription.Server によるビルド時生成 |
| CI/CD での取得 | 起動待ち・リトライ・停止を明示 | 固定 sleep のみ・停止忘れ |
| スキーマ管理 | アセンブリ属性+関数属性で明確化 | 匿名型・暗黙の JSON |
| バージョン統制 | SDK/拡張/ツールを固定し差分監視 | 各自が任意のバージョンを使用 |
API Management と組み合わせる場合の考え方
API Management(APIM)を使う場合、上記で生成した swagger.json をパイプラインの成果物として保存し、それを APIM へインポートする手順を自動化すれば、仕様の配布と運用を一元化できます。ステージング環境用(vNext)と本番用(vCurrent)で OpenAPI を分け、互換性チェックのゲートを設けると安全です。
トラブルシューティング
- Swagger UI は出るが JSON 取得が 404:ルートプレフィクス(通常
api)の変更や、関数の無効化設定を確認。関数が 1 つも公開されていないと JSON が生成されないことがあります。 - 関数ホストが起動しない:Core Tools のバージョンと .NET SDK の組み合わせを見直し。新旧が混在すると起動時に警告→停止するケースがあります。
- CI でポート使用中:多並列ビルドではポートを変える、または起動時にランダムポートを選び、ログからポートを抽出して取得先 URL を動的に切り替える実装にします。
- 出力 JSON が毎回微妙に変わる:DTO のプロパティ順序(シリアライザ設定)や
DateTimeのフォーマットが原因。シリアライザの設定を固定し、スキーマのformatを明示します。
FAQ
Q:ビルド時に静的生成する方法は全くない?
A:公式には用意されていません。Functions のモデル上、ランタイム生成が前提です。どうしても静的化したい場合は、本記事の通り「ビルド後に抽出」してください。
Q:NSwag や Swashbuckle を使えば?
A:それらは ASP.NET Core 向けです。Functions(分離)に無理に組み込むと内部前提が崩れ、安定しません。
Q:swagger.json のファイル名やパスを変えたい
A:パイプライン側で保存時にリネームしてください(例:swagger.v1.json)。
Q:OpenAPI v2/v3 の切り替えは?
A:拡張のオプション/環境変数で切り替えられます。チームで値を固定し、成果物名にも反映しておくと混乱を避けられます。
まとめ
Azure Functions(分離プロセス)で「ビルド時に swagger.json を生成」しようとして Microsoft.Extensions.ApiDescription.Server を追加すると、ASP.NET Core 前提が崩れてビルドエラー(HostEndpoint など)になります。これは仕様に起因するもので、個別ワークアラウンドでの攻略は非現実的です。最も安全で保守しやすい解は、OpenAPI 拡張が提供するランタイム生成を前提にし、CI/CD(またはローカルの AfterBuild)で関数ホストを短時間だけ起動して JSON を抽出する方式です。待機や停止を含む堅牢なスクリプトを用意し、ツールのバージョンを固定すれば、毎回同じ品質の OpenAPI を安定して得られます。結果として、クライアント SDK の自動生成・API カタログ・APIM 連携といった後工程もスムーズになり、開発速度と運用品質がともに向上します。

コメント