Kubernetes II: Декларативное управление Kubernetes. Объекты Kubernetes. Описание в YAML

June 3, 2022
cloud kubernetes kubectl yaml api service deployment create delete apply metadata label selector
Share on:

Это одна из глав книги “Программирование Cloud Native. Микросервисы, Docker и Kubernetes”. По ссылке ее можно скачать бесплатно. Книга обновляется и поддерживается активнее, чем эта статья.

Предыдущая статья из этой серии - введение в Kubernetes, основные термины, первые развертывания и сервисы.

Примеры для этой статьи можно найти на Github в репозитории ivanporty/cloud-docker-k8s

Декларативное управление Kubernetes. Объекты Kubernetes. Описание в YAML

В первой статье про Kubernetes мы рассмотрели его основные составляющие, изучили термины, и провели первое развертывание и масштабирование своего минимального сервиса, просто вызывая команду kubectl из командной строки. Это не потребовало у нас больших усилий, а результаты получились отличные, по большому счету достаточные для того, чтобы развернуть элементарные сетевые сервисы и приложения на кластере под управлением Kubernetes.

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

Конечно, можно было бы создать единый скрипт развертывания системы целиком, и запускать его на сервере, предназначенном исключительно для этой задачи. Отслеживать изменения в процессе запуска и развертывания проекта можно храня исходный текст этого скрипта в системе контроля версий. Но скрипты для запуска сложных действий из командной строки имеют склонность становится чрезмерное сложными, и разрастаться до порядочных размеров. К тому же языки командных оболочек, таких как shell, не совсем предназначены для разветвленной логики, и в итоге их сложность становится серьезным препятствием к быстрому изменению.

Kubernetes не стал бы столь популярным без модели управления кластером с помощью объектов своего программного интерфейса, следующего стандарту REST (RESTful API). Желаемое состояние (desired state) кластера Kubernetes в каждый момент описывается набором объектов, являющихся частью программного интерфейса Kubernetes. У каждого из этих объектов есть набор полей, допустимых значений и вложенных объектов, однозначно описывающих, что именно требуется от управляющих компонентов кластера для того, чтобы текущее состояние кластера пришло к желаемому состоянию, описанному объектами. Объекты Kubernetes - это RESTful ресурсы программного интерфейса Kubernetes, но как мы увидим, работать с ним напрямую нам не придется, все сделает команда kubectl.

Когда мы создавали свое первое развертывание, вызывая команду kubectl create deployment, за кулисами для нас создали объект Kubernetes под названием Deployment (развертывание). После этого мы открыли порт для доступа и взаимодействия со своим приложением в контейнере (kubectl expose) - то есть создали точку доступа в сервис, и нетрудно догадаться, эта привело к созданию объекта Service.

Основной способ управления кластером Kubernetes - именно описание объектов его программного интерфейса и последующая работа с ними. Вместо запуска хотя и простых и понятных команд kubectl, гораздо более гибким и легким для последующих изменений способом управления является прямое описание этих объектов. Для описания обычно используется язык YAML, хотя в итоге, перед отправкой на управляющий сервер Kubernetes, он преобразуется в JSON. Файлы формата YAML хранятся в системе контроля версий, зачастую рядом с исходным кодом и данными приложения, и изменение способа развертывания приложения в кластере Kubernetes легко обновлять и отслеживать.

Несколько слов об “ужасах” YAML

Работа с YAML в Kubernetes стала практически притчей во языцех. Так как мощь и гибкость Kubernetes требует создания немалого количества файлов YAML и работы с ними, разработчики часто саркастически замечают, что Kubernetes это и есть редактирование YAML, не самого удобного для человека языка разметки. Как иронично подметил Келси Хайтауэр в своем Твиттере (Kelsey Hightower, один из самых заметных проводников и пропагандистов идей Kubernetes компании Google), “Я был на конференции, которая была полностью посвящена только YAML. Эта была конференция KubeCon (главная конференция Kubernetes)”.

