Start Debugging

解決: IIS へのデプロイ後に発生する HTTP Error 500.30 - ASP.NET Core app failed to start

500.30 は、アプリが w3wp.exe の中で起動中に例外をスローしたという意味です。実際の例外は Windows のアプリケーション イベント ログに IIS AspNetCore Module V2 として既に記録されています。まずそれを読み、次に原因を順に絞り込みます。共有フレームワークの未インストール、アプリケーション プールの x86/x64 不一致、構成の欠落、プールの権限です。

500.30 は原因ではありません。ASP.NET Core Module が w3wp.exe の中で CLR を起動したものの、リッスンを開始する前にアプリが例外をスローしたことを IIS が報告しているだけです。実際の例外は、ほぼ確実にサーバー上に既に存在します。イベント ビューアーを開き、Windows ログ > Application に移動して、ソースが IIS AspNetCore Module V2 である最新のエントリを探してください。stdoutLogEnabled が false のとき、モジュールは起動時のエラーを取得し、そのイベントに最大 30 KB まで、スタックトレースを含めて書き込みます。エントリに exception code = '0xe0434352' しか書かれていない場合は、web.config で stdoutLogEnabled="true" を設定し、もう一度サイトにアクセスしてください。その先は、実際に原因となる 4 つの要素を順位付けしていく作業です。

HTTP Error 500.30 - ASP.NET Core app failed to start

古いビルドの ASP.NET Core Module は、まったく同じ失敗を HTTP Error 500.30 - ANCM In-Process Start Failure と表示します。この文字列は Microsoft のドキュメントのエラー一覧で今も使われています。どちらも意味は同じです。以下の内容はすべて、現行の .NET Hosting Bundle に含まれる ANCM V2 を用いて .NET 11 (Preview 6、SDK 11.0.100-preview.6.26359.118) で検証しています。この仕組みは ASP.NET Core 3.0 で in-process ホスティングが既定になって以来変わっていないため、各手順は net8.0、net9.0、net10.0 のデプロイにもそのまま当てはまります。

500.30 が診断ではなく症状である理由

ASP.NET Core 3.0 以降、アプリは既定で in-process ホスティング モデル を使用します。MSBuild プロパティ <AspNetCoreHostingModel> の既定値は InProcess で、dotnet publish は web.config に hostingModel="inprocess" を書き込みます。このモデルでは別プロセスの dotnet.exe は存在しません。aspnetcorev2.dll が in-process リクエスト ハンドラーを IIS ワーカー プロセスに読み込み、そこで CoreCLR を起動し、Program.cs は Kestrel ではなく IISHttpServer を使って w3wp.exe の内部で実行されます。

これによりプロセスは 2 つではなく 1 つになり、スループットも実際に向上しますが、エラー報告は崩壊します。app.Run() がリッスン状態に達する前にアプリが例外をスローすると、モジュールは自身のプロセス内に死んだ CLR を抱え、ブラウザーに渡せる情報は 1 バイト分、つまり「起動に失敗した」だけになります。だからこそ、接続文字列の欠落、64 ビット ワーカー内の 32 ビット バイナリ、未インストールのランタイム、データ保護キー リングに対する DirectoryNotFoundException のすべてが、単一のステータス コードに集約されるのです。

何かを変更し始める前に、2 つの帰結を理解しておく価値があります。

最小の再現

これを再現する最小のデプロイは、ローカルには存在してサーバーには存在しない構成を読むアプリです。

// .NET 11 preview 6, C# 14. Program.cs
var builder = WebApplication.CreateBuilder(args);

string cs = builder.Configuration.GetConnectionString("Default")
    ?? throw new InvalidOperationException("Connection string 'Default' is missing.");

builder.Services.AddDbContext<AppDbContext>(o => o.UseSqlServer(cs));

var app = builder.Build();
app.MapGet("/", () => "ok");
app.Run();

ローカルで動作するのは、appsettings.Development.json にそのセクションがあり、ASPNETCORE_ENVIRONMENT が Development だからです。サーバーでは環境が Production で、appsettings.Production.json は発行出力に一度も追加されておらず、3 行目で例外が発生します。F5 は通り、デプロイは 500.30 を返し、それでいてアプリ自体には何の問題もありません。

