Helm: пошаговое создание чарта и деплоймента из Jenkins

Автор: | 14/05/2020
 

Продолжаем трогать Kubernetes и Helm.

Сам Хельм в общих чертах рассмотрели в посте Helm: Kubernetes package manager — обзор, начало работы – теперь надо прикрутить его в Jenkins. И не просто прикрутить его вызов – а создать чарт, потому что сейчас приложение деплоится через “голые” манифест-файлы Kubernetes, в котором sed проставляет теги Докер-образа и значения переменных для загрузки на окружение через kubectl apply -f.

Данный пост очередной НЕ-HowTo, а пример знакомства с Helm: возьмём существующий проект, который уже деплоится в Kubernetes-кластер, обновим его манифесты, что бы использовать их с Helm, продумаем – как именно и куда будем деплоить, и создадим Jenkins-джобу.

По ходу дела будем ближе знакомиться с Helm, и его подводными камнями.

В общем, ещё один пример того, как очередная задача из смутной идеи в голове превращается в рабочий сервис – длиннопост в вольном стиле, аналог AWS Elastic Kubernetes Service: автоматизация создания кластера, часть 1 — CloudFormation и AWS Elastic Kubernetes Service: — автоматизация создания кластера, часть 2 — Ansible, eksctl, где шаг за шагом реализуем свою задумку.

Планирование

Репозитории

Итак, у нас имеется некое веб-приложение, которое надо деплоить в Kubernetes.

Само приложение разбито на две части – фронтенд на React, хостится и деплоится в AWS S3, и бекенд – NodeJS, работает в Docker-контейнере.

Первый вопрос, который появляется в голове – монорепа, или нет?

Т.е. – где хранить код приложения, где – файлы чартов Helm-а, а где – jenkins-файлы?

Хотелось бы монорепу, и всё держать в одном репозитории, но у нас подобных проектов будет не один, и смысла под каждый писать отдельные дженкис-файлы нет. Тем более, что если (когда!) захочется что-то изменить в одном – то придётся делать это во всех по отдельности.

При этом и их файлы манифестов для Kubernetes будут практически одинаковы – и можно было бы заморочится вообще, и используя возможности шаблонизатора Go – использовать один общий чарт на всех.

Но так как их одинаковость пока под вопросом – запускаемые веб-проекты ещё только разрабатываются, да ещё и отдельными группами девелоперов – то чарты будем делать под каждый проект отдельно.

Значит, у нас будет:

  • отдельный репозиторий с кодом проекта
    • там же в отдельной папке храним файлы чартов
  • отдельный репозиторий с файлами для Jenkins-pipeline

Хочется прикрутить ещё и релизы, да ещё и связать их с Github Releases – но это, может быть, потом. В целом – реализуемо, и даже есть /chart-releaser.

Jenkins build steps

Дальше.

Билд Docker-образов – и билд/деплой самих чартов – делать одной джобой – или разными?

Сейчас у нас образы собираются в одной джобе, после чего пушатся в DockerHub, после чего девелопер должен сходить в джобу деплоймента, и при запуске в параметрах указать тег (номер билда) + дополнительные параметры:

Это, конечно, ни разу неудобно, поэтому переделаем.

Итак, будет один пайплайн, в котором нам надо:

  1. чекаут кода проекта
  2. линт чарта
  3. сгенерить kubeconfig
  4. helm install example-chart --dry-run --debug example-chart/
  5. helm install example-chart example-chart/
  6. тест деплоя – helm get manifest example-chart

Опционально – удалять с 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.


Что дальше?

Дальше локально сделаем:

  1. создадим чарт для приложения – используем реальное, но отбранчуемся, что бы не мешать девелоперам.
  2. настроим kubectl на новый Dev-кластер
  3. выполним helm install

Потом пойдём делать джобу в дженкисе.

Джоба будет клонировать репозиторий с приложением, и делать helm install в нужный namespace.

А что с релизами-то? Го, почитаем Charts and Versioning.

А релиз-версии нужны судя по всему только если мы будем заливать чарты в виде архивов в свой Helm-репозиторий – в самом Кубере они никак не применяются…

Значит – пока этот вопрос отложим, потом вернёмся к chart-releaser.

Поехали тестить?

Helm test deploy

kubectl config

У меня есть новый тестовый кластер bttrm-eks-dev-1 – создаём для него конфиг.

Генерим конфиг, создаём новый контекст с именем arseniy@bttrm-eks-dev-1, см. Kubernetes: kubectl и kubeconfig — обзор файла, добавление кластера, пользователя и контекста:

[simterm]

$ aws --profile arseniy eks update-kubeconfig --name bttrm-eks-dev-1 --alias arseniy@bttrm-eks-dev-1
Added new context arseniy@bttrm-eks-dev-1 to /home/setevoy/.kube/config

[/simterm]

Создаём тестовый чарт:

[simterm]

$ cd /tmp/
$ helm create deployment-testing-chart
Creating deployment-testing-chart

[/simterm]

Helm: the server has asked for the client to provide credentials

Пробуем залить в default namespace:

[simterm]

$ helm install deployment-testing-chart --dry-run deployment-testing-chart/
Error: Kubernetes cluster unreachable: the server has asked for the client to provide credentials

[/simterm]

WTF?

Проверяем контекст:

[simterm]

$ kk config current-context 
arseniy@bttrm-eks-dev-1

[/simterm]

(kk – алиас для kubectl в ~/.bashrc: alias kk="kubectl")

Всё верно…

А доступ есть?

[simterm]

$ kk get pod
error: You must be logged in to the server (Unauthorized)

[/simterm]

