Pod

Это группа состоящая из одного или несколько контейнеров размещенная в одной сетевом неймспейсе, и имеющая общий том. Pod - базовый строительный блок в k8s.

Исходя из здравого смысла, принято запускать 1 процесс на 1 контейнер. Но существуют такие кейсы, что для поддержания корректной работы нашего основного приложения нужен дополнительный процесс. Допустим наше приложение не может обрабатывать изменения в файле, и нам нужно добавить какой-либо механизм автоматического перезапуска нашего контейнера. Что бы не нарушать не гласное правило: 1 Процесс - 1 Контейнер, мы рядом запускаем дополнительный контейнер. И избегаем проблем связанных с работой контейнера, например когда один процесс умер а другой живет и вроде бы все в порядке, но приложение не работает. Так же оба процесса будут делать вывод в стандартный stdout, что тоже привидет к не понимаю того, что происходит.

Соотвественно, нам нужна какая-то более высокоуровневая абстракция, которая позволит сгрупировать наши контейнеры вместе и управлять ими как единое целое. И эту задачу решают поды (Pod). Как раннее упоминалось, в поде все контейнеры живут в одном сетевом namespace (в линуксовом). Это означает что приложение из контейнера A может постучать на локальный порт (loopback) приложения из контейнера B. Также контейнеры могут писать на общую файловую систему. k8s-components-pod

Запуская pod с нашим приложением, kubernetes на самом деле запускает еще один служебный (инфраструктурный) контейнер, который как раз таки предназначен для инициализации пространства имен (namespace), а конкретнее сетевого неймспейса. Обычно такой контейнер в названии имеет приставку POD_.

Собственно перейдем в консоль, и попробуем на практике запустить под. Для этого подготовим описание нашего пода, в формате yaml.

---
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - image: nginx:1.20
    name: nginx
	  ports:
	  - containerPort: 80
  • apiVersion: v1 - Первым указывается, версия API. В зависимости от версии могут быть разные типы объектов. В данном случаи указываем объект типа: Pod.
  • metadata: - В поле метаданных указываем имя (name) нашего пода: my-pod
  • spec - в этом поле указывается описание нашего пода. В данном кейсе мы описываем наличие контейнера, а директива containers - представляет из себя список с контейнерами.
  • containers: - тут достаточно знакомое нам описание самого контейнера. image - из какого образа запускаем контейнер, name - имя контейнера. ports: - список с портами.

Итак, для того что бы запустить наш под из файла, воспользуется командой:

$ kubectl create -f pod-sample.yml

Что бы проверить, состояние нашего пода запускаем команду:

$ kubectl get pod
---
NAME     READY   STATUS    RESTARTS   AGE
my-pod   1/1     Running   0          4m9s

Для удаления пода, юзаем команду:

$ kubectl delete pod my-pod

Или же эту, для удаление всех подов:

$ kubectl delete pod --all

Для эксперимента запустим еще один под из образа busybo:latest с командой:

sh -c 'while true; do echo New random number is $(( ( RANDOM % 100 ) +1 )); sleep 2; done'

В значении имени укажем: hello-random-num. Структура пода в yml:

apiVersion: 1
kind: Pod
metadata:
  name: hello-random-num
spec:
  containers:
  - image: busybox:latest
    name: hello-random-num
    command: ["/bin/sh"]
    args: ["-c", "while true; do echo New random number is $(( ( RANDOM % 100 ) +1 )); sleep 2; done"]

Пробуем запустить под из файла,

$ kubectl create -f pod-random-sample.yml

Если посмотреть логи созданного пода, то в логи он каждые две минуты пишет рандомное число.

$ kubectl logs hello
---
New random number is 85
New random number is 95
New random number is 91
New random number is 69
New random number is 28
New random number is 31

ReplicaSet

Что бы отмасштабировать наше приложение горизонтально, в описании нашего пода мы можем поменять значение директивы name, и запустить под. На этом задача скейлинга будет решена. Но данный подход не совсем удобен в kubernetes, существует более правильный подход к масштабированию приложений, через создание ReplicaSet объектов. Создадим такой объект, и на примере разберем его свойства.

