Windows Server / IIS で ASP.NET Core を動かすための自動セットアップスクリプト(自己署名証明書付き)

ASP.NET Core を Windows Server / IIS で動かすことが，個人的には多いです:spy:。

本番セットアップ・リリース時のリハーサルや，デモも兼ねて，IaaS の Windows Server 環境を作って実際にアプリケーションを動かしてみて，終わったら捨てる，ということを結構やるのですが，[ここの手順](https://docs.microsoft.com/ja-jp/aspnet/core/tutorials/publish-to-iis?view=aspnetcore-3.0&tabs=visual-studio)に書かれているような，(1) IIS のインストール～アプリケーションプール作成～ASP.NET Core ランタイム (Runtime & Hosting Bundle) インストール～ IIS 再起動，といった手順を GUI でやっていると途中待ち時間などもあって結構面倒です (かつ，自分や周りがなのかもしれませんが，Windows Server のセットアップは GUI を使って行うという先入観のようなものがあった)。

また，(2) テスト的な環境とはいえせめて自己署名証明書を作成して HTTPS 通信にしておいた方がよいかなと思いますが，それも結構面倒です。

ということで (1) と (2) をできるだけ自動化できないか？ということを前に試みました。結果からいうとそこそこできているので，IaaS 捨て環境で既に何十回も使っています。そのスクリプト (PowerShell) について紹介します。部分的にでも参考になれば幸いです。

ただ，完璧ではありません。(1) に関しては ASP.NET Core の Runtime & Hosting Bundle のインストールに問題があって IIS サービス再起動を別途手動で行う必要があります。その辺りについても以下で触れます。よい知恵があればご指摘下さい:bow_tone1:。

なお，以下で紹介するコマンドは **Windows Server にリモートデスクトップ接続して PowerShell を管理者として起動して実行** しています。また確認している環境は GCP Compute Engine で，例えば
`gcloud compute instances create instance-gcloud --image-project windows-cloud --image-family windows-2016 --machine-type n1-standard-4 --boot-disk-size 50 --boot-disk-type pd-standard --tags=http-server,https-server`
のようなスクリプトで作成できる Windows Server 2016 環境で，英語版です(あんまり関係ないと思いますが)。

# (1)-1 IIS のインストール
これは簡単で，IIS マネージャ含め以下のコマンドでできます。

```powershell
# IIS インストール
Install-WindowsFeature Web-Server
# IIS マネージャインストール
Install-WindowsFeature Web-Mgmt-Service
Install-WindowsFeature Web-Mgmt-Console
```

# (1)-2 アプリケーションプールの作成
アプリケーションプール名を `TestAppPool` にする場合は以下のようにします。ASP.NET Core 用なので IIS マネージャ上で「マネージコードなし」にする相当になります。

```powershell
# アプリケーションプール作成
C:\Windows\system32\inetsrv\appcmd add apppool /name:TestAppPool /managedRuntimeVersion: /managedPipelineMode:Integrated
```

# (1)-3 ASP.NET Core ランタイム (Hosting Bundle) のインストール (問題あり)
IIS で ASP.NET Core を動作させるには，[ダウンロードサイト](https://dotnet.microsoft.com/download/dotnet-core) から Runtime & Hosting Bundle をインストールする必要があります。ダウンロード後，実行時に `/quiet /install` オプションを付けることで GUI が出なくなります。ただ，インストーラが非同期で実行されるようで，**その後 IIS の再起動をするタイミングが難しい**です。コールバック的なものを指定できればよいのですが。
そもそもここで指定しているダウンロード URL も，サイトからリダイレクトされる URL を直指定するイレギュラーな方法で，あまり推奨できるものではない気もします。バージョン 2.2.6 以外を使う場合は勿論変更が必要です。

```powershell
# ASP.NET Core ランタイム (2.2.6) のダウンロード及びインストーラ起動
mkdir C:\tools
Invoke-WebRequest -Uri "https://download.visualstudio.microsoft.com/download/pr/a9bb6d52-5f3f-4f95-90c2-084c499e4e33/eba3019b555bb9327079a0b1142cc5b2/dotnet-hosting-2.2.6-win.exe" -OutFile C:\tools\dotnet-hosting-2.2.6-win.exe
C:\tools\dotnet-hosting-2.2.6-win.exe /quiet /install
```

なお [dotnet-install スクリプト](https://docs.microsoft.com/ja-jp/dotnet/core/tools/dotnet-install-script) というのもあるのですが，Runtime & Hosting Bundle インストール相当のことはできないように思います。

# (1)-4 IIS 再起動
上で述べた ASP.NET Core Runtime のインストールが終わっていれば，IIS の再起動が必要なので，おなじみの？以下でサービスを再起動します。上で述べたようにタイミングが微妙なので，一括スクリプトにした場合はここで再起動しても意味がなくなる可能性が高いため，最後に手動で行うのが無難かもしれません。

```powershell
# IIS 再起動
net stop was /y
net start w3svc
```

# (1)-5 アプリケーションの配置
**仮に `dotnet publish` したものを zip 圧縮したものが，ある URL からダウンロードできるとする** と，以下のようにすれば，(1)-2で作成したアプリケーションプールを指定して TestApp という名称で配置できます。
なお (1)-3 の手順で `mkdir C:\tools` されていることが前提のスクリプトです。

```powershell
Invoke-WebRequest -Uri "(zipファイルのURL)" -OutFile C:\tools\TestApp.zip
Expand-Archive -Path C:\tools\TestApp.zip -Destination C:\inetpub\wwwroot\TestApp
C:\Windows\system32\inetsrv\appcmd add app /site.name:"Default Web Site" /path:/TestApp /physicalPath:C:\inetpub\wwwroot\TestApp /applicationPool:TestAppPool
```

# (1)-6 フォルダ権限の付与
IIS 上に配置したフォルダに，Users や IIS_IUSRS の書き込み権限が必要なケースって結構あると思います。それも以下のようにして権限付与を自動化できます。

```powershell
# フォルダ権限の付与
$folder_path = 'C:\inetpub\wwwroot\TestApp'
$permission = ("Users", "Modify", "ContainerInherit, ObjectInherit", "None", "Allow")
$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule $permission
$acl = Get-Acl $folder_path
$acl.SetAccessRule($accessRule)
$acl | Set-Acl $folder_path
$permission = ("IIS_IUSRS", "Modify", "ContainerInherit, ObjectInherit", "None", "Allow")
$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule $permission
$acl.SetAccessRule($accessRule)
$acl | Set-Acl $folder_path
```

# (2) 自己署名証明書の作成及び設定
HTTP でのアクセスは (1) でもう OK ですが (ファイアウォールなどは別途設定が必要なケースが多いと思います)，デモ等で「何で HTTPS じゃないの(-_-)？」と突っ込まれた時のために，最低限自己署名証明書での HTTPS 対応はしておきたいと思い…ませんか:thinking:？警告は出ますけど平文でデータ送受信するよりはマシということで…。
ということでそれも自動でセットアップできました。

なお pfx のパスワードは適当なものなので実際に試す際は変更します。

```powershell
# localhost 向け
$dns = "localhost"
## 有効期間 ← Windows Server 2012 R2 ではできなかったのでコメントアウト
$after = (Get-Date).AddYears(2)

# 「個人」に証明書を作成
$my = "cert:\CurrentUser\My"
$cert = New-SelfSignedCertificate -DnsName $dns -CertStoreLocation $my -NotAfter $after # Windows Server 2016 用
#$cert = New-SelfSignedCertificate -DnsName $dns -CertStoreLocation $my #Windows Server 2012 R2 用 (2016 も可)

# cer を作成
$root = "cert:\CurrentUser\Root"
$cerfile  = "localhost.cer"
Export-Certificate -Cert $cert -FilePath $cerfile

# pfx を作成
$pfxfile  = "localhost.pfx"
$password = "test1-pass2-word3"
$sspwd = ConvertTo-SecureString -String $password -Force -AsPlainText 
Export-PfxCertificate -Cert $cert -FilePath $pfxfile -Password $sspwd

# 証明書インポート (参考：https://knowledge.autodesk.com/ja/search-result/caas/CloudHelp/cloudhelp/2018/JPN/AutoCAD-Customization/files/GUID-19D6716A-0AD1-4A7A-82BA-A067E6D65F66-htm.html)
certutil -addstore ROOT $cerfile
certutil -p $password -importpfx $pfxfile
certutil -p $password -user -importpfx $pfxfile

# IIS の "Default Web Site" に先ほど作成した証明書をバインド
New-WebBinding -Name "Default Web Site" -IP "*" -Port 443 -Protocol https
cd IIS:
cd SslBindings
dir cert:\LocalMachine\my\ | Where-Object {$_.Subject -eq "CN=localhost"} | new-item 0.0.0.0!443
```

以上で自己署名証明書による HTTPS 対応できます。

# スクリプトまとめ
以上を全てまとめたスクリプトが以下です。
(1)-5 で述べた publish ファイルがダウンロードできるようになっていない場合は，ダウンロードとその後の (1)-6 の権限付加でエラーになると思います。その部分を手動にする場合などは削除します。

もし条件通りになっていれば，インストール (またはインスタンス作成) 直後の Windows Server にログインして，以下のスクリプトを管理者権限の PowerShell にコピペして実行すれば，ASP.NET Core アプリケーションの HTTPS 接続までできます。
なお (1)-4 IIS の再起動は前述の通りタイミングの問題があるため最後に分割しました。

```powershell
#(1)-1 IIS のインストール
# IIS インストール
Install-WindowsFeature Web-Server
# IIS マネージャインストール
Install-WindowsFeature Web-Mgmt-Service
Install-WindowsFeature Web-Mgmt-Console
#(1)-2 アプリケーションプール作成
C:\Windows\system32\inetsrv\appcmd add apppool /name:TestAppPool /managedRuntimeVersion: /managedPipelineMode:Integrated
#(1)-3 ASP.NET Core ランタイム (2.2.6) のダウンロード及びインストーラ起動
mkdir C:\tools
Invoke-WebRequest -Uri "https://download.visualstudio.microsoft.com/download/pr/a9bb6d52-5f3f-4f95-90c2-084c499e4e33/eba3019b555bb9327079a0b1142cc5b2/dotnet-hosting-2.2.6-win.exe" -OutFile C:\tools\dotnet-hosting-2.2.6-win.exe
C:\tools\dotnet-hosting-2.2.6-win.exe /quiet /install
#(1)-4 IIS再起動→別途
#(1)-5 アプリケーションの配置
Invoke-WebRequest -Uri "(ダウンロードURL)" -OutFile C:\tools\TestApp.zip
Expand-Archive -Path C:\tools\TestApp.zip -Destination C:\inetpub\wwwroot\TestApp
C:\Windows\system32\inetsrv\appcmd add app /site.name:"Default Web Site" /path:/TestApp /physicalPath:C:\inetpub\wwwroot\TestApp /applicationPool:TestAppPool
#(1)-6 フォルダ権限の付与
$folder_path = 'C:\inetpub\wwwroot\TestApp'
$permission = ("Users", "Modify", "ContainerInherit, ObjectInherit", "None", "Allow")
$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule $permission
$acl = Get-Acl $folder_path
$acl.SetAccessRule($accessRule)
$acl | Set-Acl $folder_path
$permission = ("IIS_IUSRS", "Modify", "ContainerInherit, ObjectInherit", "None", "Allow")
$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule $permission
$acl.SetAccessRule($accessRule)
$acl | Set-Acl $folder_path

#(2) 自己署名証明書の作成及び設定
# localhost 向け
$dns = "localhost"
## 有効期間 ← Windows Server 2012 R2 ではできなかったのでコメントアウト；2016 なら有効にして使ってよい
#$after = (Get-Date).AddYears(2)
# 「個人」に証明書を作成
$my = "cert:\CurrentUser\My"
#$cert = New-SelfSignedCertificate -DnsName $dns -CertStoreLocation $my -NotAfter $after # Windows Server 2016 用
$cert = New-SelfSignedCertificate -DnsName $dns -CertStoreLocation $my #Windows Server 2012 R2 用 (2016 も可)
# cer を確保
$root = "cert:\CurrentUser\Root"
$cerfile  = "localhost.cer"
Export-Certificate -Cert $cert -FilePath $cerfile
#### [信頼されたルート証明機関]に cer を登録 (直接は作れなかった)
###Import-Certificate -FilePath $cerfile -CertStoreLocation $root
# pfx を確保
$pfxfile  = "localhost.pfx"
$password = "test1-pass2-word3"
$sspwd = ConvertTo-SecureString -String $password -Force -AsPlainText 
Export-PfxCertificate -Cert $cert -FilePath $pfxfile -Password $sspwd
# 証明書インポート (参考：https://knowledge.autodesk.com/ja/search-result/caas/CloudHelp/cloudhelp/2018/JPN/AutoCAD-Customization/files/GUID-19D6716A-0AD1-4A7A-82BA-A067E6D65F66-htm.html)
certutil -addstore ROOT $cerfile
certutil -p $password -importpfx $pfxfile
certutil -p $password -user -importpfx $pfxfile
# IIS の "Default Web Site" に先ほど作成した証明書をバインド
New-WebBinding -Name "Default Web Site" -IP "*" -Port 443 -Protocol https
cd IIS:
cd SslBindings
dir cert:\LocalMachine\my\ | Where-Object {$_.Subject -eq "CN=localhost"} | new-item 0.0.0.0!443
```

ASP.NET Core ランタイムインストールが終わっていたら以下を実行。

```powershell
# IIS 再起動
net stop was /y
net start w3svc
```

# 終わりに
Docker や PaaS で Web をホスティングすることも最近は多いのかもしれませんが，(特に Windows でマイクロソフト系の技術を使う場合は) IaaS やオンプレで環境を構築することも相対的に多いのではないかと思います。
ここで紹介したようなスクリプトは，オンプレでも IaaS でも，Windows OS を直接触ることができるのであれば使えるし，部分的にでも使えるので開発・テスト環境や本番環境など結構つぶしが利くのではないかと思います。

私と同じ境遇の？方に少しでも参考になればそれに優る喜びはありません。