Хабрахабр

Как расширять Kubernetes

Сегодня разговор пойдет о DevOps, а точнее — в основном об Ops. Говорят, что существует очень мало людей, которые довольны уровнем автоматизации своего operations. Но, кажется, ситуация исправима. В этой статье Николай Рыжиков расскажет о своем опыте расширения Kubernetes.

Под катом — видео и текстовая расшифровка доклада.
Материал подготовлен на основе выступления Николая на осенней конференции DevOops 2017.

Член питерского сообщества функциональных программистов FPROG. В данный момент Николай Рыжиков работает в Health-IT секторе над созданием медицинских информационных систем. Занимается программированием 15 лет.
Активный участник Online Clojure community, member стандарта по обмену медицинской информацией HL7 FHIR.

Каким боком мы относимся к DevOps? Наша формула DevOps уже на протяжении 10 лет достаточно проста: разработчики отвечают за operations, разработчики деплоят, разработчики мейнтейнят. При такой расстановке, которая выглядит немного жестковато, вы в любом случае станете DevOps. Если хотите быстро и болезненно внедрить DevOps — заставьте разработчиков отвечать за ваш продакшн. Если ребята смышленые, то начнут выкручиваться и всё поймут.

Наша история: давно, когда еще не было никаких Chef и автоматизации, мы уже деплоили автоматический Capistrano. Потом начали его растачивать, чтобы он провижинил моды. Но потом появился Chef. Мы перешли на него и уехали в облако: нам надоели свои дата-центры. Потом появился Ansible, возник Docker. После этого мы переехали на Terraform с собственноручно-написанным супервизором докеров Condo на Camel. И вот теперь мы переезжаем на Kubernetes.

Что самое страшное в operations? Очень мало кто удовлетворен уровнем автоматизации своего operations. Это страшно, я подтверждаю: мы затратили очень много ресурсов и усилий, чтобы все эти стеки собирать для себя, а результат неудовлетворительный.

Я приверженец бережливого производства и, с его точки зрения, operations вообще не приносит пользы. Есть ощущение, что с приходом Kubernetes что-то может измениться. Ценность создается тогда, когда разработчик делает продукт. Идеальный operations — это отсутствие или минимум operations в проекте. А ведь нужно сокращать издержки.

Для меня идеалом всегда являлся heroku. Когда он уже готов, доставка не добавляет ценность. Это занимает минуту.

Как быть? Мы его использовали для простых приложений, где разработчику, чтобы развернуть свой сервис, достаточно было сказать git push и настроить heroku. И советую купить, иначе есть шанс потратить больше денег на разработку нормального operations. Можно купить NoOps — тоже heroku.

Есть cloud foundry, который тоже дает платформу, на которой можно работать.

Но если заморачиваться на что-то более сложное или крупное, можно сделать самому. Есть ребята Deis, они пытаются сделать что-то типа heroku на Kubernetes. Когда-то это было слишком жестко.

Теперь вместе с Docker и Kubernetes это становится задачей, выполнимой за разумные сроки с разумными затратами.

Немного о Docker и Kubernetes

Одна из проблем operations — это повторяемость. Прекрасная вещь, которую привнес докер — две фазы. У нас есть фаза билда.

Кто-то собрал Docker, что-то внутрь положил, а operations достаточно сказать Docker run и запустить. Второй момент, который радует в докере — универсальный интерфейс для запуска произвольных сервисов.

Вот мы сделали Docker и нам нужно его где-то запускать, объединять, конфигурировать и связывать с другими. Что такое Kubernetes? Он вводит ряд абстракций, которые называются «ресурс». Kubernetes позволяет это делать. Мы быстро по ним пройдемся и даже попытаемся создать.

Абстракции

Первая абстракция — POD или набор контейнеров. Грамотно сделано, что именно набор контейнеров, а не один. Наборы могут шарить между собой volumes, которые видят друг друга через localhost. Это позволяет использовать такой паттерн, как sidecar (это когда мы запускаем основной контейнер, а рядом есть вспомогательные контейнеры, которые ему помогают).

Это когда вы не хотите, чтобы контейнер думал, где находятся какие-то сервисы. Например, подход ambassador. И они становятся доступны главному контейнеру по localhost. Вы ставите рядом контейнер, который знает, где эти сервисы лежат. Локально можно разрабатывать minikube. Таким образом environment начинает выглядеть так, как будто вы работаете локально.

