﻿# 如何在 Windows Containers 安全的調整 IIS 站台設定

當你想要對 IIS 站台調整許多設定，又怕調整到一半突然出問題，這時候會殘留許多調整到一半的設定，如果想要做到 IaC ([Infrastructure as code](https://en.wikipedia.org/wiki/Infrastructure_as_code)) 自動化，這個問題就必須被解決。還好 [IISAdministration](https://learn.microsoft.com/en-us/powershell/module/iisadministration/) 模組中有提供「交易」相關的 Cmdlets 可用，本篇文章就來說說如何使用它。

### 建立容器

官方的 `mcr.microsoft.com/dotnet/framework/aspnet:4.8` 容器映象，已經預先安裝好 `Web Server (IIS)` 與 `ASP.NET 4.8` 角色服務，我將會使用這個 image 為主要的執行環境。

```powershell
docker run --name=mysite -d --isolation=process -p 80:80 -p 443:443 -v C:\Projects\WebApplication1:C:\Inetpub\wwwroot mcr.microsoft.com/dotnet/framework/aspnet:4.8
```

以下對上述參數進行說明：

-   `--name=mysite` 設定容器好記名稱為 `mysite`，方便後續命令說明。
-   `-d` 代表我們要將容器執行在 detach 模式（背景執行）。
-   `--isolation=process` 由於我們要複製現有的 PFX 憑證進容器，在 Windows 10 的 Windows Containers 必須要使用 `proccess` 隔離模式執行，才能執行 `docker cp` 命令複製檔案。
-   `-p 80:80 -p 443:443` 由於 Windows Containers 執行容器時，預設採用 `nat` 模式，所以需要將容器的 `IP:Ports` 對應到本機 `Ports` 比較方便測試。
-   `-v C:\Projects\WebApplication1:C:\Inetpub\wwwroot` 將一個現有的 ASP.NET MVC 5 專案對應到容器的 `C:\Inetpub\wwwroot` 路徑。
-   `--entrypoint powershell` 因為微軟官方的 `aspnet` 容器映象預設只能跑在 `detach` (`-d`) 模式，為了方便我們測試，我想改用 `powershell` 來當成預設進入程式。
-   `mcr.microsoft.com/dotnet/framework/aspnet:4.8` 已經預載 `Web Server (IIS)` 與 `ASP.NET 4.8` 角色服務。這是微軟官方的「多架構」容器映象，詳見 [Windows Container 版本相容性與多重架構容器映像介紹](/post/2019/06/24/Windows-Containers-version-compatibility-and-multi-arch-images) 文章。

接著透過 `docker exec` 進入容器中操作：

```powershell
docker exec -it -w c:\inetpub\wwwroot mysite powershell
```

### 新增站台繫結

1.  新增 HTTP 站台繫結
    
    綁定特定域名 ( 以 `blog.miniasp.com` 為例 )
    
    ```powershell
    New-IISSiteBinding -Name "Default Web Site" -Protocol http -BindingInformation "*:80:blog.miniasp.com"
    ```
    
    綁定特定ＩＰ ( 以 `192.168.9.100` 為例 )
    
    ```powershell
    New-IISSiteBinding -Name "Default Web Site" -Protocol http -BindingInformation "192.168.9.100:80:"
    ```
    
    綁定特定埠號 ( 以 `8080` 為例 )
    
    ```powershell
    New-IISSiteBinding -Name "Default Web Site" -Protocol http -BindingInformation ":8080:"
    ```
    
    移除綁定
    
    ```powershell
    Remove-IISSiteBinding -Name "Default Web Site" -Protocol http -BindingInformation "*:80:blog.miniasp.com" -Confirm:$false
    Remove-IISSiteBinding -Name "Default Web Site" -Protocol http -BindingInformation "192.168.9.100:80:" -Confirm:$false
    Remove-IISSiteBinding -Name "Default Web Site" -Protocol http -BindingInformation ":8080:" -Confirm:$false
    ```
    
2.  新增 HTTPS 站台繫結
    
    綁定 IP-based SSL (不指定特定域名)
    
    ```powershell
    $cert = New-SelfSignedCertificate -DnsName blog.miniasp.com -CertStoreLocation cert:\LocalMachine\My
    
    New-IISSiteBinding -Name "Default Web Site" -Protocol https -BindingInformation "*:443:" -CertificateThumbPrint $cert.Thumbprint -CertStoreLocation My
    ```
    
    綁定 SNI-based SSL (指定特定域名)
    
    ```powershell
    $cert = New-SelfSignedCertificate -DnsName blog.miniasp.com -CertStoreLocation cert:\LocalMachine\My
    
    New-IISSiteBinding -Name "Default Web Site" -Protocol https -BindingInformation "*:443:blog.miniasp.com" -SslFlag Sni -CertificateThumbPrint $cert.Thumbprint -CertStoreLocation My
    ```
    
    移除綁定
    
    ```powershell
    Remove-IISSiteBinding -Name "Default Web Site" -Protocol https -BindingInformation "*:443:" -Confirm:$false
    Remove-IISSiteBinding -Name "Default Web Site" -Protocol https -BindingInformation "*:443:blog.miniasp.com" -Confirm:$false
    ```
    

### 批次新增站台繫結

如果你一次需要新增好幾個站台繫結，或是一口氣要用 PowerShell 調整多項 IIS 設定的話，透過一行一行執行其實是不太好的。因為這些 PowerShell Cmdlets 每次執行時，都會確實寫入到 `applicationHost.config` 設定檔中，若你有其中一段設定掛掉的話，就可能會有設定不一致的問題！

在 [IISAdministration](https://learn.microsoft.com/en-us/powershell/module/iisadministration/) 有兩個 Cmdlets 可以讓你的批次作業擁有「交易特性」(Transaction)。

-   [Start-IISCommitDelay](https://learn.microsoft.com/en-us/powershell/module/iisadministration/start-iiscommitdelay)
-   [Stop-IISCommitDelay](https://learn.microsoft.com/en-us/powershell/module/iisadministration/stop-iiscommitdelay)

以下是使用範例：

```powershell
Start-IISCommitDelay

New-IISSite -Name "Test" -PhysicalPath "c:\Inetpub\wwwroot" -BindingInformation ":80:www.example.com" -Protocol http
New-IISSiteBinding -Name "Test" -Protocol http -BindingInformation "*:80:i1.example.com"
New-IISSiteBinding -Name "Test" -Protocol http -BindingInformation "*:80:i2.example.com"
New-IISSiteBinding -Name "Test" -Protocol http -BindingInformation "*:80:i3.example.com"
New-IISSiteBinding -Name "Test" -Protocol http -BindingInformation "*:80:i4.example.com"
New-IISSiteBinding -Name "Test" -Protocol http -BindingInformation "*:80:i5.example.com"

Stop-IISCommitDelay -Commit $True
```

如果你發現設定的過程有問題發生，就可以用 `Stop-IISCommitDelay -Commit $False` 來取消取消之前所有的變更！

以下是完整的使用範例：

```powershell
$ErrorActionPreference = "Stop"

Get-IISSite

try
{
  Write-Host "開始建立站台"
  Start-IISCommitDelay

  New-IISSite -Name "Test" -PhysicalPath "c:\Inetpub\wwwroot" -BindingInformation ":80:www.example.com" -Protocol http
  New-IISSiteBinding -Name "Test" -Protocol http -BindingInformation "*:80:i1.example.com"
  New-IISSiteBinding -Name "Test" -Protocol http -BindingInformation "*:80:i2.example.com"
  New-IISSiteBinding -Name "Test" -Protocol http -BindingInformation "*:80:i2.example.com" # 重複的域名
  New-IISSiteBinding -Name "Test" -Protocol http -BindingInformation "*:80:i4.example.com"
  New-IISSiteBinding -Name "Test" -Protocol http -BindingInformation "*:80:i5.example.com"

  Write-Host "站台建立成功！"
  Stop-IISCommitDelay -Commit $True
}
catch
{
  Write-Host "建立站台發生錯誤，復原所有設定"
  Stop-IISCommitDelay -Commit $False
}

Get-IISSite
```

### 使用 [Get-IISServerManager](https://learn.microsoft.com/zh-tw/powershell/module/iisadministration/get-iisservermanager) 批次設定

透過 [IISAdministration](https://learn.microsoft.com/en-us/powershell/module/iisadministration/) 模組下的 [Get-IISServerManager](https://learn.microsoft.com/zh-tw/powershell/module/iisadministration/get-iisservermanager) 也可以讓你的批次作業擁有「交易特性」(Transaction)。

透過 [Get-IISServerManager](https://learn.microsoft.com/zh-tw/powershell/module/iisadministration/get-iisservermanager) 可以取回 .NET 的 [Microsoft.Web.Administration](https://learn.microsoft.com/en-us/dotnet/api/microsoft.web.administration?view=iis-dotnet) 物件，讓你直接透過 .NET 物件來操作 IIS 內的各項設定。以下是使用範例：

```powershell
$sm = Get-IISServerManager

$sm.Sites["Default Web Site"].Bindings.Add("*:80:www.example.com", "http")
$sm.Sites["Default Web Site"].Bindings.Add("*:80:a.example.com", "http")
$sm.Sites["Default Web Site"].Bindings.Add("*:80:b.example.com", "http")
$sm.Sites["Default Web Site"].Bindings.Add("*:80:c.example.com", "http")
$sm.Sites["Default Web Site"].Bindings.Add("*:80:d.example.com", "http")
$sm.Sites["Default Web Site"].Bindings.Add("*:80:e.example.com", "http")

$sm.CommitChanges();
```

我個人還是比較喜歡用 [IISAdministration](https://learn.microsoft.com/en-us/powershell/module/iisadministration/) 模組下標準的 Cmdlets 來調整 IIS 設定，一來比較簡單，二來就算沒有 .NET 經驗也可以使用。

不過，透過 .NET 物件操作的彈性，會比用 PowerShell 來的強大很多，至於你想用哪種方式調整設定，就全看你個人喜好囉！ 🙂

### 相關連結

-   [about\_Preference\_Variables - PowerShell | Microsoft Docs](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_preference_variables)
    -   [$ErrorActionPreference](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_preference_variables?view=powershell-7#erroractionpreference)
-   [Difference between $? and $LastExitCode in PowerShell - Stack Overflow](https://stackoverflow.com/questions/10666035/difference-between-and-lastexitcode-in-powershell)
-   [PowerShell: detecting errors in script functions - Stack Overflow](https://stackoverflow.com/a/25979930/910074)
-   [Infrastructure as code - Wikipedia](https://en.wikipedia.org/wiki/Infrastructure_as_code)
-   [IISAdministration | Microsoft Docs](https://learn.microsoft.com/en-us/powershell/module/iisadministration/)
    -   [Get-IISServerManager](https://learn.microsoft.com/zh-tw/powershell/module/iisadministration/get-iisservermanager)
    -   [Start-IISCommitDelay](https://learn.microsoft.com/en-us/powershell/module/iisadministration/start-iiscommitdelay)
    -   [Stop-IISCommitDelay](https://learn.microsoft.com/en-us/powershell/module/iisadministration/stop-iiscommitdelay)
-   [Microsoft.Web.Administration Namespace | Microsoft Docs](https://learn.microsoft.com/en-us/dotnet/api/microsoft.web.administration?view=iis-dotnet)
    -   [ServerManager](https://learn.microsoft.com/en-us/dotnet/api/microsoft.web.administration.servermanager?view=iis-dotnet)
    -   [SiteCollection](https://learn.microsoft.com/en-us/dotnet/api/microsoft.web.administration.sitecollection?view=iis-dotnet)
    -   [Site](https://learn.microsoft.com/en-us/dotnet/api/microsoft.web.administration.site?view=iis-dotnet)
    -   [ApplicationPoolCollection](https://learn.microsoft.com/en-us/dotnet/api/microsoft.web.administration.applicationpoolcollection?view=iis-dotnet)
    -   [ApplicationPool](https://learn.microsoft.com/en-us/dotnet/api/microsoft.web.administration.applicationpool?view=iis-dotnet)
