Локальная разработка в Kubernetes с помощью werf 1.2 и minikube

Kate

Administrator
Команда форума
В этой статье на примере простого приложения будут описаны подготовка и развертывание инфраструктуры для локальной разработки с Kubernetes и последующий запуск проекта в этом окружении.

04b35f1caa9573d56c5cedd630d66d3f.png

Зачем это может понадобиться? Предоставляя услуги поддержки инфраструктуры нашим клиентам, нам часто приходится искать баланс между эффективностью и затраченными ресурсами. Когда есть потребность в нескольких рабочих окружениях для разработчиков (stage, dev, test, review и т.п.), как правило, ограничивающим фактором в их реализации являются деньги. Поэтому для решения такой задачи — в дополнение к динамическим окружениям (или вместо них) — задействуются локальные окружения прямо на рабочих местах разработчиков, о которых и пойдет речь далее.

Если вы разработчик и у вас часто возникает необходимость быстро проверить результаты своей работы, а такой возможности нет из-за уже отстроенной и налаженной системы тестовой сборки и тестирования — эта статья для вас. В ней показано, как с помощью werf и minikube можно настроить локальный тестовый стенд со всем необходимым для работы и расскажем о тонкостях и нюансах его использования.

Введение​

Разработка под Kubernetes накладывает определенные правила на приложения в части сетевого взаимодействия сервисов между собой, использования постоянного хранилища файлов, распределения трафика с ingress’ов на конечные endpoint’ы и т.п. И в ситуации, когда разработчику нужно получать обратную связь о том, как приложение будет работать в «боевых» условиях, все эти инфраструктурные аспекты так или иначе влияют на конечный результат и скорость разработки. Для того, чтобы протестировать свои изменения, обычно разработчику необходимо сделать push своего commit’а в удаленный репозиторий и выкатить проект в какое-нибудь тестовое окружение. Метод традиционный и уже всеми используемый — работает хорошо. Но тут могут возникнуть определённые неудобства, например:

  • pipeline может быть довольно большим, и может потребоваться немало времени, пока изменения доедут до конечной точки;
  • количество окружений может быть ограниченным, и нужно узнать у коллег, никто ли сейчас не работает с тем или иным стендом.
Для более гибкого управления своей разработкой было бы удобно иметь возможность запускать окружение локально на своем ПК. Поэтому я попытаюсь изложить в статье наш подход к организации локального окружения для разработчиков, в котором будет смоделирована идентичная по функциональности инфраструктура, как в «боевом» окружении, работающем в кластере Kubernetes.

Про werf​

Хочется также отдельно сказать несколько слов про наш Open Source-инструмент для выстраивания процесса CI/CD, в котором Git является единственным источником истины (этот принцип мы называем «гитерминизм»). Речь про CLI-утилиту под названием werf. С её помощью мы собираем контейнеры с кодом, публикуем их в registry, выкатываем Helm-чарты в Kubernetes и отслеживаем состояние выката до того момента, пока он не станет успешным.

Так как мы используем подход Infrastructure as a Code (описание всей имеющейся инфраструктуры в виде кода), основной паттерн при развёртывании приложений с использованием werf — хранение в одном Git-репозитории и описания используемых инфраструктурных компонентов, и самого кода проекта. Это позволяет гарантировать конечное состояние системы исходя из того, что находится в Git. Когда что-то изменяется в Git, эти изменения появятся и в рабочем окружении. Такую же задачу мы преследуем и в контексте локальной разработки, с чем нам поможет werf. Очевидно, это не единственный возможный путь, но тем, кто уже использует werf, он будет явно интересен.

Установка зависимостей​

Возвращаясь к предмету повествования, рассмотрим локальный запуск приложения в Kubernetes на конкретном примере. Для этого будет использоваться упрощенный вариант K8s — minikube. Это инструмент для запуска одноузлового кластера Kubernetes на виртуальной машине (или в контейнере) на персональном компьютере.

