Кубернетіс вже перестає бути чимось таким з чим працюють лише інженери платформи. Все більше і більше застосунків загортаються в контейнери та запускаються в контейнерних середовищах. Що робити, якщо з певних причин у вас немає доступу до хмарної платформи, але треба вести розробку застосунку, який працюватиме в хмарі? Ви можете скористатись Kind для локального розгортання Кубернетіс.

Kind — це інструмент для запуску локальних кластерів Kubernetes з використанням «вузлів» контейнерів Docker. В першу чергу kind був розроблений для тестування самого Kubernetes, але може бути використаний для локальної розробки або CI.

Для запуску Kind вам знадобиться docker, podman або інший рушій для роботи з контейнерами. Ви можете звернутись до Швидкого початку роботи з Kind на офіційному сайті.

Створення кластера

Отже, ми маємо встановлений Kind та середовище для роботи з контейнерами, kubectl – інструмент командного рядка для виконання маніпуляцій з кластером.

Для створення локального кластера скористаємось командою kind create cluster.

kind create cluster

Ви завжди можете отримати потрібну довідку скориставшись командою виду kind [command] --help.

kind --help

Ми створили свій локальний кластер з іменем kind-kind. Це стандартне імʼя, яке використовує kind, якщо параметр -n назва або --name назва не вказано.

Крім використання ключів для kind ми можемо використати маніфест для створення кластера з потрібними нам параметрами.

cat <<EOF > kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: my-super-cluster
nodes:
- role: control-plane
- role: worker
  extraMounts:
  - hostPath: /path/to/local/data
    containerPath: /data
# - role: worker
# - role: worker
#   extraMounts:
#   - hostPath: /path/to/local/data/dump
#     containerPath: /data/dump
#   - hostPath: /path/to/local/data/diff
#     containerPath: /data/diff

☝️ тут ми можемо зазначити кількість потрібних вузлів, їх роль та, головне, в нашому випадку, — шлях у локальній файловій системі, який ми будемо монтувати у вузли нашого кластера та використовувати, як систему зберігання для наших Постійних Томів (Persistent Volumes). Див Extra Mounts в документації Kind.

Застосуємо нашу конфігурацію для створення кластера

kind create cluster --config kind-config.yaml
Creating cluster "kind" ...
 ✓ Ensuring node image (kindest/node:v1.32.2) 🖼
 ✓ Preparing nodes 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
Set kubectl context to "kind-my-super-cluster"
You can now use your cluster with:

kubectl cluster-info --context kind-my-super-cluster

Have a question, bug, or feature request? Let us know! https://kind.sigs.k8s.io/#community 🙂

Перевіримо, що файлову систему хосту змонтовано у вузол worker нашого кластера.

docker container inspect osm-cluster-worker \
  | jq '[{"Name": .[0].Name,
          "BindMounts": (
            .[] |
            .Mounts[] |
            select(.Type == "bind")
        )}]'

І бачимо, що все ОК, файлову систему змонтовано.

[
  {
    "Name": "/my-super-cluster-worker",
    "BindMounts": {
      "Type": "bind",
      "Source": "/host_mnt/path/to/local/data",
      "Destination": "/data",
      "Mode": "",
      "RW": true,
      "Propagation": "rprivate"
    }
  },
  {
    "Name": "/my-super-cluster-worker",
    "BindMounts": {
      "Type": "bind",
      "Source": "/lib/modules",
      "Destination": "/lib/modules",
      "Mode": "ro",
      "RW": false,
      "Propagation": "rprivate"
    }
  }
]

Створення PersistentVolume та PersistentVolumeClaim

Створимо маніфест для Постійного Тому

apiVersion: v1
kind: PersistentVolume
metadata:
  name: my-super-cluster-pv
spec:
  capacity:
    storage: 100Gi
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  hostPath:
    path: "/data"
  storageClassName: my-storageclass

А також створимо Заявку PersistentVolumeClaim яку будемо використовувати для монтування Постійного Тому в робочі навантаження.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name:  my-super-cluster-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 100Gi
  storageClassName: my-storageclass

Тепер найголовніше 🥁, потрібно створити StorageClass, який дозволить нам явно повʼязати Заявку PVC з Постійним Томом PV.

Примітка: Зверніть увагу що kind створює стандартний StorageClass під час створення кластера. Однак цей StorageClass не задовольняє нашим вимогам маючи reclaimPolicy:Delete.

kubectl get storageclass
NAME                 PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
standard (default)   rancher.io/local-path   Delete          WaitForFirstConsumer   false                  80m

Це означає, що вміст нашого Постійного Тому буде очищатись після його розмонтування з пода, а це не те що нам треба.

Створимо наш StorageClass в кластері.

kubectl apply -f -  <<EOF
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: my-storageclass
provisioner: rancher.io/local-path
parameters:
  nodePath: /data
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
EOF
storageclass.storage.k8s.io/my-storageclass created

Перевіримо наш StorageClass.

kubectl get storageclass
NAME                 PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
my-storageclass      rancher.io/local-path   Retain          WaitForFirstConsumer   false                  5m27s
standard (default)   rancher.io/local-path   Delete          WaitForFirstConsumer   false                  91m

І зробимо його типовим, на про всяк випадок

kubectl patch storageclass my-storageclass -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

а StorageClass standard навпаки, зробимо звичайним.

kubectl patch storageclass standard -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'