---
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: my-replicaset
spec:
  replicas: 2
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - image: nginx:1.20
        name: nginx
        ports:
          - containerPort: 80
  • apiVersion: apps/v1, kind: ReplicaSet - версия апи сервера, и тип создаваемого объекта.
  • metadata:, name: my-replicaset - в метаданных, указывается имя для нашего replicaset (my-replicaset).
  • spec: - с этого поля начинается описание свойств для репликасета.
  • replicas: 2 - указание, колличество создаваемых реплик.
  • selector:, matchLabels:, app: my-app - объект replicaset, создает новые поды из шаблонов (template). Селектор указывается, чтобы репликасет понимал какие поды были созданые из шаблона, относятся к нему. То есть в данном случаи мы говорим, что все созданные поды с ярлыком label - app: my-app принадлежать данному репликасету.
  • template - описание шаблона пода,
  • metadata, labels, app: my-app - в свойствах метаданных не указывается имя, как это делалось ранее. Имя будет сгенерировано автоматически репликасетом. Зато здесь указывается новое поле - labels со значением app: my-app, как ранее говорилось по этому полю, репликасет будет понимать что эти поды, с указанным label, относятся к нему.
  • spec, containers - здесь уже идет описание создаваемых контейнеров. Мы будем запускать контейнер с именем nginx, из образа nginx:1.20. И открываем порт 80.

Для визуального понимания связей селекторов и лейблов, можно взглянуть на эту картинку: k8s-components-rs

У объекта replicaset, селектор со значением лейбла - app: ubuntu. Cоотвественно у подов данного сета ярлыки будут - app: ubuntu.

Запускаем объект репликасет, командой:

$ kubectl create -f replicas-sample.yml
$ kubectl get pod
--
NAME                  READY   STATUS    RESTARTS   AGE
my-replicaset-nhh8z   1/1     Running   0          13s
my-replicaset-p5zcn   1/1     Running   0          13s

kubectl get replicaset
NAME            DESIRED   CURRENT   READY   AGE
my-replicaset   2         2         2       12m

Теперь обновим число реплик replicas: 3, и снова обратимся к api-серверу:

$ kubectl apply -f replicas-sample.yml
$ kubectl get pod
---
NAME                  READY   STATUS    RESTARTS   AGE
my-replicaset-nhh8z   1/1     Running   0          4m21s
my-replicaset-p5zcn   1/1     Running   0          4m21s
my-replicaset-v4zk2   1/1     Running   0          23s

$ kubectl get replicaset
--
NAME            DESIRED   CURRENT   READY   AGE
my-replicaset   3         3         3       13m

Появилась новая реплика.

Стоит отметить разницу команд - kubectl create и kubectl apply. Если уже к поднятой репликесет, мы обратимся через команду create, api-сервер выкинет ошибку о том, что такой объект уже существует: Error from server (AlreadyExists):. Поэтому для существуюших объектов применяем изменения через - apply. Основное отличие это идемпотентность.

Теперь удалим один под, и посмотрим на поведения кластера:

$ kubectl delete pod my-replicaset-bzl5g && kubectl get pod
---
pod "my-replicaset-bzl5g" deleted
NAME                  READY   STATUS    RESTARTS   AGE
my-replicaset-8h2lh   1/1     Running   0          2s
my-replicaset-nhh8z   1/1     Running   0          17m
my-replicaset-p5zcn   1/1     Running   0          17m

Из вывода видим, что под - my-replicaset-bzl5g был удален, последующий вывод показывает, поднят новый под - my-replicaset-8h2lh. В этом и заключается работа принципа - Self-healing, кластер постоянно следит что бы количество реплик соответсвовало описанию манифеста, все зависимости от работающий нод в кластере.

Рассмотрим кейс, противоположный предыдущему примеру. Попробуем поднять 4 под вне работающего репликасета. На под навесим лейбл: app: my-app:

---
apiVersion: v1
kind: Pod
metadata:
  name: my-pod-1
  labels:
    app: my-app
spec:
  containers:
  - image: nginx:1.20
    name: nginx
    ports:
    - containerPort: 80

Запускаем под, и сразу выполняем листинг все запущенных подов:

$ kubectl create -f pod-sample.yml && kubectl get pod
---
pod/my-pod-1 created
NAME                  READY   STATUS        RESTARTS   AGE
my-pod-1              0/1     Terminating   0          0s
my-replicaset-8h2lh   1/1     Running       0          16m
my-replicaset-nhh8z   1/1     Running       0          34m
my-replicaset-p5zcn   1/1     Running       0          34m

Собственно, вновь отработал принцип Self-healing только в обратную сторону. Kubernetes прибил 4 под, так как в описании манифеста replicas.yml указано, что в данном наборе реплик может быть только 3 экземпляра нашего приложения.

Давайте попробуем проапгрейдить образ контейнеров в шаблоне с nginx:1.20 на nginx:1.22, для этого воспользуемся командой:

$ kubectl set image replicaset my-replicaset nginx=nginx:1.22                     
---
replicaset.apps/my-replicaset image updated

Выведим информация по объекту - my-replicaset:

$ kubectl describe replicaset my-replicaset       
---
Name:         my-replicaset
Namespace:    default
Selector:     app=my-app
Labels:       <none>
Annotations:  <none>
Replicas:     3 current / 3 desired
Pods Status:  3 Running / 0 Waiting / 0 Succeeded / 0 Failed
Pod Template:
  Labels:  app=my-app
  Containers:
   nginx:
    Image:        nginx:1.22
    Port:         80/TCP
    Host Port:    0/TCP
    Environment:  <none>
    Mounts:       <none>

Как видно из вывода, версия nginx поменялась. Но если пролистим информацию по запушенному поду, то увидим что под работает на более старой версии.

$ kubectl describe pod my-replicaset-nhh8z
---
Containers:
  nginx:
    Image:          nginx:1.20

Для того, что бы решить этот кейс достаточно перезапустить под. По аналогии с докером, когда мы запускаем новый контейнер. Я удалил этот под, и сделал вывод информации по новому созданному поду:

$ kubectl delete pod my-replicaset-nhh8z
$ kubectl get pod
---
NAME                  READY   STATUS              RESTARTS   AGE
my-replicaset-8h2lh   1/1     Running             0          50m
my-replicaset-chpgc   0/1     ContainerCreating   0          14s
my-replicaset-p5zcn   1/1     Running             0          68m

$ kubectl describe pod my-replicaset-chpgc
---
Containers:
  nginx:
    Image:          nginx:1.22

Как видно из вывода последней команды, replicaset согласно указанному шаблону запутил новый под уже с актуальной версией образа nginx:1.22.

Deployment

В предыдущем разделе рассмотрели последний кейс, в котором решалась задача апгрейда версии нашего приложения. Напомню, мы через консоль повысили версию nginx для репликасета и удалили один под. Репликасет перезапустил под уже с актуальной версией. Данный подход хоть и решает задачу, но не совсем применим с точки зрения удобства.

В кубернетес проблему обновления\отката приложения решает более высокая абстракция - deployment. В продакшене скорее всего мы и будем запускать наши приложения через deployment, так как эта абстракция и была задумана для удобства запуска приложения, с последующей возможностью его отката или обновления. Описание манифеста практически не отличается от описания replicaset, за исключением некоторых деталей. Давайте создадим деплоймент, по аналогии с репликайсетом, и выделим отличающие их детали.

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: my-app
  stategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
    type: RollingUpdate
  tempate:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - image: nginx:1.12
        name: nginx
        ports:
          - containerPort: 80