Все это так, и создавать объекты Kubernetes с нуля, мучительно вспоминая все поля и правила записи YAML, действительно непросто. Однако все гениальное просто, и, как правило, эта проблема элементарно решается шаблонами и помощью редактора. Особенно хороши в этом различные расширения и плагины для сред разработки, таких как IntelliJ и VS Code. Ну а в этом главе мы попробуем обойтись без дополнительных средств, используя шаблоны команды kubectl (да, они есть и там!). Список самых удобных на данный момент инструментов для работы с объектами Kubernetes и YAML вы сможете найти на сайте книги, ну и конечно простым поиском в Интернете.

Объект Deployment вместо kubectl create deployment

Как мы уже упомянули, команда kubectl create deployment, примененная нам в предыдущей главе для развертывания и запуска контейнера в кластере Kubernetes, на самом деле создает объект Kubernetes Deployment и отправляет его на управляющий узел.

К нашей радости все та же самая команда поможет нам получить этот объект в виде YAML. Используем мы ее с сочетанием пары флагов --dry-run=client -o yaml, что означает не что иное, как запуск в “холостом режиме” без отправки в кластер (client), и вывод созданного объекта Kubernetes на консоль. Формат вывода мы запрашиваем как раз YAML.

Итак, развернем сервис time-service из предыдущего раздела в холостом режиме, используя для простоты образ image из моего репозитория Docker Hub (ivanporty). Вы можете заменить репозиторий на свой собственный.

   $ kubectl create deployment time-service --image=ivanporty/time-service:0.1.0 --dry-run=client -o yaml

Мы легко получаем описание объекта Deployment, которое команда kubectl create deployment отправила бы на сервер программного интерфейса Kubernetes apiserver в случае не холостого, а реального вызова:

# Версия программного интерфейса Kubernetes
apiVersion: apps/v1
# Тип объекта
kind: Deployment
# Метаданные нашего объекта, вложенный объект ObjectMeta
metadata:
  creationTimestamp: null
  # список меток самого объекта Deployment
  labels:
    app: time-service
  name: time-service
# Описание собственно правил развертывания контейнера
# Вложенный объект DeploymentSpec
spec:
  # Количество запущенных отсеков pods для масштабирования 
  replicas: 1
  selector:
    matchLabels:
      app: time-service
  strategy: {}
  # описание шаблона для создания новых отсеков
  template:
    metadata:
      creationTimestamp: null
      # список меток для нового отсека
      labels:
        app: time-service
    # непосредственно описание контейнера в отсеке
    spec:
      containers:
      - image: ivanporty/time-service
        name: time-service:0.1.0
        resources: {}
status: {}

Сразу можно видеть, что информации объект Deployment предоставляет намного больше, чем мы указывали при запуске команды kubectl run/create deployment, но последняя многим полям присваивает разумные значения по умолчанию. Для простоты комментарии с описанием самых важных полей и вложенных объектов добавлены прямо в шаблон YAML.

Для начала мы просто хотим запустить наш контейнер в кластере, поэтому для нас самым важным является способ указать, где находится образ этого контейнера. Этим заведует шаблон, по которому создаются все отсеки и контейнеры внутри них - .tempate.spec. Еще мы видим загадочный набор меток (labels), повторяющийся три раза - это уникальные “бирки” на объектах Kubernetes, позволяющие найти нужные объекты в сложном большом кластере. Пока же давайте считать, что для простоты они все должны совпадать.

Используя этот шаблон, создадим свой первый объект Kubernetes, оставив лишь нужные нам поля. Сохраним его в отдельном файле YAML, поместим его в папку k8s, и назовем k8s-time-service-deploy.yaml. Оставим ту же самую метку app, которая используется командой kubectl create deployment. Это будет просто описание приложения app[lication], довольно популярная метка для простых развертываний.

Вот что у нас получится за объект:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: time-service
  name: time-service
spec:
  replicas: 1
  selector:
    matchLabels:
      app: time-service
  template:
    metadata:
      labels:
        app: time-service
    spec:
      containers:
      - image: ivanporty/time-service:0.1.0
        name: time-service

Остается только создать (create) свое развертывание с помощью объекта Kubernetes. Передать команде kubectl описание объекта из файла YAML нужно с помощью флага -f. (Внимание: Если у вас еще остался запущенным сервис и развертывание time-service из предыдущего раздела, не забудьте их удалить с помощью команд kubectl delete!)