Результаты всех выполненных ниже действий были опубликованы в репозитории с готовым примером. В целом, там есть инструкция по запуску, и можно сразу же посмотреть на результат
Для начала нужно установить у себя все необходимые для работы компоненты. А это — Docker, minikube, werf и kubectl. Ниже приведены ссылки на инструкции по установке всех используемых утилит. Каждая утилита поддерживает несколько операционных систем: определённые Linux-дистрибутивы, а также macOS и Windows. Формально эта инструкция тестировалась только для Ubuntu 20.04, хотя пользователям других ОС не составит труда адаптировать её под свой случай (пример одной такой адаптации под macOS при организации стенда см. ниже).

Тестовый стенд​

Теперь, когда базовые компоненты установлены, в качестве приложения, которое мы будем разрабатывать и деплоить, возьмём один из проектов-демонстраций возможностей werf, написанный на Node.js. Наша задача — адаптировать репозиторий под деплой в minikube. Для этого выполним следующие шаги:

  • изменим файл werf.yaml, описав в нём все стадии сборки проекта;
  • опишем Helm-чарты проекта;
  • разберемся с переменными окружения, передаваемыми в Deployment приложения;
  • запустим тестовый стенд и убедимся, что он работает.
Рассмотрим все шаги подробнее.

Сборка: werf vs Docker​

Немного теории​

По умолчанию Docker хранит все свои слои и пересобирает их только в том случае, если файлы, используемые в соответствующем слое, были изменены. Принцип довольно простой, полезный и понятный. Единственный минус такого подхода в том, что слои хранятся локально на машине, на которой была запущена команда docker build.

Если же рассматривать ситуацию, когда процесс сборки запускается из pipeline, то он окажется на каком-нибудь из списка доступных worker’ов (например, в GitLab это gitlab-runner’ы), и все собранные слои останутся только на этом worker’е. Когда мы делаем commit с минимальными изменениями кода, и запускается pipeline, в результате которого задание на сборку образа попадёт на другой worker, образ будет собираться с нуля! И тут очень сильно теряется скорость сборки. Увы и ах!

В werf принцип сборки построен немного иначе. Сборочный процесс образов, описанных в файле конфигурации werf.yaml, разбивается на этапы с четкими функциями и назначением. Каждый такой этап соответствует промежуточному образу подобно слоям в Docker, но за тем исключением, что он публикуется в registry (и автоматически становится доступен всем worker’ам). В werf такой этап называется стадией, а конечный образ соответствует последней собранной стадии для определённого состояния Git и конфигурации werf.yaml.

Более подробную информацию об этой и некоторых других отличительных особенностях можно узнать в нашей статье, посвященной Docker и werf.

Практика​

Рассмотрим тестовое приложение. В проекте присутствует Dockerfile, который используется werf для сборки образа. Но, так как в результате сборки этой конфигурации werf.yaml будет опубликована только одна стадия (последняя), то выигрыша в скорости сборки разными worker’ами мы не получим. Поэтому вместо Dockerfile будет использовать альтернативный Stapel-синтаксис и перепишем инструкции в werf.yaml.

project: werf-guide-app # Название проекта.
configVersion: 1 # Используемая конфигурация (на данный момент поддерживается только 1 версия).

---
image: builder # Название собираемого образа.
from: node:12-alpine # Базовый образ.
git: # Секция с директивами для добавления исходных файлов из git-репозитория.
- add: / # Исходный путь в репозитории.
to: /app # Путь назначения в образе.
excludePaths: # Набор путей или масок, применяемых для исключения файлов и папок при добавлении в образ.
- local
- .helm
- Dockerfile
stageDependencies: # Настройка перевыполнения сборочных инструкций при изменениях определённых файлов в репозитории.
install: # Для стадии Install.
- package.json
- package-lock.json
setup: # Для стадии Setup.
- "**/*"
shell: # Shell сборочные инструкции.
install: # Для стадии Install.
- cd /app
- npm ci
setup: # Для стадии Setup.
- cd /app
- npm run build

