Gitlab CI K8s II

Ende zu Ende Tests für Deine Applikation

Posted by eumel8 on February 15, 2026 · 26 mins read

Intro

In Teil I haben wir uns mit Ende zu Ende Tests von Kubernetes Apps auf Gitlab-CI beschäftigt. Nach den letzten Erfahrungen auf gehärteten AWS Sysbox Runnern (siehe auch Sysbox) muss man sagen:

Es ist alles noch viel schlimmer

Sysbox Runner und DIND

Wie im Teil 1 zu sehen, die Installation von K3D oder Kind-Cluster klappt zunächst, wenn man nicht ganz so aktuelle Docker DIND Versionen nimmt. In der Vierfachverschachtelung von Sysbox-VM, Docker Dind, Kind Node und Pod Netzwerk gibt es aber ein Problem: Die Verbindung vom Pod Netzwerk zum Cluster Netzwerk über die veth Bridge und eingerichteten DNAT vo kube-proxy, ist untersagt! Die typische Meldung ist “no route to host” und betrifft alle Pods, die ausgehende Netzwerkverbindungen aufbauen, sei es etwa zur Kubernetes API, um dort eine Configmap abzurufen. Deswegen scheitert die local-provisioner CSI Installation vom Kind Cluster und wir haben keinen Storage.

Der Hack ist die Streichung des vierten Levels und Verbindung solcher Pods über das HostNetwork:

kubectl -n local-path-storage patch deployment local-path-provisioner \
  --type=json \
  -p='[
    {"op":"add","path":"/spec/template/spec/hostNetwork","value":true},
    {"op":"add","path":"/spec/template/spec/dnsPolicy","value":"ClusterFirstWithHostNet"}
  ]'

Das geht. Der Pos startet mit HostNetwork, DNS beziehen wir auch von dort, um die Kubernetes API als Hostnamen auflösen zu können. Wir kriegen eine Verbindung, können die Configmap abrufen, das Ding läuft.

Genauso müssen wir es mit allen anderen Deployments handhaben, die ähnliche Probleme haben, etwa auch in unserer App. Ich weiss nicht, ob das dann noch ein valider Ende zu Ende Test wäre.

Ausserdem: Das funktioniert nur bis Kind v0.30.0 (Kubernetes 1.34). Bei v0.31.0 (Kubernetes 1.35), crasht das Ding total beim Installieren unserer Helm App:

5s          Warning   FailedCreatePodSandBox    pod/my-app-74c4d4f4c-8fj6w      Failed to create pod sandbox: rpc error: code = Unknown desc = failed to start sandbox "0be3636d18f46927397dcd6238ace8570ebbade55c0992e32e2f913bf5e94de1": failed to create containerd task: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: open sysctl net.ipv4.ip_unprivileged_port_start file: unsafe procfs detected: openat2 /proc/./sys/net/ipv4/ip_unprivileged_port_start: invalid cross-device link

Also man könnte es mit Hacks noch eine Weile nutzen, aber gerade in den Ende zu Ende Tests will man ja neue Kubernetes Versionen testen: 1.35, 1.36, 1.37 … unbrauchbar, in dem Zusammenhang.

Auch andere CNI Treiber brachten keinen Erfolg (getestet mit Cilium eBPF, Calico, kube-proxy ipvs, nftables)

Intressant ist auch die Issue Liste auf Github. Dort gibt es auch immer wieder Probleme mit neueren Kubernetes-Versionen und es wird auch schon mal nach dem Projektstatus gefragt, da das letzte Release mehr als ein halbes Jahr zurückliegt.

Besonders tragisch für die Leute, die von Kaniko zu Sysbox gewechselt sind, um ein besseres Leben in CI/CD zu haben.

Kubernetes Service als Sidecar

Zwei weitere Sachen, die NICHT funktionieren, auch nicht mit priveligierten eigenen Runner mit Host-Pid mount.

k3s als sidecar service:

k3s_sidecar:
  stage: k3s
  services:
    - name: docker.io/rancher/k3s:v1.29.1-k3s1
      alias: k3s
      command:
        - server
        - --cluster-init
        - --disable=traefik
        - --disable=servicelb
        - --disable-network-policy
        - --flannel-backend=none
        - --tls-san=k3s

k3s rootless:

k3s_rootless:
  stage: k3s
  services:
    - name: docker.io/rancher/k3s:v1.29.1-k3s1
      alias: k3s
      command:
        - server
        - --rootless
        - --cluster-init
        - --disable=traefik
        - --disable=servicelb
        - --disable-network-policy
        - --flannel-backend=none
        - --tls-san=k3s

Die Ursache liegt im cgroup mount. Mit Host Pid mount, kriegt man diesen zwar in den Runner:

2026-02-15T16:39:00.494458Z 01O 0::/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pode80cf426_b ║  ║
║  ║ c4d_4e25_ab0e_9f7ad4276746.slice/cri-containerd-e4a4116b393ae98ac3943bf080a779c8d92b9967bda46afdab2386631f1276 ║  ║
║  ║ 16.scope 

Aber es ist nur der Mount vom Pod, in dem der Runner läuft. Um den Kubernetes Server zu starten, bräuchte es

0::/

was quasi vollen Node-Zugriff bedeutet, also könnte man es gleich auf einer VM starten, also ein Shell-Runner.

Kubernetes Agent in Gitlab

Brauch man gar nicht probieren, es setzt auf die gleiche Logik auf, brauch privilegierten Zugriff und ist so für die meisten Umgebungen ungeeignet.

Damit haben wir auch alle Möglichkeiten ausgeschöpft. Es gibt keine brauchbare Lösungen, um Kubernetes in der Pipeline zu betreiben. Dort wo es noch funktioniert, ist es ein aussterbendes Feature. Also wie geht es weiter?

Kubernetes Simulator Kwok

Kwok ist ein Simulator. der so tut, als wäre er ein Kubernetes Cluster. Wenn man also keinen in der Gitlabä-Ci-Pipeline installieren kann, muss man wenigstens so tun als ob:

Wir können hier mit einem Ubuntu Container Image in einem Pipeline Stage anfangen und ein paar Sachen installieren:

```yaml
deploy-kwok:
  stage: deploy
  variables:
    # Version pinning for stability
    K8S_VERSION: "v1.34.3"
    KWOK_REPO: "kubernetes-sigs/kwok"
    KWOK_VERSION: "v0.7.0"
    ETCD_VERSION: "v3.5.9"
    HELM_VERSION: "v4.1.0"
    # KWOK Configuration
    KWOK_KUBE_CONFIG: /tmp/kubeconfig
    KUBECONFIG: /tmp/kubeconfig
  image: dockerhub.devops.telekom.de/ubuntu:24.04
  tags:
    - aws_run_sysbox
  parallel:
    matrix:
      - K8S_VERSION: ["v1.34.3", "v1.35.0"]
  before_script:
    # 1. Isolate from Host Cluster (Critical for Sysbox/Runner environments)
    - unset KUBERNETES_SERVICE_HOST
    - unset KUBERNETES_SERVICE_PORT
    - echo ${K8S_VERSION}
    # 2. Install System Dependencies
    - apt-get update && apt-get install -y curl ca-certificates git socat jq
    # 3. Install KWOK (Kubernetes WithOut Kubelet)
    - curl -L -o /usr/local/bin/kwokctl "https://github.com/kubernetes-sigs/kwok/releases/download/${KWOK_VERSION}/kwokctl-linux-amd64"
    - curl -L -o /usr/local/bin/kwok "https://github.com/kubernetes-sigs/kwok/releases/download/${KWOK_VERSION}/kwok-linux-amd64"
    - chmod +x /usr/local/bin/kwokctl /usr/local/bin/kwok
    # 4. Install kubectl
    - curl -LO "https://dl.k8s.io/release/${K8S_VERSION}/bin/linux/amd64/kubectl"
    - chmod +x kubectl && mv kubectl /usr/local/bin/
    # 5. Install Kubernetes Control Plane Binaries (API, Controller, Scheduler)
    # KWOK uses these to simulate a full cluster in user-mode
    - curl -L -o /usr/local/bin/kube-apiserver https://dl.k8s.io/release/${K8S_VERSION}/bin/linux/amd64/kube-apiserver
    - curl -L -o /usr/local/bin/kube-controller-manager https://dl.k8s.io/release/${K8S_VERSION}/bin/linux/amd64/kube-controller-manager
    - curl -L -o /usr/local/bin/kube-scheduler https://dl.k8s.io/release/${K8S_VERSION}/bin/linux/amd64/kube-scheduler
    - chmod +x /usr/local/bin/kube-apiserver /usr/local/bin/kube-controller-manager /usr/local/bin/kube-scheduler
    # 6. Install Etcd (Required for Binary Runtime)
    - curl -L https://github.com/etcd-io/etcd/releases/download/${ETCD_VERSION}/etcd-${ETCD_VERSION}-linux-amd64.tar.gz -o etcd.tar.gz
    - tar xzf etcd.tar.gz && mv etcd-${ETCD_VERSION}-linux-amd64/etcd /usr/local/bin/ && rm -rf etcd*
    # 7. Install Helm
    - curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
  script:
    - echo "=== Starting Fake Kubernetes Cluster (KWOK) ==="
    # Define Stage Policy: Simulate Pods as Running immediately
    - |
      cat <<EOF > kwok.yaml
      apiVersion: kwok.x-k8s.io/v1alpha1
      kind: Stage
      metadata:
        name: fast-forward
      spec:
        resourceRef:
          apiGroup: v1
          kind: Pod
        selector:
          matchExpressions:
          - key: .metadata.deletionTimestamp
            operator: DoesNotExist
        next:
          statusTemplate: |
            phase: Running
            conditions:
            - type: Ready
              status: "True"
              lastProbeTime: null
              lastTransitionTime: 
      EOF
    # Start Cluster in Binary Mode (No Docker required!)
    - kwokctl create cluster --runtime binary --kubeconfig $KWOK_KUBE_CONFIG --config kwok.yaml
    - echo "=== Configuring Cluster ==="
    # Create a Fake Node (Required for Scheduling)
    - |
      cat <<EOF | kubectl apply -f -
      apiVersion: v1
      kind: Node
      metadata:
        name: kwok-node-0
        labels:
          beta.kubernetes.io/arch: amd64
          beta.kubernetes.io/os: linux
          kubernetes.io/arch: amd64
          kubernetes.io/hostname: kwok-node-0
          kubernetes.io/os: linux
          kubernetes.io/role: agent
          node-role.kubernetes.io/agent: ""
          type: kwok
        annotations:
          node.alpha.kubernetes.io/ttl: "0"
          kwok.x-k8s.io/node: fake
      status:
        allocatable:
          cpu: 32
          memory: 256Gi
          pods: 110
        capacity:
          cpu: 32
          memory: 256Gi
          pods: 110
        conditions:
        - lastHeartbeatTime: "2023-01-01T00:00:00Z"
          lastTransitionTime: "2023-01-01T00:00:00Z"
          message: kubelet is posting ready status
          reason: KubeletReady
          status: "True"
          status: "True"
          type: Ready
      EOF
    # Ensure Stage CRD is present (Fallback for older KWOK versions)
    - kubectl apply -f "https://github.com/kubernetes-sigs/kwok/raw/main/kustomize/crd/bases/kwok.x-k8s.io_stages.yaml" || true
    - kubectl apply -f kwok.yaml || true

    - echo "=== Cluster Ready ==="
    - kubectl get nodes
    # EXAMPLE: Installing other controllers (e.g. Ingress, Prometheus)
    # If your chart needs CRDs (e.g. ServiceMonitor), install them here:
    # - kubectl apply -f https://github.com/prometheus-operator/prometheus-operator/releases/download/v0.68.0/bundle.yaml
    # If you need an Ingress Controller fake:
    # - kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
    # (Note: Ingress pods will stay Pending/Running fake, but API resources will be accepted)
    - echo "=== Running E2E Test ==="
    - helm install e2e-test charts/my-app --wait --timeout 60s || { kubectl get pods; kubectl get events; exit 1;  }
    - echo "=== Verification ==="
    - kubectl get all -A
    - kubectl describe pod -l app=my-app
```

