﻿# 如何正確地發行 ASP.NET Core 網站到遠端 IIS 站台

我們最近有專案需要發行一個用 ASP.NET Core 5 開發的網站，有經驗的人應該知道，當網站正在執行時，尤其是 Windows 作業系統，過程中所有 `*.dll` 檔案都會被鎖定，無法成功覆蓋檔案，以致於自動部署失敗。今天這篇文章我打算來分享幾個常見的作法，以及我認為最好的作法。

### 手動關閉 IIS 站台 (WebSite)

這種方法的基本流程是：

1.  停止 IIS 站台
2.  進行網站程式更新
3.  啟動 IIS 站台

我發現很多人會用 **關閉 IIS 站台** 的方式讓網站停下來，但我覺得這方法有點問題！🔥

1.  首先，你今天的目的是要「更版」，而不是要「關閉站台」，用這種方式雖然可以讓網站關閉，但是卻**不一定會關閉**應用程式集區 (Application Pool)。怎麼說呢？因為一台 IIS 伺服器中，一個應用程式集區可以讓多個**站台**或**應用程式目錄**使用，你就算停掉一個站台，可能還有其他的站台還在使用**應用程式集區**。
    
    重點是，應用程式集區負責用來管理 `w3wp.exe` 程序 (Process)，你的 Process 不停下來，現有被鎖定的 `*.dll` 組件一樣還是被鎖定，你一樣無法更版！
    
    > 一般我們都**不建議**應用程式集區共用，一個站台使用一個獨立的應用程式集區才正常！
    
2.  其次，你在關閉站台的時候，網站是被直接關閉的狀態，完全連不上，是連 HTTP 狀態碼都不會回應的狀態，我覺得這很不正常，我個人很不喜歡這種方法！
    
3.  其三，我們都知道 IIS 可以支援同一個 Port 80 同時服務多個不同**域名**的站台。所以當你在關閉站台的時候，很有可能使用者連進網址看到的確實「另一個網站」的內容，這就更怪了。這點我是完全無法接受！
    

📝 **小結**：我覺得這是個有問題的更版方法，請大家不要再這麼做了！

### 手動關閉應用程式集區 (Application Pool)

這種方法的基本流程是：

1.  找出要關閉的**站台**所使用的**應用程式集區**
2.  停止**應用程式集區**
3.  進行網站程式更新
4.  啟動**應用程式集區**

這種方法還行，不過若你有多個網站站台共用同一個應用程式集區，會將所有網站也一起關閉。

關閉**應用程式集區**之後，站台其實不會關閉，而是改回應 **HTTP 503 Service Unavailable** 錯誤：

![HTTP Error 503. The service is unavailable.](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/042148a5a3cd25d43a75-120966194-782b6b80-c798-11eb-9847-7a561e6c4ca2.png)

這種作法有幾個優點：

1.  網站依然可以正常回應，而且 **HTTP 503** 可以表達非常明確的意圖，就是**服務無法使用**！
2.  網站不會像上面第一種方法那樣，讓網站連到其他完全不相關的站台。

如果你想用 PowerShell 自動化這個過程，可以參考以下命令：

```powershell
# 停止應用程式集區
(Get-IISAppPool "DefaultAppPool").Stop()

# >>> 進行網站程式更新 <<<

# 啟動應用程式集區
(Get-IISAppPool "DefaultAppPool").Start()
```

📝 **小結**：我覺得這種方法還能接受，只是手動關閉應用程式集區程序比較麻煩一點！

### 建立應用程式離線檔 (App Offline file)

這種方法的基本流程是：

1.  在站台根目錄加入 `app_offline.htm` 檔案 (檔案不需要內容)
2.  進行網站程式更新
3.  在站台根目錄刪除 `app_offline.htm` 檔案

你看，多簡單！這其實是微軟官方建議的部署方法！👍

.NET Runtime 有個 [ASP.NET Core Module](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/aspnet-core-module?view=aspnetcore-5.0&WT.mc_id=DT-MVP-4015686) (**ANCM**)，它是一個 IIS 模組，用來幫助 IIS 管理 ASP.NET Core 的執行程序。

事實上 **ANCM** 會在啟動 Kestrel 的時候，同時監控應用程式根目錄下是否有個 `app_offline.htm` 檔案出現，只要有出現，就會立即對應用程式進行**優雅的關閉**(gracefully shut down)，並且停止接聽從用戶端發來的要求(Requests)。

