I am trying to deploy a test Blazor server-side application in a local Kubernetes cluster. I have it configured such that I can access it via either the service or an nginx-ingress. However, both routes have their own problems that make the application not function properly once served.
When served over the service, it looks like this. I believe this is how it is supposed to look. However, there are a bunch of errors that make it so that I can't intereact with the page at all. Errors:
blazor.server.js:1 WebSocket connection to 'ws://172.25.217.142:30500/_blazor?id=1qrCqjxomf15Rd6QTY11rw' failed:
(anonymous) @ blazor.server.js:1
blazor.server.js:1 [2024-01-23T20:20:31.036Z] Information: (WebSockets transport) There was an error with the transport.
blazor.server.js:1 [2024-01-23T20:20:31.036Z] Error: Failed to start the transport 'WebSockets': Error: WebSocket failed to connect. The connection could not be found on the server, either the endpoint may not be a SignalR endpoint, the connection ID is not present on the server, or there is a proxy blocking WebSockets. If you have multiple servers check that sticky sessions are enabled.
log @ blazor.server.js:1
blazor.server.js:1 [2024-01-23T20:20:31.107Z] Warning: Failed to connect via WebSockets, using the Long Polling fallback transport. This may be due to a VPN or proxy blocking the connection. To troubleshoot this, visit https://aka.ms/blazor-server-using-fallback-long-polling.
log @ blazor.server.js:1
:30500/_blazor?id=eC3fba0OdI6vu_KNZl9_Kg:1
Failed to load resource: the server responded with a status of 404 (Not Found)
blazor.server.js:1 Uncaught (in promise) Error: No Connection with that ID: Status code '404'
at ut.send (blazor.server.js:1:39443)
at async rt (blazor.server.js:1:36427)
at async _t._sendLoop (blazor.server.js:1:61855)
:30500/_blazor?id=eC3fba0OdI6vu_KNZl9_Kg:1
Failed to load resource: the server responded with a status of 404 (Not Found)
blazor.server.js:1 Uncaught (in promise) Error: No Connection with that ID: Status code '404'
at ut.send (blazor.server.js:1:39443)
at async rt (blazor.server.js:1:36427)
at async _t._sendLoop (blazor.server.js:1:61855)
:30500/_blazor?id=eC3fba0OdI6vu_KNZl9_Kg:1
Failed to load resource: the server responded with a status of 404 (Not Found)
blazor.server.js:1 Uncaught (in promise) Error: No Connection with that ID: Status code '404'
at ut.send (blazor.server.js:1:39443)
at async rt (blazor.server.js:1:36427)
at async _t._sendLoop (blazor.server.js:1:61855)
:30500/_blazor?id=eC3fba0OdI6vu_KNZl9_Kg:1
Failed to load resource: the server responded with a status of 404 (Not Found)
blazor.server.js:1 Uncaught (in promise) Error: No Connection with that ID: Status code '404'
at ut.send (blazor.server.js:1:39443)
at async rt (blazor.server.js:1:36427)
at async _t._sendLoop (blazor.server.js:1:61855)
It appears that there is some problem with the WebSockets, but I don't know how to fix that.
When served over the ingress, it looks like this. This is obviously not correct, but I'm only getting one error:
blazor.server.js:2 Uncaught SyntaxError: Unexpected token '<'
I don't know what file that is because I can't open it from the DevTools Console and I am unable to locate it in the DevTools Source pane. It doesn't appear to be in the codebase for the application either.
I am running Minikube on Windows Hyper-V. The application is the default server-side Blazor application from Visual Studio 2022. I made some alterations to the default Dockerfile though:
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["WebApp.csproj", "WebApp/"]
RUN dotnet restore "./WebApp/./WebApp.csproj"
COPY . .
WORKDIR "/src/WebApp"
RUN dotnet build "./WebApp.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./WebApp.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "WebApp.dll"]
Any clues as to what could be causing these two issues?
Edit: Here are my Kubernetes .yaml files:
apiVersion: apps/v1
kind: Deployment
metadata:
name: blazor
labels:
app: blazor
spec:
replicas: 3
selector:
matchLabels:
app: blazor
template:
metadata:
labels:
app: blazor
spec:
containers:
- name: blazor
image: registry/...
ports:
- containerPort: 80
imagePullSecrets:
- name: test-webapp
apiVersion: v1
kind: Service
metadata:
labels:
app: blazor
name: blazor
namespace: default
spec:
ports:
- nodePort: 30500
port: 80
protocol: TCP
targetPort: 80
selector:
app: blazor
type: NodePort
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: blazor-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$1
nginx.ingress.kubernetes.io/affinity: "cookie"
nginx.ingress.kubernetes.io/session-cookie-name: "affinity"
nginx.ingress.kubernetes.io/session-cookie-expires: "14400"
nginx.ingress.kubernetes.io/session-cookie-max-age: "14400"
spec:
rules:
- host: blazor.test
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: blazor
port:
number: 80
We have 2 issues here
nginx.ingress.kubernetes.io/rewrite-target: /$1
Rewrite has $1
it means to place first capturing group
, but what is it? Well it's something that can be used only when nginx.ingress.kubernetes.io/use-regex
is added, so it just doesn't make sens, when request is send to get for file like /styles.css
it would end up with something like /$1styles.css
in Blazor app or anything else but not valid from your app point of view.
You can have for example
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: blazor-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/affinity: "cookie"
nginx.ingress.kubernetes.io/session-cookie-name: "affinity"
nginx.ingress.kubernetes.io/session-cookie-expires: "14400"
nginx.ingress.kubernetes.io/session-cookie-max-age: "14400"
spec:
rules:
- host: blazor.test
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: blazor
port:
number: 80
And everything should work, but rewrite /
(path: /
) to /
(rewrite-target: /
end with what we had at beginning, so removing line with rewrite-target
has exactly same effect.
Another example using regex
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: blazor-ingress
annotations:
nginx.ingress.kubernetes.io/use-regex: "true"
nginx.ingress.kubernetes.io/rewrite-target: /$1
nginx.ingress.kubernetes.io/affinity: "cookie"
nginx.ingress.kubernetes.io/session-cookie-name: "affinity"
nginx.ingress.kubernetes.io/session-cookie-expires: "14400"
nginx.ingress.kubernetes.io/session-cookie-max-age: "14400"
spec:
rules:
- host: blazor.test
http:
paths:
- path: /(.*)
pathType: Prefix
backend:
service:
name: blazor
port:
number: 80
In this case in path
we have /(.*)
and we have one capturing group
what is (.*)
(to make path /
work correctly you must have *
instead of +
), then rewrite
comes in and it says /$1
as we have regex enabled in place of $1
the value stored in first capturing group will be used here.
SignalR to establish connection with with Blazor backend needs to send 2 request
The first one will send connectionToken
in the response then this token will be used to connect, so if you have more then one replica there is a chance(with number of replica it's growing) that those two requests will end in two different one. To resolve it Microsoft suggest suggest using some additional annotations to force ingress(nginx) to use always the same replica based on cookie, so Negotiate
and Connect
will always hit the same replica and issue will not occur(also some GitHub issue about it.
To answer the question why it doesn't work with NodePort
as we already got it sends two request they have to hit the same replica, but service NodePort
doesn't have feature like nginx.ingress.kubernetes.io/affinity
to force requests to the same replica all the time, and there is no solution for it expect using just one replica(or at least I don't know).