Run ASP.NET WebForms site in Kubernetes
I did not believe but it did worked out
After adding Windows node pool to cluster and playing with sample project I was able to get it up and running
Here are samples which were used
Dockerfile
Here we are adding optional log monitoring, which will forward all logs to stdout, so we can use kubectl logs to see whats going on
FROM mcr.microsoft.com/dotnet/framework/sdk:4.8 as build
WORKDIR /code
COPY App.sln .
COPY App/App.csproj App/App.csproj
COPY App/packages.config App/packages.config
COPY Lib/Lib.csproj Lib/Lib.csproj
COPY Lib/packages.config Lib/packages.config
RUN nuget restore App.sln
COPY . .
RUN msbuild App/App.csproj /p:PublishUrl=publish /p:DeployOnBuild=true /p:Configuration=Release /p:WebPublishMethod=FileSystem /p:DeployTarget=WebPublish
FROM mcr.microsoft.com/dotnet/framework/aspnet:4.8-windowsservercore-ltsc2019
# Optional: will forward logs to stdout, so we can use kubectl logs to see whats going on
WORKDIR /LogMonitor
ADD https://github.com/microsoft/windows-container-tools/releases/download/v1.2/LogMonitor.exe .
COPY LogMonitorConfig.json .
WORKDIR /
COPY entrypoint.ps1 .
WORKDIR /inetpub/wwwroot
COPY --from=build /code/App/publish .
ENTRYPOINT ["/LogMonitor/LogMonitor.exe", "powershell.exe", "-f", "/entrypoint.ps1"]
entrypoint.ps1
Once again, it is kind of optional, this script replaces app settings with corresponding environment variables like it is done in dotnet core. Technically we may rewrite app to do it out of the box, but in my case I wish not to touch app.
$webConfigPath = Get-ChildItem -Include Web.config -Recurse | Select-Object -First 1 -ExpandProperty FullName
$webConfig = [xml](Get-Content $webConfigPath)
foreach($appSetting in $webConfig.configuration.appSettings.add) {
$value = [Environment]::GetEnvironmentVariable($appSetting.key)
if ($value) {
$appSetting.value = $value
Write-Host "$($appSetting.key): $value"
}
}
foreach($connectionString in $webConfig.configuration.connectionStrings.add) {
$value = [Environment]::GetEnvironmentVariable($connectionString.name)
if ($value) {
$connectionString.connectionString = $value
Write-Host "$($connectionString.name): $value"
}
}
$webConfig.configuration.'system.web'.compilation.RemoveAttribute('debug')
$value = [Environment]::GetEnvironmentVariable('AUTHENTICATION_FORMS_NAME')
if ($value) {
$webConfig.configuration.'system.web'.authentication.forms.SetAttribute('name', $value)
Write-Host "authentication.forms.name: $value"
}
$value = [Environment]::GetEnvironmentVariable('AUTHENTICATION_FORMS_DOMAIN')
if ($value) {
$webConfig.configuration.'system.web'.authentication.forms.SetAttribute('domain', $value)
Write-Host "authentication.forms.domain: $value"
}
$webConfig.Save($webConfigPath)
/ServiceMonitor.exe w3svc
github-action.yml
Nothing special in github action, the only important change is that we are using windows-latest
instead of ubuntu-lates
. In my case I was on a macbook so literaly had no chance to build it localy.
name: experiment
on:
push:
branches: [ main ]
env:
IMAGE: gcr.io/my/app
jobs:
experiment:
runs-on: windows-latest
if: github.actor != 'dependabot[bot]'
steps:
- uses: actions/checkout@v3
- name: variables
uses: rabotaua/actions-version@main
with:
version: 4.1
- name: docker build
run: docker build -t $env:IMAGE .
- name: docker login
run: $env:GOOGLE_KEY | docker login --username _json_key --password-stdin https://gcr.io
env:
GOOGLE_KEY: ${{ secrets.GOOGLE_KEY }}
- name: docker push
run: |
docker tag "$($env:IMAGE)" "$($env:IMAGE):$($env:VERSION)"
docker push "$($env:IMAGE):$($env:VERSION)"
- name: latest
if: github.ref == 'refs/heads/master'
run: |
docker tag "$($env:IMAGE)" "$($env:IMAGE):latest"
docker push "$($env:IMAGE):latest"
kube.yml
And here is our kubernetes manifest. There is nothing special inside, except node selectors asking to run this deploymen on windows nodes.
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo
labels:
app: demo
spec:
replicas: 1
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
type: RollingUpdate
selector:
matchLabels:
app: demo
template:
metadata:
labels:
app: demo
spec:
containers:
- name: demo
image: gcr.io/my/app:latest
imagePullPolicy: Always
ports:
- containerPort: 80
env:
- name: AUTHENTICATION_FORMS_DOMAIN
value: ".mac-blog.org.ua"
- name: AUTHENTICATION_FORMS_NAME
value: "FOO"
- name: DATABASE
value: "Server=localhost;Initial Catalog=Demo;User ID=foo"
- name: ReleaseVersion
value: "123"
resources:
limits:
cpu: 500m
memory: 500Mi
requests:
cpu: 50m
memory: 100Mi
readinessProbe:
failureThreshold: 1
periodSeconds: 5
timeoutSeconds: 1
successThreshold: 1
tcpSocket:
port: 80
livenessProbe:
failureThreshold: 3
httpGet:
path: /login.aspx
port: 80
scheme: HTTP
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
startupProbe:
failureThreshold: 10
httpGet:
path: /login.ashx
port: 80
scheme: HTTP
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: agentpool
operator: In
values:
- win
- key: kubernetes.io/os
operator: In
values:
- windows
---
apiVersion: v1
kind: Service
metadata:
name: demo
spec:
type: ClusterIP
selector:
app: demo
ports:
- protocol: TCP
port: 80
targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo
spec:
ingressClassName: external
rules:
- host: demo.mac-blog.org.ua
http:
paths:
- backend:
service:
name: demo
port:
number: 80
path: /
pathType: ImplementationSpecific
It was running in production more than month at moment of writing this, and the only one complain was when app was OOM killed so just added more resources, in all other aspects its seems to be working solution
Notes
- Thanks to ingress we got some basic metrics for this app even so it does not have prometheus
- To get metrics about resources windows exporter daemon set should be deployed
- Thanks to kubernetes we have warmup with startup probe, rolling update and autoscale