apiVersion: apps/v1 kind: Deployment metadata: # Beside the api, in the same wave, and NOT because it is independent of it — measured on a real # cluster, this pod's `/readyz` is nginx proxying the api's `/api/health/live`, so it answers 502 # and stays unready for as long as there is no api behind it. # # The same wave is still right. Argo waits for a wave as a whole, so the api coming up is what # makes this ready and there is nothing for a later wave to add; putting it earlier would only # mean it sat unready for longer. What it does mean is that a stuck api presents as TWO unhealthy # Deployments rather than one, and the api is the one to look at. annotations: argocd.argoproj.io/sync-wave: "1" name: jarvis-web namespace: jarvis spec: replicas: 2 selector: matchLabels: app.kubernetes.io/name: jarvis app.kubernetes.io/component: web strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 0 maxSurge: 1 template: metadata: labels: app.kubernetes.io/name: jarvis app.kubernetes.io/component: web spec: # Nothing here holds a long-lived connection of its own — the websockets go to the API — so # this is only about letting in-flight responses finish. terminationGracePeriodSeconds: 30 # Required by the `restricted` Pod Security Standard that 00-namespace.yaml enforces. securityContext: seccompProfile: type: RuntimeDefault # Same reasoning as 04-api.yaml: the scheduler will happily put both replicas on one node, # and a pair that shares a node is a pair that shares a failure. Cheaper to satisfy here — # nothing about serving static files pins a pod anywhere. topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: ScheduleAnyway labelSelector: matchLabels: app.kubernetes.io/name: jarvis app.kubernetes.io/component: web containers: - name: web # By digest, like the API. It matters more here than it looks: the bundle's JavaScript # chunks are content-hashed PER BUILD and answer 404 rather than falling through to # index.html, so during a rollout a browser holding the previous shell can fail a lazy # import. Two pods on two different builds widen that window; two pods on one digest do # not have it at all. image: git.luxit.be/luxit/jarvis-web@sha256:REPLACE_ME ports: - name: http containerPort: 8080 lifecycle: preStop: exec: command: ["/bin/sh", "-c", "sleep 5"] envFrom: - configMapRef: name: jarvis-web-config # Is this nginx answering? Nothing more. Restarting the web container because the API is # down would take out the one thing still able to show an error page. livenessProbe: httpGet: path: /healthz port: http periodSeconds: 10 failureThreshold: 3 # Can THIS pod reach the API? It proxies /api/health/live, not /ready, on purpose: the # question is whether this pod's route works — a broken DNS name, a NetworkPolicy — and # not whether the API's dependencies are healthy. Pointed at /ready, one Postgres blip # would fail every web pod at once and take the console offline for a fault that has # nothing to do with serving a bundle. readinessProbe: httpGet: path: /readyz port: http periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 3 resources: requests: cpu: 50m memory: 64Mi limits: memory: 256Mi securityContext: # uid 101 is the `nginx` user the image already ships and the Dockerfile switches to. runAsNonRoot: true runAsUser: 101 allowPrivilegeEscalation: false capabilities: drop: ["ALL"] readOnlyRootFilesystem: true volumeMounts: # With a read-only root filesystem, the three paths this container writes at runtime # have to be given to it. All three are ephemeral by nature — the substituted config is # regenerated at every start from the template and the ConfigMap. - name: nginx-conf mountPath: /etc/nginx/conf.d - name: nginx-cache mountPath: /var/cache/nginx - name: tmp # The pid file lives here rather than in /var/run, which the nginx user does not own. mountPath: /tmp volumes: - name: nginx-conf emptyDir: {} - name: nginx-cache emptyDir: {} - name: tmp emptyDir: {} --- apiVersion: v1 kind: Service metadata: # With the Deployment it belongs to. annotations: argocd.argoproj.io/sync-wave: "1" name: jarvis-web namespace: jarvis spec: type: ClusterIP selector: app.kubernetes.io/name: jarvis app.kubernetes.io/component: web ports: - name: http port: 8080 targetPort: http --- # Web scales on CPU; the API deliberately does not. # # Every API scale-in kills a pod holding agent websockets, live assistant runs and open shells — # the fleet reconnects, runs are interrupted and resumed, terminals die — and CPU is a poor proxy # for a load made almost entirely of long-lived connections. Raise `jarvis-api` deliberately # instead. If it ever does get an HPA, give it a scale-down stabilization window of ten minutes or # more. apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: # A WAVE AFTER the Deployment it scales, in the last one, and that is the whole point of the line. # # Argo waits for every resource of a wave to report healthy before it starts the next, and reads # an HPA as Degraded while the HPA reads no metric for its pods. On a FIRST install those pods are # seconds old and metrics-server has not scraped them yet ("the HPA was unable to compute the # replica count … no metrics returned"), so in wave 1 the first sync failed on it every time, and # only an automated policy's retry got past it. The last wave is applied and not waited on: the # sync succeeds, and the HPA turns healthy by itself within a minute. Nothing reads it before. annotations: argocd.argoproj.io/sync-wave: "2" name: jarvis-web namespace: jarvis spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: jarvis-web minReplicas: 2 maxReplicas: 5 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70