(yhub) allow to configure yhub worker and server

We want to be able to configure both server and worker, the idea is to
be able to deploy separately the server and the worker and to scale
them.
This commit is contained in:
Manuel Raynaud
2026-09-22 16:00:56 +02:00
parent 6c3e9f53a1
commit dd85b6f321
9 changed files with 319 additions and 8 deletions
@@ -197,7 +197,11 @@ jwtKeys:
enabled: true
yhub:
replicas: 1
replicas: 3
worker:
enabled: true
replicas: 2
image:
repository: localhost:5001/impress-yhub
@@ -166,7 +166,11 @@ jwtKeys:
enabled: true
yhub:
replicas: 1
replicas: 3
worker:
enabled: true
replicas: 2
image:
repository: lasuite/impress-yhub
+10
View File
@@ -350,6 +350,16 @@
| `yhub.command` | Override the yhub container command | `[]` |
| `yhub.args` | Override the yhub container args | `[]` |
| `yhub.replicas` | Amount of yhub replicas | `3` |
| `yhub.worker.enabled` | Deploy the worker apart from the server, each scaling on its own | `false` |
| `yhub.worker.replicas` | Amount of yhub worker replicas | `1` |
| `yhub.worker.resources` | Resource requirements for the yhub worker container, the server ones when empty | `{}` |
| `yhub.worker.podAnnotations` | Annotations to add to the yhub worker Pod, the server ones when empty | `{}` |
| `yhub.worker.dpAnnotations` | Annotations to add to the yhub worker Deployment, the server ones when empty | `{}` |
| `yhub.worker.nodeSelector` | Node selector for the yhub worker Pod, the server one when empty | `{}` |
| `yhub.worker.tolerations` | Tolerations for the yhub worker Pod, the server ones when empty | `[]` |
| `yhub.worker.affinity` | Affinity for the yhub worker Pod, the server one when empty | `{}` |
| `yhub.worker.terminationGracePeriodSeconds` | Grace period given to a worker pod to finish its task, the server one when empty | `nil` |
| `yhub.worker.pdb.enabled` | Enable pdb on the yhub worker | `true` |
| `yhub.shareProcessNamespace` | Enable share process namespace between containers | `false` |
| `yhub.sidecars` | Add sidecars containers to yhub deployment | `[]` |
| `yhub.terminationGracePeriodSeconds` | Grace period given to a yhub pod to drain before it is killed | `60` |
+36
View File
@@ -204,6 +204,42 @@ Requires top level scope
{{ include "impress.fullname" . }}-yhub
{{- end }}
{{/*
Full name for the yhub worker, when it is deployed apart from the server
Requires top level scope
*/}}
{{- define "impress.yhub.worker.fullname" -}}
{{ include "impress.yhub.fullname" . }}-worker
{{- end }}
{{/*
yhub worker env vars - combines common yhub.envVars with yhub.worker.envVars
*/}}
{{- define "impress.yhub.worker.env" -}}
{{- $topLevelScope := index . 0 -}}
{{- $workerScope := index . 1 -}}
{{- include "impress.env.transformDict" $workerScope.envVars -}}
{{- include "impress.env.transformDict" (($workerScope.worker | default dict).envVars | default dict) -}}
{{- end }}
{{/*
The role a yhub pod runs, as an environment variable. Only when the worker is
deployed apart: a single deployment runs both halves, which is what yhub does
when the variable is absent. Skipped when the deployment names the role itself,
in either env map an explicit value wins, as everywhere else here.
Usage: {{ include "impress.yhub.roleEnv" (dict "root" $ "role" "server") }}
*/}}
{{- define "impress.yhub.roleEnv" -}}
{{- $root := .root -}}
{{- $named := merge (dict) (($root.Values.yhub.worker | default dict).envVars | default dict) ($root.Values.yhub.envVars | default dict) -}}
{{- if and $root.Values.yhub.worker.enabled (not (hasKey $named "YHUB_ROLE")) }}
- name: "YHUB_ROLE"
value: {{ .role | quote }}
{{- end }}
{{- end }}
{{/*
JWT signing keys the RSA keys the services sign the calls they make to each
other with. The jwt-keys job generates them once into a secret every service
@@ -54,12 +54,14 @@ spec:
args:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- if or $envVars .Values.jwtKeys.enabled }}
{{- $roleEnv := include "impress.yhub.roleEnv" (dict "root" . "role" "server") }}
{{- if or $envVars .Values.jwtKeys.enabled $roleEnv }}
env:
{{- $envVars | indent 12 }}
{{- if .Values.jwtKeys.enabled }}
{{- include "impress.jwtKeys.yhubEnv" . | nindent 12 }}
{{- end }}
{{- $roleEnv | indent 12 }}
{{- end }}
{{- if .Values.yhub.envFrom }}
envFrom:
@@ -0,0 +1,160 @@
{{- if and .Values.yhub.enabled .Values.yhub.worker.enabled -}}
{{- $envVars := include "impress.yhub.worker.env" (list . .Values.yhub) -}}
{{- $fullName := include "impress.yhub.worker.fullname" . -}}
{{- $component := "yhub-worker" -}}
{{- $worker := .Values.yhub.worker -}}
# The half of yhub that drains the redis stream into postgres, deployed apart
# from the one serving the websockets (YHUB_ROLE). It scales on the write
# throughput rather than on the connected editors, and redis consumer groups
# hand each task to exactly one of these pods.
#
# No service, no ports and no probes: a worker binds nothing, it claims tasks.
# Its health is its process being alive — the task loop logs and backs off on
# error rather than dying, so a pod that stopped working is one that exited,
# and kubelet restarts it.
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ $fullName }}
namespace: {{ .Release.Namespace | quote }}
annotations:
{{- with ($worker.dpAnnotations | default .Values.yhub.dpAnnotations) }}
{{- toYaml . | nindent 4 }}
{{- end }}
labels:
{{- include "impress.common.labels" (list . $component) | nindent 4 }}
spec:
replicas: {{ $worker.replicas }}
selector:
matchLabels:
{{- include "impress.common.selectorLabels" (list . $component) | nindent 6 }}
template:
metadata:
annotations:
{{- with ($worker.podAnnotations | default .Values.yhub.podAnnotations) }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "impress.common.selectorLabels" (list . $component) | nindent 8 }}
spec:
{{- if $.Values.image.credentials }}
imagePullSecrets:
- name: {{ include "impress.secret.dockerconfigjson.name" (dict "fullname" (include "impress.fullname" .) "imageCredentials" $.Values.image.credentials) }}
{{- end}}
{{- if .Values.yhub.serviceAccountName }}
serviceAccountName: {{ .Values.yhub.serviceAccountName }}
{{- end }}
shareProcessNamespace: {{ .Values.yhub.shareProcessNamespace }}
# a task claimed by a pod that goes away is redelivered to another one,
# but letting the current one finish saves that round trip
terminationGracePeriodSeconds: {{ $worker.terminationGracePeriodSeconds | default .Values.yhub.terminationGracePeriodSeconds }}
containers:
{{- with .Values.yhub.sidecars }}
{{- toYaml . | nindent 8 }}
{{- end }}
- name: {{ .Chart.Name }}
image: "{{ (.Values.yhub.image | default dict).repository | default .Values.image.repository }}:{{ (.Values.yhub.image | default dict).tag | default .Values.image.tag }}"
imagePullPolicy: {{ (.Values.yhub.image | default dict).pullPolicy | default .Values.image.pullPolicy }}
{{- with .Values.yhub.command }}
command:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.yhub.args }}
args:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- $roleEnv := include "impress.yhub.roleEnv" (dict "root" . "role" "worker") }}
{{- if or $envVars .Values.jwtKeys.enabled $roleEnv }}
env:
{{- $envVars | indent 12 }}
{{- if .Values.jwtKeys.enabled }}
{{- include "impress.jwtKeys.yhubEnv" . | nindent 12 }}
{{- end }}
{{- $roleEnv | indent 12 }}
{{- end }}
{{- if .Values.yhub.envFrom }}
envFrom:
{{- toYaml .Values.yhub.envFrom | nindent 12 }}
{{- end }}
{{- with .Values.yhub.securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with ($worker.resources | default .Values.yhub.resources) }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
{{- if .Values.jwtKeys.enabled }}
{{- include "impress.jwtKeys.volumeMount" . | nindent 12 }}
{{- end }}
{{- range $index, $value := .Values.mountFiles }}
- name: "files-{{ $index }}"
mountPath: {{ $value.path }}
subPath: content
{{- end }}
{{- range .Values.yhub.extraVolumeMounts }}
- name: {{ .name }}
mountPath: {{ .mountPath }}
subPath: {{ .subPath | default "" }}
readOnly: {{ .readOnly }}
{{- end }}
{{- with ($worker.nodeSelector | default .Values.yhub.nodeSelector) }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with ($worker.affinity | default .Values.yhub.affinity) }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with ($worker.tolerations | default .Values.yhub.tolerations) }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
volumes:
{{- if .Values.jwtKeys.enabled }}
{{- include "impress.jwtKeys.volume" . | nindent 8 }}
{{- end }}
{{- range $index, $value := .Values.mountFiles }}
- name: "files-{{ $index }}"
configMap:
name: "{{ include "impress.fullname" $ }}-files-{{ $index }}"
{{- end }}
{{- range .Values.yhub.extraVolumes }}
- name: {{ .name }}
{{- if .existingClaim }}
persistentVolumeClaim:
claimName: {{ .existingClaim }}
{{- else if .secret }}
secret:
{{ toYaml .secret | nindent 12 }}
{{- else if .hostPath }}
hostPath:
{{ toYaml .hostPath | nindent 12 }}
{{- else if .csi }}
csi:
{{- toYaml .csi | nindent 12 }}
{{- else if .configMap }}
configMap:
{{- toYaml .configMap | nindent 12 }}
{{- else if .emptyDir }}
emptyDir:
{{- toYaml .emptyDir | nindent 12 }}
{{- else }}
emptyDir: {}
{{- end }}
{{- end }}
---
{{ if $worker.pdb.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ $fullName }}
namespace: {{ .Release.Namespace | quote }}
spec:
maxUnavailable: 1
selector:
matchLabels:
{{- include "impress.common.selectorLabels" (list . $component) | nindent 6 }}
{{ end }}
{{- end }}
+39 -3
View File
@@ -936,11 +936,47 @@ yhub:
args: []
## @param yhub.replicas Amount of yhub replicas
## Every replica also runs a worker (redis consumer groups hand each task to
## one of them), and clients editing the same document need not land on the
## same pod: updates travel through redis.
## Clients editing the same document need not land on the same pod: updates
## travel through redis. Each replica also runs a worker unless the worker is
## deployed apart, see below.
replicas: 3
## @param yhub.worker.enabled Deploy the worker apart from the server, each scaling on its own
## @param yhub.worker.replicas Amount of yhub worker replicas
## @param yhub.worker.resources Resource requirements for the yhub worker container, the server ones when empty
## @param yhub.worker.podAnnotations Annotations to add to the yhub worker Pod, the server ones when empty
## @param yhub.worker.dpAnnotations Annotations to add to the yhub worker Deployment, the server ones when empty
## @param yhub.worker.nodeSelector Node selector for the yhub worker Pod, the server one when empty
## @param yhub.worker.tolerations Tolerations for the yhub worker Pod, the server ones when empty
## @param yhub.worker.affinity Affinity for the yhub worker Pod, the server one when empty
## @param yhub.worker.terminationGracePeriodSeconds Grace period given to a worker pod to finish its task, the server one when empty
## @param yhub.worker.pdb.enabled Enable pdb on the yhub worker
## @skip yhub.worker.envVars Environment variables of the worker only, on top of yhub.envVars
##
## yhub is two halves sharing nothing but redis and postgres: the server
## holds the websockets and serves the routes, the worker drains the stream
## into postgres. One process runs both by default. Enabling this splits them
## into two deployments — `YHUB_ROLE=server` and `YHUB_ROLE=worker`, the only
## difference between them — so the server scales with the connected editors
## and the worker with the write throughput.
##
## The worker binds nothing: no service, no ingress, and no probes to give it
## (its liveness is its process). Everything not named here is the server's:
## same image, same envVars, same secrets, same volumes.
worker:
enabled: false
replicas: 1
envVars: {}
resources: {}
podAnnotations: {}
dpAnnotations: {}
nodeSelector: {}
tolerations: []
affinity: {}
terminationGracePeriodSeconds: null
pdb:
enabled: true
## @param yhub.shareProcessNamespace Enable share process namespace between containers
shareProcessNamespace: false