---
image: backend
from: node:12-alpine
docker: # Набор директив для конечного манифеста образа (aka Docker).
WORKDIR: /app
git:
- add: /
to: /app
includePaths:
- package.json
- package-lock.json
stageDependencies:
install:
- package.json
- package-lock.json
shell:
beforeInstall:
- apk update
- apk add -U mysql-client
install:
- cd /app
- npm ci --production
setup:
- mkdir -p /app/dist
import: # Импорт из образов (и артефактов).
- image: builder # Имя образа, из которого выполняется импорт.
add: /app # Абсолютный путь до файла или директории в образе, из которого осуществляется импорт.
to: /app # Абсолютный путь в конечном образе.
after: setup # Выбор стадии импортирования файлов при сборке.

---
image: frontend
from: nginx:stable-alpine
docker:
WORKDIR: /www
git:
- add: /.werf/nginx.conf
to: /etc/nginx/nginx.conf
import:
- image: builder
add: /app/dist
to: /www/static
after: setup

---
image: mysql
from: mysql:5.7
Подробное описание всех возможных директив можно посмотреть в описании конфигурационного файла werf.yaml.
Для локальной разработки можно ограничиться использованием Dockerfile, но мы думаем наперед и поэтому переписали инструкции с помощью альтернативного синтаксиса werf, который даёт больше гибкости, а главное — эффективное кэширование, которое значительно сокращает время при инкрементальных сборках.

Описание Helm-чартов​

Для деплоя проекта в Kubernetes нужна декомпозиция отдельных его частей до строго декларативных объектов, с которыми взаимодействует Kubernetes. Таких объектов может быть очень много, но в данном случае нужно лишь несколько: Deployment, StatefulSet, Service, Job, Secret и Ingress. Каждый объект выполняет свою функцию. Подробнее с их описанием и назначением можно ознакомиться в документации Kubernetes и его API.

Для более гибкой настройки, установки и обновления приложений в Kubernetes был создан Helm — менеджер пакетов и шаблонизатор для Kubernetes. Он позволяет описать приложение в виде чарта (который может содержать один или несколько субчартов) со своими параметрами и шаблонами конфигураций. Экземпляр чарта со своими параметрами и настройками, установленный в Kubernetes, называется релизом.

Сформируем структуру Helm-чартов нашего проекта таким образом, чтобы выделить компоненты основного приложения в свой собственный субчарт, а компоненты инфраструктуры — в отдельные субчарты.

В нашем примере инфраструктурный компонент всего один — это MySQL, но могли бы быть еще, например, Redis, RabbitMQ, PostgreSQL, Kafka и т. д.

.helm/
├── Chart.lock
├── charts
│ ├── app
│ │ ├── Chart.yaml
│ │ └── templates
│ │ ├── deployment.yaml
│ │ ├── _envs_app.tpl
│ │ ├── ingress.yaml
│ │ ├── job-db-setup-and-migrate.yaml
│ │ ├── secret.yaml
│ │ └── service.yaml
│ └── mysql
│ ├── Chart.yaml
│ └── templates
│ ├── _envs_database.tpl
│ ├── mysql.yaml
│ ├── secret.yaml
│ └── service.yaml
├── Chart.yaml
├── secret-values.yaml
└── values.yaml

5 directories, 16 files
Как вы могли заметить, в корне с чартом лежат два файла values. Если вы работали с Helm раньше, то вам сразу понятно, для чего используется values.yaml. Но вы можете задаться вопросом, зачем же нужен secret-values.yaml. Использование переменных окружения и сторонних решений (Vault, Consul и т. п.) не соответствует принципу гитерминизма (который лежит в основе работы werf), где Git является единственным источником истины. Именно поэтому был реализован механизм, благодаря которому можно хранить секреты внутри Git-репозитория в зашифрованном виде.

В субчартах компонентов приложения и их параметрах заложена логика, которая позволяет рендерить манифесты в зависимости от окружения, в которое выкатывается Helm-релиз. Реализовано это средствами Go-шаблонизатора, который использует Helm. Рассмотрим, как описаны в шаблоне .helm/charts/app/templates/_envs_app.tpl переменные окружения, которые передаются в манифест с Deployment’ом приложения.

- name: MYSQL_HOST
value: "{{ pluck .Values.global.env .Values.envs.MYSQL_HOST | first | default .Values.envs.MYSQL_HOST._default }}"
В поле value подставляется значение, описанное в .Values.envs.MYSQL_HOST, в соответствующем переменной .Values.global.env ключе.

