Azure Static Web AppsでOryxがglobal.jsonの.NET SDKバージョンを尊重しない原因と対策(setup-dotnetと8.0.413問題の完全整理)

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 / latestPatch8.0.412 のみ「8.0.413 以上」が存在せず失敗
version: 8.0.410 / latestPatch8.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 SDK8.0.413dotnet build / dotnet publish など開発用 CLI を含む
.NET ランタイム8.0.18 / 8.0.19アプリ実行時のランタイム本体
ASP.NET Core ランタイム8.0.18 / 8.0.19Blazor / 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 ではなくランタイム側のパッチであり、今回のエラーの本質とは無関係。
  • 本命の対策
    • GitHub ランナー上で dotnet publish まで実施し、SWA には「ビルド済み成果物」を skip_app_build: true / skip_api_build: true でアップロードさせる。
    • この構成にすると、global.json + setup-dotnet で指定した SDK(8.0.413)で一貫してビルドできる。
  • 暫定の回避策
    • Oryx が 8.0.413 をサポートするまでの間だけ、global.json の version を 8.0.410 + latestPatch などに下げておき、8.0.412 を許容する。
  • 補助的なポイント
    • 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 以降の長期運用もグッと楽になるはずです。

この記事を書いた人

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

コメント

コメントする

目次