$ cd k8s
$ kubectl create -f k8s-time-deploy.yaml

Как видите, теперь указывать тип объекта и его свойства, такие как образ контейнера, больше нет необходимости - все описано объектом в файле YAML. Убедиться в том, что развертывание создано, можно с помощью уже хорошо знакомых команд:

$ kubectl get deployments
NAME           DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
time-service   1         1         1            1           9m

В качестве простого упражнения на повтор материала прошлой главы посмотрите детали и события развертывания (describe, events), чтобы убедиться, что оно создано без ошибок.

Чем сложнее и более распределена по небольшим микросервисам будет ваша система и приложение в целом, тем больше преимуществ предоставит управление кластером с помощью объектов, а не вызовов команды kubect xyz ...:

Объект Service вместо kubectl expose

При знакомстве с Kubernetes мы использовали команду kubectl expose для открытия сетевого порта и доступа к работающему контейнеру, и этой же командой в холостом режиме сможем получить отличный шаблон для объекта сервиса Kubernetes Service:

$ kubectl expose deployment time-service --port=8080 --type=NodePort --dry-run=client -o yaml

Вот что у нас получится в результате. Структура объекта будет очень похожа на развертывание Deployment и для краткости здесь удалены лишние поля, а метки совпадают с теми, чтобы были использованы в шаблоне развертывания (app):

apiVersion: v1
kind: Service
metadata:
  labels:
    app: time-service
  name: time-service
spec:
  # список портов. Дополнительно можно указать протокол
  ports:
  - port: 8080
  # по этим меткам идет поиск отсеков, куда отправляются запросы
  selector:
    app: time-service
  # тип сервиса. В облаке можно использовать LoadBalancer
  type: NodePort

Базовое описание полей сервиса есть прямо в файле YAML, ну а полностью мы рассмотрим все детали и возможности сервисов Kubernetes в отдельной главе.

Назовем файл по аналогии с развертыванием, намекнув в его названии что он содержит описание сервиса: k8s-time-svc.yaml. Создадим новый сервис (если у вас запущен предыдущий вариант сервиса, лучше будет его удалить):

$ kubectl create -f k8s-time-svc.yaml

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

$ kubectl get services
NAME           TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
kubernetes     ClusterIP   10.96.0.1      <none>        443/TCP          17d
time-service   NodePort    10.98.92.168   <none>        8080:30721/TCP   5s

В прошлом разделе мы посмотрели, как в локальных кластерах Minikube или Docker обратиться к сервисам типа NodePort, это будет хорошее повторение материала.

Если вы пользуетесь публичным полноценным кластером, таким как Google Cloud Kubernetes Engine, вы сможете получить доступ к сервису из Интернета, поменяв в описании объекта тип сервиса на LoadBalancer, а затем получив его открытый IP-адрес из описания сервиса, полученного такой же командой kubectl get services.

Императивное управление кластером Kubernetes - create и delete

После описания объектов Kubernetes в формате YAML остается их создать (kubectl create -f), а потом удалить (kubectl delete -f). Подобная работа с объектами в программировании называется императивной (imperative), или процедурной - мы напрямую создаем объекты и управляем их жизненным циклом в “приказном порядке”, при необходимости обновляем, в конце концов удаляем, и затем создаем заново с обновленными настройками. Именно так мы пока и поступали с созданными нами развертыванием и сервисом для своего приложения time-service. Система лишь исполняет наши инструкции и в управлении непосредственно объектами не участвует - она лишь использует их для определения, каким должно быть “желаемое состояние” (desired state) нашего кластера.

Интересно (и удобно), что инструмент kubectl поддерживает создание и удаление объектов не только из одного файла (а в одном файле могут содержаться множество объектов разного типа, например развертывание и сервисы вместе), а множества файлов YAML, находящихся в директории. Учитывая, что мы поместили наши декларации Kubernetes (два файла с объектами развертывания и сервиса) в директорию k8s, создать и удалить все необходимое для микросервиса time-service можно и так:

$ kubectl delete -f k8s/
deployment.apps "time-service" deleted
service "time-service" deleted

$ kubectl create -f k8s/
deployment.apps "time-service" created
service "time-service" created

