Использование универсальных Helm чартов в проектах
Если у вас 1–2 проекта, а в каждом до 5 приложений, и вы раз в полгода создаете новый чарт в Helm, вы можете просто копировать старые чарты или создавать новые с нуля. Но что делать, если у вас 5 проектов, в каждом по 20 микросервисов, и каждые 2 недели ваши разработчики выкатывают новые и выкидывают старые? А еще надо постоянно что-то обновлять. А если проектов 20 или 50, что делать в этом случае? Тогда вы копируете одни и те же паттерны и шаблоны с общим костяком. Мы заметили это на одном из проектов, в котором нужно было запустить 15 микросервисов, и подумали, а почему бы не запилить единый чарт, через который будет запускаться вообще всё?
Меня зовут Роман Андреев, я DevOps инженер Nixys. Расскажу о практическом применении универсального чарта. На теории останавливаться почти не будем, только в общих чертах, потому что о ней мы уже говорили в предыдущих статьях. Если нужна более подробная информация, ее можно взять тут.
Когда мы запустили проект с 15 микросервисами и решили запилить единый чарт — нам понравилось! Мы экономили время на подготовку к релизу, а также накапливали и переиспользовали в новых проектах понравившиеся решения, которые оказались действительно успешными. Так получалось делать релизы практически любой сложности и конфигурации. Быстро подготавливать микросервисы к развёртыванию в k8s. С совместимостью версий Kubernetes от 1.13 до 1.22. Хранить чарт отдельно от кода и размещать в публичном registry Nixys, чтобы все потребители сразу получали все новые фичи.
Для того чтобы легко добавлять к существующему набору шаблонов новые, мы сделали возможным работу с их конструкциями прямо внутри values файла.
В примере через extraDeploy опцию добавляется шаблонный NetworkPolicy, но при этом нет никаких ограничений и можно добавить любые манифесты. Это могут быть кастомные ресурсы, нужные вашему приложению, и вообще все, что угодно.
К сожалению, тут не обошлось без ложки дегтя. Из-за гибкости у шаблонов и Values файлов достаточно сложная структура. Поэтому без высокого уровня понимания и навыков создания сложных Helm-чартов будет сложно. А из-за того, что в values используются конструкции шаблонов, есть проблемы с использованием Go templates. Это ограничение на длину строки из конструкции шаблона и использование символов отличных от букв и цифр. Из-за этого иногда вылазят непонятного вида ошибки, которые решаются только опытным путем.
Но несмотря на недостатки плюсов все-таки больше. Чтобы оценить их, давайте используем универсальный чарт на практике.
Использование универсального чарта
Открываем GitHub Nixys universal Helm chart. Это наш репозиторий, здесь лежит проект и документация. Мы будем разворачивать проект на Python — это Django Celery связка. Django в качестве web, а Celery в качестве асинхронного воркера, который будет обрабатывать задачи. Проект максимально простой, но при этом отлично показывает, как работает асинхронное приложение. В окружении уже развернут Redis и Postgres. Предполагаю, что у вас тоже развернуты базы и с этим нет проблем. Но на всякий случай они также развернуты с помощью Helm, эти values файлы тоже лежат в репозитории (не в этом, а во втором, в каталоге k8s).
Посмотрим, сколько времени займет развернуть такой не сильно крупный проект, используя универсальный чарт.
Для того чтобы протестировать, как работает приложение, я уже накидал манифесты Kubernetes и деплоил вручную.
Создание values файла
Начнем с создания пустого values файла и его заполнения. Если будут нужны подсказки по структуре, можно заглядывать в документацию и манифесты, чтобы не тратить лишнее время. Сначала укажем дефолтный image и дефолтный pull policy. Образ я собрал и заранее положил в свой Docker Hub registry. Будем использовать lightest образ, поэтому defaultImagePullPolicy поменяем на Always.
Подготовка переменных окружения
У нашего чарта есть особенность — мы используем 2 встроенных сущности:
Из envs генерируется ConfigMap в качестве префикса название релиза приложения, которое мы запускаем. Постфикс envs.
В secretEnvs создается secret. В качестве префикса название релиза. Постфикс secretEnvs. Как правило, почти все проекты имеют переменные окружения.
Для CELERY_BROKER из уже проверенного манифеста забираем Redis. POSTGRES_USER у нас дефолтный, а пароль сгенерированный. База будет называться Demo, порт используем стандартный.
Одна из фич, которую мы реализовали в работе с секретами, в чарте можно написать секрет в открытом виде или сразу в закодированном в Base64, добавив префикс. Это удобно, очень рекомендую.
Так как приложение на Django, а Django умеет обрабатывать логику, но не умеет отдавать статику, будем отдавать ее с помощью nginx. Он же будет проксировать запросы на Django. Для этого создадим ConfigMap, в который положим конфиг nginx. А в него файл со строкой содержащей конфиг (сервер на 80 порту слушает все, отдает статику из каталога, в котором она лежит и проксирует все запросы на Django сервер). Его будем монтировать внутрь контейнера с nginx.
Настройка самого приложения
Django использует миграции, которые мы будем катать с помощью хука. Для этого потребуется наше приложение и дефолтный контейнер. У него минимум настроек и параметров: название контейнера и команды с помощью которых выполняется миграция. Добавим переменную окружения для подключения к БД. Вообще, чтобы сразу подключить все переменные окружения в контейнер воспользуемся envConfigMap и envSecret.
В этот параметр передается список сущностей, создаваемых чартом и содержащих переменные окружения. По дефолту всегда создается ConfigMap-envs и секрет secret-envs, у которых префикс — это название релиза.
Если отдельным блоком вы добавите секреты или ConfigMaps, в которых также будут содержаться переменные окружения, их тоже можно подключить с помощью этих опций.
На этом настройка хука для миграции закончена, больше здесь ничего не нужно.
Настройка деплойментов
Деплойментов всего три:
Web-приложение;
Celery воркер;
Небольшой дашборд для Celery Flower-dash, чтобы видеть, какие воркеры работают, сколько они задач выполнили, в каком статусе находятся.
Настроим каждый из них.
Настройка Web-приложения
Так как мы работаем с несколькими деплойментами в рамках одного приложения, нам потребуется дополнительный селектор лейблов для того, чтобы корректно работала маршрутизация. Укажем каждому из приложений, к какому компоненту они относятся, и настроим их контейнеры. Запустим Django. В данном образе Django стартует по умолчанию, поэтому дополнительно команду прописывать не нужно, но нам потребуются переменные окружения, которые мы также подключим с помощью опций envConfigMap и envSecret.
Вторым контейнером нам потребуется nginx, который будет проксировать запросы и отдавать статику. Для того, чтобы запустить nginx, нужно указать образ, из которого он будет стартовать, и тэг образа. Мы взяли Alpine, потому что он маленький, быстрый и легкий.
Для nginx нам не потребуются переменные окружения, но нужны конфиг и статика. Поэтому здесь подключаем volumeMounts с конфигом. Мы будем подключать в дефолтный конфиг, который лежит по дефолтному пути. Нам нужно заменить ровно один файлик. Также будем отдавать статику, которая лежит по пути, указанном в конфиге nginx.
Еще надо объявить volumes, которые будем использовать. В данном чарте есть стандартные, но через блок volumes мы добавляем специальную сущность. Это типизированный volume, который позволяет подключить конкретную сущность (ConfigMap, секрет или PersistentVolumeClaim) к вашему контейнеру. Но если вам требуется подключить нативную дефолтную сущность Kubernetes volume, то для этого есть опция extraVolumes, через которую мы как раз создадим статику. Использовать будем emptyDir.
Таким образом при запуске будет распаковываться, а при завершении удаляться статика. Достаточно удобно и просто.
Volume с конфигом nginx подключим из ConfigMap. Из-за того, что мы используем subpath, а путь у нас немного отличается, надо указать items. Внутри ConfigMap у нас есть файл nginx.conf, но мы хотим, чтобы он был доступен как default.conf.
Практически все. Осталось добавить init контейнер, в котором будет распаковываться статика. Здесь выполняется команда Python, которую мы тоже возьмем из подготовленного yml.
Теперь для запуска основного web-приложения все готово.
Настройка Celery-воркера
Укажем, что мы хотим на старте сразу получить пару реплик. Контейнер будет похож на тот, что мы запускали ранее, только команда немного отличается. Назовем контейнер воркером, добавим команду и переменные окружения, которые также используются в приложении.
В вашем случае может понадобиться работа с volumes. Тогда логичнее поместить статику в persistent volume и добавить туда какие-то пользовательские файлы. Но в этом примере воркеру для работы больше ничего не нужно.
Настройка dash
Dash также будет состоять из одного контейнера, в котором мы запустим Flower. Для работы ему нужна всего одна переменная окружения broker. Подключим ее с помощью опции envFrom и ConfigMap или Secret. Укажем имя envs и перечислим списком, какие переменные окружения хотим здесь видеть. В данном случае это broker, но в нашем ConfigMap она называется по-другому. Укажем по какому имени искать нашу переменную.
Деплоймент тоже настроили. Теперь нужно добавить Service и Ingress для получения доступа к web-приложению снаружи, из интернета.
Service и Ingress
Для этого мы сходим в GitHub, откроем дефолтный values файл, чтобы посмотреть, как добавить интересующие нас сущности, в частности сервисы:
Web, который будет работать на 80 порту, а обращаться в tier: Web;
Dash, который будет обращаться к дашборду Flower.
Я укажу порт 5555, а extraSelectorLabels для этого workload у нас dash.
Осталось только настроить Ingress. Возьмем подготовленные данные. В качестве ключа в названии используем доменное имя. Понадобится только одна аннотация. В кластере в котором мы работаем развернут cert-manager. Там есть cluster-issuer.
Настроим сервисы для Ingress. Дефолтный корневой путь будет смотреть на сервис — web, а servicePort на — http. Добавим дашборд, который доступен по пути /flower. Он будет смотреть на serviceName: dash, а servicePort у него 5555.
Обратите внимание на один важный момент! У нас свежий Kubernetes и Ingress-контроллер, поэтому есть Ingress класс. Чтобы на этапе доставки манифестов в Kubernetes, не поймать ошибку надо указать IngressClassName.
С настройками всё! Остается проверить.
Тестирование и отладка
Мы создали небольшой values файл, который описывает деплой нашего приложения. Для проверки корректной работы, укажем файл из которого будем подключать values и посмотрим съест он сгенерированные Kubernetes манифесты или нет. В первый раз мы получили ошибку и внесли правки:
Чтобы убедиться, что все на 100% работает, заменим команду на install и запустим. У нас не стартует миграция, потому что потерялись секреты. Из-за того, что я набирал вручную, вместо secret-envs — ввёл secrets-env. После исправления всё заработало и начали создаваться контейнеры с приложением.
Теперь можно пойти в web, ввести название домена и увидеть, что у нас не заработала статика. Всё из-за того, что когда запускали init-контейнер, не подключили volume, в который распаковывается статика. Поправим это.
Теперь посмотрим в Flower: есть дашборд — мы видим воркеры, не работает только UI. Проблема в еще одной ошибке ручного ввода. Мы не не видим init контейнер, потому что его нет. Блок называется initContainer, а чарт проглатывает initContainers. Дело раскрыто!
Отлично, у нас появился UI!
Вместо заключения можно сказать, что использование универсальных Helm чартов в проектах, конечно, экономит время. Главное, не забывать читать документацию. Тогда не придется тратить полчаса на дебаг, а хватит всего полчаса на подготовку запуска небольшого приложения.
Приглашаем вас на Saint Highload++ 2022 — конференцию разработчиков высоконагруженных систем, которая пройдет 22–23 сентября в Санкт-Петербурге. Расписание опубликовано. Билеты можно бронировать здесь.