Нет.

В автоматизации создания кластера не добавлен мой IAM user…

Что там вообще? Читаем пост AWS Elastic Kubernetes Service: RBAC-авторизация через AWS IAM и RBAC группы, вспоминаем – делал через роли, отдельных пользователей в aws-auth ConfigMap уже не добавляю.

Надо локально настроить AssumeRole.

Надеюсь я прописал в полиси этой роли доступ для моего юзера…

Пробуем – обновляем ~/.aws/config:

...

[profile iam-bttrm-eks-root]
role_arn = arn:aws:iam::534***385:role/iam-bttrm-eks-root-role
source_profile = arseniy
region = us-east-2

Удаляем созданный контекст:

[simterm]

$ kk config delete-context arseniy@bttrm-eks-dev-1
warning: this removed your active context, use "kubectl config use-context" to select a different one
deleted context arseniy@bttrm-eks-dev-1 from /home/setevoy/.kube/config

[/simterm]

Генерим новый:

[simterm]

$ aws --profile iam-bttrm-eks-root eks update-kubeconfig --name bttrm-eks-dev-1 --alias iam-bttrm-eks-root@bttrm-eks-dev-1
Updated context iam-bttrm-eks-root@bttrm-eks-dev-1 in /home/setevoy/.kube/config

[/simterm]

Проверяем доступ:

[simterm]

$ kk get po
NAME                                 READY   STATUS    RESTARTS   AGE
reloader-reloader-55448df76c-wdzqp   1/1     Running   0          28h

[/simterm]

(про reloader – см. Kubernetes: ConfigMap и Secrets — auto-reload данных в подах)

Окей – возвращаемся к хельму:

[simterm]

$ helm install deployment-testing-chart --dry-run deployment-testing-chart/
NAME: deployment-testing-chart
LAST DEPLOYED: Thu May  7 20:06:44 2020
NAMESPACE: default
STATUS: pending-install
...

[/simterm]

Найс!

Helm – deploy to  a Namespace

А что с деплоем в кастомный неймспейс?

[simterm]

$ helm install deployment-testing-chart --dry-run deployment-testing-chart/ --namespace deployment-testing-chart-ns
NAME: deployment-testing-chart
LAST DEPLOYED: Thu May  7 20:08:00 2020
NAMESPACE: deployment-testing-chart-ns
STATUS: pending-install
...

[/simterm]

Тоже вроде ОК.

Helm и неймспейс сам создаёт? Не верится…

Попробуем реальный запуск, без --dry-run:

[simterm]

$ helm install deployment-testing-chart deployment-testing-chart/ --namespace deployment-testing-chart-ns
Error: create: failed to create: namespaces "deployment-testing-chart-ns" not found

[/simterm]

Нет, не создаёт)

Окей – это хорошо

Гуглим – понятное дело, что до нас люди с таким уже сталкивались, например – https://github.com/helm/helm/issues/4456, и там комментарий https://github.com/helm/helm/issues/4456#issuecomment-412134651:

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.

Ага!

И ещё ниже – https://github.com/helm/helm/issues/4456#issuecomment-607927999:

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.

А у нас какая?

[simterm]

$ helm version
version.BuildInfo{Version:"v3.2.0", ...

[/simterm]

Ага!

Пробуем:

[simterm]

$ helm install deployment-testing-chart deployment-testing-chart/ --namespace deployment-testing-chart-ns --create-namespace
NAME: deployment-testing-chart
LAST DEPLOYED: Thu May  7 20:15:38 2020
NAMESPACE: deployment-testing-chart-ns
STATUS: deployed
REVISION: 1
...

[/simterm]

Бимба!

Проверяем НСы:

[simterm]

$ kk get ns
NAME                          STATUS   AGE
bttrm-apps-dev-1-ns           Active   28h
bttrm-workouts-dev-1-ns       Active   28h
default                       Active   28h
deployment-testing-chart-ns   Active   19s
...

[/simterm]

Няшка.

Сервисы:

[simterm]

$ kk -n deployment-testing-chart-ns get svc    
NAME                       TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
deployment-testing-chart   ClusterIP   172.20.227.114   <none>        80/TCP    2m1s

[/simterm]

Окей, вроде работает – можно думать за 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-ресурсы через него деплоить).

Окей – значит так и сделаем.

Создание чарта

Поехали – клонируем репозиторий, создаём каталог, создаём чарт:

[simterm]

$ mkdir k8s
$ cd k8s/
$ helm create bttrm-apps-backend
Creating bttrm-apps-backend

[/simterm]

Удаляем шаблоны, копируем из старого репозитория:

[simterm]