Давайте поднимем POD и посмотрим, как он описывается. Он сжирает кучу CPU, но позволяет на virtualbox поднять маленький Kubernetes-кластер и с ним работать.

Я сказал Kubernetes apply и залил POD. Давайте задеплоим POD. Это означает, что Kubernetes у себя запустил эти контейнеры. Могу посмотреть какие у меня есть POD-ы: вижу, что задеплоен один POD.

Я могу даже зайти в этот контейнер.

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

Он запускается как Docker run: если его кто-то остановит, никто его не переподнимет. Но POD — он смертный. Это такой супервизор, который следит за POD-ом, следит за их количеством, и если POD-ы падают, он их переподнимает. Поверх этой абстракции Kubernetes начинает строить следующую — например, replicaset. Это важная концепция самоизлечения в Kubernetes, которая позволяет спокойно спать по ночам.

Например, работает один replicaset. Поверх replicaset есть абстракция deployment — тоже ресурс, который позволяет сделать zero time deployment. Мы дожидаемся, когда эти контейнеры стартанут, пройдут свои health-чеки, и потом мы быстро переключаемся на новый replicaset. Когда мы делаем deploy и меняем версию контейнера, например, нашего, внутри деплоймента, то поднимается другой replicaset. Например, у нас есть deployment. Тоже классическая и хорошая практика.

Давайте поднимем простенький сервис. Можем применить этот deployment, посмотреть, что у нас есть. Внутри он описывает шаблон POD-ов, которые он будет поднимать. Крутая фича Kubernetes — все лежит в базе данных, и мы можем смотреть, что происходит в системе.

Если мы попробуем посмотреть на POD-ы, то мы видим, что поднялся какой-то POD. Вот мы видим один deployment. Что происходит с POD-ами? Можем взять и удалить этот POD. Это replicaset-контроллер не нашел нужный POD и запустил другой. Один уничтожается, а второй сразу поднимается.

Далее, если это какой-то веб-сервис, или внутри наши сервисы должны связываться, нужен service discovery. Надо дать сервису имя и точку входа. Для этого Kubernetes предлагает ресурс, который называется service. Он может заниматься load balancing и отвечать за service discovery.

Давайте посмотрим простенький service. Мы связываем его с деплойментом и POD-ами через лейблы: такое динамическое связывание. Очень важная концепция в Kubernetes: система динамическая. Неважно, в каком порядке все это будет создано. Service будет пытаться найти POD-ы с такими лейблами и начать их load balance.

Апплаим service, смотрим, какие у нас есть сервисы. Заходим в наш тестовый POD, который был поднят, и делаем nslookup. Kubernetes нам дает DNS-ку, через которую сервисы могут друг друга видеть и обнаруживать.

Service — это скорее некий интерфейс. Там есть несколько разных реализаций, потому что задачи load balancing и service достаточно сложные: одним способом работаем с обычными базами данных, другим — с нагруженными, а какие-то простые делаем совсем по-простому. Это тоже важная концепция в Kubernetes: некоторые вещи скорее можно назвать интерфейсами, а не реализациями. Они закреплены не жестко, и разные, например, облачные провайдеры дают разные реализации. Т.е., например, есть ресурс persistent volume, который уже в каждом конкретном облаке реализуется своими штатными средствами.

В Kubernetes есть абстракция ingress. Дальше мы обычно веб-сервис хотим вывести куда-нибудь наружу. Там мы пишем правила: по каким url-ам, по каким хостам, к какому внутреннему сервису перенаправлять запрос. Обычно там же добавляют SSL.

Самый простой ingress выглядит как-то так. Точно так же можем поднять и наш ingress.

После чего, прописав локально в хостах, можно этот сервис увидеть отсюда.

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

Зачистим все это, удалим ingress и посмотрим на все ресурсы.

Есть еще ряд ресурсов, например configmap и secret. Это чисто информационные ресурсы, которые вы можете замаунтить в контейнер и передать туда, например, пароль от postgres. Можете это связать с environment variables, которые будут инжектиться в контейнере во время запуска. Можете замаунтить файловую систему. Все достаточно удобно: стандартные задачи, симпатичные решения.

Он разбит на две части: есть persistent volume claim (запрос), и потом создается какая-нибудь EBS-ка, которая тащится к контейнеру. Есть persistent volume — интерфейс, который реализуется по разному у разных клауд-провайдеров. Можно работать со stateful-сервисом.

