Mehr Spaß mit Kubernetes

Kubernetes ist mächtig – aber machen wir uns nichts vor: Das Deployment kann frustrierend sein. Dieses Tutorial zeigt dir, wie du mit Podman, Skaffold und Kind ein entwicklerfreundliches Setup baust, das dir beim Speichern (CTRL+S) automatisch deine NestJS-App neu baut und in dein lokales Kubernetes-Cluster deployed.

Voraussetzungen

  • Fedora, Arch oder Ubuntu mit Podman + Buildah
  • kind installiert
  • skaffold installiert
  • Node.js & Nest CLI
1
2
3
sudo dnf install podman buildah nodejs
npm install -g @nestjs/cli
brew install skaffold kind # macOS oder via pacman/dnf

NestJS Projekt erzeugen

Wir starten mit einem neuen NestJS-Projekt, das wir später in ein Docker-Image verpacken und in Kubernetes deployen.

1
2
nest new hello-k8s
cd hello-k8s

Dockerfile für Podman

Damit Skaffold und Podman wissen, wie deine App gebaut werden soll, definieren wir ein einfaches Dockerfile. Es baut deine NestJS-App und startet sie über Node.

1
2
3
4
5
6
7
8
# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
CMD ["node", "dist/main"]

Kind-Cluster starten

Wir nutzen kind, um ein lokales Kubernetes-Cluster zu starten – ideal für schnelles Feedback beim Entwickeln. Der Port 3000 wird auf den Host weitergeleitet.

1
2
3
4
5
6
7
8
9
cat <<EOF | kind create cluster --name dev --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
    extraPortMappings:
      - containerPort: 3000
        hostPort: 3000
EOF

Deployment YAML

Die Kubernetes-Ressourcen definieren, wie dein Container ausgeführt wird. Deployment und Service sorgen für einen laufenden Pod und den Netzwerkzugang.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-k8s
spec:
  replicas: 1
  selector:
    matchLabels:
      app: hello-k8s
  template:
    metadata:
      labels:
        app: hello-k8s
    spec:
      containers:
        - name: app
          image: hello-k8s
          ports:
            - containerPort: 3000

# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: hello-k8s
spec:
  selector:
    app: hello-k8s
  ports:
    - port: 3000
      targetPort: 3000

Skaffold config

Skaffold orchestriert den gesamten Build- und Deploy-Prozess. Mit dieser Konfiguration wird dein Projekt bei jeder Änderung automatisch neu gebaut und ins Cluster geladen.

skaffold init

erzeugt die golgende Datei

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# skaffold.yaml
apiVersion: skaffold/v4beta6
kind: Config
metadata:
  name: hello-k8s
build:
  artifacts:
    - image: hello-k8s
  local:
    useDockerCLI: false
    useBuildkit: false
    push: false
manifests:
  rawYaml:
    - k8s/deployment.yaml
deploy:
  kubectl: {}
portForward:
  - resourceType: service
    resourceName: hello-k8s
    port: 3000
    localPort: 3000

Dev-Modus starten

Sobald alles vorbereitet ist, startest du Skaffold im Dev-Modus. Das bedeutet: Änderungen am Code lösen automatisch einen Build & Redeploy aus.

1
skaffold dev

Du kannst jetzt im Editor speichern (CTRL+S), und Skaffold wird automatisch:

  • den Container mit Buildah bauen
  • das Image ins Kind-Cluster bereitstellen
  • das Deployment updaten
  • die Logs anzeigen
  • Port 3000 forwarden

Spaß haben

Ruf im Browser auf: http://localhost:3000 – deine NestJS-App läuft nun direkt im Kubernetes-Cluster. Und wenn du was änderst? Einfach speichern.

Weiterführend


Bonus: Infrastruktur mit Pulumi (TypeScript)

Für komplexere Konfigurationen wie Rollen, Rechte und Namespaces nutzen wir Pulumi. Damit lässt sich die Clusterstruktur in TypeScript beschreiben – wiederverwendbar und versionierbar.

Wenn du zusätzliche Ressourcen wie ServiceAccounts, Roles oder Policies brauchst, kannst du Pulumi verwenden. Beispiel:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// infra/index.ts
import * as pulumi from "@pulumi/pulumi";
import * as k8s from "@pulumi/kubernetes";

const namespace = new k8s.core.v1.Namespace("dev", {
  metadata: { name: "dev" }
});

const sa = new k8s.core.v1.ServiceAccount("app-sa", {
  metadata: { name: "app-sa", namespace: namespace.metadata.name }
});

const role = new k8s.rbac.v1.Role("read-pods", {
  metadata: { namespace: namespace.metadata.name },
  rules: [{ apiGroups: [""], resources: ["pods"], verbs: ["get", "list"] }]
});

new k8s.rbac.v1.RoleBinding("app-sa-binding", {
  metadata: { namespace: namespace.metadata.name },
  subjects: [{ kind: "ServiceAccount", name: sa.metadata.name }],
  roleRef: {
    kind: "Role",
    name: role.metadata.name,
    apiGroup: "rbac.authorization.k8s.io"
  }
});

Dann einfach ausführen mit:

1
2
npm install @pulumi/pulumi @pulumi/kubernetes
pulumi up

Damit vermeidest du YAML-Orgien und bleibst trotzdem deklarativ und wiederholbar.


Diagramm: Dev-Flow mit Skaffold

  graph TD
  A[CTRL+S im Editor] --> B[Skaffold erkennt Änderung]
  B --> C[Buildah baut neues Image]
  C --> D[Image wird im lokalen Kind bereitgestellt]
  D --> E[Kubectl Deployment aktualisieren]
  E --> F[PortForwarding: 3000 → 3000]
  F --> G[App im Browser erreichbar]