$ cd bttrm-apps-backend
$ rm -rf templates/*
$ cp ~/Work/devops/projects/EKS/roles/eks/templates/bttrm-apps/* templates/

[/simterm]

Что у нас тут есть:

[simterm]

$ ls -1 templates/
bttrm-apps-deployment.yaml
bttrm-apps-ns-ingress-hpa.yaml
bttrm-apps-secrets.yaml

[/simterm]

Посмотрим файл деплоймента:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: bttrm-apps-EKS_ENV
  annotations:
    secret.reloader.stakater.com/reload: "bttrm-docker-secret"
spec:
  replicas: DEPLOY_REPLICAS_NUM
  strategy:
    type: RollingUpdate
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      app: bttrm-apps-EKS_ENV
  template:
    metadata:
      labels:
        app: bttrm-apps-EKS_ENV
    spec:
      containers:
      - name: bttrm-apps-EKS_ENV
        image: bttrm/bttrm-apps:IMAGE_TAG
        env:
        - name: CLIENT_HOST
          value: CLIENT_HOST_VAl
        - name: DATABASE_HOST
          value: DATABASE_HOST_VAL
...

Собственно, как говорилось в начале – все КАПСы вырезаются в Jenkins с помощью sed, и меняются на реальные значения, полученные из параметров джобы, а потом выполняется kubectl apply -f.

Как-то так:

...
        dir ('eks') {
            git branch: "${DEVOPS_REPO_BRANCH}", url: "${DEVOPS_REPO_URL}", credentialsId: "jenkins-github"

            def templates_path = "projects/EKS/roles/eks/templates/bttrm-apps" 

            // .env
            sed_env("$templates_path","bttrm-apps-deployment.yaml","BACKEND_HOST_VALUE","$BACKEND_HOST")
            sed_env("$templates_path","bttrm-apps-deployment.yaml","CLIENT_HOST_VAl","$CLIENT_HOST")
            sed_env("$templates_path","bttrm-apps-deployment.yaml","DEPLOY_REPLICAS_NUM","$DEPLOY_REPLICAS_NUM")
...

Ужас, правда?)

Для тестов было ОК, давайте приводить в порядок.

Сначала во всех файлах удаляем все metadata: namespace: – неймспейс будет определяться самим Хельмом.

Удаляем файл values.yaml, который сгененрировал Helm во время инициализации чарта, создаём новый файл, пустой.

Открываем в одном окне values.yaml, в другом – файл деплоймента, начинаем обновлять:

  • kind: Deployment metadata: name: bttrm-apps-EKS_ENV
    испольуем {{ .Chart.Name }}
  • 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 }} – по ходу дела посмотрим. Да и не факт, что использовать версии в имени хорошая идея.

По теме:

Пока в лейблах сделал так:

...
  selector:
    matchLabels:
      application: {{ .Chart.Name }}
  template:
    metadata:
      labels:
        application: {{ .Chart.Name }}
        version: {{ .Chart.Version }}-{{ .Chart.AppVersion }}
        managed-by: {{ .Release.Service }}-{{ .Release.Name }}
...

{{ Chart.version }} и {{ .Chart.appVersion }} будет задаваться в Jenkins, пока будем брать дефолтные значения из Chart.yaml.

Далее, для выбора Docker-образа надо в values задать имя образа и тег.

Мы сейчас хостим образы в DockerHub, но потом думаю переезжать на AWS Elastic Container Registry, поэтому сразу пусть будет с возможностью поменять.

Заполняем values.yaml – добавляем image:

replicaCount: 2
    
image:
  repository: bttrm
  name: bttrm-apps
  tag: ""

Получится строка типа bttrm/bttrm-apps:TAG, а TAG думаю формировать при сборке образа из $BUILD_NUMBER + $GIT_COMMIT, т.е. получим что-то вроде bttrm/bttrm-apps:123.67554433.

Дальше у нас идёт пачка переменных:

...
        env:
        - name: CLIENT_HOST
          value: CLIENT_HOST_VAL
        - name: DATABASE_HOST
          value: DATABASE_HOST_VAL
        - name: DB_USERNAME
          value: DB_USERNAME_VAL
...

Значения для плейсхолдеров типа CLIENT_HOST_VAL в основном будут задаваться из параметров джобы Дженкинса, но добавляем их в values тоже. Где можно – задаём дефолтные значения, где нет – просто “”.

Пока, кстати, вообще все значения можно задать в values – до Jenkins-джобы ещё будем тестить локально.

Кавычки

А что там с кавычками, кстати?

Тоже мрак)

С одной стороны – документация по созданию чарта (тыц>>>) и по переменным (тыц>>>) говорит использовать quote-функцию, которая будет “оборачивать” наши значения в кавычки.

При этом, если посмотреть файлы, сгенерированные при helm create – то там нет ни quote, ни кавычек – в большинстве случаев. Но не везде 🙂

Например, для:

...
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
...

Кавычки есть. Почему? Потому что “составная строка” – конкатенируется из нескольких? Окей, допустим.

Но почему:

...
      containers:
        - name: {{ .Chart.Name }}
...

Без кавычек, если документация говорит их использовать? Потому что это данные из Chart.yaml, а не values.yaml?

В чём принципиальная разница?

Ну и сам YAML, как всегда – “радует”, читаем в той же документации по Values – Make Types Clear:

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 – тогда само значение в переменных будет с кавычками. Посмотрел, как приложение запущено сейчас – без кавычек:

[simterm]

$ kk -n bttrm-apps-stage-ns describe pod bttrm-apps-stage-7c5cfd9698-tkmjp | grep CLIENT_HOST
      CLIENT_HOST:                          https://test.example.com

[/simterm]

Значит пока без quote, и без явного указания “” в шаблоне вообще.

(у нас тут Stage, который pre-Dev даже)

values.yaml

Получается такой файл:

replicaCount: 2
    
image:
  repository: "bttrm"
  name: "bttrm-apps"
  tag: ""

# backend app config
backendConfig:
  clientHost: "https://test.example.com"
  nodeEnv: "dev"
  host: "https://test.api.example.com"
  port: 3001
  database:
    host: "stage.aurora.example"
    user: "dbuser"
    password: "dbpass"
    connection: "mysql"
    port: 3306

Passwords… Secrets…

Пароли пока хардкодим – потом будем передавать из Credentials parameter в Jenkins-джобе.

А ещё лучше – таки добавить helm secrets – но это потом (добавлено – см. Helm: helm-secrets — шифрование sensitive данных с AWS KMS и деплой из Jenkins)

Секреты

Так, секреты.

В деплойменте передаём секретами, секреты создаём из файла bttrm-apps-secrets.yaml.

Сейчас он выглядит так же, как деплоймент – обрабатывается с sed в старой джобе Jenkins:

---
apiVersion: v1
kind: Secret
metadata:
  name: bttrm-app-secret
type: Opaque
stringData:
  backend-db-password: DB_PASSWORD_VAL
...

В Деплойменте маунтим из секретов:

...
    spec:
      containers:
      - name: {{ .Chart.Name }}
        image: {{ .Values.image.repository }}/{{ .Values.image.name }}:{{ .Values.image.tag }}
        env:
        ...
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: bttrm-app-backend-secret
              key: backend-db-password
...

Обновляем секрет – передаём из values:

--- 
apiVersion: v1
kind: Secret
metadata:
  name: bttrm-app-backend-secrets
type: Opaque
stringData:
  backend-db-password: {{ .Values.backendConfig.db.password }}
  backend-apple-cert-passphrase: {{ .Values.backendConfig.apple.certPassphrase }}
  backend-apple-sigin-key-id: {{ .Values.backendConfig.apple.signInKeyID }}

chart install

Ну и что – пробуем?

[simterm]

$ helm install --debug bttrm-apps-backend-release bttrm-apps-backend
install.go:159: [debug] Original chart version: ""
install.go:176: [debug] CHART PATH: /home/setevoy/Work/landing-backend/k8s/bttrm-apps-backend

client.go:108: [debug] creating 6 resource(s)
Error: Deployment in version "v1" cannot be handled as a Deployment: 
v1.Deployment.Spec: v1.DeploymentSpec.Template: v1.PodTemplateSpec.Spec: 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}
...

[/simterm]

Ах ты ж…

Вот честно – я тут очень матерился.

Может я не догоняю чего-то, но ты же мне везде в документации говоришь, что 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}

Непонятно, конечно, где именно – но так как порты в деплойменте встречаются не так часто – то просто добавляем кавычки для всех, меняем:

...
        - name: DB_PORT
          value: {{ .Values.backendConfig.port }} 
...

На:

...
        - name: DB_PORT
          value: {{ .Values.backendConfig.db.port | quote }}
...

При этом в ports: - containerPort: на кавычки он ругается.

А в livenessProbe: port: при --dry-run не ругается. А при install – ругается.

Рукалицо. Ладно.

Деплоим ещё раз:

[simterm]

$ helm install --debug bttrm-apps-backend-release bttrm-apps-backend/ 
install.go:159: [debug] Original chart version: ""                                                                                                                                                                                            
install.go:176: [debug] CHART PATH: /home/setevoy/Work/landing-backend/k8s/bttrm-apps-backend                                                                                                                                  

client.go:108: [debug] creating 6 resource(s)                                                                                                                                                                                                 
NAME: bttrm-apps-backend-release                                                                                                                                                                                                                   
LAST DEPLOYED: Sun May 10 14:35:37 2020                                                                                                                                                                                                       
NAMESPACE: default                                                                                                                                                                                                                            
STATUS: deployed                                                                                                                                                                                                                              
REVISION: 1
...

[/simterm]

Йай!

Проверяем:

[simterm]

$ helm ls
NAME                    NAMESPACE       REVISION        UPDATED                                         STATUS          CHART                   APP VERSION
bttrm-apps-backend-release   default         1               2020-05-10 14:35:37.364401788 +0300 EEST        deployed        bttrm-apps-backend-0.1.0     1.16.0

[/simterm]

Ну – бимба!

Оно 100% не работает, но нам важен сам файкт, что оно задеплоилось:

[simterm]

$ kk get pod
NAME                                 READY   STATUS    RESTARTS   AGE
bttrm-apps-backend-c4d8f8c5d-98vd2   1/1     Running   0          54s
bttrm-apps-backend-c4d8f8c5d-v6slt   1/1     Running   0          54s
...

[/simterm]

Хорошо.

Удаляем, передеплоим в нормальный НС:

[simterm]

$ helm uninstall bttrm-apps-backend-release
release "bttrm-apps-backend-release" uninstalled

[/simterm]

Деплоим ещё раз, указываем --create-namesapce:

[simterm]

$ helm install bttrm-apps-backend-release bttrm-apps-backend/ --namespace bttrm-apps-dev-1-ns --create-namespace
NAME: bttrm-apps-backend-release
LAST DEPLOYED: Sun May 10 14:45:23 2020
NAMESPACE: bttrm-apps-dev-1-ns
STATUS: deployed
REVISION: 1
TEST SUITE: None

[/simterm]

Проверяем:

[simterm]

$ kk -n bttrm-apps-dev-1-ns get po
NAME                                READY   STATUS    RESTARTS   AGE
bttrm-apps-backend-6c54d5c676-5j6mc   0/1     Pending   0          47s
bttrm-apps-backend-6c54d5c676-vchvh   0/1     Pending   0          47s

[/simterm]

Супер, но почему Pending?

Что-то с реквестами, наверно?

Глянем events:

[simterm]

$ kk -n bttrm-apps-dev-1-ns get event
LAST SEEN   TYPE      REASON                         OBJECT                                      MESSAGE
7s          Warning   FailedScheduling               pod/bttrm-apps-backend-6c54d5c676-5j6mc       0/8 nodes are available: 8 Insufficient cpu.
10s         Normal    NotTriggerScaleUp              pod/bttrm-apps-backend-6c54d5c676-5j6mc       pod didn't trigger scale-up 
(it wouldn't fit if a new node is added): 4 Insufficient cpu

[/simterm]

А почему?

В values.yaml 500 CPU requests, WorkerNodes у нас на ЕС2 t3.medium – у каждого 2000 CPU юнитов…

Проверим реквесты:

[simterm]

$ kk -n bttrm-apps-dev-1-ns describe pod bttrm-apps-backend-6c54d5c676-5j6mc
Name:           bttrm-apps-backend-6c54d5c676-5j6mc
Namespace:      bttrm-apps-dev-1-ns
...
    Host Port:  0/TCP
    Requests:
      cpu:      500
...

[/simterm]

Да, вроде всё верно.

Или реквесты криво описал…

Что там… Как они задаются вообще, где дока?

Вот тут вроде было толково – https://cloud.google.com/blog/products/gcp/kubernetes-best-practices-resource-requests-and-limits.

Хм…

Кавычки и m?

Редактируем values – вместо 500 указываем “500m”:

...
resources:
  requests:
    cpu: "500m"
...

И добавим quote в деплоймент:

...
        resources:
          requests:
            cpu: {{ .Values.resources.requests.cpu | quote }} 
...

Пробуем:

[simterm]

$ helm upgrade bttrm-apps-backend-release bttrm-apps-backend/ --namespace bttrm-apps-dev-1-ns
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

[/simterm]

Евенты:

[simterm]

$ 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
...

[/simterm]

Ага!

Всё создалось.

Теперь поды не стартуют, потому что не файлов сертификатов для Apple – но это уже детали, сейчас их создадим.

Helm secrets from files

Добавим файлы сертификата – в Jenkins они будут маунтиться из Credentials типа Secret file, тут добавляем в текущий репозиторий:

[simterm]

$ 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

[/simterm]

Добавляем в ../.gitignore репозитория и в .helmignore чарта:

...
k8s/bttrm-apps-backend/secrets/

Обновляем bttrm-apps-secrets.yaml – используем Files, см https://helm.sh/docs/chart_template_guide/accessing_files/:

...
---
apiVersion: v1
kind: Secret
metadata:
  name: apple-certificates
type: Opaque
data:
  ApplePay.crt.pem: {{ .Files.Get "secrets/ApplePay.crt.pem" | b64enc }}
  ApplePay.key.pem: {{ .Files.Get "secrets/ApplePay.key.pem" | b64enc }}
---
apiVersion: v1
kind: Secret
metadata:
  name: apple-key
type: Opaque
data:
  AppleAuth.key.p8: {{ .Files.Get "secrets/AppleAuth.key.p8" | b64enc }}

Запускаем:

[simterm]

$ helm upgrade bttrm-apps-be-release bttrm-apps-be/ --namespace bttrm-apps-dev-1-ns
Release "bttrm-apps-be-release" has been upgraded. Happy Helming!
NAME: bttrm-apps-be-release
LAST DEPLOYED: Sun May 10 17:36:47 2020
NAMESPACE: bttrm-apps-dev-1-ns
STATUS: deployed
REVISION: 4
TEST SUITE: None

[/simterm]

Проверяем секреты:

[simterm]

$ kk -n bttrm-apps-dev-1-ns get secret 
NAME                                          TYPE                                  DATA   AGE
apple-certificates                            Opaque                                2      37s
apple-key                                     Opaque                                1      37s

[/simterm]

Но поды серты не увидели, и не запустилиись, так и будут висеть в FailedMount.

Пересоздаём их – вызываем upgrade с --recreate-pods:

[simterm]

$ helm upgrade bttrm-apps-be-release bttrm-apps-be/ --namespace bttrm-apps-dev-1-ns --recreate-pods
Flag --recreate-pods has been deprecated, functionality will no longer be updated. Consult the documentation for other methods to recreate pods
Release "bttrm-apps-be-release" has been upgraded. Happy Helming!
NAME: bttrm-apps-be-release
LAST DEPLOYED: Sun May 10 17:39:39 2020
NAMESPACE: bttrm-apps-dev-1-ns
STATUS: deployed
REVISION: 5
TEST SUITE: None

[/simterm]

Проверяем:

[simterm]

$ helm -n bttrm-apps-dev-1-ns ls    
NAME                    NAMESPACE               REVISION        UPDATED                                         STATUS          CHART                   APP VERSION
bttrm-apps-be-release   bttrm-apps-dev-1-ns     2               2020-05-10 17:44:56.505478002 +0300 EEST        deployed        bttrm-apps-be-0.1.0     1.16.0     

$ kk -n bttrm-apps-dev-1-ns get pod
NAME                                READY   STATUS    RESTARTS   AGE
bttrm-apps-be-dp-6f86d5c7d7-k4rjm   1/1     Running   0          2m
bttrm-apps-be-dp-6f86d5c7d7-ztcvp   1/1     Running   0          83s

[/simterm]

Тут готово.

.dockerconfigjson

Сейчас в секретах файла bttrm-apps-secrets.yaml значение для .dockerconfigjson задан хардкодом:

--- 
apiVersion: v1
kind: Secret
metadata:
  creationTimestamp: null
  name: bttrm-docker-secret
type: kubernetes.io/dockerconfigjson
data:
  .dockerconfigjson: eyJhdXRo[...]
...

Находим пример в документации – Creating Image Pull Secrets.

Обновляем values.yaml – в блок image добавляем registry, username и password, пароль позже будем передавать из Jenkins:

...
image:
  registry: "docker.io" 
  username: "username"
  password: "pass"
  repository: "project"
  name: "bttrm-apps"
  tag: "120"
...

Создаём templates/_helpers.tpl, см. Nested template definitions.

Копируем функцию из примера в Creating Image Pull Secrets, меняем .Values.imageCredentials на наш .Values.image:

{{- define "imagePullSecret" }}
{{- printf "{\"auths\": {\"%s\": {\"auth\": \"%s\"}}}" .Values.image.registry (printf "%s:%s" .Values.image.username .Values.image.password | b64enc) | b64enc }}
{{- end }}

Обновляем секреты – вместо хардкод-строки в base64 вызываем этот самый {{ template "imagePullSecret" . }}:

--- 
apiVersion: v1
kind: Secret
metadata:
  creationTimestamp: null
  name: bttrm-docker-secret
type: kubernetes.io/dockerconfigjson
data:
  .dockerconfigjson: {{ template "imagePullSecret" . }} 
...

Передеплоиваем для проверки, и если всё ОК – то можно приступать в Jenkins.

Jenkins

Поехали к сиаю.

Что нам надо?

Сначала давай подумаем про шаги, которые надо выполнить в джобе:

  1. склонировать репозиторий приложения с helm-чартом, кодом приложения и докерфайлом
  2. билд образа – тегаем Jenkins $BUILD_NUMBER + $GIT_COMMIT
  3. docker push собранного образа, пока в DockerHub
  4. упаковка релиза + задать номера релиза и AppVersion, AppVersion == Docker Tag. т.е. $BUILD_NUMBER + $GIT_COMMIT
    1. позже – релиз в Гитхаб
  5. сгенерить кубконфиг
  6. выполнить helm install

Версинирование

Тут, конечно, по желанию каждого.

Мне пока в голову пришла такая схема:

  • chart release number == Jenkins $BUILD_NUMBER джобы
  • AppVersion == $BUILD_NUMBER + $GIT_COMMIT
  • Docker image tag == $BUILD_NUMBER + $GIT_COMMIT

Неймспейсы опять (снова?)

Что там с НС-ами было?

Давайте вспоминать – день четвёртый, на самом деле.

Хочется давать на выбор три варианта:

  1. дефолтный неймпейс – APP_NAME + CLUSTER_ENV + “NS”, т.е. поулчим bttrm-apps-dev-1ns
  2. неймспейс с именем бранча – APP_NAME + CLUSTER_ENV + GIT_BRANCH_NAME + “NS”, т.е. bttrm-apps-dev-1-WLIOS-5848-Projects-deployments
  3. кастомный неймспейс, задаётся юзером

Есть сомнения по второму пункту – 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

Ну и ладно – версии в НС мы включать и не думали.

Но подсказали в Kubernetes Slack:

Alan J Castonguay  3 days ago

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-парамтеров, и при необходимости обрезать его.

Про удаление НС-ов – потом, потом…

Тестирование

https://dzone.com/articles/easily-automate-your-cicd-pipeline-with-jenkins-he

Хорошая идея запускать тесты во время билда/деплоя – сначала какие-то юнит-тесты после сборки Docker-образа, потом – smoke-тесты после деплоя.

Но добавим потом – достаточно будет дописать функции на их вызов, пока задача запустить билд-деплой вообще.

Поехали?

Jenkins job

Создаём новую джобу, тип пайплайн:

Будем использовать Jenkins Scripted Pipelines.

Теперь поехали по шагам, которые напланировали выше.

Jenkins job stages

Init stage

Начнём с такого себе init-стейджа, который будет клонировать репозиторий с кодом, настраивать kubectl, и определять общие параметры и переменные.

Что нам в нём надо выполнить?

  • клонируем репозиторий
  • генерируем kubeconfig
  • вызываем kubctl cluster-info – для проверки kubeconfig
  • вызываем helm version – для проверки Helm вообще
  • задаём параметры:
    • release version – $BUILD_NUMBER, получаем из самого Jenkins
    • Docker Tag: будет $BUILD_NUMBER + $GIT_COMMIT, приходит в джобу из Git plugin после чекаута
    • AppVersion – $BUILD_NUMBER + $GIT_COMMIT, приходит в джобу из Git plugin после чекаута

Получается такой код:

node {
    docker.image('bttrm/kubectl-aws:4.0').inside('-v /var/run/docker.sock:/var/run/docker.sock') {
        stage('Init') {
            
            gitenv = git branch: "master",
                         credentialsId: 'jenkins-github',
                         url: '[email protected]:example-org/bttrm-apps-backend.git'
        
            GIT_COMMIT_SHORT = gitenv.GIT_COMMIT.take(8)            
        
            RELEASE_VERSION = "${BUILD_NUMBER}"
            DOCKER_TAG = "${BUILD_NUMBER}.${GIT_COMMIT_SHORT}"
            APP_VERSION = "${BUILD_NUMBER}.${GIT_COMMIT_SHORT}"
            
            echo "DOCKER_TAG == ${DOCKER_TAG}"
            echo "APP_VERSION == ${APP_VERSION}"
            echo "RELEASE_VERSION == ${RELEASE_VERSION}"                
                
            sh "aws eks update-kubeconfig --region us-east-2 --name bttrm-eks-dev-1"
            sh "kubectl cluster-info"
            sh "helm version"
            sh "helm -n bttrm-apps-dev-1-ns ls "
        }
    }
}

Весь билд будет выполняться нашим кастомным 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 передаём созданные выше данные:

node {

    docker.image('bttrm/kubectl-aws:4.0').inside('-v /var/run/docker.sock:/var/run/docker.sock') {

        stage('Init') {
            ...            
        }
        
        stage("Docker build") {
            
            withDockerRegistry(registry: [credentialsId: 'bttrm-docker-hub']){
                docker.build("bttrm/bttrm-apps:${DOCKER_TAG}").push()
            }
        }
    }
}

Запускаем, проверяем, всё работает?

Хорошо, что дальше?

helm package

Дальше генерируем пакет. Пока надо только для того, что бы можно было задать release verion и app-version. Позже хочется добавить их пуш куда-то в репозиторий – или Github, или S3-backended.

...
        stage("Helm package") {
            
            sh "helm package k8s/bttrm-apps-be --version ${RELEASE_VERSION} --app-version ${APP_VERSION}"
        }
...

Запускаем – докер-образ собрался, локальный пакет хельм-чарта собрался – всё хорошо:

[simterm]

...
4d1ab3827f6b: Layer already exists
5.872a4dc3: digest: sha256:558e3b0ec0a7c5fc6302adca8321de2a3ee75656202930dcb979960d49273c0b size: 4089
[Pipeline] }
[Pipeline] // withDockerRegistry
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Helm package)
[Pipeline] sh
+ helm package k8s/bttrm-apps-backend --version 5 --app-version 5.5.872a4dc3
Successfully packaged chart and saved it to: /var/lib/jenkins/workspace/DO_EKS/Deployments/bttrm-apps/bttrm-apps-TEST/bttrm-apps-backend-5.tgz
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
$ docker stop --time=1 023b7970d36e20b85912062b35bc7aa4136af7807b975a373c655c0cf38a6010
$ docker rm -f 023b7970d36e20b85912062b35bc7aa4136af7807b975a373c655c0cf38a6010
[Pipeline] // withDockerContainer
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS

[/simterm]

Дальше – собственно деплой приложения.

helm install

Ну и деплоим – добавляем создание Namespace.

Сам неймспейс пока ставим дефолтный.

Можно добавить --atomic, что бы удалять зафейлившиеся деплои:

...
        stage("Helm install") {
            
            sh "helm install --atomic bttrm-apps-be bttrm-apps-be-${RELEASE_VERSION}.tgz"
        }

Запускаем, и:

[simterm]

...
[Pipeline] { (Helm install)
[Pipeline] sh
+ helm install --atomic bttrm-apps-backend bttrm-apps-backend-6.tgz
Error: unable to build kubernetes objects from release manifest: error validating "": error validating data: 
[unknown object type "nil" in Secret.data.ApplePay.crt.pem, unknown object type "nil" in Secret.data.ApplePay.key.pem]
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
$ docker stop --time=1 fa1e59d52af1c622d5122ddc8db25b72e5e2afff82a211f077fd8c3a8d1a48bb
$ docker rm -f fa1e59d52af1c622d5122ddc8db25b72e5e2afff82a211f077fd8c3a8d1a48bb
[Pipeline] // withDockerContainer
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
ERROR: script returned exit code 1
Finished: FAILURE

[/simterm]

Ага, ну да – секреты в репозиторий мы не добавляли, они заигнорены через .gitignore, и должны мапится из Secret file самой джобы.

Собственно – пора добавить и параметры.

Job parameters

Посмотрим на весь скрипт, который у нас уже получился:

node {
    docker.image('bttrm/kubectl-aws:4.0').inside('-v /var/run/docker.sock:/var/run/docker.sock') {
        stage('Init') {
            
            gitenv = git branch: "WLIOS-5848-Projects-deployments",
                         credentialsId: 'jenkins-github',
                         url: '[email protected]:example-org/bttrm-apps-backend.git'
        
            GIT_COMMIT_SHORT = gitenv.GIT_COMMIT.take(8)            
        
            RELEASE_VERSION = "${BUILD_NUMBER}"
            DOCKER_TAG = "${BUILD_NUMBER}.${GIT_COMMIT_SHORT}"
            APP_VERSION = "${BUILD_NUMBER}.${DOCKER_TAG}"
            
            echo "DOCKER_TAG == ${DOCKER_TAG}"
            echo "APP_VERSION == ${APP_VERSION}"
            echo "RELEASE_VERSION == ${RELEASE_VERSION}"                
                
            sh "aws eks update-kubeconfig --region us-east-2 --name bttrm-eks-dev-1"
            sh "kubectl cluster-info"
            sh "helm version"
            sh "helm -n bttrm-apps-dev-1-ns ls "
        }
        
        stage("Docker build") {
            
            withDockerRegistry(registry: [credentialsId: 'bttrm-docker-hub']){
                docker.build("bttrm/bttrm-apps:${DOCKER_TAG}").push()
            }
        }
        
        stage("Helm package") {
            
            sh "helm package k8s/bttrm-apps-backend --version ${RELEASE_VERSION} --app-version ${APP_VERSION}"
        }
        
        stage("Helm install") {
            
            sh "helm install --atomic bttrm-apps-backend bttrm-apps-backend-${RELEASE_VERSION}.tgz"
        }
    }
}

Что из этого надо вынести в параметры, и в какие?

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

Пока сделаем:

  • репозиторий проекта – String parameter
  • бранч проекта – String parameter
  • регион для aws eks update-kubeconfig – можно задать в скрипте, вряд ли кто-то будет деплоить в кластера в других регионах (хотя Jenkins-джоба для создания кластеров в любом регионе есть, см. AWS Elastic Kubernetes Service: — автоматизация создания кластера, часть 2 — Ansible, eksctl)
  • имя кластера для создания kubeconfig – в параметры джобы, String parameter
  • namespace – пока сделаем один, дефолтный – выносим в параметр джобы, Choice parameter
  • имя чарта – String parameter

Добавляем секретные файлы.

Выбираем тип Credentials parameter, тип Secret file:

Либо, если ключи для Dev/Stage/Prod одинаковые – то не выносить их в параметры, а сразу использовать в скрипте.

Теперь нам надо замапить эти ключи в контейнер во время билда, см. Jenkins: Credentials Binding Plugin и использование нескольких Secret file в Jenkins pipeline.

Получается такой скрипт:

node {
    docker.image('bttrm/kubectl-aws:4.0').inside('-v /var/run/docker.sock:/var/run/docker.sock') {
        stage('Init') {
            
            gitenv = git branch: '${APP_REPO_BRANCH}',
                         credentialsId: 'jenkins-github',
                         url: '${APP_REPO_URL}'
        
            GIT_COMMIT_SHORT = gitenv.GIT_COMMIT.take(8)            
        
            RELEASE_VERSION = "${BUILD_NUMBER}"
            DOCKER_TAG = "${BUILD_NUMBER}.${GIT_COMMIT_SHORT}"
            APP_VERSION = "${BUILD_NUMBER}.${DOCKER_TAG}"
            AWS_EKS_REGION = "us-east-2"
            
            echo "DOCKER_TAG == ${DOCKER_TAG}"
            echo "APP_VERSION == ${APP_VERSION}"
            echo "RELEASE_VERSION == ${RELEASE_VERSION}"                
            
            sh "aws eks update-kubeconfig --region ${AWS_EKS_REGION} --name ${AWS_EKS_CLUSTER}"
            sh "kubectl cluster-info"
            sh "helm version"
            sh "helm -n ${AWS_EKS_NAMESPACE} ls "
        }
        
        stage("Docker build") {
            
            /*
            withDockerRegistry(registry: [credentialsId: 'bttrm-docker-hub']){
                docker.build("bttrm/bttrm-apps:${DOCKER_TAG}").push()
            }
            */
            echo "Docker build"
        }
        
        stage("Helm package") {
            dir("k8s") {
                sh "helm package ${APP_CHART_NAME} --version ${RELEASE_VERSION} --app-version ${APP_VERSION}"
            }
        }
        
        stage("Helm install") {
            
            dir("k8s") {
                
                withCredentials([
                    file(credentialsId: 'bttrm-apps.applepay.crt', variable: 'APPLEPAY_CERT'),
                    file(credentialsId: 'bttrm-apps.applepay.key', variable: 'APPLEPAY_KEY'),
                    file(credentialsId: 'bttrm-apps.appleauth.key', variable: 'APPLEAUTH_KEY'),
                    ]) {
                    
                    sh "test -d bttrm-apps-backend/secrets || mkdir bttrm-apps-backend/secrets"
                    sh "cp \$APPLEPAY_CERT bttrm-apps-backend/secrets/ApplePay.crt.pem"
                    sh "cp \$APPLEPAY_KEY bttrm-apps-backend/secrets/ApplePay.key.pem"
                    sh "cp \$APPLEAUTH_KEY bttrm-apps-backend/secrets/AppleAuth.key.p8"
                    
                    sh "helm install --namespace ${AWS_EKS_NAMESPACE} --create-namespace --atomic ${APP_CHART_NAME} ${APP_CHART_NAME}-${RELEASE_VERSION}.tgz"
                }
            }
        }
    }
}