Переглянемо поточні відомості про StorageClass

kubectl get storageclass
NAME                        PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
my-storageclass (default)   rancher.io/local-path   Retain          WaitForFirstConsumer   false                  12m
standard                    rancher.io/local-path   Delete          WaitForFirstConsumer   false                  98m

Використання PersistentVolumeClaim в поді

Застосуємо маніфести PV та PVC в кластері.

kubectl apply -f pv.yaml -f pvc.yaml

Створимо под, який використовує Заявку на постійний том для зберігання даних.

kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: debug-pod
spec:
  containers:
  - name: debug-container
    image: busybox:latest
    command: ["sh", "-c", "sleep 3600"]
    volumeMounts:
    - mountPath: "/data"
      name: my-super-cluster
  volumes:
  - name: my-super-cluster
    persistentVolumeClaim:
      claimName: my-super-cluster-pvc
EOF

Перевіримо що Заявка PVC має привʼязку до PV та використовується в нашому тестовому поді

kubectl get pv
NAME                  CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                          STORAGECLASS      VOLUMEATTRIBUTESCLASS   REASON   AGE
my-super-cluster-pv   100Gi      RWO            Retain           Bound    default/my-super-cluster-pvc   my-storageclass   <unset>                          9m20s
kubectl get pvc
NAME                   STATUS   VOLUME                CAPACITY   ACCESS MODES   STORAGECLASS      VOLUMEATTRIBUTESCLASS   AGE
my-super-cluster-pvc   Bound    my-super-cluster-pv   100Gi      RWO            my-storageclass   <unset>                 8m48s

Зверніть увагу що статус PV та PVC має значення Bound, що означає що Заявку було успішно звʼязано з Постійним Томом.

kubectl describe pod
Name:             debug-pod
Namespace:        default
Priority:         0
Service Account:  default
Node:             kind-control-plane/172.20.0.4
Start Time:       Fri, 04 Apr 2025 18:17:09 +0300
Labels:           <none>
Annotations:      <none>
Status:           Running
IP:               10.244.0.5
IPs:
  IP:  10.244.0.5
Containers:
  debug-container:
    Container ID:  containerd://d030a6edfc13c314853f22efc505990bbbb8e3954ed1c9887b9c7b3be575a0be
    Image:         busybox:latest
    Image ID:      docker.io/library/busybox@sha256:37f7b378a29ceb4c551b1b5582e27747b855bbfaa73fa11914fe0df028dc581f
    Port:          <none>
    Host Port:     <none>
    Command:
      sh
      -c
      sleep 3600
    State:          Running
      Started:      Fri, 04 Apr 2025 18:17:13 +0300
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /data from my-super-cluster (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-5wdzj (ro)
Conditions:
  Type                        Status
  PodReadyToStartContainers   True
  Initialized                 True
  Ready                       True
  ContainersReady             True
  PodScheduled                True
Volumes:
  my-super-cluster:
    Type:       PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
    ClaimName:  my-super-cluster-pvc
    ReadOnly:   false
  kube-api-access-5wdzj:
    Type:                    Projected (a volume that contains injected data from multiple sources)
    TokenExpirationSeconds:  3607
    ConfigMapName:           kube-root-ca.crt
    ConfigMapOptional:       <nil>
    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  8m36s  default-scheduler  Successfully assigned default/debug-pod to kind-control-plane
  Normal  Pulling    8m36s  kubelet            Pulling image "busybox:latest"
  Normal  Pulled     8m32s  kubelet            Successfully pulled image "busybox:latest" in 3.395s (3.395s including waiting). Image size: 1855985 bytes.
  Normal  Created    8m32s  kubelet            Created container: debug-container
  Normal  Started    8m32s  kubelet            Started container debug-container

Наш под було успішно створено і він працює.

Отримаємо доступ до термінала в нашому поді та перевіримо, що том змонтований у нашій файловій системі і все працює належним чином.

kubectl exec -it debug-pod -- sh
/ # ls -l / | grep data
drwxr-xr-x    2 root     root          4096 Apr  4 15:17 data
/ # touch /data/somefile.txt
/ # ls -l /data
total 0
-rw-r--r--    1 root     root             0 Apr  4 15:31 somefile.txt
/ #
/ # exit

Тепер перегляньте файлову систему хосту змонтовану у вузол worker нашого кластера і ви побачите там тільки що створений файл somefile.txt.

Підсумки

Ми створили Заявку на використання Постійного Тому в робочому навантажені, яка використовує Клас Зберігання (StorageClass) для звʼязування Заявки з Томом. Постійний том використовує систему зберігання наявну на вузлі нашого кластера. Система зберігання вузла кластера базується на файловій системі хосту, на якому розгорнуто наш кластер.

У такий спосіб ми можемо надійно зберігати та повторно використовувати дані розміщені в Постійному Томі в робочих навантаження нашого кластера, які за своєю природою мають обмежений життєвий цикл. Окрім цього ми також можемо передавати в наші робочі навантаження попередньо створені дані з нашого хосту та використовувати їх в подах.

Очищення

Для очищення (вилучення) кластера скористайтесь наступною командою.

kind delete cluster --name kind-my-super-cluster
Deleting cluster "kind-my-super-cluster" ...

Зачекайте допоки Kind видаліть кластер. За потреби видаліть створені файли у файловій системі хосту.

Додаткові матеріали