Это продолжение практикума по развертыванию Kubernetes-кластера на базе облака Mail.ru Cloud Solutions и созданию MVP для реального приложения, выполняющего транскрибацию видеофайлов из YouTube.  Я Василий Озеров, основатель агентства Fevlake и действующий DevOps-инженер (опыт в DevOps — 8 лет), покажу все этапы разработки Cloud-Native приложений на K8s: от запуска кластера до построения CI/CD и разработки собственного Helm-чарта. Напомню, что в первой части статьи мы выбрали архитектуру приложения, написали API-сервер, запустили Kubernetes c балансировщиком и облачными базами, развернули кластер RabbitMQ через Helm в Kubernetes. Во второй части настроили и запустили приложение для преобразования аудио в текст, сохранили результат и настроили автомасштабирование нод в кластере.  В третьей части осталось добавить небольшие улучшения, в частности настроить мониторинг, а также мы построим CI/CD и разработаем собственный Helm-чарт. Также запись практикума можно посмотреть: часть 1, часть 2, часть 3. Первое, что нам нужно сделать для получения кастомных метрик — это установить Prometheus, который будет собирать метрики с наших сервисов. Устанавливать его, как и RabbitMQ, будем через Helm. Но это не Prometheus в чистом виде, по его компонентам пройдемся позднее. Используя команду helm search repo prometheus-stack, находим prometheus-stack: Чтобы поиск выполнился корректно, необходимо предварительно убедиться в наличии репозитория prometheus-community с помощью helm repo list и добавить его при необходимости: Если репозитория в списке нет, добавьте его командой helm repo add prometheus-community https://prometheus-community.github.io/helm-charts. В результатах поиска нас интересует kube-prometheus-stack — скачиваем его и распаковываем: Копируем values-файл в текущую директорию и называем его values.prometheus.stage.yaml: При загрузке Helm-чартов придерживаемся стандартного правила. Загруженные Values мы не редактируем, чтобы чарт можно было легко обновлять в дальнейшем. Вместо этого переносим их на уровень выше и называем по имени сервиса (prometheus или rabbit) и окружения (stage, dev, prod и так далее). Очевидно, что для разных окружений настройки будут отличаться. Отредактируем values.prometheus.stage.yaml. Чтобы включить Ingress для Prometheus, находим в файле блок ingress, устанавливаем в поле enabled значение true и прописываем имя хоста prometheus.stage.kis.im: Далее создаем новый Namespace — monitoring: И деплоим в него prometheus-stack: Деплой запущен: Проверим статус деплоя: Запись появилась, значит все ок: Теперь проверим, что запустились поды с помощью kubectl -n monitoring get pods: Запустилось сразу несколько сервисов: alertmanager, prometheus, grafana, prometheus-operator, kube-state-metrix, node-exporter. Экспортеры (node-exporter) будут собирать информацию с каждой ноды кластера — поэтому их три штуки. Они управляются DaemonSet. При добавлении новых узлов в кластере для них будут автоматически созданы отдельные экспортеры. Так как мы на этапе настройки подключали Ingress для Prometheus, мы можем обратиться по адресу prometheus.stage.kis.im (предварительно добавив его в DNS) и посмотреть все таргеты. Видим, что с kube-proxy метрики у нас не собираются, а все остальное успешно работает: Prometheus Operator создает кастомные ресурсы. Просмотреть их можно, используя команду get crd (Custom Resource Definition) kubectl get crd: Если необходимо собирать метрики со своих приложений, нужно добавить в этот список новый ресурс: либо serviceMonitor, который находит сервисы и обращается к их Endpoints, либо podMonitor, который находит поды и начинает мониторить их. Как только вы создадите этот ресурс, Prometheus Operator это обнаружит и обновит конфигурацию Prometheus. В результате ресурсы попадут в общий список, и Prometheus начнет их мониторить. Это логика работы любого оператора в Prometheus: операторы создают кастомные ресурсы, управляя которыми, вы можете влиять на работу мониторинга. Для добавления нашего serviceMonitor открываем файл values.rabbitmq.stage.yaml и находим секцию Prometheus Metrics. Устанавливаем в поле enabled значение true. Теперь этот Helm Chart подключит к RabbitMQ специальный плагин rabbitmq_prometheus, который сможет отдавать метрики в Prometheus: В секции serviceMonitor также прописываем enabled: true. Эта настройка отвечает за создание serviceMonitor —  ресурса, необходимого Prometheus: В секции additionalLabels добавляем метку release: prometheus-stack: Эта метка необходима для того, чтобы установленный нами ранее Prometheus-stack обнаружил новый serviceMonitor. В настройках Prometheus-stack при установке указываются метки, на основании которых он будет определять нужные podMonitor и serviceMonitor. Благодаря этому можно установить несколько Prometheus-stack в одном кластере, и они не будут пересекаться. Теперь нужно обновить RabbitMQ. Предварительно мы указываем аутентификационные переменные auth.password и auth.erlangCookie, загрузив их из секретов, так как иначе будет предупреждение о том, что переменные уже были созданы: Теперь можно обновлять RabbitMQ: После перезапуска RabbitMQ с помощью kubectl get crd в списке ресурсов появится servicemonitors — наш новый ресурс: И в нем — rabbitmq, который создал Helm Chart RabbitMQ через kubectl -n stage get servicemonitors: Если вывести содержимое сервисного монитора rabbitmq, то увидим правила его работы. В namespaceSelector указан отслеживаемый Namespace — stage. В matchLabels описаны искомые метки подов — instance: rabbitmq, name: rabbitmq. Таким образом, монитор находит Endpoints всех подходящих подов и добавляет их в список мониторинга Prometheus: Если мы сейчас обновим таргеты в Prometheus, то увидим в списке stage/rabbitmq/0. Helm Chart добавил serviceMonitor для RabbitMQ, и Prometheus увидел наш новый сервис: Теперь здесь можно посмотреть, например, метрику rabbitmq_queue_messages_ready — количество сообщений в статусе Ready. На текущий момент их 0: Давайте отправим новое сообщение с использованием нашего API: Запрос добавлен: Значение метрики в Prometheus обновится: Следует учитывать, что метрика rabbitmq_queue_messages_ready показывает общее число сообщений. Если для вашего приложения требуется получение числа сообщений с разбиением по очередям, можно установить другой RabbitMQ Exporter. Теперь необходимо научиться передавать метрики из Prometheus в Kubernetes, в кастомную группу custom.metrics.k8s.io, чтобы мы могли использовать их для нашего автоскейлинга. По умолчанию Prometheus этого делать не умеет, поэтому установим через Helm еще один продукт — prometheus-adapter с помощью helm search repo prometheus-adapter: Загружаем и распаковываем первый Helm-чарт: Копируем его values.yaml и открываем полученный файл values.pa.stage.yaml для редактирования: В файле нас интересует установка URL Prometheus. Прочие настройки можно оставить по умолчанию: Получить искомый URL поможет команда kubectl -n monitoring get svc: URL формируется по маске <NAME из вывода команды>.<Namespace (в нашем случае monitoring)>.svc. Сохраняем yaml-файл и деплоим prometheus-adapter: Все получилось: После успешной установки он появляется в списке подов, проверяем kubectl -n monitoring get pods: Prometheus-adapter создает новую группу метрик в apis/custom.metrics.k8s.io/v1beta1. Мы можем обратиться к ней напрямую: Мы видим все метрики Prometheus, которые prometheus-adapter перенес в Kubernetes: Чтобы ограничить число метрик, необходимо в файле values.pa.stage.yaml в блоке с правилами переноса метрик rules отключить признак default и указать свои, кастомные метрики. Это более правильный подход: Мы можем посмотреть интересующую нас метрику rabbitmq_queue_messages_ready для всех подов в Namespace stage: Значение метрики 5751. Это количество сообщений в очереди на текущий момент: Теперь мы можем отредактировать ранее созданный файл hpa.yaml для Horizontal Pod Autoscaler. В типе метрики type укажем Pods, в названии метрики metricName — rabbitmq_queue_messages_ready, в целевом значении — 1: Сохраняем файл под именем hpa-custom.yaml, удаляем прежнюю версию converter-hpa и применяем новый файл. Теперь при увеличении числа сообщений в очереди Horizontal Pod Autoscaler будет создавать новые поды: Примечание: во время вебинара эта кастомная метрика у нас почему-то не собиралась. Но позже все заработало, поэтому если у вас будет такая же проблема — попробуйте повторно применить настройки из файла. Следующая задача — автоматизировать развертывание нашего приложения. До этого мы деплоили его через YAML-файлы. Но очевидно, что при большом числе файлов это становится крайне неудобным. Поэтому и появились чарты, внутри которых можно описывать множество шаблонов. Создадим собственный Helm-чарт для деплоя приложения.  Для начала создадим новый репозиторий в GitLab. Назовем его converter-api, сделаем приватным: У нашего приложения есть Dockerfile, с помощью которого мы его собираем файлы с описанием зависимостей (go.mod, go.sum), и непосредственно код нашего приложения (main.go, requests.go). Эти файлы мы и поместим в репозиторий: Инициализируем Git, создаем начальный коммит: Добавляем удаленный репозиторий origin и отправляем изменения в GitLab: Вот ссылка на созданный репозиторий: https://gitlab.com/vozerov/converter-api Рассмотрим содержимое файла gitlab-ci.yml, который описывает наш пайплайн.  В начале мы экспортируем в variables DOCKER_TLS_CERTDIR — это нужно для работы сервиса Docker-in-Docker. Так как мы будем запускаться внутри Docker, в нем нам еще раз понадобится Docker, чтобы собрать контейнер. Далее идет описание stages: сначала мы билдим, затем деплоим. На стадии build мы логинимся в Docker Hub (используя переменную DOCKER_HUB_TOKEN), после этого применяем команды docker_build и docker_push. И указываем наш образ (IMAGE_NAME), куда это все запушить, и номер версии. Версия будет указываться с помощью хэша Git-коммита (CI_COMMIT_SHORT_SHA).  Примечание: поясню подробнее про назначение CI_COMMIT_SHORT_SHA.  На каждый push собирается контейнер с тегом хэша коммита. Но кроме сборки и тестирования в автоматическом режиме больше с контейнером ничего не происходит, задеплоить его никуда нельзя. Деплоить можно только теги. Соответственно, когда GitLab видит тег, привязанный к коммиту, он больше не собирает Docker-контейнер, а скачивает предыдущий, который уже был собран и прошел тестирование с этим хэшом: Далее переходим к деплою. Деплой использует отдельный образ helm-deployer. Внутри это Alpine с установленными Helm и kubectl. После этого, используя переменную KUBECONFIG, экспортируем настройки Kubernetes, которые необходимы для подключения к кластеру. Добавляем приватный ключ командой gpg (об этом более подробно пойдет речь ниже). И деплоим, используя Helm: переходим в директорию .infra и выполняем стандартный helm upgrade, как мы делали это ранее с RabbitMQ, prometheus-adapter, prometheus-stack и так далее.  При деплое мы устанавливаем converter-api в заданный Namespace и указываем два values-файла: values и secrets. Secrets необходимы нам для того, чтобы не хранить секретные переменные в environment либо файле, о чем мы ранее говорили. Также добавляем image.tag, который устанавливает версию (тег) нашего Docker-образа, который необходимо задеплоить. И в завершение указываем директорию с чартом (chart/). Примечание: совместно с ключом --wait можно использовать ключ --atomic. В таком случае, если при деплое не обновится какой-то ресурс, произойдет откат релиза. Однако wait не выводит логи. Поэтому вместо него можно использовать дополнительные утилиты для вывода логов при деплое, например, werf/kubedog. Он позволяет в real-time режиме получать сообщения о запуске контейнеров, ошибках и так далее: Далее в gitlab-ci.yml следует описание двух environment: deploy_dev и deploy_prod. Для первой будут браться файлы values.dev.yaml и secrets.dev.yaml, для второй values.stage.yaml и secrets.stage.yaml. Так как мы в своем проекте вместо dev использовали stage, отредактируем файл — пропишем в блоке deploy_dev в переменных KUBE_NAMESPACE и BUILD_VARIANT значение stage вместо dev: Переходим к настройке переменных, на которые ссылается gitlab-ci.yml.  Во-первых, необходимо указать DOCKER_HUB_TOKEN, чтобы мы могли подключиться к DOCKER_HUB. Для этого я перехожу на hub.docker.com и создаю новый публичный репозиторий converter-api: Далее в Docker Hub выбираем «Account Settings» и в пункте меню «Security» создаем новый токен с именем mcs: После создания открывается окно, откуда можно скопировать сгенерированный токен: Возвращаемся в GitLab и выбираем раздел «Settings» — «CI/CD» — «Variables». Создаем новую переменную с именем DOCKER_HUB_TOKEN, присваиваем ей значение скопированного токена, убираем флажок Protect variable, чтобы переменная была доступна во всех Branch: Следующая переменная для настройки — KUBECONFIG. Она необходима, чтобы мы из Docker-образа могли подключиться к нашему кластеру. Самый простой вариант ее заполнения — взять содержимое файла kub-vc-dev_kubeconfig.yaml и полностью перенести его в переменную. Но при этом будут переданы полные права абсолютно на весь кластер. Это не самое правильное решение.  Поэтому стоит обратиться к RBAC (Role Based Access Control), доступному в Kubernetes. Принцип работы RBAC можно объяснить на примере трех простых файлов. У меня они расположены в папке roles: sa.yaml, role.yaml, rb.yaml: В файле sa.yaml описывается создание нового сервисного аккаунта. В метаданных указывается его имя —  deployer: Применяем полученный файл kubectl -n stage apply -f sa.yaml и создаем аккаунт. Если вывести список сервисных аккаунтов с помощью kubectl -n stage get sa, в нем отобразится созданный нами deployer: При создании сервисный аккаунт сразу формирует себе секрет — токен, под которым он может обращаться к Kubernetes API: Видим имя этого секрета: Мы можем вывести содержимое секрета по его имени с помощью kubectl -n stage get secret deployer-token-p4r7q -o yaml: Если мы декодируем его в base64, то получим соответствующий токен. Запишем его в переменную TOKEN с помощью TOKEN=$(echo '<...token...>' | base64 -d)`: Переходим к файлу role.yaml. В нем создается роль и описываются действия, доступные для пользователей, которым эта роль назначена. Здесь для различных API групп определяются доступные ресурсы (в секции resources) и способ доступа к ним (в секции Verbs). Символ * в Verbs означает, что возможны любые действия с ресурсом: get, list, update, watch, create и так далее: Применяем файл role.yaml для создания роли deployer: Наконец, в файле rb.yaml (Role Binding) сервисный аккаунт и роль связываются друг с другом: Применяем файл rb.yaml. После этого аккаунту deployer будет сопоставлена роль deployer: Следует учесть, что созданные нами Role и Role Binding не кластерные, они относятся только к одному Namespace. В остальных Namespace аккаунту все будет запрещено. Для добавления доступа к нескольким Namespace нужно использовать Cluster Role и Cluster Role Binding. Логика работы с ними точно такая же. Теперь применим команду set-credentials для добавления нового пользователя deployer-stage в kubectl config. При добавлении укажем токен, сохраненный ранее в переменную TOKEN: Теперь мы можем работать под данным пользователем. Далее изменяем текущий контекст, выбирая пользователя deployer-stage: И можем проверить созданные ограничения доступа. Попробуем вывести поды в Namespace stage с помощью kubectl -n stage get pods: Команда выполнилась успешно,  а теперь попробуем вывести поды в Namespace default с помощью kubectl get pods default: Команда завершилась с ошибкой Forbidden и сообщением «User “system:serviceaccount:stage:deployer” cannot list resource “pods” in API group ““ in the namespace “default”». Также мы не добавляли пользователю права на выполнение pods exec, то есть заходить внутрь пода — поэтому мы, например, не сможем выполнить команду  kubectl -n stage exec -it ubuntu -- bash: Но созданного пользователя вполне хватит для того, чтобы деплоить наши изменения. Теперь нам остается лишь добавить переменную в GitLab. Для этого копируем содержимое обновленного файла kub-vc-dev_kubeconfig.yaml в значение новой переменной KUBECONFIG в GitLab: И видим содержимое файла kub-vc-dev_kubeconfig.yaml: При копировании важно в секции users оставить только добавленного нами пользователя deployer-stage с его токеном. Остальных пользователей необходимо убрать при добавлении значения переменной.  В качестве типа переменной необходимо выбрать File. Это означает, что в переменной KUBECONFIG будет путь к файлу с введенным содержимым. Это важно, так как kubectl будет смотреть на этот файл: Последняя переменная, которую нам необходимо заполнить в GitLab — это HELM_GPG_KEY. Она связана с использованием утилиты Sops для шифрования секретов. Давайте перейдем в папку Infra. У нас здесь находятся файлы values и secrets для сред prod и stage: В файлах values мы храним открытые переменные, например, информацию по Ingress, public_env_variables и так далее. Это все можно отдавать в  открытый доступ: А в файлах secrets у нас хранятся зашифрованные переменные: RABBIT_URI, PGSQL_URI и так далее. Посмотрим cat secrets.stage.yaml: Для шифрования используется Sops — утилита от создателей Mozilla, которая применяет PGP-ключи для шифрования и дешифрования файлов. Для просмотра ключей можно вызвать команду gpg -k: Для демонстрации работы Sops создадим новый текстовый файл test.txt, используя публичную часть PGP-ключа: Внутри файла укажем некоторый текст и сохраним его: Теперь при открытии файла в поле data можно увидеть зашифрованное значение: Чтобы дешифровать файл, применим команду sops –d test.txt: То есть логика такая: вы создаете у себя локально PGP-ключ, с помощью него и Sops выполняете шифрование секретных переменных и отправляете все в Git-репозиторий.  А для того, чтобы это расшифровали члены вашей команды, вы передаете им приватную часть PGP-ключа, например, через One Time Secret или One Password. Они импортируют ключ себе и после этого могут работать с секретными переменными. Конечно, использовать Sops необязательно, это один из возможных вариантов работы с секретами. Но в нашем проекте мы его используем — поэтому заполним переменную HELM_GPG_KEY значением приватной части PGP-ключа. Для этого возьмем публичную часть ключа и применим к ней команду: Создадим в GitLab переменную HELM_GPG_KEY и скопируем в нее результат вывода предыдущей команды: Вернемся к файлу gitlab-ci.yml, чтобы пояснить работу с ключами PGP. Здесь сначала происходит импорт приватной части ключа из переменной HELM_GPG_KEY. Затем с использованием плагина secrets секреты расшифровываются и передаются в helm, который выполняет деплой:  Теперь кратко пройдемся по описанию нашего Helm-чарт. Переходим в папку /app-git/.infra/chart. Основное, что здесь есть — это папка templates, в которой описаны все ресурсы, которые необходимо создать для приложения: Открываем deployment.yaml. В нем указываются метаданные, Labels, количество реплик, селекторы и так далее. Здесь стоит обратить внимание на environment-переменные. Переменные из public_env_variables сразу прописываются в Deployment. Приватные переменные из private_env_variables предварительно создаются в секретах, а здесь для них в секции valueFrom указывается, в каком они находятся секрете и под каким ключом. Сами секреты создаются в secret.yaml: Выполняем отправку всех внесенных изменений в GitLab и переходим к запуску наших пайпланов. Сначала выполняем Build: Затем деплоим на Dev и Prod: В случае успешной сборки можно применить команду helm -n stage list и увидеть наш релиз converter-api, который автоматически создаст все необходимые ресурсы: Например, если применить команду kubectl -n stage get ingress, то можно увидеть добавленный converter-api-chart: Соответственно, с этого момента все изменения можно отправлять в Git: чарты, переменные и так далее. Будет производиться автоматический деплой, а ручные действия больше не понадобятся. Мы с вами рассмотрели основные этапы построения Cloud-Native приложения на базе Kubernetes-кластера. Подобрали оптимальную архитектуру для нужд нашей системы и последовательно добавили все необходимые компоненты: API-сервер, брокер сообщений RabbitMQ, базу данных PostgreSQL, бакет S3 и обработчики, отвечающие за конвертацию видео в текст. Много работали непосредственно с самим кластером: выполнили его настройку в облаке и познакомились с большим количеством команд его клиента kubectl. Научились деплоить приложения как через YAML-файлы, так и через Helm Charts. Наконец, коснулись таких важных моментов, как автомасштабирование, мониторинг и построение CI/CD-конвейеров. Разумеется, мы охватили далеко не все возможности K8s. Да и приложение было далеко не самым сложным. Однако перед собой я ставил цель показать вам, что Kubernetes, о котором многие пишут и говорят, но далеко не все решаются использовать, не так сложен на практике. А при использовании Managed-решений от облачных провайдеров работа с Kubernetes-кластером становится по-настоящему удобной и быстрой: как минимум, сокращается время настройки и уменьшаются затраты на оборудование благодаря автомасштабированию.   Надеюсь, представленная в статье информация поможет вам начать работу с Kubernetes либо вывести ее на более качественный уровень. Новым пользователям платформы Mail.ru Cloud Solutions доступны 3000 бонусов после полной верификации аккаунта. Вы сможете повторить сценарий из статьи или попробовать другие облачные сервисы.   Что еще почитать по теме: Как развернуть кластер Kubernetes на платформе MCS. Запускаем etcd-кластер для Kubernetes.  Как устроен Kubernetes aaS на платформе Mail.ru Cloud Solutions.