この形は実際の 500.30 報告のかなりの割合を占めます。障害が環境依存であるため、構造上、開発マシンでは見えないのです。

アプリケーション イベント ログを読む。たいていはこれで調査が終わります

web.config に触れる前にこれを行ってください。サーバーでイベント ビューアーを管理者として実行し、Windows ログ > Application を開くか、直接クエリします。

# Windows Server 2022+, PowerShell 5.1 or 7.x. Run elevated on the web server.
Get-WinEvent -FilterHashtable @{
    LogName      = 'Application'
    ProviderName = 'IIS AspNetCore Module V2'
} -MaxEvents 5 | Format-List TimeCreated, Id, LevelDisplayName, Message

探しているのは次の 3 つのパターンのいずれかです。

パターン 1、有用なもの。 完全なマネージド スタックトレースです。stdoutLogEnabled が false であるため、モジュールが未処理の起動時例外を取得してイベント ログに出力しています。例外の型と最上位のフレームを読み、そこを直せば終わりです。ブラウザーのページが何も教えてくれなかったのでサーバーも同じだろうと決めつけ、多くの人が見落とすのがこのケースです。

パターン 2、不透明なもの。

Application '/LM/W3SVC/5/ROOT' with physical root 'C:\inetpub\wwwroot\myapp\'
hit unexpected managed exception, exception code = '0xe0434352'.
Please check the stderr logs for more information.
Application '/LM/W3SVC/5/ROOT' with physical root 'C:\inetpub\wwwroot\myapp\'
failed to load clr and managed application. CLR worker thread exited prematurely

0xe0434352 は「マネージド例外が外に出た」ことを示す汎用の Win32 コードで、それ以上の意味はありません。型もメッセージも持ちません。これは 32 ビット アプリケーションが有効になっていないプールに x86 アプリを配置したときの文書化されたシグネチャですが、モジュールが詳細を取得できない場所で例外が抜けた場合にも現れます。次は stdout ログに進んでください。

パターン 3、何もない。 リクエストから 1 分以内に ANCM のイベントが 1 件もない場合です。これはたいてい、モジュールが CLR の起動まで到達していないことを意味し、実際には起動時例外ではなく 500.0、500.31、500.32 を見ていることになります。末尾の派生パターンのセクションを参照してください。

stdout ログを有効にする

プロジェクト内のものではなく、サーバー上にデプロイされた web.config を編集します。このファイルは発行のたびに再生成されるので、一時的な診断スイッチとしてはまさに好都合です。

<?xml version="1.0" encoding="utf-8"?>
<!-- Deployed web.config, ASP.NET Core Module V2, .NET 11 -->
<configuration>
  <location path="." inheritInChildApplications="false">
    <system.webServer>
      <handlers>
        <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
      </handlers>
      <aspNetCore processPath="dotnet"
                  arguments=".\MyApp.dll"
                  stdoutLogEnabled="true"
                  stdoutLogFile=".\logs\stdout"
                  hostingModel="inprocess" />
    </system.webServer>
  </location>
</configuration>

web.config を保存するとアプリケーション プールがリサイクルされるので、もう一度サイトにリクエストするだけで済みます。stdoutLogFile 用の logs フォルダーはモジュール自身が作成し、タイムスタンプとプロセス ID を含む名前のファイル、たとえば stdout_20260805184032_5412.log を書き出します。アプリケーション プール ID にはそのフォルダーへの書き込み権限が必要です。

icacls "C:\inetpub\wwwroot\myapp\logs" /grant "IIS AppPool\MyAppPool":(OI)(CI)M

時間を節約できる読み方のポイントが 3 つあります。

stdout がそれでも沈黙している場合、障害はマネージド コードより下の層にあります。モジュール自身のデバッグ ログを追加します。

<!-- ASP.NET Core Module V2 diagnostic logging. Remove after troubleshooting. -->
<aspNetCore processPath="dotnet"
            arguments=".\MyApp.dll"
            stdoutLogEnabled="false"
            stdoutLogFile=".\logs\stdout"
            hostingModel="inprocess">
  <handlerSettings>
    <handlerSetting name="debugFile" value=".\logs\aspnetcore-debug.log" />
    <handlerSetting name="debugLevel" value="FILE,TRACE" />
  </handlerSettings>
