Mert Özen Her Satırda Daha İleri
DevOps Web Server'lar ve Hosting — #09

IIS'te ASP.NET Core: Hosting Bundle, ASP.NET Core Module, In-Process ve Out-of-Process

Mert Özen 8 Kas 2026 9 dk 4 görüntülenme
IIS'te ASP.NET Core: Hosting Bundle, ASP.NET Core Module, In-Process ve Out-of-Process

IIS'te ASP.NET Core'u ASP.NET Core Module çalıştırır; Hosting Bundle IIS'ten önce kurulursa onarılmalıdır, yoksa 500.19 alırsın ve uygulama tek satır çalışmaz. Publish'in ürettiği web.config'i, in-process ile out-of-process farkını, app pool ve ApplicationPoolIdentity izinlerini ve 500.30, 500.31, 500.38, 502.5 gibi hataların nereye baktırdığını anlatıyoruz; IIS bu Mac'te çalıştırılmadı, web.config çıktıları gerçek publish'lerden.

Sürüm notu: Bu yazıdaki publish çıktıları ve web.config dosyaları .NET 10 SDK (Software Development Kit) 10.0.402 ile üretilmiştir. IIS (Internet Information Services) yalnız Windows'ta çalıştığı için IIS tarafındaki adımlar (kurulum, PowerShell komutları, hata sayfaları) bu yazı için denenmedi; Microsoft Learn'deki ASP.NET Core on IIS dokümantasyonuna (Kasım 2026) dayanıyor.

Önce Tahmin Et

Bookstore.Api'yi Windows Server 2025'e taşıyorsun. Sunucu yeni; önce .NET Hosting Bundle'ı kuruyorsun, sonra Server Manager'dan IIS rolünü ekliyorsun, siteyi oluşturup publish klasörünü kopyalıyorsun. Tarayıcıda siteyi açınca uygulama yerine bir IIS hata sayfası çıkıyor: 500.19, "the related configuration data for the page is invalid".

Tahmin et: web.config'te bir yazım hatası mı var? Uygulamanın log'unda ne yazıyor? Ve sunucuyu yeniden başlatmak işe yarar mı?

IIS ve ASP.NET Core Nasıl Konuşur?

IIS, Windows'un kendi web server'ı; HTTP (Hypertext Transfer Protocol) trafiğini çekirdekteki bir sürücüyle karşılıyor. Klasik ASP.NET (.NET Framework) uygulamaları doğrudan IIS'in içindeki CLR'da (Common Language Runtime) çalışıyordu. ASP.NET Core'un çalışma modeli farklı: IIS uygulamayı kendisi bilmiyor, aradaki köprü ASP.NET Core Module (ANCM). ANCM, IIS'e takılan native bir modül; gelen isteği alıp ya aynı process'te çalışan ASP.NET Core'a veriyor ya da ayrı bir process'te Kestrel'i başlatıp isteği ona iletiyor. Serinin ilk yazısındaki beş topolojiden üçüncüsü bu.

Bir isteğin yolu: client'tan gelen bağlantıyı Windows çekirdeğindeki HTTP.sys sürücüsü kabul ediyor, isteği ilgili sitenin application pool'una, yani w3wp.exe worker process'ine veriyor. Oradan sonrası hosting modeline bağlı.

09-iis-hosting-models-tr.png

Hosting Bundle: Kurulum Sırası ve Seçenekler

Sunucuda iki şey gerekiyor: .NET runtime ve ANCM. İkisini birlikte kuran paket .NET Hosting Bundle. Microsoft Learn'deki "Install the .NET Hosting Bundle" sayfası kurulum sırası için açık bir uyarı veriyor: Hosting Bundle IIS'ten önce kurulduysa kurulumun onarılması gerekiyor; IIS'i ekledikten sonra Hosting Bundle'ı yeniden çalıştır. Tahminin cevabı bu. Bundle kurulurken IIS yoktu, ANCM IIS'e kaydedilemedi; web.config'teki AspNetCoreModuleV2 handler'ı IIS için bilinmeyen bir modül, IIS de bunu "configuration data is invalid" olarak raporluyor. Microsoft'un hata referansı 500.19'u tam bu durum için listeliyor: yanlış rol, ANCM kurulu değil ya da yetki sorunu; Application Event Log'da kayıt yok, stdout log'u hiç oluşmuyor. Uygulama tek satır çalışmadığı için onun log'unda bir şey aramak boşuna, sunucuyu yeniden başlatmak da hiçbir şeyi değiştirmiyor.

