k8s-logo

Итак, мы пробежались по верхам матчасти кубера – настало время понажимать кнопки. Сразу хочу заметить, что эта статья будет в некотором роде поверхностна и неполна. В первую очередь, кубер огромен и необъятен, тем более в рамках одной статьи. Во-вторых, Kubernetes – это система кластеризации, а значит для практики нам потребуется кластер. В отличие от докера, где детально разобраться в основных командах и синтаксисе докерфайла можно на собственном ноутбуке – хорошая демонстрация работы планировщика, распределяющего поды по нодам кластера, будет возможна только когда нод в этом кластере будет больше одного.

Создание кластера

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

Локальный вариант

Если хочется работать полностью локально, то надо собирать кластер на виртуалочках с помощью Minikube, его можно назвать грубой аналогией docker-machine. Не могу рекомендовать данный вариант по той причине, что создание кластера из нескольких нод нетривиально. Плюс, есть шанс, что потребуется разобраться в устройстве тех частей кубера, знание о которых бесполезно в повседневной работе программиста.

Kubernetes-as-a-Service

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

Среди вариантов: GKE (Google), EKS (Amazon), AKS (Microsoft). Обычно провайдеры дают бесплатный стартовый кредит и/или небольшое количестве бесплатных ресурсов. Этого должно с лихвой хватить для учебных целей.

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

В своих примерах я буду использовать GKE, но за пределами начальной настройки рабочего окружения работа с кластерами разных вендоров одинакова.

Подключение к кластеру

В отличие от докера, кубер изначально расчитан на удаленную работу. В отличие от docker-compose, нет необходимости копировать ямлики на ноду кластера или делать фокусы с прокидыванием докер-сокета, чтобы запустить контейнеры на удаленном кластере. Клиентская консольная утилита kubectl общается с API-сервером кубера, с шифрованием, авторизацией, поддержкой нескольких контекстов для разных кластеров и т.д.

Механизмы подключения и авторизации в кластере зависят от разновидности кубера, которой вы пользуетесь. Как правило “натравливание” kubectl на свежесозданный кластер будет заключаться в “Запустите вот эту команду, и она заполнит ваш ~/.kube/config”. За подробностями направлю в документацию выбранного провайдера.

Проверить подключение можно запустив:

kubectl get nodes

NAME                                            STATUS   ROLES    AGE   VERSION
gke-your-first-cluster-1-pool-1-aba8f8dc-9n92   Ready    <none>   1m    v1.11.8-gke.6
gke-your-first-cluster-1-pool-1-f08db197-630s   Ready    <none>   1m    v1.11.8-gke.6
gke-your-first-cluster-1-pool-1-f46c755f-2fc4   Ready    <none>   1m    v1.11.8-gke.6

Передо мной свежесозданный кластер из трех нод – можно что-нибудь позапускать.

Запускаем первое приложение

В теоретической статье я упоминал, что работа с кубером заключается в скармливании кластеру YAML-определений и ожидании, что он причешет реальность под наши хотелки. Говоря академическим языком: кубер – декларативная система.

Долго не мучая воображение, я нашел простенький пример на Гитхабе. Предлагаю открыть его в соседней вкладочке и пробежаться по нему глазами, сильно не углубляясь в происходящее, а после этого – скормить его:

kubectl apply -f https://raw.githubusercontent.com/paulbouwer/hello-kubernetes/master/yaml/hello-kubernetes.yaml

service/hello-kubernetes created
deployment.apps/hello-kubernetes created

Что сейчас произошло:

  • kubectl скачал YAML-файл и сохранил его во временный файл за меня
  • kubectl распарсил файл и отправил запрос с содержащимися в нем определениями ресурсов в API-сервер
  • API-сервер понял, что этих ресурсов не существует и создал их

Если я повторю ту же самую команду, то получу в ответ:

kubectl apply -f https://raw.githubusercontent.com/paulbouwer/hello-kubernetes/master/yaml/hello-kubernetes.yaml

service/hello-kubernetes unchanged
deployment.apps/hello-kubernetes unchanged

API-сервер понял, что ресурсы уже существуют и не стал ничего делать.

И – идемпотентность.

