← Übersicht  ·  Skripte & Dateien  ·  cloud-host · OpenShift · Frontend

SPA-Frontends auf OpenShift

Das Modell steht in den Atlas-Diagrammen J1–J4. library-frontend ist ein ng build-Output, serviert von nginx – diese Anleitung baut das real nach: ein Nicht-Root-nginx mit einer edge-Route ausliefern, Pfad-Routing gegen CORS live vergleichen, ein echtes PKCE-Codepaar lokal erzeugen, und die Laufzeit-Config per env.js aus einer ConfigMap laden statt sie ins Build zu backen. EX280-relevant.

Stand: 4. September 2026 OKD 4.x / OCP 4.x ns library

Voraussetzung: Zugang zum Namespace library. Statt eines echten ng build-Outputs steht eine winzige Hand-HTML-Datei stellvertretend für „die gebaute SPA“ – die Ausliefer- und Config-Mechanik ist real und identisch. Schritt 3 braucht keinen Cluster, nur openssl lokal.

1 Eine SPA per nginx ausliefern

Ein gebautes Frontend ist nur ein Ordner statischer Dateien – ausgeliefert von einem winzigen Nicht-Root-nginx.

Laptop · Bash
cat <<'EOF' | oc apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
  name: demo-spa-dist
  namespace: library
data:
  index.html: |
    <!doctype html><html><body><h1>demo-spa</h1>
    <p>Deep-Link-Test: <a href="/kunde/42">/kunde/42</a></p></body></html>
  nginx.conf: |
    server {
      listen 8080;
      root /usr/share/nginx/html;
      location / { try_files $uri /index.html; }
    }
EOF

cat <<'EOF' | oc apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-spa
  namespace: library
spec:
  replicas: 1
  selector: { matchLabels: { app: demo-spa } }
  template:
    metadata: { labels: { app: demo-spa } }
    spec:
      containers:
        - name: nginx
          image: registry.access.redhat.com/ubi9/nginx-124:latest
          ports: [{ containerPort: 8080 }]
          volumeMounts:
            - { name: dist, mountPath: /usr/share/nginx/html/index.html, subPath: index.html }
            - { name: dist, mountPath: /etc/nginx/nginx.conf.d/spa.conf, subPath: nginx.conf }
      volumes:
        - name: dist
          configMap: { name: demo-spa-dist }
EOF
oc expose deployment/demo-spa -n library --port=8080
oc create route edge demo-spa -n library --service=demo-spa

HOST=$(oc get route demo-spa -n library -o jsonpath='{.spec.host}')
curl -sk "https://$HOST/" | head -3
curl -sk -o /dev/null -w "%{http_code}\n" "https://$HOST/kunde/42"
# 200 statt 404 - try_files reicht den Deep-Link an index.html weiter

ubi9/nginx-124 läuft schon als Nicht-Root auf Port 8080 – das Standard-nginx-Image würde nach /var/cache/nginx schreiben wollen und unter restricted-v2 scheitern (H2). Ohne try_files $uri /index.html gäbe /kunde/42 ein echtes 404, weil auf dem Server keine solche Datei existiert – das Routing macht Angular/React erst im Browser.

2 Pfad-Routing gegen CORS live vergleichen

Zwei Hosts brauchen CORS, ein Host mit Pfad-Routing nicht – live am Preflight sichtbar.

Laptop · Bash
# eine kleine API ohne CORS-Header, als zweiter Host:
oc create deployment demo-api -n library --image=registry.k8s.io/e2e-test-images/agnhost:2.39 \
  -- /agnhost netexec --http-port=8080
oc expose deployment/demo-api -n library --port=8080
oc create route edge demo-api -n library --service=demo-api

API_HOST=$(oc get route demo-api -n library -o jsonpath='{.spec.host}')
FRONT_HOST=$(oc get route demo-spa -n library -o jsonpath='{.spec.host}')

# Preflight simulieren, als kaeme der Request vom Frontend-Host:
curl -sk -X OPTIONS "https://$API_HOST/hostname" \
  -H "Origin: https://$FRONT_HOST" \
  -H "Access-Control-Request-Method: GET" -i | grep -i "access-control"
# leer - keine CORS-Header, der Browser wuerde die Antwort blocken

# jetzt Pfad-Routing: dieselbe API zusaetzlich unter /api auf dem Frontend-Host
oc patch route demo-spa -n library --type=merge \
  -p '{"spec":{"path":"/"}}'
cat <<EOF | oc apply -f -
apiVersion: route.openshift.io/v1
kind: Route
metadata:
  name: demo-spa-api
  namespace: library
spec:
  host: $FRONT_HOST
  path: /api
  to: { kind: Service, name: demo-api }
  port: { targetPort: 8080 }
  tls: { termination: edge }
EOF
curl -sk "https://$FRONT_HOST/api/hostname"
# same-origin - kein Preflight noetig, kein CORS-Header noetig