Сама концепция очень проста и прозрачна. Но как это работает внутри? Одна — это просто база данных, в которой у нас лежат все эти ресурсы. Kubernetes состоит из двух частей. Поверх Kubernetes настроен API-server. Ресурсы можете представлять как таблички: конкретно эти инстансы — просто записи в табличках. То есть когда у вас есть Kubernetes cluster, вы обычно общаетесь с API-сервером (точнее, с ним общается клиент).

Эта база данных реализована ETCD, т.е. Соответственно, то, что мы создавали (POD-ы, сервисы и т.д.), просто пишется в базу данных. так, чтобы она была устойчива на уровне high-available.

Дальше под каждый тип ресурсов есть некий контроллер. Что делается дальше? Например, делает Docker run. Это просто сервис, который следит за своим типом ресурса и что-то делает во внешнем мире. И все, что он делает, — это Docker run после очередной периодической проверки, если этого POD-а нет. Если у нас есть POD, на каждый Node есть kubelet-сервис, который следит за POD-ами, которые зашедулены на эту ноду.

Зачастую контроллер еще снимает метрики и смотрит за тем, что он запустил. Дальше, что очень важно — все происходит в реальном времени, поэтому мощность этого контроллера выше минимальной. снимает фидбек из реального мира и его же записывает в базу, чтобы вы или другие контроллеры это видели. Т.е. Например, тот же статус POD-а будет записан обратно в ETCD.

Очень прикольно, что отделена информационная модель от операционной. Таким образом, в Kubernetes реализовано все. Потом набор контроллеров пытается сделать так, чтобы все было правильно. В базе данных через обычный CRUD-интерфейс мы декларируем то, что должно быть. Правда, так получается не всегда.

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

Мы не мыслим POD-ами и Ingress-ами, и нам хочется построить некий следующий уровень абстракции.

Так, чтобы разработчику было максимально легко: чтобы он просто говорил «Хочу запустить такой-то сервис», а все остальное произошло внутри. В реальных системах мы мыслим абстракциями следующего уровня: у нас есть некие сервисы, базы данных, и мы все это связываем.

Это неправильный путь — темплейтинг в стиле ansible, где мы просто пытаемся породить набор сконфигурированных ресурсов и закинуть их в кластер Kubernetes. Есть такая вещь как HELM.

То есть много логики он не может реализовывать. Проблема, во-первых, в том, что это выполняется только в момент накатывания. Когда я иду смотреть на мой кластер, я вижу просто POD-ы и сервисы. Во-вторых, в рантайме эта абстракция исчезает. Я просто вижу там десятки подов. Я не вижу, что задеплоен такой-то сервис, что там поднята такая-то база с репликацией. Абстракция исчезает как в матрице.

Внутренняя модель решения

С другой стороны, сам Kubernetes уже внутри дает очень интересную и простую модель расширения. Мы можем объявлять новые типы ресурсов, например deployment. Это ресурс, построенный поверх POD-ов или replicaset. Мы можем написать контроллер к этому ресурсу, положить этот ресурс в базу и запустить нашу кибернетическую петлю так, чтобы все работало. Это звучит интересно, и, мне кажется, это правильный путь расширять Kubernetes.

Хотелось бы иметь возможность просто писать некий manifest для своего сервиса в стиле heroku. Очень простой пример: я хочу задеплоить какое-то приложение в своем реальном окружении. Уже есть соглашения, SSL, куплены домены. Я просто хотел бы дать разработчикам максимально простой интерфейс. Manifest мне говорит, какой контейнер поднять, какие ресурсы еще нужны этому контейнеру. Он закидывает в кластер это объявление, и все начинает работать.

Вот у нас будет лежать в базе resource application. Как это будет выглядеть с точки зрения кастомных ресурсов и контроллеров? То есть он будет прописывать в ingress правила о том, как роутить к этому сервису, запустит service для load balancing и запустит deployment с какой-то конфигурацией. И application controller будет порождать три ресурса.

Для этого у есть мета-ресурс, который называется CustomResourceDefinition. Прежде чем мы создадим кастомный ресурс в Kubernetes, нам нужно его объявить.

Считайте это create table. Для того, чтобы объявить новый ресурс в Kubernetes, нам достаточно зааплаить вот такое объявление.

Создали таблицу. После этого мы можем посмотреть через kubectl get на те third-party ресурсы, которые у нас есть. Как только мы его объявили, у нас еще и апишка появилась. Мы можем делать, например, kubeclt get apps. Но пока никаких аппов нет.