Не сложно догадаться, что если бы я отредактировал этот ямлик и вновь скормил бы его куберу – он бы заметил эти изменения и обновил бы измененные ресурсы. В целом ситуация похожа на работу docker-compose.

Давайте проверим, действительно ли что-то запустилось:

kubectl get all

NAME                               DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/hello-kubernetes   3         3         3            3           8m

NAME                                          DESIRED   CURRENT   READY   AGE
replicaset.apps/hello-kubernetes-7bf6fbdb57   3         3         3       8m

NAME                                    READY   STATUS    RESTARTS   AGE
pod/hello-kubernetes-7bf6fbdb57-5nxv7   1/1     Running   0          8m
pod/hello-kubernetes-7bf6fbdb57-b8886   1/1     Running   0          8m
pod/hello-kubernetes-7bf6fbdb57-wj84v   1/1     Running   0          8m

NAME                       TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)        AGE
service/hello-kubernetes   LoadBalancer   10.43.247.178   35.225.73.247   80:31750/TCP   8m
service/kubernetes         ClusterIP      10.43.240.1     <none>          443/TCP        13m

Прежде чем детально разбираться – откроем в браузере адрес из колонки EXTERNAL-IP и убедимся, что приложение работает.

hello-k8s

Отлично, работает, мы все сделали правильно, но… кубер сказал нам, что создал всего два ресурса, а в выхлопе контента сильно больше – давайте разбираться.

Разбираемся с результатом

Итак, в YAML-файле у нас было определено два ресурса: сервис и деплоймент, оба с именем hello-kubernetes, мы их видим в списке, к ним претензий нет. Проигнорируем service/kubernetes – это дефолтный служебный сервис для общения приложений с API кубера.

А вот дальше – интереснее. У нас есть репликасет и кучка подов. Если мы вспомним теорию – то именно так и должен работать деплоймент:

Deployment – контроллер, управляющий ReplicaSet’ами. Именно с ним обычно непосредственно работают пользователи. Дополнительный уровень абстракции нужен для процесса обновления. При обновлении на новый образ контейнера создается новый ReplicaSet, в который “перекачиваются” поды из старого. Таким образом мы можем выкатить новую версию приложения без даунтайма. Плюс деплой хранит историю конфигураций и позволяет автоматически делать откат, если в процессе релиза что-то пошло не так.

То есть деплоймент создал репликасет, который создал поды. Их “наследственность” прослеживается из имен:

deployment.apps/hello-kubernetes
replicaset.apps/hello-kubernetes-7bf6fbdb57
            pod/hello-kubernetes-7bf6fbdb57-5nxv7
            pod/hello-kubernetes-7bf6fbdb57-b8886
            pod/hello-kubernetes-7bf6fbdb57-wj84v

Играемся и ломаем вещи

Чтобы понять, зачем так сложно, попробуем немного поиграться с с этими ресурсами. У kubectl есть возможность редактировать ресурсы напрямую, причем несколькими способами.

Никогда не делайте так в продакшене, мы тут специально тренируемся все ломать.

Попробуем увеличить количество реплик нашего приложения:

kubectl scale deployment hello-kubernetes --replicas=4
deployment.extensions/hello-kubernetes scaled

и посмотрим, что мы получили:

kubectl get deploy,rs,pods

NAME                                     DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deployment.extensions/hello-kubernetes   4         4         4            4           2h

NAME                                                DESIRED   CURRENT   READY   AGE
replicaset.extensions/hello-kubernetes-7bf6fbdb57   4         4         4       2h

NAME                                    READY   STATUS    RESTARTS   AGE
pod/hello-kubernetes-7bf6fbdb57-2sf7t   1/1     Running   0          31s
pod/hello-kubernetes-7bf6fbdb57-5nxv7   1/1     Running   0          2h
pod/hello-kubernetes-7bf6fbdb57-b8886   1/1     Running   0          2h
pod/hello-kubernetes-7bf6fbdb57-wj84v   1/1     Running   0          2h

Чтобы не засорять вывод сервисами я сделал не get all, а указал конкретные ресурсы, плюс – задал нужный мне порядок, соответствующий “наследственности” ресурсов.

