Azure Static Web Apps(SWA)に .NET 8 アプリをデプロイしたら、「actions/setup-dotnet で 8.0.413 を入れているのに、Oryx が 8.0.412 を拾おうとして失敗する」「ログに 8.0.18 / 8.0.19 など謎のバージョンが混在している」──そんな状況にハマると、原因がとても分かりづらく感じます。本記事では、この現象がなぜ起きるのかを “SWA と Oryx の仕組み” から整理し、実務で取りやすい回避策(特に「自前ビルド+アップロードのみ」パターン)を具体的な YAML サンプル付きで解説します。
現象の整理:setup-dotnet と global.json を設定しても Oryx が従わない
まず、問題のシナリオをもう少し噛み砕いて整理します。
- GitHub Actions 内で
actions/setup-dotnetを使い、.NET SDK 8.0.413 をインストールしている。 - リポジトリの
global.jsonでも、version: 8.0.413、rollForward: latestPatch を指定している。 - しかし Azure Static Web Apps の GitHub Action(
Azure/static-web-apps-deploy)実行時、内部で動く Oryx が- 「Detected platforms: dotnet: 8.0.18」などと表示しつつ
- .NET SDK 8.0.413 ではなく 8.0.412 を取得しようとして最終的にビルド失敗する
- プロジェクトのパッケージ参照には 8.0.19 系が含まれており、ログ上は8.0.18 / 8.0.19 / 8.0.413 / 8.0.412と複数の数字が飛び交う。
この結果、典型的には以下のようなエラーで止まります(イメージ)。
It was not possible to find any compatible .NET SDK
The specified SDK version [8.0.413] from global.json [..] not found
Install the [8.0.413] .NET SDK or update global.json to match an installed SDK.
一見「global.json を無視している」「setup-dotnet が効いていない」ように見えますが、実際はもう少し構造的な理由があります。
Azure Static Web Apps と Oryx の仕組みを理解する
GitHub ランナー と Oryx コンテナは別世界
この問題の根っこは「ビルドが実行される場所が 2 つある」ことです。
| 場所 | 何が動いているか | .NET SDK はどこから来るか | global.json はどう扱われるか |
|---|---|---|---|
| GitHub ランナー | actions/setup-dotnet や任意の dotnet コマンド | setup-dotnet がインストールする SDK | ランナー上で dotnet build などを実行した場合にのみ効く |
| Azure Static Web Apps のビルドコンテナ | Azure/static-web-apps-deploy が起動する Oryx コンテナ | Oryx イメージに事前インストールされた SDK 群 | コンテナ内で dotnet を呼ぶときに参照される(ただし「存在する SDK の中から」) |
Azure Static Web Apps の GitHub Action は、裏側で Linux コンテナを立ち上げ、その中で Oryx がフロントエンドと API をビルドします。 このコンテナは、GitHub ランナーとは完全に別のファイルシステム・別のインストール済み SDK を持つ世界です。
つまり、
actions/setup-dotnetはランナー側の .NET SDK を変更しているだけで、- その後に実行される
Azure/static-web-apps-deployが使う Oryx コンテナの中身(SDK バージョン)には一切影響しない
という構図になっています。Microsoft Q&A でも、この「setup-dotnet と Oryx は別世界」という点が指摘されています。
Oryx が持つ SDK バージョンは固定リスト
Oryx コンテナには、事前に「ビルド対象とする .NET SDK のバージョン一覧」が組み込まれており、バッチ的に更新されています。Oryx のリポジトリには、バージョンごとのリストを定義したファイルと、それを更新する PR が定期的にマージされています。
今回のケースでは、
- Oryx の SDK リストに 8.0.412 まではあるが 8.0.413 がまだない
global.jsonでは 8.0.413 + rollForward: latestPatch を要求している
というタイミングに当たってしまい、「要求されている SDK を満たせない」状態になっています。
global.json と rollForward の挙動を正しく理解する
global.json は「要求」であって、存在しなければ失敗する
global.json は、.NET CLI に対して「この SDK バージョン(もしくは許容範囲)を使ってほしい」と指示するファイルです。
{
"sdk": {
"version": "8.0.413",
"rollForward": "latestPatch"
}
}
rollForward: "latestPatch" の意味はざっくり言うと:
- メジャー/マイナー/機能バンド(8.0.xxx)が同じで
- パッチが 413 以上 の SDK のうち、インストールされているものの中から「最も新しいもの」を選ぶ
- そのような SDK が 1 つもインストールされていなければ失敗
というルールです。つまり、次のようになります。
global.json の指定 | Oryx コンテナにある SDK | 解決結果 |
|---|---|---|
| version: 8.0.413 / latestPatch | 8.0.412 のみ | 「8.0.413 以上」が存在せず失敗 |
| version: 8.0.410 / latestPatch | 8.0.412 のみ | 8.0.412 を選択(410 以上の最新) |
このため、「Oryx が 8.0.412 しか持っていないのに、global.json で 8.0.413 以上を要求している」状態になると、どのみち .NET CLI はエラーになります。
8.0.18 と 8.0.19 の謎:SDK とランタイムのバージョン差
ログの中で
Detected platforms: dotnet: 8.0.18- プロジェクト側は
Microsoft.AspNetCore.Components.WebAssemblyなど version 8.0.19
といった具合に、8.0.18 / 8.0.19 / 8.0.413 が混在して見えるのも混乱の元です。
ここで押さえておくべきポイントは:
- SDK のバージョン(8.0.413 など)と、
- .NET ランタイム(NETCoreApp)、ASP.NET Core ランタイムのパッチ(8.0.18 / 8.0.19 など)
は別ものだということです。
| 種類 | 例 | 役割 |
|---|---|---|
| .NET SDK | 8.0.413 | dotnet build / dotnet publish など開発用 CLI を含む |
| .NET ランタイム | 8.0.18 / 8.0.19 | アプリ実行時のランタイム本体 |
| ASP.NET Core ランタイム | 8.0.18 / 8.0.19 | Blazor / ASP.NET Core のサーバー/WASM ランタイム |
Oryx の内部定義を見ると、「ASPNET_CORE_APP_80: 8.0.18」「NET_CORE_APP_80: 8.0.18」のように、ランタイム側のパッチが 8.0.18 に固定されている箇所があります。 一方で、NuGet パッケージとして参照するライブラリが 8.0.19 だったとしても、それは「SDK 8.0.413 に付属するランタイム 8.0.19 系」と対応しているだけで、ログ上の表記がきれいに揃うとは限りません。
今回の問題の本質は「SDK 8.0.413 が Oryx 側に無いこと」なので、8.0.18 / 8.0.19 の差はあくまで「ログがややこしく見える」程度の副作用と捉えて OK です。
実務的な解決策:Oryx に頼らない「自前ビルド + アップロードのみ」
方針:GitHub ランナーで publish して、SWA は配布だけさせる
一番再現性が高く、チーム運用にも乗せやすいのが「アプリも API も GitHub ランナーでビルド → SWA には成果物をアップロードするだけ」という構成です。 Static Web Apps の公式ドキュメントでも、skip_app_build / skip_api_build を使うことで、事前ビルドした成果物をそのまま配信させる方法が説明されています。
ここでは、次のような構成を例にします。
- フロントエンド:Blazor WebAssembly(.NET 8)
- API:Azure Functions(.NET 8 Isolated)
サンプル YAML(GitHub Actions)
name: Build & Deploy to Azure Static Web Apps
on:
push:
branches:
- main
jobs:
build_and_deploy:
runs-on: ubuntu-latest
env:
DOTNET_VERSION: 8.0.413
CLIENT_PROJECT: ./CareerWeb/CareerWeb.csproj
API_PROJECT: ./Api/Api.csproj
ARTIFACT_CLIENT: ./artifacts/client
ARTIFACT_API: ./artifacts/api
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Install Blazor WASM workload
run: dotnet workload install wasm-tools
- name: Publish WebApp
run: dotnet publish ${{ env.CLIENT_PROJECT }} -c Release -o ${{ env.ARTIFACT_CLIENT }}
- name: Publish API
run: dotnet publish ${{ env.API_PROJECT }} -c Release -o ${{ env.ARTIFACT_API }}
- name: Deploy to Azure Static Web Apps
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.SWA_TOKEN }}
action: "upload"
# フロントエンドは既に静的成果物なので wwwroot を指定
app_location: "artifacts/client/wwwroot"
output_location: "" # skip_app_build 時は空文字が推奨
# API も publish 済みフォルダを指定
api_location: "artifacts/api"
skip_app_build: true
skip_api_build: true
ポイントを整理すると:
- ランナー上の
dotnetは global.json + setup-dotnet の組み合わせで 8.0.413 を使える。 - フロントエンドは
publish出力(Blazor WASM の場合はpublish/wwwroot相当)を SWA のapp_locationに指定する。 skip_app_build: true+output_location: ""にすることで、SWA 側のビルドを完全にスキップできる。- API も
dotnet publish済みのフォルダをapi_locationに指定し、skip_api_build: trueで Oryx の API ビルドをスキップする。
staticwebapp.config.json で API ランタイムを明示する
API 側を事前ビルドしてデプロイする場合は、staticwebapp.config.json に apiRuntime を指定しておくと安心です。
{
"platform": {
"apiRuntime": "dotnet-isolated:8.0"
}
}
この値は、「Supported languages and runtimes in Azure Static Web Apps」に記載されている apiRuntime 値と合致させる必要があります。
成果物側に設定ファイルを必ずコピーする
公式ドキュメントでも強調されていますが、staticwebapp.config.json や(古い構成なら)routes.json は publish 出力側に含める必要があります。
- フロントエンドの場合:
artifacts/client/wwwroot配下に配置されていること - API の場合:
artifacts/api直下にstaticwebapp.config.jsonが含まれていること
をビルドスクリプトやプロジェクト設定で保証しておきましょう。
この方式のメリット・デメリット
| 観点 | メリット | デメリット |
|---|---|---|
| SDK バージョン | 8.0.413 固定など、組織ポリシーどおりに制御できる | 新しい SDK を入れ忘れるとビルドステップで失敗する |
| 再現性 | ローカルと CI で同じ SDK を使う構成にしやすい | パイプライン側の管理項目がやや増える |
| Oryx 依存 | Oryx の SDK 更新待ちを気にしなくて済む | 「おまかせビルド」が効かなくなる(自分で publish コマンドを書く必要) |
暫定回避策:global.json の下限を少し下げる
「いまはとにかく Oryx ビルドを通して動かしたい」「パッチ番号の厳密さは当面そこまで重要ではない」というケースでは、global.json の version を少し古めにしておき、rollForward: latestPatch に任せるという手もあります。
{
"sdk": {
"version": "8.0.410",
"rollForward": "latestPatch"
}
}
この設定であれば、
- ローカル環境:8.0.413 など、より新しい 8.0.41x 系が入っていればそれを使用
- Oryx コンテナ:8.0.412 しかなければ 8.0.412 を選択
という動作になり、いずれの環境でもビルドが成功しやすくなります。
ただし、この方法には次のような注意点があります。
- 「必ず 8.0.413 でビルドする」という厳格な再現性は失われる(環境によって 8.0.412 / 8.0.413 が混在し得る)。
- セキュリティポリシーやコンプライアンス上、「このパッチ以外は許可しない」というルールがある場合には採用しにくい。
そのため、あくまで「Oryx の SDK が追いつくまでの暫定措置」として位置づけるのが無難です。
global.json の配置とモノレポ構成の落とし穴
.NET CLI は、現在の作業ディレクトリから親ディレクトリを辿りながら、最初に見つかった global.json を使用します。 Oryx も基本的には同様のルールで global.json を探しますが、「どこをカレントディレクトリとしてビルドしているか」が分かりづらいため、モノレポ構成だと次のような事故が起きがちです。
- リポジトリルートにだけ
global.jsonを置いているが、app_locationをサブディレクトリにしており、そこから親を辿ると別のglobal.jsonを拾ってしまう。 - フロントエンド用と API 用で違う global.json が存在し、どちらが使われているかログからは判別しにくい。
安定させるためのおすすめは:
- ビルド対象ごと(app / api)に 1 つだけ global.json を用意する
app_location/api_location直下から見て最初にヒットする global.json が、想定したものだけになるよう整理する
です。特に、モノレポで複数の Static Web Apps を管理している場合は、workflow ごとに paths を絞る構成もあわせて検討すると管理しやすくなります。
Oryx 側の SDK 更新を「待つ」という選択肢
もちろん、Oryx のイメージ自体がアップデートされて .NET SDK 8.0.413 を含むようになれば、global.json による指定がそのまま効くようになります。Oryx の GitHub リポジトリでは、「New DotNetCore SDKs …」といったタイトルで新しい SDK バージョンを追加するリリースが継続的に公開されています。
ただし、この方針は次のような性質を持ちます。
- いつ該当バージョンが Oryx に取り込まれるかはコントロールできない(数日〜数週間のラグがあり得る)。
- Oryx の更新が SWA のビルド環境に反映されるまでにもタイムラグがある。
- 本番環境のビルドが「外部プロジェクトの更新タイミング」に依存してしまう。
そのため、運用上は次のような使い分けがおすすめです。
| ケース | おすすめ戦略 |
|---|---|
| PoC / 個人プロジェクト | Oryx 更新を待ちつつ、困ったら global.json の version を少し下げて latestPatch に任せる |
| 業務・本番環境 | 基本は「自前ビルド+アップロードのみ」に固定し、Oryx 更新に依存しない |
よくある誤解と注意点
「skip_api_build だけ true にすればいい」は半分正解・半分危険
skip_api_build: true を設定すると、SWA 側の API ビルドはスキップされますが、その代わりに「publish 済みの API を api_location に置いておく責任」が完全にデベロッパー側に移ります。 「API だけ事前ビルドする」場合も、フロントエンドと同様に dotnet publish の出力フォルダをきちんと指定する必要があります。
「actions/setup-dotnet のバージョン=本番の実行バージョン」ではない
setup-dotnet はあくまで「GitHub ランナー上のビルド環境」を用意するためのものです。
- Oryx にビルドを任せている限り、本番でコンパイルされる SDK バージョンは Oryx のイメージに依存する。
- 本番で動く Functions のランタイムは、SWA 側の「対応ランタイム一覧」と
apiRuntime(指定していれば)で決まる。
「CI では 8.0.413 を入れているから、本番も 8.0.413 だろう」と思い込むと、今回のような齟齬に気づきづらくなります。
dotnet_version / DOTNET_VERSION の指定は「ヒント」であって銀の弾丸ではない
一部のブログ記事や Q&A では、Azure/static-web-apps-deploy の with: に dotnet_version を指定したり、環境変数 DOTNET_VERSION を設定する例が紹介されています。 しかし、Oryx 側にそもそも 8.0.413 の SDK が存在しない場合、これらは存在しないバージョンを要求しているだけになり、根本的な解決にはなりません。
「Oryx が 8.0.4xx 系を複数持っていて、その中からどれを使うかを微調整したい」という用途では役に立ちますが、今回のように「必要なパッチがイメージに入っていない」ケースでは、結局は次のどちらかに落とし込む必要があります。
- 自前ビルド+アップロードのみ(Oryx の .NET ビルドを使わない)
- global.json の version を下げるなど、Oryx が持っている SDK に合わせる
まとめ:なぜ起きるのか・どう回避するか
ここまでの内容を、実務視点でざっくりまとめると次のようになります。
- 原因
- Azure Static Web Apps の GitHub Action は Oryx コンテナ内でビルドしており、
actions/setup-dotnetがインストールした SDK は見えない。 - Oryx のイメージには 8.0.412 までしか入っておらず、
global.jsonが 8.0.413 + latestPatch を要求しているため、「満たせる SDK が無い」状態になる。 - ログに出てくる 8.0.18 / 8.0.19 は SDK ではなくランタイム側のパッチであり、今回のエラーの本質とは無関係。
- Azure Static Web Apps の GitHub Action は Oryx コンテナ内でビルドしており、
- 本命の対策
- GitHub ランナー上で
dotnet publishまで実施し、SWA には「ビルド済み成果物」をskip_app_build: true/skip_api_build: trueでアップロードさせる。 - この構成にすると、global.json + setup-dotnet で指定した SDK(8.0.413)で一貫してビルドできる。
- GitHub ランナー上で
- 暫定の回避策
- Oryx が 8.0.413 をサポートするまでの間だけ、
global.jsonの version を 8.0.410 + latestPatch などに下げておき、8.0.412 を許容する。
- Oryx が 8.0.413 をサポートするまでの間だけ、
- 補助的なポイント
- global.json は app/api 単位で配置し、Oryx がビルドするディレクトリから期待どおりに見えるよう整理する。
- API を事前ビルドしてデプロイする場合は、
staticwebapp.config.jsonのapiRuntimeと .csproj のTargetFrameworkを揃える。
Azure Static Web Apps は「GitHub に push するだけでビルド・デプロイしてくれる便利な PaaS」ですが、その裏側には Oryx コンテナという独立したビルド環境が存在します。この構造を理解しておけば、「setup-dotnet したはずなのにバージョンが合わない」「global.json を書いたのに無視される」といった罠を避けやすくなり、.NET 8 以降の長期運用もグッと楽になるはずです。

コメント