Давайте какой-нибудь апп запишем. После этого мы можем сделать кастомный resource instance. Посмотрим на него в YAML и создадим его постом на определенный URL.

Если мы побежим и через kubectl посмотрим, то появился один апп. Но пока ничего не происходит, он просто лежит в базе данных. Можно, например, взять и запросить все апп ресурсы.

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

Дальше наш контроллер должен делать темплейтинг, похожий на то, что делает HELM. То есть получив описание нашего аппа, я должен сгенерить resource deployment и resource service, а также сделать запись в ingress. Это самая простая часть: здесь в clojure — это erlmacro. Я передаю структуру данных, она дергает функцию deployment, передает в debug, который pipeline. И это — чистая функция: простой темплейтинг. Соответственно, в самом наивном виде я мог бы тут же это создать, превратить в консольную утилиту и начать распространять.

То же самое делаем для сервиса: функция service принимает декларацию и генерит нам ресурс Kubernetes.

То же самое мы делаем и для строчки в ingress.

Как это все будет работать? Будет что-то в реальном мире и будет то, что мы хотим. То что мы хотим — мы берем application resource и генерим по нему то, что должно быть. И теперь нам нужно посмотреть, что есть. Что есть мы запрашиваем через REST API. Мы можем получить все сервисы, все деплойменты.

Он будет получать то, что мы хотим и то что есть, брать от этого div и применять к Kubernetes. Как будет работать наш кастомный контроллер? Придумал виртуальный DOM, когда какие-то функции просто генерят дерево JS-объектов. Это похоже на React. А потом некий алгоритм вычисляет патч и применяет к реальному DOM-у.

Это делается в 50 строк кода. То же самое мы сделаем и здесь. В итоге мы должны получить функцию reconcile-actions. Захотите — все есть на Гитхабе.

Она берет то, что есть, плюс то, что нужно. У нас есть функция reconcile-actions, которая ничего не делает и просто вычисляет этот div. И потом выдает то, что нужно сделать, чтобы привести первое ко второму.

Давайте ее дернем. Ничего страшного в ней нет, ее можно дебажить. Она говорит, что нужно создать ingress-сервис, сделать в нем две записи, создать деплоймент 1 и 2, создать сервис 1 и 2.

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

Для этого мы просто передадим reconcile-actions в функцию reconcile, и все применится. Дальше все, что остается, это написать функцию, которая применит этот патч к Kubernetes-кластеру. И вот мы видим, что поднялся POD, сделался deployment и запустился сервис.

Давайте добавим еще один сервис: еще раз выполним функцию reconcile-actions. Посмотрим, что произошло. Все запустилось, все хорошо.

Как с этим быть? Мы все это упаковываем в Docker-container. После этого пишем функцию, которая периодически просыпается, делает reconcile и засыпает. Скорость не очень важна, она может спать по пять секунд и делать reconcile-actions не так часто.

Наш кастомный контроллер — это просто сервис, который будет просыпаться и периодически вычислять патч.

Посмотрим, как отреагировал наш кластер: все ок. Сейчас у нас два сервиса задеплоино, давайте удалим одно из приложений. Удаляем второй: все очистилось.

Давайте посмотрим глазами разработчика. Ему нужно просто сказать Kubernetes apply и задать имя нового сервиса. Делаем это, наш контроллер все подцепил и создал.

Дальше мы все это собираем в deployment сервис, и штатными средствами Kubernetes этот кастомный контроллер закидываем в кластер. Мы создали абстракцию за 200 строчек кода.

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

Собственный CI

Рассмотрим примеры расширения Kubernetes. Мы решили, что CI должен быть частью инфраструктуры. Это хорошо, это удобно с точки зрения безопасности — приватный репозиторий. Мы пытались использовать jenkins, но он — устаревшее средство. Захотелось хакерского CI. Интерфейсы нам не нужны, мы любим ChatOps: пусть просто скажет в чат, упал билд или нет. Кроме того, хотелось все локально отлаживать.

Мы сели и за неделю написали свой CI. Просто как расширение к Kubernetes. Если задуматься над CI, то это просто инструмент, который запускает какие-то jobs. В рамках этого job мы что-то билдим, прогоняем тесты, зачастую деплоим.

Оно построено на такой же концепции кастомных контроллеров. Как это все работает? Контроллер просто идет на гитхаб и добавляет web-hook. Во-первых, мы закидываем в Kubernetes описание того, за какими репозиториями мы следим. У нас остается интроспекция.

