Kubernetes Daten auf Samba Freigabe speichern

In dieser Anleitung geht es darum, wie wir Daten aus unserem Kubernetes-Cluster auf einer Samba-Freigabe speichern können. So können Daten, die von Pods generiert werden, z.B. auf einem zentralen Dateiserver gespeichert werden. In der Regel verwendet man für Kubernetes, eine NFS-Freigabe oder einen dedizierten Blockstorage. Die Einrichtung erfolgt in 6 Schritten:

  1. Namespace erstellen
  2. RBAC Ressourcen erstellen (Berechtigungen)
  3. CSI-SMB Treiber installieren
  4. CSI-SMB Controller ausrollen
  5. CSI-SMB Node Daemon installieren
  6. SMB-Secret erstellen

Durchführung

Namespace erstellen

Im ersten Schritt benötigen wir für die Speicherung der Daten auf einer Samba-Freigabe einen dedizierten Namespace. Diesen können wir direkt über kubectl oder über eine Manifest-Datei erstellen.

Wenn wir direkt den Namespace über kubectl erstellen möchten, verwenden wir den folgenden Befehl:

kubectl create namespace smb-provisioner

Wahlweise können wir auch den Weg über eine Manifest-Datei gehen. Dann sieht die Manifest-Datei zum Beispiel wie folgt aus:

apiVersion: v1
kind: Namespace
metadata:
  name: smb-provisioner

Im Anschluss müssen wir die Manifest-Datei ganz normal über den kubectl apply Befehl aktivieren. Im Anschluss können wir dann mit dem folgenden Befehl überprüfen, ob der Namespace erfolgreich angelegt wurde:

kubectl get namespaces

RBAC Ressourcen erstellen

Im zweiten Schritt erstellen wir jetzt die benötigten RBAC-Ressourcen. Diese dienen dazu, die Berechtigungen mit unserem Kubernetes-Cluster zu vereinen. Mit diesem können dann die Container auf die entsprechenden Samba-Freigaben zugreifen und dort Daten ablegen oder lesen.