</aspNetCore>

stdoutLogFile とは異なり、モジュールは debugFile 用のフォルダーを 作成しません。logs ディレクトリはあらかじめ存在し、プール ID が書き込める必要があります。そうでないと何も得られず、誤った結論に至ります。このログには hostfxr の解決、検討されたフレームワークのバージョン、読み込みに失敗した DLL が記録されます。

対処 1: アプリが起動中に例外をスローした。これが大半です

イベント ログか stdout ログでスタックトレースが得られたなら、これがあなたのケースです。実務上の分類は次のとおりです。

  1. ローカルには存在し、サーバーには存在しない構成。 appsettings.Production.json が発行出力に含まれていない、本番相当が存在しない User Secrets の値、自分のマシンにだけ設定された環境変数などです。これは 接続文字列が見つからない失敗 のデプロイ版です。
  2. builder.Build() での DI グラフの失敗。 ASP.NET Core は Development でビルド時にスコープとサービス グラフを検証するため、Unable to resolve service for type やキャプティブ依存の問題は、親切なページではなく 500.30 として表面化します。unable to resolve service for type while attempting to activate と cannot consume scoped service from singleton を参照してください。
  3. 起動時に接続する外部依存。 アプリケーション プールのマネージド ID をカバーしていないアクセス ポリシーの Key Vault は、Microsoft が 500.30 について名指しで挙げているケースです。起動時に実行するマイグレーション、データベースに接続する構成プロバイダー、外向き通信のないサーバーでの OIDC ディスカバリ ドキュメントの取得。いずれもネットワークの問題を起動失敗に変えます。
  4. 証明書とデータ保護へのアクセス。 マシン ストアからの X.509 証明書の読み込みや、プール ID が書き込めないパスへのデータ保護キー リングの永続化は、最初のリクエストより前に例外をスローします。

このカテゴリ全体に対する構造的な対処は、起動失敗を偶発的なものではなく明示的で読めるものにすることです。IValidateOptions<T> と ValidateOnStart による起動時の構成検証 は、「アプリが 500.30 を返す」を、どの設定が欠けているかを正確に列挙する名前付きの OptionsValidationException に変えます。これは 5 分で終わる修正と、午後をまるごと使う調査との違いです。

ステージング環境のブラウザーで生の例外を確認したい場合は、環境変数を web.config に追加します。公開サーバーでは絶対に行わないでください。

<!-- Staging and test servers only. Do not ship this to an internet-facing host. -->
<aspNetCore processPath="dotnet" arguments=".\MyApp.dll" hostingModel="inprocess">
  <environmentVariables>
    <environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" />
    <environmentVariable name="ASPNETCORE_DETAILEDERRORS" value="true" />
  </environmentVariables>
</aspNetCore>

対処 2: アプリが対象とする共有フレームワークがインストールされていない

Microsoft は 500.30 の原因としてこれを最初に挙げています。アプリが、存在しないバージョンの ASP.NET Core 共有フレームワークを対象にしているケースです。サーバーに実際に何があるかを確認します。

dotnet --list-runtimes

TargetFramework とメジャー バージョンが一致する Microsoft.AspNetCore.App の行が必要で、しかもアプリケーション プールと同じアーキテクチャである必要があります。アプリが net11.0 でサーバーの最新が Microsoft.AspNetCore.App 10.0.x までなら、それが答えです。ASP.NET Core は既定でメジャー バージョンをまたいだロールフォワードを行いません。

.NET Hosting Bundle をインストールしてください。ランタイム、ASP.NET Core 共有フレームワーク、ANCM が 1 つのパッケージで入ります。ダウンロードそのものよりも多くの 500.30 を生んでいるのは、次の 2 つのインストール規則です。

net stop was /y
net start w3svc

完全な iisreset でも構いません。この手順を飛ばすことが、「ランタイムを入れたのにまだ失敗する」という続きが非常に多い理由です。

対処 3: アプリとアプリケーション プールのビット数が食い違っている