Стейдж сборки докер-образа пока комментируем, что бы не захламлять репозиторий.

Тут в Helm install мы:

  1. переходим в каталог k8s репозитория
  2. проверяем наличие каталога bttrm-apps-backend/secrets, если его нет – создаём
  3. из Jenkins Credentials создаём три файла ключей
  4. и выполняем установку чарта

Параметры при запуске выглядят так:

Запускаем, проверяем:

Супер!

“It works!” (c)

Helm: “Error: cannot re-use a name that is still in use”

Но – если мы запустим билд повторно – он уже сфейлится с ошибкой “Error: cannot re-use a name that is still in use“:

Ожидаемо – раз уже есть задплоенный резил с таким именем – Helm не будет его перезаписывать.

Тут достаточно просто заменить install на helm upgrade --install:

В целом-то это всё…

Что осталось ещё?

Но пока, думаю, хватит.

helm upgrade – resource cannot be imported into the current release

P.S. Когда добавлял RBAC-роль – то при апдейте получил ошибку:

[simterm]

+ helm upgrade --install --namespace bttrm-apps-dev-1-ns --create-namespace --atomic bttrm-apps-backend bttrm-apps-backend-17.tgz
Error: UPGRADE FAILED: rendered manifests contain a resource that already exists. Unable to continue with update: 
RoleBinding "rbac-bttrm-web-apps-ro-role-binding" in namespace "bttrm-apps-dev-1-ns" exists and cannot be imported into the current release: 
invalid ownership metadata; label validation error: missing key "app.kubernetes.io/managed-by": 
must be set to "Helm"; annotation validation error: missing key "meta.helm.sh/release-name": must be set to "bttrm-apps-backend"; 
annotation validation error: missing key "meta.helm.sh/release-namespace": must be set to "bttrm-apps-dev-1-ns"

[/simterm]

Собственно, это к тому, что Helm задаёт аннотации и лейблы сам – но есть смысл добавлять их в шаблоны манифестов:

[simterm]

$ kk -n bttrm-apps-dev-1-ns get rolebinding rbac-bttrm-web-apps-ro-role-binding -o yaml                              
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  annotations:
    meta.helm.sh/release-name: bttrm-apps-backend
    meta.helm.sh/release-namespace: bttrm-apps-dev-1-ns
  creationTimestamp: "2020-05-14T16:02:42Z"
  labels:
    app.kubernetes.io/managed-by: Helm
...

[/simterm]

Такие дела.

Ссылки по теме