Пример:

Есть следующий сегмент глобального файла
values.yaml:

...
app:
envs:
MYSQL_HOST:
_default: mysql
local: mysql2
production: mysql3
...
Тут переменная MYSQL_HOST представлена в виде так называемого a string-keyed map — map-объекта, в котором находятся ключи с более простыми типами данных. В данном случае это string, но можно использовать и более сложные структуры. Например, в map-объекте envs находится map-объект MYSQL_HOST и т.п.

Если в манифесте используется конструкция:


- name: MYSQL_HOST
value: "{{ pluck .Values.global.env .Values.envs.MYSQL_HOST | first | default .Values.envs.MYSQL_HOST._default }}"
… при его рендере командой werf render –env local –set app.enabled=true получится отрендеренный сегмент:

...
- name: MYSQL_HOST
value: mysql
...
Примечание:

При рендере всего чарта в субчарт передается map-объект с именем, соответствующим названию соответствующего субчарта, из глобального контекста
Values. Обратите внимание, что файл _envs_app.tpl находится внутри субчарта app, в котором отсутствует локальный values.yaml, и внутри него идёт обращение к переменной .Values.envs.MYSQL_HOST, которая отсутствует в глобальном контексте Values. Но в то же время в глобальном Values присутствует переменная .Values.app.envs.MYSQL_HOST. При рендере werf подставит глобальный map-объект .Values.app в локальный .Values субчарта app.

Таким образом, мы можем моделировать и деплоить чарты в разные окружения с различными параметрами, не создавая при этом несколько разных файлов values.yaml. Согласитесь, очень удобно.

Если в проекте помимо values.yaml присутствует также и secret-values.yaml, то для расшифровки и рендера манифеста потребуется задать переменную окружения WERF_SECRET_KEY (подробно об этом можно прочитать в документации). После расшифровки werf объединит ключи с одинаковыми названиями в один. Условно, если в values.yaml у нас описано:

...
app:
envs:
MYSQL_HOST:
_default: mysql
local: mysql2
production: mysql3
MYSQL_PASSWORD:
local: Qwerty123
...
А в secret-values.yaml:

...
app:
envs:
MYSQL_PASSWORD:
_default: 100037f97cb2629e2cab648cabee0b33d2fe381d83a522e1ff2e5596614d50d3a055
production: 1000b278b1a888b2f03aba0d21ad328007ab7a63199cb0f4d1d939250a3f6ab9d77d
...
… то werf расшифрует secret-values.yaml, объединит ключи в values.yaml, и на выходе получится следующее содержимое Values:

...
app:
envs:
MYSQL_HOST:
_default: mysql
local: mysql2
production: mysql3
MYSQL_PASSWORD:
local: Qwerty123
_default: Qwerty456
production: NoQwerty321
...

Организация стенда для локальной разработки​

Подробное описание возможностей werf даёт следующие рекомендации по организации Git-репозитория:

  • В переменных окружения проекта достаточно хранить лишь WERF_SECRET_KEY. Все остальные секретные переменные кладём в secret-values.yaml и шифруем их с помощью werf;
  • Определим название окружения, в которое мы поместим наш экземпляр проекта. Будем использовать окружение с именем local (оно же будет соответствовать названию namespace в Kubernetes);
  • Все переменные, используемые в чартах/субчартах для окружения local, описываем только в values.yaml в открытом виде, не шифруя. Это нужно для того, чтобы не предоставлять доступ к WERF_SECRET_KEY рядовому разработчику, тем самым лишая его возможности расшифровать secret-values и получить Secret’ы.
Для примера разберём запуск готового приложения, которое я подготовил к развертыванию в рамках написания данной статьи.

Клонируем репозиторий и переходим в каталог с проектом:

git clone https://github.com/flant/examples
cd examples/2022/01-werf-local-dev
Начинаем подготовку окружения.

Необходимо разрешить Docker’у pull’ить образы по протоколу HTTP:

echo \
'{
"insecure-registries": ["registry.local.dev"]
}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
Запустим minikube в Docker-контейнере и разрешим ему использовать registry по протоколу HTTP:

minikube start --driver=docker --insecure-registry="registry.local.dev"
Примечание для macOS
В случае macOS настройки Docker будут выглядеть так:
5545bf603f211f60c5cc5c0b77e8f213.png

… а последующая команда — так:
minikube start --driver='hyperkit' --insecure-registry="registry.local.dev"
Включаем расширения ingress и registry:

minikube addons enable ingress
minikube addons enable registry
Добавляем IP-адрес будущего ingress’а в /etc/hosts внутри контейнера с minikube:

minikube ssh -- "echo $(minikube ip) registry.local.dev | sudo tee -a /etc/hosts"
Добавляем IP-адрес будущих ingress’ов registry и основного приложения к себе в /etc/hosts:

echo "$(minikube ip) registry.local.dev" | sudo tee -a /etc/hosts
echo "$(minikube ip) test.application.local" | sudo tee -a /etc/hosts
Создаём ingress для нашего локального registry:

kubectl create -f - <<EOF
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: registry
namespace: kube-system
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "0"
spec:
rules:
- host: registry.local.dev
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: registry
port:
number: 80
EOF
Разрешим werf использовать registry по протоколу HTTP, задав переменную окружения WERF_INSECURE_REGISTRY:

export WERF_INSECURE_REGISTRY=1
Активируем werf:

source $(trdl use werf 1.2 ea)
Переходим к сборке и деплою инфраструктурных компонентов:

werf converge --repo registry.local.dev/app --release infra --env local --namespace local --dev --set mysql.enabled=true --ignore-secret-key=true
После завершения проверяем результат:

kubectl -n local get pod

NAME READY STATUS RESTARTS AGE
mysql-0 1/1 Running 0 39s
Pod с базой данных запустился и работает.

Запустим сборку и деплой основного приложения:

werf converge --repo registry.local.dev/app --release app --env local --namespace local --dev --set app.ci_url=test.application.local --set app.enabled=true --ignore-secret-key=true
После завершения проверяем результат:

kubectl -n local get pod

NAME READY STATUS RESTARTS AGE
app-c7958d64d-5tmqp 2/2 Running 0 66s
mysql-0 1/1 Running 0 4m53s
setup-and-migrate-db-rev1--1-xtdsc 0/1 Completed 0 66s
Pod с миграциями отработал успешно, о чём говорит статус Completed. Pod с основным приложением запущен и работает.

Посмотрим, по какому ingress’у доступен проект:

kubectl -n local get ingress

NAME CLASS HOSTS ADDRESS PORTS AGE
app <none> test.application.local localhost 80 1m
Откройте браузер и перейдите по ссылке http://test.application.local — вы должны увидеть стартовую страницу проекта.

Управление изменениями в проекте происходит с помощью двух режимов:

  1. Режим разработчика: позволяет ослабить ограничения гитерминизма и работать с некоммитнутыми изменениями. Он активируется опцией --dev или переменной окружения WERF_DEV;
  2. Режим отслеживания изменений: позволяет перезапускать команду при изменении состояния Git-репозитория. Активируется опцией --follow или переменной окружения WERF_FOLLOW. Команда перезапускается при появлении новых commit’ов. А при использовании совместно с реимом--dev — при любых изменениях.
Таким образом, для запуска сборки и деплоя основного приложения в режиме слежения за любыми изменениями можно использовать следующую команду:

werf converge --repo registry.local.dev/app --release app --env local --namespace local --dev --follow --set app.ci_url=test.application.local --set app.enabled=true --ignore-secret-key=true
После этого werf останется запущенной в терминале и будет следить за изменениями в репозитории. Попробуйте внести какие-нибудь изменения в код проекта и посмотрите, как werf на них отреагирует.

Выводы​

Вот такой интересный опыт мы получили в процессе поиска оптимального баланса затраты/эффективность. Тема довольно непростая: чтобы самостоятельно использовать такую практику, нужно обладать базовыми знаниями в работе с Kubernetes и написанию Helm-чартов. Надеюсь, что вы не зря потратили своё время и, читая этот материал, получили такое же удовольствие, как и я в процессе его написания.

Выводы​

Читайте также в нашем блоге:


 
Сверху