Если после предыдущих примеров развертывание и сервис уже существуют, сначала удалите их, а затем попробуйте эти команды. Для множества микросервисов вероятно появление и множества деклараций с объектами кластера Kubernetes, и такая команда становится весьма кстати, если вы развертываете все сервисы разом.

Что же дальше? Управлять созданием и удалением объектов кластера и менять таким образом его желаемое состояние понятно и прямолинейно. Однако дополнительные преимущества Kubernetes использовать будет непросто - например, что, если вы хотите создать автоматически масштабируемое приложение? Или хотите обновить контейнеры только на части отсеков (pods), или меняете версию контейнера только для одного отсека? Удалять и создавать под-множества объектов отдельными командами будет неудобно, также как очень тяжело отследить историю внесенных изменений. Для больших кластеров, сложных приложений и сервисов, почти всегда применяется декларативное управление.

Декларативное управление кластером. Команда apply.

Декларативное описание объектов Kubernetes не требует от нас прямого вмешательства в дела кластера и оставляет управление объектами, включая их создание и удаление, в руках системы управления кластером. Девиз декларативного описания - неважно в каком порядке, в течение какого времени и как часто система получает описание объектов кластера. Управление кластера обязуется преобразовать простое, понятное и легко отслеживаемое описание объекта в желаемое состояние кластера (desired state).

Если мы передали системе объект с развертыванием Deployment, и такого развертывания в кластере еще нет, он будет создан, как если бы была вызвана команда create. Если мы передали обновленный сервис Service, в котором, например, добавили еще один порт, система динамически обновит соответствующий объект (или же удалит и создаст заново), и приведет кластер в его желаемое состояние.

Получить описание объектов и сделать на их основе необходимые действия позволяет команда kubectl apply. Учитывая, что основы объектов развертывания и сервисов мы уже знаем, и создали файлы YAML с их описанием, передача объектов в управление кластера теперь происходит в одну строчку

$ kubectl apply -f k8s/
deployment.apps "time-service" created
service "time-service" created

Как мы видим, при отсутствии объектов в системе они будут созданы. Если бы они там уже существовали, изменений в системе не было бы.

Предположим, что мы получили первых пользователей для микросервиса и одного экземпляра сервиса нам мало, а использовать автоматическое масштабирование мы пока не хотим. Отредактируем объект развертывания и укажем, что нам требуется два экземпляра (replicas) отсека и контейнера в нем при развертывании:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: time-service
  name: time-service
spec:
  replicas: 2
  selector:
    matchLabels:
      app: time-service
  template:
    metadata:
      labels:
        app: time-service
    spec:
      containers:
      - image: ivanporty/time-service
        name: time-service

Декларативное описание позволит не волноваться о том, удалено ли старое развертывание и проверять, в скольких экземпляров оно находится сейчас (а может быть, оно вообще еще не создано). Передадим объекты в управление кластера и получим свое желаемое состояние без дополнительных усилий:

$ kubectl apply -f k8s/
deployment.apps "time-service" configured
service "time-service" unchanged

Как мы видим, изменений в сервисе не было, а вот конфигурация развертывания была изменена - причем команда apply описывает, что именно было сделано с объектами - создание, удаление или пере-конфигурация.

Проверим, в каком состоянии и в скольких экземплярах работает развертывание:

$ kubectl get deploy
NAME           DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
time-service   2         2         2            2           2m

Состояние кластера верно. Декларативное описание развертываний и сервисов с помощью объектов Kubernetes помогло нам достичь своего желаемого состояния без дополнительных проверок командой get, создания и удаления объектов, масштабирования scale, и множества других “ручных” команд.

Если мы управляем кластером исключительно декларативным способом, то в случае отсутствия ошибок всегда можем быть уверены в том, что его состояние совпадает с тем, что описано нашими объектами Kubernetes. Управление сложными развертываниями, их обновлениями, и масштабированием становится намного проще и понятнее.

Резюме

Это одна из глав книги “Программирование Cloud Native. Микросервисы, Docker и Kubernetes”. По ссылке ее можно скачать бесплатно. Книга обновляется и поддерживается активнее, чем эта статья.

comments powered by Disqus