Doğru sıra ve sonrası (PowerShell, yönetici olarak; bu Mac'te çalıştırılmadı):

# 1. IIS rolü
Install-WindowsFeature -Name Web-Server -IncludeManagementTools

# 2. Hosting Bundle (IIS'ten sonra; daha önce kurulduysa yeniden çalıştırıp Repair)
.\dotnet-hosting-10.0.x-win.exe /quiet /norestart

# 3. IIS'i yeniden başlat ki worker process'ler yeni PATH'i görsün
net stop was /y
net start w3svc

Kurulum programı birkaç seçenek kabul ediyor: OPT_NO_ANCM=1 modülü atlıyor, OPT_NO_RUNTIME=1 ve OPT_NO_SHAREDFX=1 runtime'ı atlıyor (sunucuda yalnız self-contained uygulamalar varsa), OPT_NO_X86=1 32-bit runtime'ları kurmuyor. Dokümanın notu önemli: seçenekler registry'ye yazılıyor ve aynı Major.Minor bandındaki sonraki kurulumlar, komut satırında başka bir şey verilmedikçe aynı seçenekleri kullanıyor. Bir kez OPT_NO_RUNTIME=1 ile kurulan bir sunucu, sonraki güncellemelerde de runtime almıyor.

Kurulu ANCM'nin sürümünü %PROGRAMFILES%\IIS\Asp.Net Core Module\V2\aspnetcorev2.dll dosyasının özelliklerinden okuyabilirsin. ANCM, desteklenen .NET sürümleriyle geriye ve ileriye uyumlu; en güncel Hosting Bundle'ı kurmak eski uygulamaları bozmuyor.

Publish'te Üretilen web.config

IIS'in ANCM'ye ne yapacağını söyleyen dosya web.config ve onu sen yazmıyorsun; dotnet publish üretiyor. Bu kısım platformdan bağımsız, bu yüzden aşağıdaki çıktıların hepsi gerçek publish'lerden:

$ dotnet publish -c Release -o out/pub-fdd
$ cat out/pub-fdd/web.config
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <location path="." inheritInChildApplications="false">
    <system.webServer>
      <handlers>
        <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
      </handlers>
      <aspNetCore processPath="dotnet" arguments=".\Bookstore.Api.dll" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" hostingModel="inprocess" />
    </system.webServer>
  </location>
</configuration>

Satırlar: bütün istekleri (path="*") AspNetCoreModuleV2'ye ver; uygulamayı dotnet .\Bookstore.Api.dll ile başlat; hosting modeli inprocess; stdout log'u kapalı. inheritInChildApplications="false", bu ayarların altına eklenecek sub-application'lara miras kalmasını engelliyor; onuncu yazıda bunun neden önemli olduğunu göreceğiz. Self-contained bir Windows publish'inde yalnız processPath değişiyor:

$ dotnet publish -c Release -r win-x64 --self-contained true -o out/pub-win-scd
$ grep "<aspNetCore" out/pub-win-scd/web.config
      <aspNetCore processPath=".\Bookstore.Api.exe" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" hostingModel="inprocess" />

Ortam adını da publish'te gömebilirsin; EnvironmentName property'si web.config'e bir ortam değişkeni ekliyor:

$ dotnet publish -c Release -p:EnvironmentName=Staging -o out/pub-staging
$ sed -n '/<aspNetCore/,/<\/aspNetCore>/p' out/pub-staging/web.config
      <aspNetCore processPath="dotnet" arguments=".\Bookstore.Api.dll" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" hostingModel="inprocess">
        <environmentVariables>
          <environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Staging" />
        </environmentVariables>
      </aspNetCore>

Dokümana göre web.config'teki environmentVariables, sistem ortam değişkenlerinden önce geliyor. Secret'ları buraya yazma: dosya site klasöründe duruyor ve her publish'te yeniden üretiliyor.

In-Process ve Out-of-Process

ASP.NET Core 3.0'dan beri varsayılan in-process. Uygulama IIS worker process'inin (w3wp.exe) içinde çalışıyor ve Kestrel hiç kullanılmıyor; yerine IIS HTTP Server (IISHttpServer) isteği native yapıdan managed yapıya çeviriyor. Microsoft'un dokümanı, loopback üzerinden proxy'leme olmadığı için in-process modelin belirgin şekilde daha yüksek throughput verdiğini söylüyor. Bedelleri de yazıyor: her uygulama kendi application pool'unda olmalı, uygulamanın bitness'i (x64 ya da x86) app pool'unkiyle aynı olmalı ve requestTimeout ayarı geçerli değil.

Out-of-process'te ANCM, dotnet Bookstore.Api.dll'i ayrı bir process olarak başlatıyor, ona ASPNETCORE_PORT ortam değişkeniyle rastgele bir port veriyor ve istekleri http://localhost:{port}'a HTTP ile iletiyor. Uygulamada Kestrel çalışıyor ve IIS integration otomatik olarak forwarded headers'ı açıyor; üçüncü yazıdaki KnownProxies ayarı bu modelde senin işin değil. Kestrel'in yalnız IIS'ten gelen istekleri kabul etmesi için ANCM her process'e bir pairing token veriyor (ASPNETCORE_TOKEN) ve her isteğe MS-ASPNETCORE-TOKEN header'ını ekliyor; token'ı tutmayan istekler reddediliyor. Modeli proje dosyasında seçiyorsun:

<PropertyGroup>
  <AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel>
</PropertyGroup>
$ dotnet publish -c Release -p:AspNetCoreHostingModel=OutOfProcess -o out/pub-oop
$ grep "<aspNetCore" out/pub-oop/web.config
      <aspNetCore processPath="dotnet" arguments=".\Bookstore.Api.dll" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" hostingModel="OutOfProcess" />

Hangisini ne zaman? Varsayılan in-process çoğu uygulama için doğru seçim. Out-of-process'i şu durumlarda düşün: uygulamanın Kestrel'e özgü bir özelliği kullanması (örneğin Kestrel'in connection middleware'leri), aynı app pool'da birden çok uygulama çalıştırma zorunluluğu (in-process bunu desteklemiyor, 500.35 hatası veriyor) ya da uygulamanın çökmesinin IIS worker process'ini etkilemesini istememen. Single-file publish ise in-process'le hiç çalışmıyor; aşağıdaki hata tablosunda 500.38.

Site, Binding ve Application Pool

Siteyi IIS Manager'dan ya da PowerShell'in WebAdministration modülüyle oluşturabilirsin. Bookstore için (bu Mac'te çalıştırılmadı):

Import-Module WebAdministration

# app pool: .NET CLR Version "No Managed Code"
New-WebAppPool -Name "BookstoreApi"
Set-ItemProperty IIS:\AppPools\BookstoreApi -Name managedRuntimeVersion -Value ""

# site: fiziksel klasör, binding ve app pool
New-Website -Name "BookstoreApi" -PhysicalPath "C:\sites\bookstore-api" `
    -ApplicationPool "BookstoreApi" -HostHeader "api.bookstore.example" -Port 80

# app pool kimliğine klasör üzerinde okuma ve çalıştırma izni
ICACLS C:\sites\bookstore-api /grant "IIS AppPool\BookstoreApi:(OI)(CI)RX"

"No Managed Code" (managedRuntimeVersion boş), app pool'a .NET Framework CLR'ını yüklememesini söylüyor; ASP.NET Core kendi runtime'ını (CoreCLR) kendisi başlatıyor. Binding, sitenin hangi IP (Internet Protocol) adresi, port ve host adıyla eşleşeceğini belirliyor; Nginx'teki listen ile server_name'in karşılığı. HTTPS (HTTP Secure) binding'i için certificate'ı Windows certificate store'a koyup binding'e seçiyorsun; TLS (Transport Layer Security) burada IIS'te, yani HTTP.sys'te bitiyor.

Varsayılan app pool kimliği ApplicationPoolIdentity: IIS her app pool için IIS AppPool\{ad} adında sanal bir hesap oluşturuyor. Gerçek bir kullanıcı değil, ama klasör izinlerinde kullanılabiliyor. Uygulama site klasörünü okuyabilmeli; log klasörüne, Data Protection key'lerinin klasörüne ya da upload klasörüne yazacaksa o klasörlere ayrıca yazma izni gerekiyor. Sekizinci yazıdaki systemd kullanıcısıyla aynı fikir: uygulama kendi kimliğiyle ve yalnız gerektiği kadar yetkiyle çalışsın.

Hata Ayıklama Tablosu: 500.30, 500.31, 502.5, 500.19

IIS'te bir ASP.NET Core uygulaması açılmadığında tarayıcıda gördüğün kod, sorunun nerede olduğunu söylüyor. Microsoft Learn'deki "Troubleshoot ASP.NET Core on Azure App Service and IIS" sayfasından en sık rastlananlar:

KodAnlamıİlk bakılacak yer
500.19IIS config'i okuyamadı: ANCM kurulu değil, IIS rolü eksik, yetki sorunu ya da sub-app'te tekrar eden handlersHosting Bundle'ı onar; Event Log'da kayıt yok
500.30In-process başlatma hatası: CLR başladı ama uygulama ayağa kalkamadıApplication Event Log ve stdout log'u
500.31ANCM gereken runtime'ı bulamadı (Microsoft.NETCore.App ya da Microsoft.AspNetCore.App yok)Sunucudaki runtime sürümleri; self-contained publish
500.32DLL (Dynamic Link Library) yüklenemedi; çoğunlukla x86 app pool ile x64 publish uyumsuzluğuApp pool'un "Enable 32-Bit Applications" ayarı
500.34 / 500.35Aynı app pool'da karışık hosting modelleri ya da birden çok in-process uygulamaHer uygulamaya ayrı app pool
500.37Uygulama startupTimeLimit içinde (varsayılan 120 saniye) başlayamadıBaşlangıçta yavaş iş: migration, cache ısıtma
500.38Single-file publish in-process ile yüklenemiyorPublishSingleFile'ı kapat ya da out-of-process'e geç
502.5Out-of-process'te process başlatılamadı ya da hemen çöktüApplication Event Log ve stdout log'u

500.38 için küçük bir deney: win-x64 için single-file publish aldım. SDK hiçbir uyarı vermedi ve web.config'e yine hostingModel="inprocess" yazdı:

$ dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o out/pub-win-single
$ grep -o 'processPath="[^"]*"\|hostingModel="[^"]*"' out/pub-win-single/web.config
processPath=".\Bookstore.Api.exe"
hostingModel="inprocess"

Yani bu çıktı build'de sorunsuz görünüyor ve hata ilk kez sunucuda, 500.38 olarak çıkıyor. Single-file kullanacaksan proje dosyasına AspNetCoreHostingModel olarak OutOfProcess yaz.

Üç log kaynağı var. Application Event Log'da IIS AspNetCore Module V2 kaynaklı kayıtlar başlangıç hatalarının özetini veriyor. stdout log'u, uygulamanın console çıktısını dosyaya yazıyor: web.config'te stdoutLogEnabled="true" yap, logs klasörünü oluştur ve app pool kimliğine yazma izni ver. Dokümanın uyarısı net: stdout log'unun boyut ya da dosya sayısı sınırı yok, sorun çözülünce kapat. Daha derine inmek için ANCM'nin kendi debug log'u:

<aspNetCore ...>
  <handlerSettings>
    <handlerSetting name="debugLevel" value="file" />
    <handlerSetting name="debugFile" value="c:\temp\ancm.log" />
  </handlerSettings>
</aspNetCore>

Son çare uygulamayı sunucuda komut satırından çalıştırmak: site klasöründe dotnet .\Bookstore.Api.dll. Exception doğrudan ekrana düşüyor ve çoğu zaman eksik bir configuration değeri ya da erişilemeyen bir veritabanı olarak çıkıyor.

Üretimde Dikkat

Hosting Bundle'ı güncellemeyi bakım takvimine koy. Güvenlik yamaları runtime'a geliyor; Hosting Bundle'ın yeni sürümünü kurmak framework-dependent bütün uygulamaları günceller. Kurulumdan sonra IIS'i yeniden başlatman gerekebilir, bu da sitelerin kısa süre kapanması demek.

Her uygulamaya kendi app pool'unu ver. In-process için zorunlu, out-of-process için de önerilen bu; bir uygulamanın çökmesi ya da bellek sızıntısı diğerlerini etkilemiyor, klasör izinleri de uygulama bazında ayrılıyor.

32-bit ile 64-bit'i baştan netleştir. Publish'teki RID (Runtime Identifier), sunucudaki runtime ve app pool'un "Enable 32-Bit Applications" ayarı aynı şeyi söylemeli; uyumsuzluk 500.32 ya da Event Log'da 0x800700c1 hata kodu olarak geliyor.

Sık Düşülen Tuzaklar

Hosting Bundle'ı IIS'ten önce kurmak. 500.19, Event Log'da kayıt yok, stdout log'u yok; çözüm bundle'ı onarmak.

stdout log'unu açık unutmak. Sınırsız büyüyen dosyalar diski dolduruyor ve sunucuyu durdurabiliyor; kalıcı log için rotation yapan bir logging kütüphanesi kullan.

Birden çok in-process uygulamayı aynı app pool'a koymak. 500.35; IIS Manager'ın site eklerken her site için ayrı pool önermesi bu yüzden.

Single-file publish'i in-process ile denemek. Build temiz, sunucu 500.38.

App pool kimliğinin yazma iznini unutmak. Uygulama log ya da key yazamadığında hata uygulama içinde oluşuyor ve çoğu zaman 500.30 olarak görünüyor.

Küçük Bir Deneme

★☆☆ Kendi projende dotnet publish al ve web.config'i incele. AspNetCoreHostingModel'i OutOfProcess yapıp tekrar publish et; değişen tek satır hangisi? -r win-x64 --self-contained eklediğinde ne değişiyor?

★★☆ Bir Windows makinen ya da VM'in (virtual machine) varsa IIS'i Hosting Bundle'dan sonra kur ve 500.19'u kendin üret. Sonra Hosting Bundle'ı Repair ile onar ve aynı siteyi aç. Event Log'da bu sırada hangi kayıtlar oluşuyor?

★★★ Aynı makinede uygulamayı önce in-process, sonra out-of-process ile çalıştır ve basit bir yük testiyle (örneğin bombardier ya da k6) /api/books'un saniyedeki istek sayısını karşılaştır. Out-of-process'te Kestrel'e giden isteklerin MS-ASPNETCORE-TOKEN header'ını uygulama içinde log'la; token'ı olmayan bir isteği doğrudan Kestrel'in portuna gönderdiğinde ne oluyor?

Uygulama IIS'te çalışıyor. Ama ikinci deploy'da dosyalar kilitli, ilk istek yavaş ve Angular'ın derin linkleri 404 veriyor. Bir sonraki yazıda IIS'te production'ı ele alıyoruz: Web Deploy ve app_offline.htm, shadow copy, Application Initialization, Data Protection, sub-application ve URL (Uniform Resource Locator) Rewrite ile Angular.