Посмотреть список всех доступных ресурсов, их краткие и полные имена можно через kubectl api-resources.

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

Пока все предсказуемо, продолжаем баловаться: что случится, если мы удалим под, симулировав падение ноды, где он был запущен:

kubectl delete pod/hello-kubernetes-7bf6fbdb57-5nxv7
pod "hello-kubernetes-7bf6fbdb57-5nxv7" deleted

Результат все еще предсказуем:

kubectl get pods

NAME                                READY   STATUS    RESTARTS   AGE
hello-kubernetes-7bf6fbdb57-2sf7t   1/1     Running   0          4m
hello-kubernetes-7bf6fbdb57-b8886   1/1     Running   0          2h
hello-kubernetes-7bf6fbdb57-wj84v   1/1     Running   0          2h
hello-kubernetes-7bf6fbdb57-x58hh   1/1     Running   0          49s

Вместо пода *-5nxv7 был создан *-x58hh, это видно из колонки AGE: новому меньше минуты от роду. Репликасет проследил за ситуацией и восстановил задекларированное состояние. Таким образом мы застрахованы от падения нод и еще целого класса неприятностей, приводящих к исчезновению подов.

У – устойчивость

Делаем релизы

Внимательный читатель может заметить, что для этих целей хватило бы и репликасета, зачем нам тогда надстройка в виде деплоймента? Давайте попробуем выкатить новый релиз нашего приложения:

kubectl set image deploy/hello-kubernetes hello-kubernetes=paulbouwer/hello-kubernetes:1.4

Здесь я указываю, что во всех подах деплоймента hello-kubernetes контейнеру hello-kubernetes нужно задать такой-то образ. Мы ведь помним, что в поде может быть больше одного контейнера, правда?

Пронаблюдаем его в реальном времени с помощью ключа --watch, а затем – разберемся в том, что же именно произошло:

kubectl get pods --watch

NAME                                READY   STATUS              RESTARTS   AGE
hello-kubernetes-55857678b4-mvwn2   0/1     Pending             0          0s
hello-kubernetes-55857678b4-mvwn2   0/1     ContainerCreating   0          0s
hello-kubernetes-55857678b4-mvwn2   0/1     Running             0          13s
hello-kubernetes-55857678b4-mvwn2   1/1     Running             0          22s
hello-kubernetes-7bf6fbdb57-2sf7t   1/1     Terminating         0          4m

hello-kubernetes-55857678b4-9vvkv   0/1     Pending             0          0s
hello-kubernetes-55857678b4-9vvkv   0/1     ContainerCreating   0          0s
hello-kubernetes-55857678b4-9vvkv   0/1     Running             0          11s
hello-kubernetes-55857678b4-9vvkv   1/1     Running             0          17s
hello-kubernetes-7bf6fbdb57-b8886   1/1     Terminating         0          2h

hello-kubernetes-55857678b4-nz2m9   0/1     Pending             0          0s
hello-kubernetes-55857678b4-nz2m9   0/1     ContainerCreating   0          0s
hello-kubernetes-55857678b4-nz2m9   0/1     Running             0          11s
hello-kubernetes-55857678b4-nz2m9   1/1     Running             0          17s
hello-kubernetes-7bf6fbdb57-wj84v   1/1     Terminating         0          2h
...

Видна явная цикличность, когда один старый под подменяется новым, с ожиданием готовности нового пода.

Подробно алгоритм работы деплоймента выглядит следующим образом:

  1. создать новый репликасет с новым шаблоном подов
  2. “перелить” поды из старого репликасета в новый:
    • инкрементнуть DESIRED у нового репликасета
    • дождаться, пока под нового репликасета станет READY
    • декрементнуть DESIRED у старого репликасета
    • дождаться удаления пода старого репликасета
  3. делать пункт 2 пока старый репликасет не опустеет, а новый – не станет равен по размеру DESIRED деплоймента

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

kubectl describe deploy hello-kubernetes

...

