В этой статье я расскажу и покажу, как при помощи Helm и некоторых дополнительных инструментов построить и настроить автоматическое развертывание в Kubernetes для системы из (микро)сервисов и не потеряться во множестве шаблонов и манифестов. Мы успешно реализовали такой подход у себя. Если у вас есть подобная задача или нечто похожее, надеюсь, статья окажется для вас полезной целиком или в качестве источника "рецептов". Меня зовут Михаил, я CTO в Exerica и Deputy CEO в Qoollo. В Exerica у нас построена система с сервисной архитектурой: больше десятка сервисов, NoSQL база данных и централизованные логирование с мониторингом.  Для работы с кодом и CI/CD мы используем собственный экземпляр GitLab. Каждое приложение имеет свой репозиторий, где происходит его сборка, в результате которой контейнер с приложением очередной версии помещается в registry. Версионирование автоматизировано: разработчик задает только старшую и младшую версии, а номер билда генерируется системой сборки. Для версий, выбираемых не из "основной" ветки также автоматически проставляется feature-версия из номера задачи в трекере. Обычно у нас бывает несколько развертываний на промышленную среду в день. Также практически у каждого разработчика есть возможность развернуть полнофункциональный экземпляр системы. Поэтому нам нужен простой механизм сборки системы из набора сервисов конкретных версий. И удобный автоматический механизм отката изменений, если что-то пошло не так. Ранее у нас была разработана автоматизация развертывания в кластер Docker Swarm с помощью Ansible. Она успешно проработала несколько лет, но стала приносить все больше проблем: Невысокая отказоустойчивость схемы с одним входным Nginx С ростом числа приложений появилось много однотипных плейбуков и шаблонных файлов для сервисов Docker Swarm, их стало сложно поддерживать Просадки производительности оверлейной сети Docker Swarm: периодически без видимых причин она падала до нескольких мегабит/с на гигабитных линках Недостаточная гибкость в управлении ресурсами для приложений Неудобное управление внешним DNS для публикуемых сервисов через Ansible-модуль Неудобное управление TLS-сертификатами  В итоге мы приняли решение перевести систему в self-managed Kubernetes. При переводе мы сразу поставили задачу максимально автоматизировать генерацию манифестов для приложений и ресурсов. В идеале для стандартных случаев включение сервиса в систему должно требовать от разработчика только задания его конфигурации, ограничения по ресурсам и размещению сервиса. БД, сервисы монторинга и сбора логов мы оставили как есть, вне Kubernetes. С переходом на Kubernetes мы решили принципиально не изменять процесс сборки и развертывания. Исторически для процесса развертывания у нас был выделен отдельный git-репозиторий (деплой-репозиторий), в котором сосредоточены все скрипты и конфиги деплоя. Деплой выполняется задачами Gitlab CI. При запуске CI-пайплайна ветки develop развертывается промышленная среда, при запуске пайплайна для ветки, связанной с задачей в трекере (начинается с уникального номера), развертывается тестовая среда. Для идентификации тестовых сред используется тот самый номер ветки. Минимально, что нужно сделать релиз-менеджеру или разработчику - это вписать нужные версии приложений и выполнить коммит. Весь остальной процесс выполняет пайплайн Gitlab CI. Его успешное завершение говорит о том, что развертывание выполнено и развернутая система работоспособна. Одним из удобных инструментов, который позволял реализовать практически все вышеприведенные требования — Helm, пакетный менеджер для Kubernetes. Как написано на сайте helm.sh: Helm помогает управлять приложениями Kubernetes — Helm Charts помогут вам определить, установить и обновить даже самое сложное приложение Kubernetes. Helm позволяет описать как отдельные приложения, так и системы в целом в виде набора так называемых "диаграмм" (charts) или пакетов. В официальной документации это понятие в основном не переводится, а часто их называют просто "чартами". Я буду пользоваться этим термином, чтобы избежать терминологической путаницы с "пакетами" и "диаграммами".  Очень кратко, чарт представляет собой набор файлов, содержащих шаблоны и параметры, из которых helm собирает манифесты для всех необходимых объектов Deployment, Service, Ingress, ConfigMap и т.п. Широкие возможности по шаблонизации позволяют определить достаточно универсальный шаблон и следовать принципу DRY. Helm позволяет управлять процессом развертывания и сделать его атомарным и обратимым. Развертывание запускается одной командой helm update. Если процесс развертывания завершился неудачно, helm может выполнить автоматический откат. При этом он возвращает все объекты системы в соответствующее предыдущее состояние. А если что-то пошло не так, откат можно выполнить одной командой helm rollback.  Подробнее про helm можно почитать в официальной документации или, например, здесь. Сначала сформулируем основные принципы, закладываемые в основу архитектуры развертывания: Развертывание системы описывается единым чартом (так называемый umbrella chart), чтобы иметь возможность разворачивать и откатывать его атомарно.  Чарт системы должен содержать минимальное число определяемых объектов, чтобы поддерживать слабую связность приложений. Развертывание каждого приложения описывается в своем чарте (так называемый subchart), это позволит нам независимо изменять приложения.  Каждое приложение имеет свое уникальное имя, с помощью которого можно легко найти связанные с ним параметры. Используя имя приложения другие приложения могут получить параметры, необходимые для связи с ним (например, имя его сервиса). Если для приложения определяется ingress, то только через поддомен, а не через путь. Это правило упрощает шаблонизацию определения для ingress, но от него можно отказаться при необходимости.  Чарт приложения в идеале не должен содержать других приложений в качестве зависимостей (исключение - для sidecar-контейнеров). Следуем DRY: если какая-то часть манифеста или шаблона является универсальной, она должна быть вынесена на более высокий уровень абстракции. Цель понятна — иметь одну точку изменений.  Минимизируем ссылки на глобальные параметры основного чарта (.Values.global), особенно, если эти параметры относятся к одному из приложений. В идеале их вообще не должно быть. Это позволит нам исключить сложно управляемые неявные зависимости. Все конфигурационные параметры приложений и другие переменные (например, DNS-имена сервисов) находятся только в одном месте — едином файле конфигурации системы, все приложения получают конфигурацию только из него. Чарты не содержат секретов (хотя могли бы) и они не находятся под управлением helm. Мы сделали это сознательно, чтобы разделить процессы управления секретами и развертывания. Чарты не предназначены для размещения на общедоступных ресурсах и отдельные приложения могут быть не функциональны вне системы. Таким образом в итоге у нас должен получиться один чарт с одним уровнем из нескольких сабчартов. Мы вынесли определение и сборку чарта для каждого приложения в отдельный репозиторий вместе с основным чартом. Это позволило переиспользовать унифицированные шаблоны, как я расскажу дальше. Как я уже отмечал, helm имеет широкие возможности по шаблонизации. Но, к сожалению шаблонизации, предлагаемой helm, оказалось недостаточно по нескольким причинам. Во-первых, невозможно шаблонизировать версии в файле Chart.yaml. Во-вторых, файлы values.yaml не могут содержать шаблонов. Существует обход этого через функцию tpl, однако он удобен не во всех случаях и имеет проблемы с производительностью. Иногда проще и понятнее написать шаблонизируемые значения в виде замещаемого текста (placeholder), например так:   Результирующий yaml с замещенными значениями можно получить одним вызовом sed. Также, как инструмент внешней шаблонизации удобно использовать утилиту yq для вставки, замены значений и объединения yaml файлов. Алгоритм сборки всего чарта выполняется автоматически и в общем виде выглядит так: Собираем сабчарт для каждого приложения используя типовые шаблоны. Проставляем текущие версии во все Chart.yaml На основе шаблонов готовим values.yaml для основного чарта. Генерируем уникальную версию системы и вставляем ее в Chart.yaml для основного чарта. Запускаем сборку и получаем релиз системы, описываемый чартом. Почему не использовать для этого kustomize? Кажется, что он более естественно решает задачи переопределения параметров конфигурации. Однако задачу начальной сборки однотипных манифестов без копирования кусков YAMLа он все равно не облегчает по сравнению с описанным в этой статье решением. И также kustomize не предоставляет средств управления развертыванием в кластере. Зачем нужны yq и bash-скрипты, если все можно сделать через шаблонизацию самого helm’a? Ну во-первых, нам тогда придется использовать шаблоны в values.yaml, а это ухудшает читаемость, за счет многоуровневых конструкций с "|", либо выносом каждого такого шаблона в именованные, что также будет сложно поддерживать при большом числе почти однотипных шаблонов. При этом надо будет использовать "tpl" с известными проблемами с производительностью. Во-вторых, нам просто не очень нравятся выразительные средства такого {{ ... }} синтаксиса.  Почему sed, а не какой-нибудь шаблонизатор типа jinja? Потому, что нам надо было решить одну простую задачу - замену плейсхолдера, и прикручиваение дополнительных движков выглядит для этого несколько избыточным. К тому же синтаксис jinja конфликтовал бы с синтаксисом helm-шаблонов. По ходу описания, я буду опускать некоторые "технические" части и детали, которые либо привязаны к нашей системе, либо легко могут быть дополнены читателем. Для начала создадим директории для основного чарта, сабчартов и типовых шаблонов. Мы решили ,что для исключения путаницы имя директории основного чарта должно совпадать с его названием, именно под ним он будет помещен в репозиторий и будет использоваться в командах helm. У нас он называется exerica, здесь я буду использовать oursystem. Создадим для него следующую структуру: Тут совсем мало файлов, поскольку в этом чарте определяются только: зависимости в Chart.yaml общие именованные шаблоны в _helpers.tpl единый конфиг системы в configmap.yaml сообщение, которое будет выведено после развертывания системы (NOTES.txt) Параметры для всех приложений будут сгенерированы скриптом при сборке и записаны в values.yaml. Определение единого конфига из configmap.yaml тоже совсем небольшое: Шаблон toPropertiesYaml конвертирует произвольный  (почти) yaml в массив пар "ключ-значение", где ключ формируется как последовательность ключей на пути от корня yaml-объекта:   (С) Этот шаблон был когда-то найден на просторах интернета и немного доработан. Буду признателен, если кто-нибудь укажет его авторство в комментах. Создадим файл versions.yaml, определяющий версии приложений, которые будем развертывать. Все параметры конфигурации, которые будем выносить в единый конфиг системы, определим в одном файле templates/variables.yaml. Этот файл мы пропустим через sed перед добавлением в чарт. В дальнейшем в процессе сборки эти параметры дополняются "вычисляемыми" значениями, которые также нужны для конфигурирования приложений, например полный URL для ingress: ingressUrl. При сборке основного чарта эти параметры будут вставлены в секцию .Values.global.configuration и сформируют ConfigMap с единым конфигом системы. В частности, на них мы будем ссылаться в переменных окружения для контейнеров. Таким образом можем передать параметры приложению webapi через переменные окружения, например такие: DEPLOY_ENVIRONMENT — глобальное имя среды в которой функционирует приложение, берется из параметра common.envName ProcessingRemoteConfiguration__RemoteAddress — адрес сервиса, с которым взаимодействует приложение webapi, берется из параметра processing.serviceUrl Переменные среды считываются и передаются приложению при его запуске. Если при развертывании очередной версии системы какое-то приложение не поменялось, но поменялась конфигурация системы, то его под не будет перезапущен. Для решения этой проблемы удобно использовать контроллер Reloader, который отслеживает изменения объектов ConfigMap, Secret и производит плавающее обновление (rolling update) для подов, которые зависят от них. Мы включили его в состав системы, как внешнюю зависимость через helm chart. В нашем подходе это делается буквально в несколько строчек. В oursystem/Chart.yaml В versions.yaml Создадим первый сабчарт при помощи скаффолдинга helm (имя должно соответствовать имени приложения). Получим каталог со следующими файлами: Назначение всех этих файлов отлично описано в официальной документации. Обратим внимание на директорию templates: все шаблоны в ней, вообще говоря, слабо зависят от приложения. Чтобы не нарушать DRY вынесем их в templates/subchart: Тут можно удалить ненужные шаблоны. Например,можно не использовать NOTES.txt для отдельных приложений. Также я опущу все, что касается тестов. При необходимости шаблонизацию тестов можно реализовать полностью аналогично. Создадим файл templates/resources.yaml, в котором соберем все определения для ресурсов, выделяемых приложениям. Все фиксированные параметры для values.yaml соберем в отдельном файле values.templ.yaml, такие как параметры "внешних" зависимостей, например redis. Также вынесем типовое определение для ingress из values.yaml в templates/values.ingress.yaml.  Этот файл мы пропускаем через sed перед добавлением в чарт. Таким образом мы получим структуру шаблонных файлов из которых затем собирается единый чарт нашей системы: oursystem — минимальный основной чарт subcharts — файлы вложенных чартов, которые не могут быть унифицированы, либо их унификация нецелесообразна templates — шаблоны для формирования параметров для всех сабчартов templates/subcharts — унифицированные helm-шаблоны для объектов сабчартов Из файлов values.*.yaml, variables.yaml и resources.yaml из директории templates в итоге собирается файл values.yaml.   Создадим скрипт для сборки build_chart.sh, воспроизводящий алгоритм сборки, описанный выше. Привожу его частями для удобства восприятия. Можно заметить, что секция configuration тут записывается в .Values.global, хотя мы ранее вводили принцип №8 (не использовать global). На самом деле это не обязательно, но для некоторых наших приложений приходилось "готовить" сложные конфигурации, собирая их из параметров секции configuration при помощи шаблонизатора helm (поскольку мы живем не в идеальном мире). Если такой необходимости нет, можно помещать configuration просто в .Values основного чарта. В результате работы скрипта мы получим релиз системы конкретной версии: скомпонованный чарт на всю систему, в котором определяются все зависимости и конфигурационные параметры для развертывания в кластере Kubernetes. При необходимости в процессе развертывания любые параметры основного чарта и сабчартов могут быть переопределены с помощью ключей --set или -f.  Развертывание подготовленного релиза системы производится одной командой:   В переменных окружения передаются: ENV_NAME — имя среды для развертывания CHART_NAME — полное имя собранного чарта системы (включая версию) После подстановки переменных окружения команда будет выглядеть примерно так: Как видно, каждая среда имеет свое пространство имен, таким образом даже одноименные объекты из разных сред в рамках одного кластера не конфликтуют по именам. Флаг --atomic говорит о том, что helm будет ожидать в течение --timeout 3m успешного развертывания всех приложений. В случае ошибки или истечения таймаута произойдет автоматический откат на предыдущий релиз. Для разных сред, например, промышленной и тестовых, можно создать разные variables.yaml, resources.yaml и подключать в скрипте сборки соответствующий среде файл. Например так:  На самом деле, у нас как раз сделанно именно так, при этом используются разные параметры для промышленной и тестовых сред. Можно отказаться от соответствия имен сабчартов и разворачиваемых приложений. Это может быть полезно при необходимости развернуть одно и то же приложение с разными настройками в рамках одного экземпляра системы.  Если будут заметны проблемы с производительнсотью сборки (а пока их нет), всю логику build_chart.sh можно реализовать другими седствами не изменяя подхода и форматов шаблонов, например на python.  Все что описано в этой статье можно применить и при другом подходе к управлению версиями, когда допустимо автоматическое развертывание при выходе новой версии каждого приложения. В этом случае можно запускать CI пайплайн сборки чарта и развертывания по триггеру. А в скрипте сборки чарта поменять источник актуальных версий приложений. Собранный чарт можно использовать для развертывания в совершенно другой среде, например, создавать отдельную среду под клиента/заказчика, которому нужна изоляция данных, либо для развертывания в инфраструктуре заказчика. Минимально достаточно для этого переопределить при деплое DNS-имена для ingress (например с помощью helm install -f ...). Данные для подключения в БД и т.п. приложения получают из объектов Secret, которые, как я упоминал выше, управляются отдельно от чарта. При помощи различных инструментов шаблонизации мы можно создать довольно удобное в плане поддержки и масштабирования решение по описанию развертывания систем в Kubernetes. Широкие возможности для этого предлагает менеджер пакетов Helm. Но если добавить дополнительную шаблонизацию для генерации однотипных чартов helm, автоматически компоновать чарт для системы в целом, то довольно просто получается построить непрерывный процесс развертывания системы в Kubernetes, требующий минимального участия человека. При этом мы получаем возможность управления развертыванием: формирование релиза из любого набора конкретных версий компонентов, автоматическую проверку успешности развертывания и удобный откат к одному из предыдущих релизов. Переход на Kubernetes также сделал для нас доступным широкий спектр решений для автоматизации многих инфраструктурных задач. Например, для управления внешними DNS записями мы взяли ExternalDNS, а для для управления TLS-сертификатами — cert-manager. The Chart Template Developer's Guide The Art of the Helm Chart: Patterns from the Official Kubernetes Charts (перевод) Создание пакетов для Kubernetes с Helm: структура чарта и шаблонизация Продвинутая Helm-шаблонизация: выжимаем максимум CTO, architect