TX Text Control .NET Server

Kubernetes

Topic

A Kubernetes cluster runs the container image of an application with TX Text Control .NET Server like any other web application, with two differences: an editing session of the Document Editor lives in one pod, and the backend process it talks to listens on a TCP port inside that pod. Both shape the manifests, from the ports of a Deployment to the session affinity of the component in front of it.

The manifests below deploy the image from Dockerfile, first with the backend inside the application pods and then as a separate Deployment.

What You Deploy

Ingress or Gateway, with cookie affinity
    |
    v
Service myapp (ClusterIP, port 80 -> 8080)
    |
    v
Pods of the Deployment: your application, each with its Document Editor backend
  • The image contains the application, the engine, the fonts and the Document Editor backend. Build and push it as described in Dockerfile.
  • No secret for TX Text Control at run time. The license is compiled into your assembly when you build it, and the credentials of the Text Control Private NuGet Feed are only needed during the image build.
  • A runtime license per pod. Each pod that runs TX Text Control needs one. See Runtime Licenses.

Deploy the Application

The Deployment runs three replicas of the image, and the Service makes them reachable in the cluster:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: myregistry.example.com/myapp:1.0.0
          ports:
            - containerPort: 8080
          env:
            - name: DocumentEditorWorker__ListenAddress
              value: "127.0.0.1"
            - name: DocumentEditorWorker__Port
              value: "4283"
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
  name: myapp
spec:
  selector:
    app: myapp
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080

The manifests work as follows:

  • containerPort: 8080 is the port the .NET container images listen on. The Service maps its port 80 to it.

  • The environment variables bind the Document Editor backend to the loopback address of the pod, so that only the application in the same pod reaches it. They set the DocumentEditorWorker section, which Program.cs binds to DocumentEditorWorkerOptions, as Prepare the Application shows. Pass the same values to UseTXWebSocketMiddleware.

  • The probes need a health endpoint in the application. Add one in Program.cs:

    builder.Services.AddHealthChecks();
    
    // Reachable at /healthz for the probes of Kubernetes
    app.MapHealthChecks("/healthz");
    
  • A liveness probe on the application doesn't notice a stopped backend. The application keeps answering requests, and only the editors fail. Watch the log of the pods for messages of the backend, which the application forwards with the prefix [TXTextControl.Web.Server.Core].

  • replicas: 3 means three runtime licenses.

Keep an Editing Session on One Pod

The Service distributes requests over all pods. An editor that reconnects after a lost connection has to reach the pod that holds its session, as Scaling Containers describes, so the component in front of the Service has to keep each browser on one pod:

  • An ingress controller or a Gateway API implementation with cookie-based affinity is the usual place for this. Which settings it offers depends on the implementation. The ingress-nginx controller that many examples use is retired, and Kubernetes points to the Gateway API instead.
  • In Azure Kubernetes Service, Application Gateway for Containers forwards WebSocket connections without configuration when it's used through the Gateway API, and it offers session affinity with a cookie. See Session affinity and WebSockets.
  • The Service itself offers only sessionAffinity: ClientIP, which is None by default. It keys on the address of the client, so browsers that share an address, for example behind a company proxy, land on the same pod, and a changing address breaks the affinity. Use it for traffic inside the cluster, not for browsers.
  • Idle connections. An open editor that nobody types in sends nothing for a while. Set the idle timeout of the ingress or load balancer higher than the interval at which the application sends WebSocket ping frames; WebSocketOptions.KeepAliveInterval is two minutes by default, as Add the Middleware to the Pipeline describes.

Run the Backend in Its Own Deployment

The Document Editor backend can run in pods of its own, built as described in Multiple Processes. Its image already starts the backend on 0.0.0.0:4283:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: txbackend
spec:
  replicas: 2
  selector:
    matchLabels:
      app: txbackend
  template:
    metadata:
      labels:
        app: txbackend
    spec:
      containers:
        - name: txbackend
          image: myregistry.example.com/mybackend:1.0.0
          ports:
            - containerPort: 4283
          livenessProbe:
            tcpSocket:
              port: 4283
            periodSeconds: 60
---
apiVersion: v1
kind: Service
metadata:
  name: txbackend
spec:
  selector:
    app: txbackend
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 10800
  ports:
    - protocol: TCP
      port: 4283
      targetPort: 4283

In the application, remove AddHostedService<DocumentEditorWorkerManager>() and the environment variables above, and connect the middleware to the Service:

app.UseTXWebSocketMiddleware("txbackend", 4283);

What matters here:

  • The middleware resolves the name once, when the application starts, and the Service must exist at that moment. The cluster IP of a Service stays the same while the Service exists, so replaced backend pods don't force a restart of the application pods.
  • sessionAffinity: ClientIP keeps each application pod on one backend pod. The client of this Service is the application pod, not the browser, so its address is stable. The timeout defaults to 10800 seconds and accepts at most 86400.
  • The TCP probe only opens a connection to port 4283. It shows that the backend is listening, not that an editor can work with it, and a long periodSeconds keeps the number of these connections small.
  • Every backend pod also needs a runtime license, because TX Text Control runs in it.

When Pods Stop

A pod that stops takes its editing sessions with it: Kubernetes sends SIGTERM, the application shuts down and stops the backend and the worker processes with the documents of all open editors. This affects rolling updates, scaling in and node maintenance alike. Save documents regularly, as Scaling Containers describes.

Next Steps