Running ASP.NET Framework application in Windows containers on Kubernetes.

Kubernetes 1.14 brought support for running native Windows containers.

The first step to running a Windows container in your Kubernetes cluster is setting up your node. This Microsoft guide is an excellent resource to get started! Windows Server 2019 brought with it support for container networking (with Flannel), so I'm going to be using that. I'm going to assume a PowerShell interface for all Windows node and pod setup and debugging, though for some debugging it may be easier to connect to the container via a PowerShell session from an RDP instance on the node.

I'm not going to cover setting up Flannel CNI or your Linux master nodes. The Microsoft guide above is a great starting point though.

Throughout this post, I'll try to describe the areas where I had trouble and what I did to resolve it.

Update Windows

There is no easy way to update Windows in a remote PowerShell session (it requires special permissions that are not inherited from the user for some reason), so we set up a scheduled task that runs as System that is set to run once, and then restart the computer.

This could take several minutes, but we want to be on version 1903 or better for container overlay networking support.

$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-Command ipmo PSWindowsUpdate; Get-WUInstall -AcceptAll -Download -Install | Out-File C:\PSWindowsUpdate.log"
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date)
$principal = New-ScheduledTaskPrincipal "SYSTEM"
$settings = New-ScheduledTaskSettingsSet
$task = New-ScheduledTask -Action $action -Principal $principal -Trigger $trigger -Settings $settings

Register-ScheduledTask t1 -InputObject $task

Start-ScheduledTask t1

Restart-Computer

Install Docker

Of course, we need Docker installed

Install-Module -Name DockerMsftProvider -Repository PSGallery -Force -AcceptLicense -SkipPublisherCheck

Install-Package -Name Docker -ProviderName DockerMsftProvider

Install the Docker images

These Docker images set up the “pause” image that Kubernetes uses to set up the pod's IP address and network namespace. This is a great post on pause containers.

docker pull mcr.microsoft.com/windows/nanoserver:1809

docker tag mcr.microsoft.com/windows/nanoserver:1809 microsoft/nanoserver:latest

docker run microsoft/nanoserver:latest

Prepare for Kubernetes

The guide mentioned in the intro puts everything in a C:\k directory, and I think that is a good convention. We also need to copy over the Kubernetes certificate.

On the node:

mkdir C:\k

On your machine:

$configPath = "C:\path\to\kubernetes\config"
$session = New-PsSession -ComputerName $windowsK8sNode

Copy-Item -path $configPath -Destination C:\k\config -ToSession $session

Downloading and installing the Kubernetes binaries

We need to download kubectl, kube-proxy, and kubelet binaries. You can find the released binaries in the releases for Kubernetes on Github.

We will need 7zip to extract the contents from the .tar.gz file (as of writing, PowerShell has no means to expand this type of archive).

Note: Please verify for yourself all links and binaries before running them!

Invoke-WebRequest https://www.7-zip.org/a/7z1900-x64.msi -outFile 7zip.msi

msiexec.exe /i 7zip.msi

$env:Path += ";C:\Program Files\7-Zip\"

Now download and install the Kubernetes binaries.

Invoke-WebRequest "https://dl.k8s.io/v1.14.0/kubernetes-node-windows-amd64.tar.gz" -outFile kubernetes.tar.gz

7z x .\kubernetes.tar.gz
7z x .\kubernetes.tar

Get-ChildItem kubernetes\node\bin\*.exe |% { Copy-Item $_.FullName C:\k }

rm .\kubernetes.tar 
rm .\kubernetes.tar.gz
rm .\7zip.msi

Verify binaries installed correctly, and the node can access the cluster. Set the PATH environment variable and the KUBECONFIG environment variable so everything knows where to look.

$env:Path += ";C:\k"
[Environment]::SetEnvironmentVariable("Path", $env:Path + ";C:\k", [EnvironmentVariableTarget]::Machine)

$env:KUBECONFIG="C:\k\config"
[Environment]::SetEnvironmentVariable("KUBECONFIG", "C:\k\config", [EnvironmentVariableTarget]::User)

kubectl version

You should not see any errors.

Joining the node to the cluster (using Flannel)

Thankfully Microsoft provides a bootstrapping script that we can take advantage of.

Set-Location C:\k

Invoke-WebRequest https://raw.githubusercontent.com/Microsoft/SDN/master/Kubernetes/flannel/start.ps1 -OutFile start.ps1

# Make sure Docker is running
Start-Service docker

.\start.ps1 -ManagementIP <Windows Node IP> -NetworkMode <network mode>  -ClusterCIDR <Cluster CIDR> -ServiceCIDR <Service CIDR> -KubeDnsServiceIP <Kube-dns Service IP> -LogDir <Log directory>

This takes a moment or two. Afterwards, you should be able to run kubectl get nodes and see your Windows node in the cluster.

Tainting the node

To ensure that only Windows containers are scheduled on your Windows node, you can taint your node, and then apply tolerations in your pod specs.

kubectl taint nodes <node name> windows=true:NoSchedule

And then in your pod.yaml or deployment.yaml

tolerations:
  - key: "windows"
    value: "true"
    effect: "NoSchedule"

You could also use nodeSelector, but I found some Linux pods, such as kube-proxy, were sometimes assigned to the Windows node. To prevent this you can taint the node instead of trying to select specific nodes for each pod. Either way would accomplish the same goal.

Other notes and considerations

Running as a service

You can set up kubelet, kube-proxy, and flanneld to run as a Windows service, so a node failure does not require manual intervention. This worked for me:

Set-Location C:\k

Start-BitsTransfer https://nssm.cc/release/nssm-2.24.zip

Expand-Archive .\nssm-2.24.zip nssm-2.24/

Copy-Item nssm-2.24\nssm-2.24\win64\nssm.exe .

Invoke-WebRequest https://raw.githubusercontent.com/Microsoft/SDN/master/Kubernetes/flannel/register-svc.ps1 -outFile register-svc.ps1

.\register-svc.ps1 -NetworkMode <Network mode> -ManagementIP <Windows Node IP> -ClusterCIDR <Cluster subnet> -KubeDnsServiceIP <Kube-dns Service IP> -LogDir <Directory to place logs>

Troubleshooting

See the Microsoft guide in the intro for some more troubleshooting, here are some issues I ran into.

My Windows node is NotReady

Make sure that the kubelet process is running on the node: Get-Process -Name kubelet. If you set it up as a Windows service, make sure the service is running: Start-Service kubelet.

My Windows pod cannot communicate with the Internet

Make sure that the pod can hit the kubelet on port 10250. Check Windows firewall on the node and open this port if necessary.

Also make sure that the kube-proxy process is running: Get-Process -Name kube-proxy. If it is not, either run the start script again, or start the service.

Setting up the .NET Framework Container

In the next post, we'll go over how to set up the Windows Dockerfile to run an ASP.NET Framework application in IIS, and some of the issues that came up with that.