From c253876ed2707cb7abd2e8a435cfa2fd26755a3e Mon Sep 17 00:00:00 2001 From: Nils Stinnesbeck Date: Sat, 17 Oct 2026 14:11:58 +0200 Subject: [PATCH] Add Kubernetes manifests for car.stinnesbeck.com Namespace, InfluxDB 2.7.12 StatefulSet with PVC and a NetworkPolicy that admits only the server, the server Deployment (non-root, read-only root filesystem), a cert-manager Certificate from letsencrypt-prod and a Traefik IngressRoute. Secrets are a template only. Nothing is applied; k8s/README.md has the steps. Server version bumped to 1.1.0. Validated offline with kustomize and kubeconform (including the cert-manager and Traefik CRD schemas). The InfluxDB container was tested locally as UID 1000 with no capabilities. Co-Authored-By: Claude Opus 5.5 --- k8s/README.md | 44 +++++++++++++ k8s/influxdb.yaml | 130 +++++++++++++++++++++++++++++++++++++++ k8s/ingress.yaml | 37 +++++++++++ k8s/kustomization.yaml | 12 ++++ k8s/namespace.yaml | 4 ++ k8s/secrets.example.yaml | 34 ++++++++++ k8s/server.yaml | 103 +++++++++++++++++++++++++++++++ web/package-lock.json | 4 +- web/package.json | 2 +- 9 files changed, 367 insertions(+), 3 deletions(-) create mode 100644 k8s/README.md create mode 100644 k8s/influxdb.yaml create mode 100644 k8s/ingress.yaml create mode 100644 k8s/kustomization.yaml create mode 100644 k8s/namespace.yaml create mode 100644 k8s/secrets.example.yaml create mode 100644 k8s/server.yaml diff --git a/k8s/README.md b/k8s/README.md new file mode 100644 index 0000000..ecbb682 --- /dev/null +++ b/k8s/README.md @@ -0,0 +1,44 @@ +# Kubernetes manifests — car.stinnesbeck.com + +Plain manifests for the car server and its InfluxDB 2. They are only files: nothing here is +applied automatically. Apply them yourself. + +| File | What | +|---|---| +| `namespace.yaml` | Namespace `mg4` | +| `influxdb.yaml` | InfluxDB 2.7.12 StatefulSet (10Gi PVC, default storage class), cluster-internal Service, NetworkPolicy allowing only the server | +| `server.yaml` | Server Deployment (1 replica, non-root, read-only root filesystem) and Service | +| `ingress.yaml` | cert-manager `Certificate` from `letsencrypt-prod`, Traefik `IngressRoute` on `websecure` | +| `secrets.example.yaml` | **Template** for the two Secrets; not in `kustomization.yaml` | +| `kustomization.yaml` | Everything except the Secrets; pins the server image tag | + +## Before the first apply + +1. **Check the ingress class** in `ingress.yaml`. It is set to `traefik-external` as a + placeholder; use the class of your public-facing Traefik. +2. **Create the Secrets.** Copy `secrets.example.yaml` outside the repository, replace every + `CHANGE-ME` (`openssl rand -hex 32` for tokens), then apply the copy: + `influxdb-auth` (InfluxDB admin password and token) and `mg4-server` (ingest token, + session secret, dashboard password, your ABRP API key and user token). +3. **Registry access.** If the `mg4-server` package on git.stinnesbeck.com is private, create + the `git-stinnesbeck-registry` pull secret (command in `secrets.example.yaml`); if it is + public, delete `imagePullSecrets` from `server.yaml`. + +## Apply + +```sh +kubectl apply -f k8s/namespace.yaml +kubectl apply -f +kubectl apply -k k8s/ +``` + +The Let's Encrypt HTTP-01 challenge needs car.stinnesbeck.com to resolve to the public +Traefik before the certificate can be issued; check with `kubectl -n mg4 get certificate`. + +## Notes + +- InfluxDB's setup mode runs only on an empty volume. Changing `admin-token` in the Secret + later does not change the token inside InfluxDB. +- The server uses the InfluxDB admin token. A token scoped to the `mg4` bucket can replace it + later (`influx auth create --read-bucket … --write-bucket …`). +- To upgrade the server, push a new image tag and change `newTag` in `kustomization.yaml`. diff --git a/k8s/influxdb.yaml b/k8s/influxdb.yaml new file mode 100644 index 0000000..72c2555 --- /dev/null +++ b/k8s/influxdb.yaml @@ -0,0 +1,130 @@ +# InfluxDB 2, cluster-internal only. On first start (empty volume) the image's setup mode +# creates the org, the bucket and the admin token from the influxdb-auth Secret; on later +# starts those variables are ignored and the data on the volume wins. +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: influxdb + namespace: mg4 + labels: + app.kubernetes.io/name: influxdb + app.kubernetes.io/part-of: mg4 +spec: + serviceName: influxdb + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: influxdb + template: + metadata: + labels: + app.kubernetes.io/name: influxdb + app.kubernetes.io/part-of: mg4 + spec: + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + containers: + - name: influxdb + image: docker.io/library/influxdb:2.7.12-alpine + ports: + - name: http + containerPort: 8086 + env: + - name: INFLUXD_REPORTING_DISABLED + value: "true" + - name: DOCKER_INFLUXDB_INIT_MODE + value: setup + - name: DOCKER_INFLUXDB_INIT_ORG + value: mg4 + - name: DOCKER_INFLUXDB_INIT_BUCKET + value: mg4 + - name: DOCKER_INFLUXDB_INIT_USERNAME + value: admin + - name: DOCKER_INFLUXDB_INIT_PASSWORD + valueFrom: + secretKeyRef: + name: influxdb-auth + key: admin-password + - name: DOCKER_INFLUXDB_INIT_ADMIN_TOKEN + valueFrom: + secretKeyRef: + name: influxdb-auth + key: admin-token + volumeMounts: + - name: data + mountPath: /var/lib/influxdb2 + - name: config + mountPath: /etc/influxdb2 + readinessProbe: + httpGet: + path: /health + port: http + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 30 + periodSeconds: 30 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + memory: 1Gi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + volumes: + - name: config + emptyDir: {} + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: ["ReadWriteOnce"] + # No storageClassName: the cluster's default class is used. + resources: + requests: + storage: 10Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: influxdb + namespace: mg4 + labels: + app.kubernetes.io/name: influxdb + app.kubernetes.io/part-of: mg4 +spec: + selector: + app.kubernetes.io/name: influxdb + ports: + - name: http + port: 8086 + targetPort: http +--- +# Only the server may reach InfluxDB. Has no effect on a CNI without NetworkPolicy support. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: influxdb-from-server-only + namespace: mg4 +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: influxdb + policyTypes: ["Ingress"] + ingress: + - from: + - podSelector: + matchLabels: + app.kubernetes.io/name: mg4-server + ports: + - port: 8086 diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml new file mode 100644 index 0000000..ff60931 --- /dev/null +++ b/k8s/ingress.yaml @@ -0,0 +1,37 @@ +# TLS certificate from cert-manager. cert-manager does not watch Traefik IngressRoutes, so the +# certificate is requested explicitly and the IngressRoute references its Secret. +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: car-stinnesbeck-com + namespace: mg4 +spec: + secretName: car-stinnesbeck-com-tls + dnsNames: + - car.stinnesbeck.com + issuerRef: + kind: ClusterIssuer + name: letsencrypt-prod +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: mg4-server + namespace: mg4 + annotations: + # CHECK: the class of your public-facing Traefik. Your internal example uses + # "traefik-internal"; this host must be served by the external one. + kubernetes.io/ingress.class: traefik-external +spec: + entryPoints: + - websecure + routes: + - kind: Rule + match: Host(`car.stinnesbeck.com`) + services: + - kind: Service + name: mg4-server + namespace: mg4 + port: 80 + tls: + secretName: car-stinnesbeck-com-tls diff --git a/k8s/kustomization.yaml b/k8s/kustomization.yaml new file mode 100644 index 0000000..7a45bf6 --- /dev/null +++ b/k8s/kustomization.yaml @@ -0,0 +1,12 @@ +# Render with `kustomize build k8s/` (or `kubectl kustomize k8s/`) and apply it yourself. +# The Secrets are not included: create them from secrets.example.yaml first. +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - namespace.yaml + - influxdb.yaml + - server.yaml + - ingress.yaml +images: + - name: git.stinnesbeck.com/nils/mg4-server + newTag: "1.1.0" diff --git a/k8s/namespace.yaml b/k8s/namespace.yaml new file mode 100644 index 0000000..dc634fe --- /dev/null +++ b/k8s/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: mg4 diff --git a/k8s/secrets.example.yaml b/k8s/secrets.example.yaml new file mode 100644 index 0000000..9b7dd47 --- /dev/null +++ b/k8s/secrets.example.yaml @@ -0,0 +1,34 @@ +# TEMPLATE ONLY — not part of kustomization.yaml. Copy it outside the repository, fill in +# real values, and apply that copy yourself. Never commit the filled-in file. +# +# openssl rand -hex 32 for every token and secret below +apiVersion: v1 +kind: Secret +metadata: + name: influxdb-auth + namespace: mg4 +type: Opaque +stringData: + admin-password: CHANGE-ME-at-least-8-characters + admin-token: CHANGE-ME-openssl-rand-hex-32 +--- +apiVersion: v1 +kind: Secret +metadata: + name: mg4-server + namespace: mg4 +type: Opaque +stringData: + # The car's write token, entered in the app. At least 24 characters. + INGEST_TOKEN: CHANGE-ME-openssl-rand-hex-32 + SESSION_SECRET: CHANGE-ME-openssl-rand-hex-32 + DASHBOARD_PASSWORD: CHANGE-ME-at-least-8-characters + # Your ABRP credentials. Leave either empty to switch forwarding off. + ABRP_API_KEY: CHANGE-ME + ABRP_USER_TOKEN: CHANGE-ME + ABRP_MIN_INTERVAL_SECONDS: "5" +--- +# Pull secret for a private image on git.stinnesbeck.com. Easier to create with: +# kubectl -n mg4 create secret docker-registry git-stinnesbeck-registry \ +# --docker-server=git.stinnesbeck.com --docker-username= --docker-password= +# If the package is public, remove imagePullSecrets from server.yaml instead. diff --git a/k8s/server.yaml b/k8s/server.yaml new file mode 100644 index 0000000..01c7039 --- /dev/null +++ b/k8s/server.yaml @@ -0,0 +1,103 @@ +# The car server. One replica on purpose: the ABRP rate limit is kept in memory, and two +# replicas would each forward to ABRP at the full rate. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mg4-server + namespace: mg4 + labels: + app.kubernetes.io/name: mg4-server + app.kubernetes.io/part-of: mg4 +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: mg4-server + template: + metadata: + labels: + app.kubernetes.io/name: mg4-server + app.kubernetes.io/part-of: mg4 + spec: + # Needed only if the image is private on git.stinnesbeck.com; see secrets.example.yaml. + imagePullSecrets: + - name: git-stinnesbeck-registry + securityContext: + runAsNonRoot: true + runAsUser: 10001 + runAsGroup: 10001 + seccompProfile: + type: RuntimeDefault + containers: + - name: server + image: git.stinnesbeck.com/nils/mg4-server:latest # pinned by kustomization.yaml + ports: + - name: http + containerPort: 3000 + env: + - name: INFLUX_URL + value: http://influxdb.mg4.svc:8086 + - name: INFLUX_ORG + value: mg4 + - name: INFLUX_BUCKET + value: mg4 + - name: INFLUX_TOKEN + valueFrom: + secretKeyRef: + name: influxdb-auth + key: admin-token + # INGEST_TOKEN, SESSION_SECRET, DASHBOARD_PASSWORD, ABRP_API_KEY, ABRP_USER_TOKEN. + envFrom: + - secretRef: + name: mg4-server + readinessProbe: + httpGet: + path: /v1/health + port: http + periodSeconds: 10 + livenessProbe: + httpGet: + path: /v1/health + port: http + initialDelaySeconds: 10 + periodSeconds: 30 + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + memory: 512Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + volumeMounts: + # Next.js writes its fetch/image cache here; the rest of the filesystem is read-only. + - name: next-cache + mountPath: /app/.next/cache + - name: tmp + mountPath: /tmp + volumes: + - name: next-cache + emptyDir: {} + - name: tmp + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: mg4-server + namespace: mg4 + labels: + app.kubernetes.io/name: mg4-server + app.kubernetes.io/part-of: mg4 +spec: + selector: + app.kubernetes.io/name: mg4-server + ports: + - name: http + port: 80 + targetPort: http diff --git a/web/package-lock.json b/web/package-lock.json index 8ad8c25..1dcb4ec 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "fatihsmg4-web", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fatihsmg4-web", - "version": "1.0.0", + "version": "1.1.0", "dependencies": { "@influxdata/influxdb-client": "^1.35.0", "next": "^15.5.4", diff --git a/web/package.json b/web/package.json index 8854c8f..a061eb4 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "fatihsmg4-web", - "version": "1.0.0", + "version": "1.1.0", "private": true, "scripts": { "dev": "next dev",