Wir erstellen jetzt eine YAML-Datei, welches die ServiceAccounts, eine ClusterRole und eine ClusterRoleBinding enthält. Der Inhalt dieser YAML-Datei sieht wie folgt aus:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: csi-smb-controller-sa
  namespace: smb-provisioner
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: csi-smb-node-sa
  namespace: smb-provisioner
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: smb-external-provisioner-role
rules:
  - apiGroups: [""]
    resources: ["persistentvolumes"]
    verbs: ["get", "list", "watch", "create", "delete"]
  - apiGroups: [""]
    resources: ["persistentvolumeclaims"]
    verbs: ["get", "list", "watch", "update"]
  - apiGroups: ["storage.k8s.io"]
    resources: ["storageclasses"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["events"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]
  - apiGroups: ["storage.k8s.io"]
    resources: ["csinodes"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["nodes"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["coordination.k8s.io"]
    resources: ["leases"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: smb-csi-provisioner-binding
subjects:
  - kind: ServiceAccount
    name: csi-smb-controller-sa
    namespace: smb-provisioner
roleRef:
  kind: ClusterRole
  name: smb-external-provisioner-role
  apiGroup: rbac.authorization.k8s.io

Im Anschluss aktivieren wir diese Datei wieder mit kubectl apply. Damit sollen die benötigten ServiceAccounts und Cluster-Rollen erstellt werden, die benötigt werden.

CSI-SMB Treiber Installation

In diesem Abschnitt installieren wir jetzt den CSI-SMB Treiber. Dieser wird zur Interaktion zwischen dem Kubernetes-Cluster und dem Samba-Protokoll benötigt. Dazu legen wir wieder eine YAML-Datei mit dem folgenden Inhalt an und aktivieren diese im Anschluss wieder.

apiVersion: storage.k8s.io/v1
kind: CSIDriver
metadata:
  name: smb.csi.k8s.io
spec:
  attachRequired: false
  podInfoOnMount: true

Um zu überprüfen, ob die Erstellung hier auch wieder geklappt hat, können wir den folgenden Befehl nutzen:

kubectl get csidrivers.storage.k8s.io

CSI-SMB Controller ausrollen

Im vierten Schritt richten wir den CSI-SMB-Controller ein. Dazu erstellen wir auch wieder eine YAML-Datei und aktivieren diese wieder nach dem Erstellen:

kind: Deployment
apiVersion: apps/v1
metadata:
  name: csi-smb-controller
spec:
  replicas: 1
  selector:
    matchLabels:
      app: csi-smb-controller
  template:
    metadata:
      labels:
        app: csi-smb-controller
    spec:
      dnsPolicy: Default  # available values: Default, ClusterFirstWithHostNet, ClusterFirst
      serviceAccountName: csi-smb-controller-sa
      nodeSelector:
        kubernetes.io/os: linux
      priorityClassName: system-cluster-critical
      tolerations:
        - key: "node-role.kubernetes.io/master"
          operator: "Exists"
          effect: "NoSchedule"
        - key: "node-role.kubernetes.io/controlplane"
          operator: "Exists"
          effect: "NoSchedule"
        - key: "node-role.kubernetes.io/control-plane"
          operator: "Exists"
          effect: "NoSchedule"
      containers:
        - name: csi-provisioner
          image: registry.k8s.io/sig-storage/csi-provisioner:v3.2.0
          args:
            - "-v=2"
            - "--csi-address=$(ADDRESS)"
            - "--leader-election"
            - "--leader-election-namespace=kube-system"
            - "--extra-create-metadata=true"
          env:
            - name: ADDRESS
              value: /csi/csi.sock
          volumeMounts:
            - mountPath: /csi
              name: socket-dir
          resources:
            limits:
              cpu: 1
              memory: 300Mi
            requests:
              cpu: 10m
              memory: 20Mi
        - name: liveness-probe
          image: registry.k8s.io/sig-storage/livenessprobe:v2.7.0
          args:
            - --csi-address=/csi/csi.sock
            - --probe-timeout=3s
            - --health-port=29642
            - --v=2
          volumeMounts:
            - name: socket-dir
              mountPath: /csi
          resources:
            limits:
              cpu: 1
              memory: 100Mi
            requests:
              cpu: 10m
              memory: 20Mi
        - name: smb
          image: registry.k8s.io/sig-storage/smbplugin:v1.9.0
          imagePullPolicy: IfNotPresent
          args:
            - "--v=5"
            - "--endpoint=$(CSI_ENDPOINT)"
            - "--metrics-address=0.0.0.0:29644"
          ports:
            - containerPort: 29642
              name: healthz
              protocol: TCP
            - containerPort: 29644
              name: metrics
              protocol: TCP
          livenessProbe:
            failureThreshold: 5
            httpGet:
              path: /healthz
              port: healthz
            initialDelaySeconds: 30
            timeoutSeconds: 10
            periodSeconds: 30
          env:
            - name: CSI_ENDPOINT
              value: unix:///csi/csi.sock
          securityContext:
            privileged: true
          volumeMounts:
            - mountPath: /csi
              name: socket-dir
          resources:
            limits:
              memory: 200Mi
            requests:
              cpu: 10m
              memory: 20Mi
      volumes:
        - name: socket-dir
          emptyDir: {}

Um im Anschluss wieder zu überprüfen ob alles funktioniert, können wir den folgenden Befehl verwenden:

kubectl -n csi-smb-provisioner get deploy,po,rs -o wide

CSI-SMB-Node Daemon installieren

Jetzt installieren wir den CSI-SMB-Node Daemon. Dafür erstellen wir mal wieder eine YAML-Datei mit dem folgenden Inhalt und aktivieren diese im Anschluss wieder:

kind: DaemonSet
apiVersion: apps/v1
metadata:
  name: csi-smb-node
spec:
  updateStrategy:
    rollingUpdate:
      maxUnavailable: 1
    type: RollingUpdate
  selector:
    matchLabels:
      app: csi-smb-node
  template:
    metadata:
      labels:
        app: csi-smb-node
    spec:
      hostNetwork: true
      dnsPolicy: Default  # available values: Default, ClusterFirstWithHostNet, ClusterFirst
      serviceAccountName: csi-smb-node-sa
      nodeSelector:
        kubernetes.io/os: linux
      priorityClassName: system-node-critical
      tolerations:
        - operator: "Exists"
      containers:
        - name: liveness-probe
          volumeMounts:
            - mountPath: /csi
              name: socket-dir
          image: registry.k8s.io/sig-storage/livenessprobe:v2.7.0
          args:
            - --csi-address=/csi/csi.sock
            - --probe-timeout=3s
            - --health-port=29643
            - --v=2
          resources:
            limits:
              memory: 100Mi
            requests:
              cpu: 10m
              memory: 20Mi
        - name: node-driver-registrar
          image: registry.k8s.io/sig-storage/csi-node-driver-registrar:v2.5.1
          args:
            - --csi-address=$(ADDRESS)
            - --kubelet-registration-path=$(DRIVER_REG_SOCK_PATH)
            - --v=2
          livenessProbe:
            exec:
              command:
                - /csi-node-driver-registrar
                - --kubelet-registration-path=$(DRIVER_REG_SOCK_PATH)
                - --mode=kubelet-registration-probe
            initialDelaySeconds: 30
            timeoutSeconds: 15
          env:
            - name: ADDRESS
              value: /csi/csi.sock
            - name: DRIVER_REG_SOCK_PATH
              value: /var/lib/kubelet/plugins/smb.csi.k8s.io/csi.sock
          volumeMounts:
            - name: socket-dir
              mountPath: /csi
            - name: registration-dir
              mountPath: /registration
          resources:
            limits:
              memory: 100Mi
            requests:
              cpu: 10m
              memory: 20Mi
        - name: smb
          image: registry.k8s.io/sig-storage/smbplugin:v1.9.0
          imagePullPolicy: IfNotPresent
          args:
            - "--v=5"
            - "--endpoint=$(CSI_ENDPOINT)"
            - "--nodeid=$(KUBE_NODE_NAME)"
            - "--metrics-address=0.0.0.0:29645"
          ports:
            - containerPort: 29643
              name: healthz
              protocol: TCP
          livenessProbe:
            failureThreshold: 5
            httpGet:
              path: /healthz
              port: healthz
            initialDelaySeconds: 30
            timeoutSeconds: 10
            periodSeconds: 30
          env:
            - name: CSI_ENDPOINT
              value: unix:///csi/csi.sock
            - name: KUBE_NODE_NAME
              valueFrom:
                fieldRef:
                  apiVersion: v1
                  fieldPath: spec.nodeName
          securityContext:
            privileged: true
          volumeMounts:
            - mountPath: /csi
              name: socket-dir
            - mountPath: /var/lib/kubelet/
              mountPropagation: Bidirectional
              name: mountpoint-dir
          resources:
            limits:
              memory: 200Mi
            requests:
              cpu: 10m
              memory: 20Mi
      volumes:
        - hostPath:
            path: /var/lib/kubelet/plugins/smb.csi.k8s.io
            type: DirectoryOrCreate
          name: socket-dir
        - hostPath:
            path: /var/lib/kubelet/
            type: DirectoryOrCreate
          name: mountpoint-dir
        - hostPath:
            path: /var/lib/kubelet/plugins_registry/
            type: DirectoryOrCreate
          name: registration-dir

Samba Secret erstellen

Im letzten Vorbereitungsschritt erstellen wir jetzt einen Samba-Secret. Dieser wird zur Authentifizierung am Samba-Server benötigt. Es werden jetzt die Anmeldeinformationen eines Benutzers für die Samba-Freigabe benötigt. Im ersten Schritt erstellen wir einen Namespace für unsere Anwendung.

kubectl create namespace test

Jetzt erstellen wir einen Secret mit den Anmeldeinformationen unseres Benutzers. Dazu verwenden wir den nachstehenden Befehl:

kubectl -n test create secret generic smb-creds \
--from-literal username=<username> \
--from-literal domain=<domain> \
--from-literal password=<password>

Durch diesen Befehl wird ein Secret erstellen, welchen wir dann innerhalb unseres Kubernetes-Clusters aufrufen können.

PersistentVolume erstellen

In diesem Schritt erstellen wir jetzt das PersistentVolume. Dieses kann man als Speicherplatzreservierung für das gesamte Kubernetes-Cluster verstehen. Damit teilen wir Kubernetes mit, wo das Cluster die persistenten Daten ablegen kann. Dafür erstellen wir auch wieder eine YAML-Datei und aktivieren diese im Anschluss wieder:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-smb
  namespace: test
spec:
  storageClassName: ""
  capacity:
    storage: <größe> #50Gi
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  mountOptions:
    - dir_mode=0777
    - file_mode=0777
    - vers=3.0
  csi:
    driver: smb.csi.k8s.io
    readOnly: false
    volumeHandle: <volume-name>  # Eindeutige Bezeichnung im Cluster
    volumeAttributes:
      source: <server-freigabepfad> #Pfad der Samba Freigabe mit rekursiven Ordnern
    nodeStageSecretRef:
      name: smb-creds
      namespace: test

PersistentVolumeClaim erstellen

Jetzt erstellen wir das PersistentVolumeClaim, welches man als Speicherplatzreservierung für eine einzelne Anwendung verstehen kann. Dazu verwenden wir die folgende YAML-Datei welche dann auch wieder aktiviert wird:

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: pvc-smb
  namespace: test
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: <größe> #10Gi
  volumeName: pv-smb
  storageClassName: ""

Freigabe testen

Um jetzt einen Test durchzuführen ob die Umsetzung funktioniert hat, können wir das folgende Deployment verwenden:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: nginx
  name: deploy-smb-pod
  namespace: test
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
      name: deploy-smb-pod
    spec:
      nodeSelector:
        "kubernetes.io/os": linux
      containers:
        - name: deploy-smb-pod
          image: mcr.microsoft.com/oss/nginx/nginx:1.19.5
          command:
            - "/bin/bash"
            - "-c"
            - set -euo pipefail; while true; do echo $(date) >> /mnt/smb/outfile; sleep 1; done
          volumeMounts:
            - name: smb
              mountPath: "/mnt/smb"
              readOnly: false
      volumes:
        - name: smb
          persistentVolumeClaim:
            claimName: pvc-smb