За ресурсом build у нас следит build controller, который считывает manifest, лежащий внутри проекта, и запускает POD. Дальше приходит web-hook, единственная задача которого — обработать входящий JSON и скинуть его в кастомный ресурс build, который тоже складывается в базу Kubernetes. В этом POD-е запускаются все необходимые сервисы.

Он их начинает выполнять. В POD-е очень простенький agent, который читает декларацию в стиле travis или circleci, а в YAML набор шагов. После чего в конце билда он закидывает свой результат в Telegram.

Вы делаете kubectl exec, оказываетесь внутри вашего билда и можете дебажить. Другая фича, которую мы получили вместе с Kubernetes — это то, что одной из команд в выполнении вашего CI или continuous delivery можно поставить просто while true sleep 10, и ваш POD зависнет на этом шаге.

На это все ушло две недели и 300 строчек кода. Другая фича — все построено на докерах и отлаживать скрипт можно локально, запуская докер.

Работа с postgres

Наш продукт построен на postgres, мы используем всякие его интересные фичи. Мы даже написали ряд extensions. Но мы не можем использовать RDS или еще что-то.

Озвучу архитектуру. Сейчас мы в процессе разработки оператора для неубиваемого postgres. Добавить к этому, что мне нужно две асинхронные реплики, одну синхронную, бэкапы ежедневно и до терабайта. Я хочу сказать «Кластер, дай мне postgres, который нельзя убить». Он создает pginstance-ресурсы, которые отвечают за каждый istance postgres. Я это все закидываю, дальше у меня cluster controller начинает заниматься оркестрацией и разворачиванием моего контейнера. Это будет cluster postgres.

Сердцем является persistent volume. Дальше pginstance controller, достаточно простой, просто пытается запустить там POD или deployment с этим postgres. Вы даете ей Docker-container, в котором есть только binary postgres. Вся эта машина берет полный контроль над postgres. Он делает это так, чтобы мы позже могли реконфигурировать, и так, чтобы он мог настроить репликацию, уровни логов и т.д. Все остальное: конфигурацию и создание стартового кластера postgres делает сам контроллер. В начале временный POD проезжается поверх persistent volume и создает там postgres cluster для master.

Потом таким же образом создается persistent volume. Далее поверх этого запускается deployment с master. Другой POD проезжается, делает базовый бекап, стягивает его, и поверх этого запускается deployment со slave.

А backup controller уже берет его и закидывает в какой-нибудь S3. Далее cluster controller создает backup ресурс (после того, как ему описали бекапы).

Что дальше?

Давайте представим с вами ближайшее будущее. Может так получиться, что рано или поздно у нас будут такие интересные кастомные ресурсы, кастомные контроллеры, что я скажу «Дай мне postgres, дай мне kafka, оставь мне CI и все это запусти». Все будет просто.

Там у нас operations семантика полностью отделена от information-семантики. Если говорить не о ближайшем будущем, то я, как декларативный программист, считаю, что выше функционального программирования только логическое или реляционное. А мы из него выводим еще три дополнительных ресурса. Если мы посмотрим внимательно на наши кастомные контроллеры, которые мы делали, то у нас в базе лежит, например, resource application. Это выведение фактов. Это очень похоже на вьюху в базе данных. Это logical или relation вьюха.

Поскольку у нас рано или поздно все стекается в базу, включая фидбек, правила могут звучать так: «Если нагрузка увеличилась вот так, то увеличь репликацию так». Следующий шаг для Kubernetes — вместо рубленого REST API дать некую иллюзию реляционной или логической базы, где можно просто написать правило. Нужен только generic движок, который за этим будет следить. У нас будет маленькое sql или логическое правило. Но это светлое будущее.

Все спикеры и программа — на сайте. Больше крутых докладов — на конференции DevOops 2018!

В дискуссионных зонах после докладов мы организуем небольшие автограф-сессии с авторами этих книг. Если у вас дома стоят на полке «The DevOps Handbook», «Learning Chef: A Guide to Configuration Management and Automation», «How to containerize your Go code» или новенькая «Liquid Software: How to Achieve Trusted Continuous Updates in the DevOps World» — захватите их с собой на конференцию.

Только вдумайтесь: уникальная возможность получить автограф от самого Джона Уиллиса!

А еще приятный бонус: до 1 октября билет на DevOops 2018 можно забронировать со скидкой.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть