Azure Functions分離プロセスでビルド時にswagger.json生成が失敗する原因と対処法(.NET 9/OpenAPI/isolated)

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 CoreAzure Functions(分離)
実行ホストWebHost/KestrelFunctions 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(分離)の実態結果
WebHostKestrel/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 &amp; 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 連携といった後工程もスムーズになり、開発速度と運用品質がともに向上します。

この記事を書いた人

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

コメント

コメントする

目次