Сам Хельм в общих чертах рассмотрели в посте Helm: Kubernetes package manager — обзор, начало работы — теперь надо прикрутить его в Jenkins. И не просто прикрутить его вызов — а создать чарт, потому что сейчас приложение деплоится через «голые» манифест-файлы Kubernetes, в котором sed проставляет теги Докер-образа и значения переменных для загрузки на окружение через kubectl apply -f.
Данный пост очередной НЕ-HowTo, а пример знакомства с Helm: возьмём существующий проект, который уже деплоится в Kubernetes-кластер, обновим его манифесты, что бы использовать их с Helm, продумаем — как именно и куда будем деплоить, и создадим Jenkins-джобу.
По ходу дела будем ближе знакомиться с Helm, и его подводными камнями.
Итак, у нас имеется некое веб-приложение, которое надо деплоить в Kubernetes.
Само приложение разбито на две части — фронтенд на React, хостится и деплоится в AWS S3, и бекенд — NodeJS, работает в Docker-контейнере.
Первый вопрос, который появляется в голове — монорепа, или нет?
Т.е. — где хранить код приложения, где — файлы чартов Helm-а, а где — jenkins-файлы?
Хотелось бы монорепу, и всё держать в одном репозитории, но у нас подобных проектов будет не один, и смысла под каждый писать отдельные дженкис-файлы нет. Тем более, что если (когда!) захочется что-то изменить в одном — то придётся делать это во всех по отдельности.
При этом и их файлы манифестов для Kubernetes будут практически одинаковы — и можно было бы заморочится вообще, и используя возможности шаблонизатора Go — использовать один общий чарт на всех.
Но так как их одинаковость пока под вопросом — запускаемые веб-проекты ещё только разрабатываются, да ещё и отдельными группами девелоперов — то чарты будем делать под каждый проект отдельно.
Значит, у нас будет:
отдельный репозиторий с кодом проекта
там же в отдельной папке храним файлы чартов
отдельный репозиторий с файлами для Jenkins-pipeline
Хочется прикрутить ещё и релизы, да ещё и связать их с Github Releases — но это, может быть, потом. В целом — реализуемо, и даже есть helm/chart-releaser.
Jenkins build steps
Дальше.
Билд Docker-образов — и билд/деплой самих чартов — делать одной джобой — или разными?
Сейчас у нас образы собираются в одной джобе, после чего пушатся в DockerHub, после чего девелопер должен сходить в джобу деплоймента, и при запуске в параметрах указать тег (номер билда) + дополнительные параметры:
Это, конечно, ни разу неудобно, поэтому переделаем.
Опционально — удалять с chart uninstall. Про удаление ресурсов вообще поговорим ниже.
Что ещё?
Namespaces
Ооо — нейспейсы жеж!
Что хочется:
иметь возможность деплоить по одной кнопке в какой-то дефолтный namespace, имя которого зависит от проекта/приложения/чарта
деплоить в кастомный namespace — по имени бранча репозитория, из которого запускался билд
деплоить в вообще кастомный namespace — дать возможность указать его при старте джобы
А как их потом удалять?
Можно добавить вебхук из Гитхаба при delete-операциях, но во-первых часть проектов хостятся в Gitlab — хотя он тоже умеет в вебхуки. Но во-вторых — а все ли девелоперы всех команд будут удалять свои закрытые бранчи?
Т.е. тут проблема как раз из-за разных команд и разных проектов, а решение хочется иметь маскимально универсальное.
А удалять НС-ы надо — иначе будет пачка заброшенных ресурсов — подов, ингресов, волюмов, и т.д.
Что делать?…
Окей. А пусть будет какой-то дефолтный неймспейс. Например, чарт/приложение называются bttrm-apps — значит, для Dev-деплой джобы НС будем создавать bttrm-apps-dev-ns.
В джобе добавить параметр, в нём сделать возможность выбора — использовать этот дефолтный НС, создать/использовать НС по имени бранча, или указать свой.
Можно так в Jenkins?
Не факт — идём глянем.
Choise parameter — тут просто задаётся список предефайненных значений — нельзя будет задать своё имя НС-а:
А Multi string parameter?
Вроде как что-то аналогичное Choise parameter?
Смотрим на него:
Нет, тоже не подходит — оно просто из параметра передаст нам в переменную дженкисфайла значение из нескольких строк, а нам нужна одна string.
Значит — или таки обычный String параметр, или Choise parameter с набором уже заданных значений… Но тогда потеряем возможность создать кастомный НС…
Или… Или… Или… Мысль…
А что на счёт Boolien parameter?
Типа «поменять НС»…
Дефолтное значение «не менять» — и используем предефайненное значение bttrm-apps-dev-1-ns…
А если юзер выбирает менять — то используем имя НС как имя бранча — а имя бранча вытянем из репозитория после чекаута…
Мысль? А может быть!
Хотя снова-таки — лишаемся возможности создать кастомный НС…
А что если просто использовать тот же Choise parameter с набором уже заданных значений, но создать три значения:
use default bm-apps-dev-ns
use github-branch-name
set own name
А потом в коде самого пайплайна проверять выбор, и если выбрано set own name — то просто запросить User input, и пользователь задаст имя неймспейса, который хочет создать.
Тоже не самое кошерное решение — но может сработать. Пока остановимся на нём.
Остаётся открытым вопрос про удаление НС-ов — будем думать потом 🙂
Хотя потом может быть больно.
В конце-концов — мы спокойно можем раз в месяц удалять весь Dev-кластер, и провиженить заново — дело 20 минут и одного клика в Jenkins, а для Stage/Prod деплоев будут использоваться только жёстко заданные неймспейсы, потому там такой проблемы не будет (но это не точно).
Окей, make sense.
Что дальше?
Дальше локально сделаем:
создадим чарт для приложения — используем реальное, но отбранчуемся, что бы не мешать девелоперам.
настроим kubectl на новый Dev-кластер
выполним helm install
Потом пойдём делать джобу в дженкисе.
Джоба будет клонировать репозиторий с приложением, и делать helm install в нужный namespace.
А релиз-версии нужны судя по всему только если мы будем заливать чарты в виде архивов в свой Helm-репозиторий — в самом Кубере они никак не применяются…
Значит — пока этот вопрос отложим, потом вернёмся к chart-releaser.
Поехали тестить?
Helm test deploy
kubectl config
У меня есть новый тестовый кластер bttrm-eks-dev-1 — создаём для него конфиг.
This is a known limitation of Helm. Helm will not do cross-namespace management, and we do not recommend setting namespace: directly in a template.
If you want to install resources in a namespace other than the default namespace, helm install —namespace=foo will install the resources in that namespace. If the namespace does not exist, it will create it.
Эээ? Так мы ж так и делали…
Читаем ниже:
For Helm 3, the namespace creation doesn’t happen during helm install any more, so you’ll have to create that namespace ahead of time as you do today.
For Helm 3, the namespace creation doesn’t happen during helm install any more, so you’ll have to create that namespace ahead of time as you do today.Right now, use kubectl create namespace foo prior to helm install. When Helm 3.2.0 is released, use helm install … —create-namespace.
Окей, вроде работает — можно думать за Helm chart для самого приложения.
Хотя тут можно начать наоборот с Jenkins-джобы и скрипта, и использовать тестовый чарт, а потом создавать чарт реального приложения.
Но давайте сначала всё-таки приведём в рабочий вид приложение.
Helm — создание чарта
Сейчас у нас файлы для Kubernetes лежат в отдельном репозитории devops, а код приложения — в репозитории самого приложения.
Что надо сделать — это создать чарт в репозитории с приложением — в этом репозитории будет и Dockerfile, там же будем хранить Chart.yaml.
Скрипты для Jenkins (jekinsfiles) хранятся в ещё одном отдельном репозитории для общего доступа из различных, но однотипных джоб.
What about RBAC?
Кстати. Задумался я о том, как мы будем деплоить с Jenkins и о правах доступа.
Сам Jenkins использует свою EC2 Instance Profile, которая даёт ему полный доступ к кластерам. Ту же роль использую сейчас я при AssumeRole, которую мы настроили выше в ~/.aws/config.
А что с девелоперами?
У них-то создаются отдельные RoleBinding из определённых неймспейсов, в которые им разрешается доступ, а не глобальный рутовый доступ, как у меня.
Значит — эти RoleBinding надо включить в чарт, и создавать их в тех неймспейсах, которые будут создавать при деплое чарта из Jenkins в — напомню — том числе и кастомные неймспейсы.
Как быть? Делать RBAC сабчартом?
Вопрос… Да и для самого Helm, по-хорошему, надо создавать отдельный Kubernetes ServiceAccount при создании кластера.
Хотя — зачем нам выносить доступы юзеров в отдельный чарт или сабчарт?
Там всего-то создать одну RoleBinding в нужном неймпсейсе. А «глобальная» ClusterRole создаётся при провижене самого кластера из Ansible (потом надо будет переписать на Helm, и все RBAC-ресурсы через него деплоить).
Собственно, как говорилось в начале — все КАПСы вырезаются в Jenkins с помощью sed, и меняются на реальные значения, полученные из параметров джобы, а потом выполняется kubectl apply -f.
spec: replicas: DEPLOY_REPLICAS_NUM
вот — тут уже пора в values.yaml идти, добавляем в нём replicaCount: 2
а в деплойменте используем replicas: {{ .Values.replicaCount }}, см. пример ниже
selector: matchLabels: app: bttrm-apps-EKS_ENV
меняем на application: {{ .Chart.Name }}
Сначала думал в имя деплоймента включить .Chart.Version или .Chart.AppVersion — но тут у Хельма всё оказалось через… Печаль какую-то, т.к. задать их значения во время helm install нельзя — только во время package, см. https://github.com/helm/helm/issues/3555 (с 2018 обсуждают!).
Ладно — оставим на совести разработчиков — жираф большой, ему виднее. Пока в имени оставлю {{ .Chart.Name }} — по ходу дела посмотрим. Да и не факт, что использовать версии в имени хорошая идея.
Получится строка типа bttrm/bttrm-apps:TAG, а TAG думаю формировать при сборке образа из $BUILD_NUMBER + $GIT_COMMIT, т.е. получим что-то вроде bttrm/bttrm-apps:123.67554433.
Значения для плейсхолдеров типа CLIENT_HOST_VAL в основном будут задаваться из параметров джобы Дженкинса, но добавляем их в values тоже. Где можно — задаём дефолтные значения, где нет — просто «».
Пока, кстати, вообще все значения можно задать в values — до Jenkins-джобы ещё будем тестить локально.
Кавычки
А что там с кавычками, кстати?
Тоже мрак)
С одной стороны — документация по созданию чарта (тыц>>>) и по переменным (тыц>>>) говорит использовать quote-функцию, которая будет «оборачивать» наши значения в кавычки.
При этом, если посмотреть файлы, сгенерированные при helm create — то там нет ни quote, ни кавычек — в большинстве случаев. Но не везде 🙂
YAML’s type coercion rules are sometimes counterintuitive. For example, foo: false is not the same as foo: «false». Large integers like foo: 12345678 will get converted to scientific notation in some cases.The easiest way to avoid type conversion errors is to be explicit about strings, and implicit about everything else. Or, in short, quote all strings.
Мрак!
Ладно — в values.yaml стринги заключаем в кавычки, целочисленные — без кавычек.
В файле шаблонов… Я не знаю 🙂
Использовать quote — тогда само значение в переменных будет с кавычками. Посмотрел, как приложение запущено сейчас — без кавычек:
kk -n bttrm-apps-stage-ns describe pod bttrm-apps-stage-7c5cfd9698-tkmjp | grep CLIENT_HOST
CLIENT_HOST: https://test.example.com
Значит пока без quote, и без явного указания «» в шаблоне вообще.
ReadString: expects " or n, but found 3, error found in #10 byte of ...|,"value":3001}
...
Ах ты ж…
Вот честно — я тут очень матерился.
Может я не догоняю чего-то, но ты же мне везде в документации говоришь, что Integer без кавычек кушаешь нормально?
Что ж ты теперь ругаешься на цифры?
Про то, как Go вообще ошибки выводит промолчим — в них часто сложно что-то понять.
Но тут в принципе видно значение, вызвавшее ошибку — 3001:
v1.PodSpec.Containers: []v1.Container: v1.Container.Env: []v1.EnvVar: v1.EnvVar.Value: ReadString: expects » or n, but found 3, error found in #10 byte of …|,»value»:3001}
Непонятно, конечно, где именно — но так как порты в деплойменте встречаются не так часто — то просто добавляем кавычки для всех, меняем:
Release "bttrm-apps-backend-release" has been upgraded. Happy Helming!
NAME: bttrm-apps-backend-release
LAST DEPLOYED: Sun May 10 14:54:57 2020
NAMESPACE: bttrm-apps-dev-1-ns
STATUS: deployed
REVISION: 2
TEST SUITE: None
Евенты:
kk -n bttrm-apps-dev-1-ns get event
LAST SEEN TYPE REASON OBJECT MESSAGE
47s Normal Scheduled pod/bttrm-apps-backend-69d74849df-nvtt2 Successfully assigned bttrm-apps-dev-1-ns/bttrm-apps-backend-69d74849df-nvtt2 to ip-10-3-33-123.us-east-2.compute.internal
15s Warning FailedMount pod/bttrm-apps-backend-69d74849df-nvtt2 MountVolume.SetUp failed for volume "apple-keys" : secret "apple-keys" not found
15s Warning FailedMount pod/bttrm-apps-backend-69d74849df-nvtt2 MountVolume.SetUp failed for volume "apple-certificates" : secret "apple-certificates" not found
47s Normal SuccessfulCreate replicaset/bttrm-apps-backend-69d74849df Created pod: bttrm-apps-backend-69d74849df-nvtt2
...
Ага!
Всё создалось.
Теперь поды не стартуют, потому что не файлов сертификатов для Apple — но это уже детали, сейчас их создадим.
Helm secrets from files
Добавим файлы сертификата — в Jenkins они будут маунтиться из Credentials типа Secret file, тут добавляем в текущий репозиторий:
ll bttrm-apps-be/secrets/
total 12
-rw-r--r-- 1 setevoy setevoy 258 May 10 17:32 AppleAuth.key.p8
-rw-r--r-- 1 setevoy setevoy 3088 May 10 17:27 ApplePay.crt.pem
-rw-r--r-- 1 setevoy setevoy 2006 May 10 17:28 ApplePay.key.pem
Добавляем в ../.gitignore репозитория и в .helmignore чарта:
неймспейс с именем бранча — APP_NAME + CLUSTER_ENV + GIT_BRANCH_NAME + «NS», т.е. bttrm-apps-dev-1-WLIOS-5848-Projects-deployments
кастомный неймспейс, задаётся юзером
Есть сомнения по второму пункту — bttrm-apps-dev-1-WLIOS-5848-Projects-deployments — не слишком ли длинно?
Загуглим «kubernetes namespace best practices», но лучшее, что нашлось — это вот эта запись в блоге, в которой говорится:
An easy to grasp anti-pattern for Kubernetes namespaces is versioning. You should not use Namespaces as a way to disambiguate versions of your Kubernetes resources
Namespace names are DNS1123 Labels: maximum 63 characters
…
You may want to populate generateName with a prefix, and let kubernetes make a unique suffix.
Хорошо — раз лимит в 63 символа — мы можем в дженкисфайле добавить хендлер, который будет проверять имя неймспейса, переданного из Jenkins-парамтеров, и при необходимости обрезать его.
Весь билд будет выполняться нашим кастомным Docker-образом kubectl-aws, собирается из такого Dockerfile:
FROM bitnami/minideb:stretch
RUN apt update && install_packages ca-certificates wget
RUN install_packages curl python-pip python-setuptools jq git
RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
RUN chmod +x ./kubectl
RUN mv ./kubectl /usr/local/bin/kubectl
WORKDIR /tmp
RUN curl --location "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz" | tar xz -C /tmp
RUN mv /tmp/eksctl /usr/local/bin
RUN pip install ansible boto3 awscli
WORKDIR /tmp
RUN curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3
RUN /bin/bash get_helm.sh
USER root
Пока все значения тпиа Github URL и имени бранча в скрипте билда хардкодим — потом вынесем в параметры джобы.
Запускаем, проверяем — всё работает.
Пошли дальше.
Docker build
Далее нам надо собрать Docker-образ, и запушить его в репозиторий.
Добавлять ли тег latest? Вопрос.
Наверно нет — билдов может быть много, из разных бранчей — перезаписывать каждый раз latest смысла нет, тем более деплоится в Dev окружение всё будет автоматом при создании пул-реквестов, см. Jenkins: Github Pull-Request Builder плагин.
Значит — делаем только docker build с тегом из переменной $DOCKER_TAG.
Создаём данные доступа для DockerHub:
Добавляем новый стейдж «Docker build», в нём с помощью Docker-плагина для Jenkins собираем образ, для логина в DockerHub через credentialsId передаём созданные выше данные:
Дальше генерируем пакет. Пока надо только для того, что бы можно было задать release verion и app-version. Позже хочется добавить их пуш куда-то в репозиторий — или Github, или S3-backended.