Из изменений у нас поменяется тип создаваемого объекта, в данном случаи это - kind: Deployment. И добавляется новое поле - strategy, как раз таки в этом поле описываем стратегию того, как мы хотим приложение обновлять или откатывать. На данный момент есть 2 стратегии обновления - rollingUpdate и Recreate.

  • Recreate стратегия может повстречаться на каких-нибудь стейджинговых или тестовых стендах, где доступность приложения не важна. Recreate - удаляет все старые поды одной пачкой, и поднимает новые с обновленной версией приложения.
  • rollingUpdate - противоположная стратегия, обновления выполняются без какого либо хендовера, проще говоря безшовно. В рамках данной стратегии мы можем настроить обновления по схеме, когда сначала по очередно будут выключаться старые поды и паралельно запускаться новые. В нашем случаи в свойствах rollingUpdate указывается - maxSurge: 1 и maxUnavailable: 1.
    • maxSurge - в этом поле, мы указываем на какое количество реплик можно поднять поды, относительно текущего количества реплик, при поступлении нового обновления. Исходя из текущего примера, у нас задано общее количество запущенных реплик равное 2м, значит при обновлении будет запущена 3 реплика с новой версией приложения.
    • maxUnavailable - значение этого поля, говорит нам на какое количество реплик можем уменьшиться, относительно основного количества реплик.

Работает это так, пришел апдейт нашего приложения, по свойству maxSurge запустится +1 новая реплика с новой версией, по свойству maxUnavailable -1 реплика выключится со старой версий.

Представим что в нашем деплойменте указано базовое количество реплик равное - 10. В этом случаи, мы меняем значения свойств - maxSurge и maxUnavailable на 5. Что бы кубернетес поднимал +5 реплик с новой версией, и удалял -5 реплик со старой версией приложения. Иначе, удаляя/создавая по 1 реплике, относительно базового количества - 10 реплик, процесс может затянуться по времени. Что бы не привязывается к базовому количеству реплик, мы может указать свойствам - maxSurge и maxUnavailable значения в процентах.

Запускаем созданный манифест:

$ kubectl apply -f my-deployment.yml

Мы можем заметить каскадную зависимость объектов. После применения нового манифеста, у нас создаться новый объект - deployment,

$ kubectl get deployment       
NAME            READY   UP-TO-DATE   AVAILABLE   AGE
my-deployment   2/2     2            2           5m30s

Который создаст объект replicaset,

$ kubectl get rs
NAME                      DESIRED   CURRENT   READY   AGE
my-deployment-58c4f5f5b   2         2         2       6m17s

и далее replicaset поднимет новые поды.

Сейчас через kubectl edit попробуем поменять версию образа нашего приложения, и проследим за поведением деплоймента. Запускаем команду, находим свойство image и повышаем версию.

$ kubectl edit deployment my-deployment
---
spec:
  containers:
  - image: nginx:1.17

Сохраняемся, и сразу же принтуем поды:

$ kubectl get po                       
NAME                             READY   STATUS              RESTARTS   AGE
my-deployment-58484bcb6c-n98nz   1/1     Running             0          2m3s
my-deployment-6bf57f5c7-5hnnd    0/1     ContainerCreating   0          2s
my-deployment-6bf57f5c7-klwmc    0/1     ContainerCreating   0          2s

Как видим один под выключился и поднимаются 2 пода с новой версией.

Принтуем репликасеты:

$ kubectl get rs
NAME                       DESIRED   CURRENT   READY   AGE
my-deployment-58c4f5f5b    0         0         0       23m
my-deployment-6bf57f5c7    2         2         2       2m13s

Из консоли замечаем, что более старый репликасет в своих колонках имеет все нулевые значения, то есть выключен. А более молодой репликасет уже запустил 2 пода.

По каждому репликасету посмотрим подробную информацию, и грепаем (grep) используемый образ.

$ kubectl describe rs my-deployment-58c4f5f5b | grep Image
Image:        nginx:1.12

У старого репликасета, который выключен, версия приложения 1.12. Новый же набор копий имеет версию:

$ kubectl describe rs my-deployment-6bf57f5c7 | grep Image
Image:        nginx:1.17

Когда мы поменяли версию нашего приложения, деплоймент создал новый репликасет уже с обновленной версией приложения, и репликасет поднял поды с новым образом. Старый деплоймент все еще доступный. И мы может его активировать, воспользовавшись откатом версии - механизмом, который поддерживается в деплоймент.

Давайте попробуем откатиться на старый деплоймент, делается командой:

$ kubectl rollout undo deployment my-deployment
$ kubectl get rs
---                                
NAME                      DESIRED   CURRENT   READY   AGE
my-deployment-58c4f5f5b   2         2         2       5m10s
my-deployment-6bf57f5c7   0         0         0       58s