這裡有個重點，那就是所謂 **優雅的關閉**(gracefully shut down)，這個概念我在 [當 .NET Core 執行在 Linux 或 Docker 容器中如何優雅的結束](/post/2020/07/22/How-to-handle-graceful-shutdown-in-NET-Core) 文章中分享過這個觀念。這個意思也就是說，當 `app_offline.htm` 檔案出現的時候，並不代表著 ASP.NET Core 程序會立即停止，如果你的程式沒寫好，或是有個長時間執行的前景執行緒沒有完成，都會延長程序終止的時間。在 IIS 中，**應用程式集區**的**進階設定**有個 `ShutdownTimeLimit` 設定值，用來定義當執行程序**不優雅的不結束**時，最長可以允許等待多久。IIS 的預設值為 `90` 秒。

> 一般來說，當你在透過 CI/CD 自動部署的階段，在建立 `app_offline.htm` 檔案之後通常會等待幾秒鐘，這個時間長短很難說，端看你的程式與電腦當下的效能狀況而定。所以比較正確的作法，通常是實作 **重試** (Rety) 機制，當部署失敗就重新再發布一次，重複個兩三次應該就非常足夠了。

### 更多關於應用程式離線檔

基本上，你的網站只要有了 `app_offline.htm` 檔案就會立刻讓網站所有要求全部都轉入 HTTP 503 的回應，而頁面中的回應內容就是 `app_offline.htm` 的檔案內容。

這裡有點要特別注意，請不要將 `app_offline.htm` 網頁中需要載入的圖檔、JS、CSS 等檔案放在原本網站中，因為網站已經**暫時無法服務**。比較正確的作法是將這些靜態檔案放到 CDN 或是其他網頁儲存空間，如果你要把這些資源全部弄成 [data URIs](https://developer.mozilla.org/zh-TW/docs/Web/URI/Reference/Schemes/data) 格式也是可以的。

另外，如果你的站台主要以 Web API 為主的話，要注意除了回應狀態碼會統一變成 HTTP 503 之外，還要注意所有回應的 `Content-Type` 都會暫時變成 `text/html` 喔！

### 相關連結

-   [How to start and stop application pool in IIS using powershell script - Stack Overflow](https://stackoverflow.com/a/44352403/910074)
-   [應用程式離線檔 (app\_offline.htm) | Microsoft Docs](https://learn.microsoft.com/zh-tw/aspnet/core/host-and-deploy/app-offline?view=aspnetcore-5.0&WT.mc_id=DT-MVP-4015686)
    -   [Walkthrough: Deploying an ASP.NET Web Application Using XCOPY](https://learn.microsoft.com/en-us/previous-versions/aspnet/f735abw9\(v=vs.100\)?WT.mc_id=DT-MVP-4015686) (.NET Framework)
-   [ASP.NET Core Module | Microsoft Docs](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/aspnet-core-module?view=aspnetcore-5.0&WT.mc_id=DT-MVP-4015686)
    -   [In-process hosting with IIS and ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/in-process-hosting?view=aspnetcore-5.0&WT.mc_id=DT-MVP-4015686)
    -   [Out-of-process hosting with IIS and ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/out-of-process-hosting?view=aspnetcore-5.0&WT.mc_id=DT-MVP-4015686)
-   [An application pool exceeds time limits - Internet Information Services | Microsoft Docs](https://learn.microsoft.com/en-us/troubleshoot/iis/application-pool-exceed-time-limits?WT.mc_id=DT-MVP-4015686)
-   [Taking an ASP.NET application offline - Rick Strahl's Web Log](https://weblog.west-wind.com/posts/2006/Jul/12/Taking-an-ASPNET-application-offline)
-   [data URIs - HTTP | MDN](https://developer.mozilla.org/zh-TW/docs/Web/URI/Reference/Schemes/data)
-   [使用 Web Deploy 讓 Web 應用程式離線 | Microsoft Docs](https://learn.microsoft.com/zh-tw/aspnet/web-forms/overview/deployment/advanced-enterprise-web-deployment/taking-web-applications-offline-with-web-deploy?WT.mc_id=DT-MVP-4015686)