Ganz schön viel Zeug. Im Detail installieren und starten wir kwok. Das würde ein Kubernetes Cluster ohne alles sein. Um mit dem sprechen zu können, brauchen wir das API-Server Binary. Damit der sowas wie Deployment und ReplicaSet versteht, brauchen wir Controller Binary. Für andere Features an der Stelle natürlich auch noch mehr. Damit wir die Pods auch irgendwo schedulen können, brauchen wir Fake Nodes, die wir natürlich auch noch definieren müssen. Letztlich dann der Lebenszyklus des Pods. Damit der von Creating, Pending nach Running kommt, brauchen wir noch Stages, die wir im Kwok definieren müssen. Und so sieht das dann aus:

=== Cluster Ready ===
$ kubectl get nodes
NAME          STATUS   ROLES   AGE   VERSION
kwok-node-0   Ready    agent   2s    
$ echo "=== Running E2E Test ==="
=== Running E2E Test ===
$ helm install e2e-test charts/my-app --wait --timeout 60s || { kubectl get pods; kubectl get events; exit 1;  }
NAME: e2e-test
LAST DEPLOYED: Sat Feb 14 22:37:09 2026
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
$ echo "=== Verification ==="
=== Verification ===
$ kubectl get all -A
NAMESPACE   NAME                          READY   STATUS    RESTARTS   AGE
default     pod/my-app-757664f475-znl7b   0/1     Running   0          2s
NAMESPACE   NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
default     service/kubernetes   ClusterIP   10.0.0.1     <none>        443/TCP   3s
NAMESPACE   NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
default     deployment.apps/my-app   1/1     1            1           2s
NAMESPACE   NAME                                DESIRED   CURRENT   READY   AGE
default     replicaset.apps/my-app-757664f475   1         1         1       2s
$ kubectl describe pod -l app=my-app
Name:             my-app-757664f475-znl7b
Namespace:        default
Priority:         0
Service Account:  default
Node:             kwok-node-0/
Labels:           app=my-app
                  pod-template-hash=757664f475
Annotations:      <none>
Status:           Running
IP:               
IPs:              <none>
Controlled By:    ReplicaSet/my-app-757664f475
Containers:
  nginx:
    Image:        dockerhub.devops.telekom.de/nginx:1.14.2
    Port:         80/TCP
    Host Port:    0/TCP
    Environment:  <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-4db5g (ro)
Conditions:
  Type           Status
  Ready          True 
  PodScheduled   True 
Volumes:
  kube-api-access-4db5g:
    Type:                    Projected (a volume that contains injected data from multiple sources)
    TokenExpirationSeconds:  3607
    ConfigMapName:           kube-root-ca.crt
    Optional:                false
    DownwardAPI:             true
QoS Class:                   BestEffort
Node-Selectors:              <none>
Tolerations:                 node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
                             node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  2s    default-scheduler  Successfully assigned default/my-app-757664f475-znl7b to kwok-node-0
Cleaning up project directory and file based variables 
Job succeeded

Ja, okay, es läuft, auch mit Kubernetes 1.35. Und da es ein Kubernetes Sig Projekt ist, wird es auch mit zukünftigen Versionen noch laufen. Dennoch bleiben meine Zweifel, ob das der richtige Weg ist.

Man kann auch Kwok weglassen und Kubernetes Vanilla mit den notwendigen Binaries installieren. Hier nochmal ein volles Beispiel mit Versions-Matrix für 4 Kubernetes-Versionen:

```yaml

deploy-kwok:
  stage: deploy
  variables:
    # Version pinning for stability
    KWOK_REPO: "kubernetes-sigs/kwok"
    KWOK_VERSION: "v0.7.0"
    ETCD_VERSION: "v3.6.8"
    HELM_VERSION: "v4.1.0"
    # KWOK Configuration
    KWOK_KUBE_CONFIG: /tmp/kubeconfig
    KUBECONFIG: /tmp/kubeconfig
  image: docker.io/ubuntu:24.04
  tags:
    - aws_run_sysbox
  parallel:
    matrix:
      - K8S_VERSION: ["v1.32.12", "v1.33.8", "v1.34.3", "v1.35.1"]
  before_script:
    # 1. Isolate from Host Cluster (Critical for Sysbox/Runner environments)
    - unset KUBERNETES_SERVICE_HOST
    - unset KUBERNETES_SERVICE_PORT
    - echo ${K8S_VERSION}
    # 2. Install System Dependencies
    - apt-get update && apt-get install -y curl ca-certificates git socat jq
    # 3. Install KWOK (Kubernetes WithOut Kubelet)
    - curl -L -o /usr/local/bin/kwokctl "https://github.com/kubernetes-sigs/kwok/releases/download/${KWOK_VERSION}/kwokctl-linux-amd64"
    - curl -L -o /usr/local/bin/kwok "https://github.com/kubernetes-sigs/kwok/releases/download/${KWOK_VERSION}/kwok-linux-amd64"
    - chmod +x /usr/local/bin/kwokctl /usr/local/bin/kwok
    # 4. Install kubectl
    - curl -LO "https://dl.k8s.io/release/${K8S_VERSION}/bin/linux/amd64/kubectl"
    - chmod +x kubectl && mv kubectl /usr/local/bin/
    # 5. Install Kubernetes Control Plane Binaries (API, Controller, Scheduler)
    # KWOK uses these to simulate a full cluster in user-mode
    - curl -L -o /usr/local/bin/kube-apiserver https://dl.k8s.io/release/${K8S_VERSION}/bin/linux/amd64/kube-apiserver
    - curl -L -o /usr/local/bin/kube-controller-manager https://dl.k8s.io/release/${K8S_VERSION}/bin/linux/amd64/kube-controller-manager
    - curl -L -o /usr/local/bin/kube-scheduler https://dl.k8s.io/release/${K8S_VERSION}/bin/linux/amd64/kube-scheduler
    - chmod +x /usr/local/bin/kube-apiserver /usr/local/bin/kube-controller-manager /usr/local/bin/kube-scheduler
    # 6. Install Etcd (Required for Binary Runtime)
    - curl -L https://github.com/etcd-io/etcd/releases/download/${ETCD_VERSION}/etcd-${ETCD_VERSION}-linux-amd64.tar.gz -o etcd.tar.gz
    - tar xzf etcd.tar.gz && mv etcd-${ETCD_VERSION}-linux-amd64/etcd /usr/local/bin/ && rm -rf etcd*
    # 7. Install Helm
    - curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
  script:
    - echo "=== Starting Fake Kubernetes Cluster (KWOK/${K8S_VERSION}) ==="
    # Define Stage Policy: Simulate Pods as Running immediately
    - |
      cat <<EOF > kwok.yaml
      apiVersion: kwok.x-k8s.io/v1alpha1
      kind: Stage
      metadata:
        name: fast-forward
      spec:
        resourceRef:
          apiGroup: v1
          kind: Pod
        selector:
          matchExpressions:
          - key: .metadata.deletionTimestamp
            operator: DoesNotExist
        next:
          statusTemplate: |
            phase: Running
            conditions:
            - type: Ready
              status: "True"
              lastProbeTime: null
              lastTransitionTime: 
      EOF
    # Start Cluster in Binary Mode (No Docker required!)
    - kwokctl create cluster --runtime binary --kubeconfig $KWOK_KUBE_CONFIG --config kwok.yaml
    - echo "=== Configuring Cluster ==="
    # Create a Fake Node (Required for Scheduling)
    - |
      cat <<EOF | kubectl apply -f -
      apiVersion: v1
      kind: Node
      metadata:
        name: kwok-node-0
        labels:
          beta.kubernetes.io/arch: amd64
          beta.kubernetes.io/os: linux
          kubernetes.io/arch: amd64
          kubernetes.io/hostname: kwok-node-0
          kubernetes.io/os: linux
          kubernetes.io/role: agent
          node-role.kubernetes.io/agent: ""
          type: kwok
        annotations:
          node.alpha.kubernetes.io/ttl: "0"
          kwok.x-k8s.io/node: fake
      status:
        allocatable:
          cpu: 32
          memory: 256Gi
          pods: 110
        capacity:
          cpu: 32
          memory: 256Gi
          pods: 110
        conditions:
        - lastHeartbeatTime: "2023-01-01T00:00:00Z"
          lastTransitionTime: "2023-01-01T00:00:00Z"
          message: kubelet is posting ready status
          reason: KubeletReady
          status: "True"
          type: Ready
      EOF
    # Ensure Stage CRD is present (Fallback for older KWOK versions)
    - kubectl apply -f "https://github.com/kubernetes-sigs/kwok/raw/main/kustomize/crd/bases/kwok.x-k8s.io_stages.yaml" || true
    - kubectl apply -f kwok.yaml || true
    
    - echo "=== Cluster Ready ==="
    - kubectl get nodes
    # EXAMPLE: Installing other controllers (e.g. Ingress, Prometheus)
    # If your chart needs CRDs (e.g. ServiceMonitor), install them here:
    # - kubectl apply -f https://github.com/prometheus-operator/prometheus-operator/releases/download/v0.68.0/bundle.yaml
    # If you need an Ingress Controller fake:
    # - kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
    # (Note: Ingress pods will stay Pending/Running fake, but API resources will be accepted)
    - echo "=== Running E2E Test ==="
    - helm install e2e-test charts/my-app --wait --timeout 60s || { kubectl get pods; kubectl get events; exit 1;  }
    - echo "=== Verification ==="
    - kubectl get all -A
    - kubectl describe pod -l app=my-app

deploy-vanilla:
  stage: deploy
  image: docker.io/ubuntu:24.04
  tags:
    - aws_run_sysbox
  parallel:
    matrix:
      - K8S_VERSION: ["v1.32.12", "v1.33.8", "v1.34.3", "v1.35.1"]
  variables:
    ETCD_VERSION: "v3.6.8"
    KUBECONFIG: /tmp/kubeconfig
  before_script:
    - unset KUBERNETES_SERVICE_HOST
    - unset KUBERNETES_SERVICE_PORT
    - apt-get update && apt-get install -y curl ca-certificates git socat jq
    # Install kubectl & k8s binaries
    - curl -LO "https://dl.k8s.io/release/${K8S_VERSION}/bin/linux/amd64/kubectl"
    - chmod +x kubectl && mv kubectl /usr/local/bin/
    - curl -L -o /usr/local/bin/kube-apiserver https://dl.k8s.io/release/${K8S_VERSION}/bin/linux/amd64/kube-apiserver
    - curl -L -o /usr/local/bin/kube-controller-manager https://dl.k8s.io/release/${K8S_VERSION}/bin/linux/amd64/kube-controller-manager
    - curl -L -o /usr/local/bin/kube-scheduler https://dl.k8s.io/release/${K8S_VERSION}/bin/linux/amd64/kube-scheduler
    - chmod +x /usr/local/bin/kube-apiserver /usr/local/bin/kube-controller-manager /usr/local/bin/kube-scheduler
    # Install etcd
    - curl -L https://github.com/etcd-io/etcd/releases/download/${ETCD_VERSION}/etcd-${ETCD_VERSION}-linux-amd64.tar.gz -o etcd.tar.gz
    - tar xzf etcd.tar.gz --no-same-owner
    - mv etcd-${ETCD_VERSION}-linux-amd64/etcd /usr/local/bin/
    - rm -rf etcd*
    # Install Helm
    - curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
  script:
    - echo "=== Starting Vanilla Kubernetes Control Plane (${K8S_VERSION}) ==="
    - ls -la /usr/local/bin/
    # 1. Start Etcd
    - mkdir -p /tmp/etcd-data
    - nohup /usr/local/bin/etcd --data-dir /tmp/etcd-data --listen-client-urls http://127.0.0.1:2379 --advertise-client-urls http://127.0.0.1:2379 > /tmp/etcd.log 2>&1 &
    - echo "Wait on Etcd..."
    - |
      timeout 30s bash -c 'until curl -s http://127.0.0.1:2379/health; do echo "Warte..."; sleep 1; done' || (cat /tmp/etcd.log && exit 1)
    # 2. Start API Server
    # Generate CA and Client Certs properly
    - mkdir -p /tmp/certs
    # 2.1 CA Authority
    - openssl genrsa -out /tmp/certs/ca.key 2048
    - openssl req -x509 -new -nodes -key /tmp/certs/ca.key -subj "/CN=kubernetes-ca" -days 365 -out /tmp/certs/ca.crt
    # 2.2 Service Account Key (for signing tokens)
    - openssl genrsa -out /tmp/certs/service-account.key 2048
    - openssl req -new -key /tmp/certs/service-account.key -out /tmp/certs/service-account.csr -subj "/CN=service-account"
    - openssl x509 -req -in /tmp/certs/service-account.csr -CA /tmp/certs/ca.crt -CAkey /tmp/certs/ca.key -CAcreateserial -out /tmp/certs/service-account.crt -days 365
    # 2.3 Admin User (CN=admin, O=system:masters)
    - openssl genrsa -out /tmp/certs/admin.key 2048
    - openssl req -new -key /tmp/certs/admin.key -out /tmp/certs/admin.csr -subj "/CN=admin/O=system:masters"
    - openssl x509 -req -in /tmp/certs/admin.csr -CA /tmp/certs/ca.crt -CAkey /tmp/certs/ca.key -CAcreateserial -out /tmp/certs/admin.crt -days 365
    # 2.4 API Server Start (One-Liner for robustness)
    - nohup /usr/local/bin/kube-apiserver --etcd-servers=http://127.0.0.1:2379 --service-cluster-ip-range=10.0.0.0/16 --cert-dir=/tmp/certs --secure-port=6443 --bind-address=127.0.0.1 --service-account-key-file=/tmp/certs/service-account.key --service-account-signing-key-file=/tmp/certs/service-account.key --service-account-issuer=https://kubernetes.default.svc.cluster.local --client-ca-file=/tmp/certs/ca.crt --disable-admission-plugins=ServiceAccount,NamespaceLifecycle,LimitRanger,DefaultStorageClass,DefaultTolerationSeconds,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,ResourceQuota --authorization-mode=Node,RBAC > /tmp/apiserver.log 2>&1 &
    - echo "Wait on API Server..."
    - |
      timeout 30s bash -c 'until curl -k -s https://127.0.0.1:6443/version; do echo "Warte..."; sleep 1; done' || (cat /tmp/apiserver.log && exit 1)
    # 2.5 Configure kubectl with Admin Cert
    - |
      /usr/local/bin/kubectl config set-cluster local --server=https://127.0.0.1:6443 --insecure-skip-tls-verify=true
      /usr/local/bin/kubectl config set-credentials admin --client-certificate=/tmp/certs/admin.crt --client-key=/tmp/certs/admin.key
      /usr/local/bin/kubectl config set-context local --cluster=local --user=admin
      /usr/local/bin/kubectl config use-context local
    # 3. Start Controller Manager (creates Pods from Deployments)
    - nohup /usr/local/bin/kube-controller-manager --master=https://127.0.0.1:6443 --kubeconfig=/tmp/kubeconfig --service-account-private-key-file=/tmp/certs/service-account.key --root-ca-file=/tmp/certs/service-account.crt > /tmp/controller.log 2>&1 &
    # 4. Start Scheduler (assigns Pods to Nodes)
    - nohup /usr/local/bin/kube-scheduler --master=https://127.0.0.1:6443 --kubeconfig=/tmp/kubeconfig > /tmp/scheduler.log 2>&1 &
    # 5. Start Fake Kubelet (Bash Loop)
    - echo "Starting Fake Kubelet..."
    - |
      (
        # Create Node Object
        cat <<EOF | kubectl apply -f -
        apiVersion: v1
        kind: Node
        metadata:
          name: fake-node-0
          labels:
            kubernetes.io/hostname: fake-node-0
            kubernetes.io/role: agent
        status:
          allocatable:
            cpu: "32"
            memory: "256Gi"
            pods: "110"
          capacity:
            cpu: "32"
            memory: "256Gi"
            pods: "110"
          conditions:
          - type: Ready
            status: "True"
            lastHeartbeatTime: "$(date -u +%FT%TZ)"
            lastTransitionTime: "$(date -u +%FT%TZ)"
            message: "Fake Kubelet is ready"
            reason: "KubeletReady"
      EOF
        while true; do
          # Node Heartbeat (Keep it Ready)
          kubectl patch node fake-node-0 --subresource=status --type=merge -p "{\"status\":{\"conditions\":[{\"type\":\"Ready\",\"status\":\"True\",\"lastHeartbeatTime\":\"$(date -u +%FT%TZ)\"}]}}" >/dev/null 2>&1
          # Pod Lifecycle (Pending -> Running)
          # Find all pods on our node (or pending pods) and set them to Running
          # Note: We use -o json and jq to be robust
          kubectl get pods --all-namespaces --field-selector=status.phase=Pending -o json | jq -r '.items[] | .metadata.name + " " + .metadata.namespace' | while read name namespace; do
             if [ ! -z "$name" ]; then
               echo "Fake Kubelet: Starting pod $name in $namespace..."
               # Patch status to Running and Ready
               kubectl patch pod $name -n $namespace --subresource=status --type=merge -p "{\"status\":{\"phase\":\"Running\",\"conditions\":[{\"type\":\"Ready\",\"status\":\"True\"}],\"containerStatuses\":[{\"name\":\"nginx\",\"ready\":true,\"state\":{\"running\":{\"startedAt\":\"$(date -u +%FT%TZ)\"}}}]}}" >/dev/null 2>&1
             fi
          done
          sleep 2
        done
      ) &
      FAKE_PID=$!
    - echo "=== Cluster Ready ==="
    - kubectl get nodes
    - echo "=== Running E2E Test ==="
    - helm install e2e-test charts/my-app --wait --timeout 60s
    - echo "=== Verification ==="
    - kubectl get all -A
    - kill $FAKE_PID
```

Fazit 2

Am Ende vom zweiten Teil bleibt alles nur ein grosser Fake. Mit Kwok/Vanilla kann ich zwar Grundfunktionalitäten testen, wie Pod Starts oder CRD Deployment, bloss was passiert, wenn ein Pod einen Storage braucht? Genau, nichts, ist ja alles nur ein Fake, es gibt ja nicht mal eine StorageClass.

Was als Lösung bleibt, ist also tatsächlich nur ein externer Kubernetes-Cluster, sei es als Kind in einer VM, oder ein dedizierter Cluster, der speziell dieser Gitlab-Pipeline zugeordnet ist und von dieser immer neu bestückt wird.

Wenn das netztechnisch nicht geht, muss man das Gitrepo im Cluster einbinden, etwa über Flux, und dann spezielle Tags oder Branches ausrollen. Dazu vielleicht mehr im dritten Teil.