in-process ホスティングでは、アプリとインストール済みランタイムのアーキテクチャがアプリケーション プールのアーキテクチャと一致している必要があります。適合レイヤーは存在しません。32 ビットのバイナリが 64 ビットの w3wp.exe の中で CoreCLR を起動することはできません。

IIS マネージャーでアプリケーション プールを選択し、詳細設定 を開いて 32 ビット アプリケーションの有効化 を設定します。

コマンド ラインからであれば次のとおりです。

%windir%\system32\inetsrv\appcmd set apppool /apppool.name:MyAppPool /enable32BitAppOnWin64:false

ついでに、基本設定で .NET CLR バージョン を マネージド コードなし に設定してください。ASP.NET Core は自分で CoreCLR を起動するため、ワーカーにデスクトップ CLR が読み込まれる必要はまったくありません。ドキュメント上は任意ですが推奨とされており、レガシー モジュールとの紛らわしい相互作用を一括して取り除けます。

Hosting Bundle 固有の落とし穴が 1 つあります。OPT_NO_X86=1 を付けてインストールした場合、そのマシンには 32 ビット ランタイムがまったく存在しないため、プールの設定に関係なく x86 アプリは失敗します。

対処 4: アプリケーション プール ID が必要なものを読めない

既定の ApplicationPoolIdentity は仮想アカウントであり、権限が原因の 500.30 は他のあらゆる 500.30 とまったく同じ見え方をします。ID が ApplicationPoolIdentity からドメイン アカウントやサービス アカウントに変更されている場合は、デプロイ フォルダーへの読み取り権限と、アプリが書き込むすべての場所への書き込み権限があるか確認してください。プール名を使ってフォルダーに権限を付与します。

icacls "C:\inetpub\wwwroot\myapp" /grant "IIS AppPool\MyAppPool":(OI)(CI)RX

直接確認する価値がある事例が 2 つあります。マシン ストアから証明書の秘密鍵を読むにはキー コンテナーへの ACL が必要であること、そして %USERPROFILE% に触れるコードにはアプリケーション プールの ユーザー プロファイルの読み込み が True である必要があることです。既定では True ですが、堅牢化された環境ではしばしば無効化されています。

IIS の外でアプリを実行して調査範囲を半分にする

IIS の構成にさらに 1 時間費やす前に、サーバーにログオンし、デプロイ フォルダーでシェルを開き、アプリを直接実行してください。

cd C:\inetpub\wwwroot\myapp
set ASPNETCORE_ENVIRONMENT=Production
dotnet MyApp.dll

例外は完全なスタックトレース付きでコンソールに出力され、ログ出力の設定は一切必要ありません。ここでスローされるなら問題はアプリかその構成にあり、IIS は無実です。そのまま対処 1 に進んでください。きれいに起動して http://localhost:5000 で応答するなら、問題はホスティング層、つまりビット数、権限、モジュールのいずれかであり、対処 2、3、4 に進みます。このコマンド 1 つで、この記事のどちらの半分が必要かが決まります。

環境変数に注意してください。自分のアカウントと自分の環境で実行することは、プール ID として実行することとは異なります。したがって、ここで正常に動いてもファイル権限が正しいことの証明にはなりません。証明されるのは、コードとデプロイされた構成ファイルが正しいということだけです。

500.30 ではない近隣のコード

500.30 の検索流入には、よく似た別のケースが多数含まれます。ページに別の表示が出ているなら、それは別の問題であり、対処も別です。

out-of-process ホスティングへの切り替えは、対処というより正当な診断手段です。web.config に hostingModel="outofprocess" を設定するとワーカーがリサイクルされ、アプリは子プロセスの dotnet.exe として実行されます。この状態では起動失敗の観測がはるかに容易で、requestTimeout と rapidFailsPerMinute も再び有効になります。読める形のエラーを得るために使い、その後は性能のために in-process へ戻してください。

500.30 の調査は、順番どおりに進めれば短時間で終わります。イベント ログ、次にコンソールからの実行、次にビット数とランタイムです。長い午後になるのは、ブラウザーのページから始めて推測に頼ったときだけです。

関連記事

参考資料

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< 戻る