Message
-------
Scaled up   replica set hello-kubernetes-7bf6fbdb57 to 4
Scaled up   replica set hello-kubernetes-55857678b4 to 1
Scaled down replica set hello-kubernetes-7bf6fbdb57 to 3
Scaled up   replica set hello-kubernetes-55857678b4 to 2
Scaled down replica set hello-kubernetes-7bf6fbdb57 to 2
Scaled up   replica set hello-kubernetes-55857678b4 to 3
Scaled down replica set hello-kubernetes-7bf6fbdb57 to 1
Scaled up   replica set hello-kubernetes-55857678b4 to 4
Scaled down replica set hello-kubernetes-7bf6fbdb57 to 0

Мы отчетливо видим, как поды “перетекают” из старого репликасета в новый.

Лошадиные силы

Момент, который стоит подметить: 4+1, 3+2, 2+3, 1+4, 0+4 – ни в один момент времени сумма реплик не опускалась ниже четырех желаемых в деплойменте. Это означает, что во время релиза у нас не будет просадки в “лошадиных силах”, обслуживающих пользователей.

П – производительность

Оперативность релизов

Может показаться, что релиз происходит поштучно, по одному поду за раз. На самом деле это не совсем так. Если мы еще раз посмотрим выхлоп команды выше:

kubectl describe deploy hello-kubernetes
...
RollingUpdateStrategy:  25% max unavailable, 25% max surge
...

то увидим, две интересных цифры:

  • maxSurge указывает количество подов на которое будет инкрементиться/декрементиться количество подов в репликасетах
  • maxUnavailable указывает количество новых подов, запуск которых должен сфейлиться, чтобы случился автоматический откат релиза

В нашем случае так уж совпало, что дефолтные 25% от 4 реплик - это ровно 1, и релиз выглядел поштучным… но в случае какого-нибудь особо сильно отмасштабированного сервиса с 50-100 репликами поштучный релиз мог бы затянуться на несколько часов.

О – оперативность

Что дальше?

В этой статье мы бегло познакомились с тем, что из себя представляет интерфейс пользователя кубера: почитали YAML-определения ресурсов и повбивали команды kubectl. Мы сделали пару релизов и понаблюдали за их процессом. Готовы ли мы ко всему? Закончился ли на этом курс молодого бойца?

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

Почему нам нужны шаблонизаторы?

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

Первый момент очевиден всем, кто хоть раз пользовался шаблонизаторами: они позволяют генерировать текст по, эм… шаблону. Т.к. YAML-файл – это по своей сути текстовый файл со вполне понятной и поддающейся шаблонизации структурой, то мы можем воспользоваться силой шаблонизатора, чтобы вынести в переменные такие вещи, как например:

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

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

Простой умозрительный пример вскроет блеск и нищету многих декларативных систем. Допустим у нас есть ямлик с двумя ресурсами: foo.v1 и bar.v1. Версия в имени означает ревизию этого ресурса. Если мы применим этот ямлик к нашему кластеру, то получим два ресурса на выходе. Если мы как-то поменяем определение и получим bar.v2, то после применения состояние кластера будет [foo.v1, bar.v2], все буднично и ожидаемо. Мы даже можем добавить новый ресурс и получить состояние [foo.v1, bar.v2, baz.v1] – система отлично работает в сторону добавления.

Но как только мы удалим из ямлика упоминание foo.v1 – он уходит из-под нашего контроля. Мы можем сколько угодно видоизменять bar и baz, и эти изменения будут отражены в кластере, но полное состояние будет выглядеть как [foo.v1, bar.vX, baz.vY]. Мы никак не можем передать с помощью манифеста просьбу удалить какой-либо ресурс.

Эта проблема проистекает из того, что в кластер может быть задеплоено множество различных ямликов: разные пользователи деполят свои проекты независимо друг от друга. Ни один из этих ямликов не претендует на то, что он описывает всё состояние кластера, а значит отсутствие в нем каких-либо ресурсов не является поводом вычистить кластер от “лишних” ресурсов.

Именно поэтому нужна некоторая надстройка, которая введет концепцию релиза, целиком описываемого одним манифестом. В таком случае можно быть уверенным, что если из манифеста что-то пропало, то это запрос на удаление данного ресурса. В следующей статье мы познакомимся с одним из таких шаблонизаторов – де-факто стандартом в мире кубера: Helm.