curl ignoriert CORS – die Regel gilt nur im Browser. Der Beweis hier ist strukturell: ohne Access-Control-Allow-Origin im Preflight würde ein echter Browser die Antwort verwerfen, obwohl der Server geantwortet hat. Mit Pfad-Routing entfällt die Frage komplett, weil beide Aufrufe von derselben Origin ausgehen.

3 Ein echtes PKCE-Codepaar erzeugen

Die SPA ist ein public OIDC-Client ohne Secret – PKCE ersetzt es durch ein selbst erzeugtes Codepaar.

Laptop · Bash
# code_verifier: 32 zufaellige Bytes, URL-safe base64
VERIFIER=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=')
echo "code_verifier: $VERIFIER"

# code_challenge: S256(verifier), ebenfalls URL-safe base64
CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=')
echo "code_challenge: $CHALLENGE"

# die Authorize-URL, die die SPA im Browser aufrufen wuerde (Schritt 1 des Flows):
echo "https://keycloak.example/realms/library/protocol/openid-connect/auth?\
response_type=code&client_id=library-frontend&\
redirect_uri=https://\$FRONT_HOST/callback&\
code_challenge=$CHALLENGE&code_challenge_method=S256&state=demo&nonce=demo"

Ab hier übernimmt ein echter Browser plus eine echte Keycloak-Instanz – der Nutzer meldet sich an, Keycloak leitet mit einem kurzen code zurück, die SPA tauscht code + code_verifier gegen ein access_token. PKCE schützt genau hier: ein abgefangener code allein nützt nichts, ohne den verifier, den nur diese SPA-Instanz kennt.

Nie der Implicit-Flow response_type=token ist abgekündigt – das Token landet direkt in der URL und damit in der Browser-History. Token-Ablage im Speicher, nie in localStorage wegen XSS.

4 env.js aus einer ConfigMap laden

Build once, deploy everywhere: dasselbe Image, andere Laufzeit-Config – ohne Rebuild.

Laptop · Bash
oc create configmap demo-spa-env -n library \
  --from-literal=env.js='window.__APP_CONFIG__ = { apiUrl: "/api", realm: "library-test" };'

oc set volume deployment/demo-spa -n library --add \
  --configmap-name=demo-spa-env --mount-path=/usr/share/nginx/html/env.js --sub-path=env.js

oc rollout status deployment/demo-spa -n library
HOST=$(oc get route demo-spa -n library -o jsonpath='{.spec.host}')
curl -sk "https://$HOST/env.js"

# dieselbe App, andere Umgebung - nur die ConfigMap aendert sich:
oc set data configmap/demo-spa-env -n library \
  --from-literal=env.js='window.__APP_CONFIG__ = { apiUrl: "/api", realm: "library-prod" };'
oc rollout restart deployment/demo-spa -n library
curl -sk "https://$HOST/env.js"
# realm hat sich geaendert - kein neues Image, kein neuer Build

Ein Mount wird nicht heiß neu geladen – deshalb der oc rollout restart nach jeder Änderung (workload-feinschliff-openshift.html). In env.js gehören nur öffentliche Werte – die Datei geht ungefiltert an jeden Browser, nie Secrets.

5 Typische Fallen

Symptommeist
Deep-Link direkt aufgerufen gibt 404try_files $uri /index.html fehlt in der nginx-Config
CrashLoopBackOff beim Standard-nginx-Imageversucht als root nach /var/cache/nginx zu schreiben – ein Nicht-Root-Image nehmen
„Network error“ im Browser, Server hat aber geantwortetfehlender CORS-Header – der Browser blockt die Antwort, nicht der Server
PKCE-Austausch scheitert mit invalid_grantcode_verifier stimmt nicht zum code_challenge, oder der code wurde schon einmal eingelöst
neue API-URL kommt nicht anConfigMap geändert, aber kein oc rollout restart – der Mount aktualisiert sich nicht von selbst

6 Wie das Projekt es macht

library-frontend ist ein ng build-Output, serviert von ubi9/nginx auf Port 8080, mit einer edge-Route. Getrennte Hosts für Frontend und api-gateway, CORS auf der api-gateway per Spring @CrossOrigin, Bearer-Token statt Cookie – damit entfällt die SameSite/Allow-Credentials-Frage aus Schritt 2 komplett. Die API-URL, das Keycloak-Realm und die Client-id kommen per env.js, nicht ins Build.

+ Die Kurzfassung

Ausliefernnur statische Dateien, Nicht-Root-nginx auf 8080, try_files fuer SPA-Routing
CORSsame-origin per Pfad-Routing vermeidet Preflight und Allow-Origin-Pflege komplett
Authpublic Client, Authorization-Code-Flow mit PKCE statt Client-Secret oder Implicit-Flow
Configenv.js aus einer ConfigMap zur Laufzeit – build once, deploy everywhere