Ультимативный гайд по созданию CI/CD в GitLab с автодеплоем в Kubernetes на голом железе всего за 514$ в год ( ͡° ͜ʖ ͡°)

Шел 2021 год, русские хакеры продолжают переигрывать и уничтожать загнивающий Запад, вмешиваясь в выборы, ломая фейсбуки и пентагоны. Тем временем на Хабре выходят статьи о создании неубиваемых Kubernetes-кластеров, которые, по видимому, всех нас переживут. А кто-нибудь подумал о простых пацанах (пацанессах)??? Как быть обычному программисту, который хочет свой небольшой кластер и ламповый CI/CD с автодеплоем приложения, чтобы кенты с района не засмеяли?

Всем привет, меня зовут Алексей и я алкоголик разработчик на Python/Go в Домклик. Сегодня мы будем понижать порог входа в self-hosted Kubernetes и GitLab AutoDevops.

Это очередная статья из серии «ультимативных». Мы уже писали раньше, например, о том, как выглядит асинхронное приложение здорового человека. Или гайд по поиску утечек памяти в Python. Сегодня тоже будет весело.

Статья получилась довольно большая, поэтому после каждой главы будет стрелочка , нажав на которую вы обратно перейдёте к оглавлению. Те, кто хочет всё и сразу — переходите к разделу TL;DR, там будет краткая выжимка из статьи.


Оглавление

  1. Введение

  2. Установка Kubernetes на голое железо

  3. Настройка CI/CD в Gitlab

  4. TL;DR


Введение

Начнем с самого животрепещущего — с ценообразования. Откуда взялись эти 514$?

Если в вас живет маленький стартапер, то вам рано или поздно придется где-то развернуть свой кластер и настроить автоматическую поставку приложения в production-окружение (не руками же деплоить в 2к21?). Платить жадным гигантам по типу гугла или амазона огромные деньги — это зашквар. Денях-то нет, вот мы и держимся. Поэтому я решил попробовать собрать свой кластер и настроить CI/CD за минимально возможные средства, но при этом попытался не перегибать палку в чрезмерной экономии.

Кластер решил делать минимальный, состоящий из двух узлов — master и worker. Обе машины имеют конфигурацию 2×2,2 ГГц, 4 Гб RAM, 40 Гб SSD. Аренда одной в месяц ~ 12$. В год за две машины ~ 278$. Платил в рублях, перевел в доллары для удобства дальнейших расчетов (курс 72 р).

CI/CD решил реализовывать в GitLab (лежит к нему душа). И как оказалось, это довольно большая статья расходов. Еще в начале года был silver-аккаунт, с более менее приличными ценами ~ 9$ в месяц. Но сейчас его упразднили и на его место пришел premium за 19$ в месяц или 228$ в год. Естественно, там много плюшек типа овер-дофига CI-минут, канареечный деплой, защищенные переменные окружения, дашборд окружений Куба и т.д., но цена довольно кусачая.

Третья статья, самая маленькая — покупка доменного имени. Мне обошлось в 8$ за первый год (за последующие там вроде в два раза больше берут).

Итого: 278$ + 228$ + 8$ = 514$.

А что, если купить еще один VPS и установить туда GitLab? Официальная документация рекомендует использовать машину с двумя ядрами и 4 Гб оперативы. Если брать VPS у того же провайдера, то выйдет 12$ в месяц, а в год — 144$. Экономия в таком случае 84$ за год. Но тут прибавляется забота об установке, настройке, администрировании GitLab. И не уверен, насколько полная функциональность в таком случае будет доступга. Знатоки self-managed GitLab, напишите в коменты.

В итоге я решил не заморачиваться с установкой своего GitLab и весь материал статьи отрабатывал на конфигурации за 514$. Но вот только пару дней назад, неудовлетворенный качеством предоставляемых облачным провайдером услуг, решил погуглить в надежде на более дешевые альтернативы. Оказалось, что есть машины не просто дешевле, так ещё и мощнее. Например, предлагают в аренду VPS [3×2,8 ГГц, 5 Гб RAM, 60 Гб SSD] за 9$ и [2×2,8 ГГц, 2 Гб RAM, 40 Гб SSD] за 6$ в месяц. За год выйдет: (9$ + 6$) * 12 + 228$ + 8$ = 416$. В будущем планирую арендовать машины именно у этого провайдера.

Вывод: если задаться целью максимально сэкономить, то варианты найдутся. А на этом вступление окончено. Дальше будет долго, нудно и моментами даже чересчур подробно; думаю, полностью прочтут эту статью два человека: я и наш шеф-редактор блога (за что ему большое спасибо!).

Установка Kubernetes на голое железо

Установка Kubernetes

Вам легко будет установить по этой замечательной инструкции. Но в ней есть ряд критических моментов, которые я разобрал под спойлером.

Подсказки по установке Kubernetes на Ubuntu 20.04 LTS

Установка делится на этапы и пункты. Здесь показаны те пункты, в которых возникает ошибка.

Этап 1. Пункт 4.

sudo systemctl enable docker

Если выполнить эту команду, будет ошибка:

Поэтому откройте файл /etc/hosts:

nano /etc/hosts

В моем случае не определен хост ruvds-2p1sn, мне надо добавить:

127.0.0.1 ruvds-2p1sn

А также в этом файле введите следующее имя (это для master-ноды; когда будете повторять для worker-ноды, то введите kubernetes-worker)

127.0.0.1 kubernetes-master

У меня файл выглядит так:

Сохраняем и выходим. После чего можно выполнить команду:

sudo systemctl enable docker

Этап 2. Пункт 1.

curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add

Эта команда приведёт к ошибке:

Здесь нужно установить curl. Скопируйте предложенную в консоли команду и выполните:

apt install curl

Потом выполните такую команду:

apt-get update && apt-get install -y gnupg2

Готово. Теперь уже выполняем команду из пункта 1 второго этапа.

Этап 2. Пункт 2.

sudo apt-add-repository "deb http://apt.kubernetes.io/ kubernetes-xenial main"

Эта команда приведёт к ошибке:

sudo apt-get install software-properties-common

И возвращайтесь к команде пункта 2.

Этап 2. Пункт 3.

Предыдущие ошибки легко гуглятся и решаются одной командой. А вот с этой командой сложнее:

sudo apt install kubeadm kubelet kubectl

Еще в начале этого года с ней не было проблем. Но на момент написания статьи эта команда приводит к проблеме инициализации master-ноды на этапе 3 пункт 4. Что изменилось? Вышла новая версия утилиты — 1.22.2. В начале же года я ставил версию 1.20.2 и всё было хорошо. Причину сбоя инициализации я на просторах интернета не нашел. Почти все руководства по установке не указывают конкретную версию утилит. А мы это исправим:

sudo apt install kubeadm=1.20.2-00 kubelet=1.20.2-00 kubectl=1.20.2-00

Вам же я предлагаю сначала попробовать с последней версией (если это, конечно, не 1.22.2), и если в пункте 4 этапа 3 есть проблемы, то возвращайтесь сюда и откатывайтесь на версию 1.20.2.

И обязательно надо выполнить эту команду:

sudo apt-mark hold kubeadm kubelet kubectl

Этап 3. Пункт 1.

sudo swapoff –a

Эта команда нерабочая. Будет ошибка:

Я заморочился и даже сравнил с перепечатанной командой вручную.

Оказалось, что тирешка вовсе не тирешка -_- . Используйте эту команду:

sudo swapoff -a

Этап 3. Пункт 4.

На этом этапе есть !критическое! замечание. Нужно выполнить такую команду:

sudo kubeadm init --pod-network-cidr=10.244.0.0/16

Да, обязательно нужно добавить этот странный диапазон IP-адресов. Это нужно для сетевого плагина Flannel, который устанавливается далее по инструкции.

Этап 3. Пункт 7.

Тут команда верная, но адрес слетел на следующую строку. Используйте это:

sudo kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

На этом всё. Повторяйте инструкцию для worker-ноды (исключая некоторые пункты, которые нужны только для master-узла).

Поздравляю! Теперь у вас есть свой собственный Kubernetes кластер!

Установка Helm

Инструкция взята с официального сайта.

curl https://baltocdn.com/helm/signing.asc | sudo apt-key add -
sudo apt-get install apt-transport-https --yes
echo "deb https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
sudo apt-get update
sudo apt-get install helm

Проверим успешность установки:

helm version

Helm установлен!

Установка Ingress-nginx

Инструкцию стянул из официальной документации.

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx helm repo update helm install ingress-nginx ingress-nginx/ingress-nginx

Также запросим установленные сервисы:

kubectl get svc

Если бы вы пользовались облачным сервисом GKE или AWS, то в графе EXTERNAL-IP появился бы выделенный IP-адрес.

А Ingress-сервис стал бы доступен по адресу: http://*external-ip*:80

В облачных "Kubernetes-as-a-service" есть прослойка между пользователем и кластером — Cloud Load Balancer.
В облачных «Kubernetes-as-a-service» есть прослойка между пользователем и кластером — Cloud Load Balancer.

Но когда вы устанавливаете Куб на голое железо, внешний балансировщик отсутствует, поэтому службы не получают EXTERNAL-IP. Это описано в документации к Ingress-nginx (эти красивые картинки стянул оттуда же), а также тут.

В "bare-metal" конфигурации эта прослойка отсутствует.
В «bare-metal» конфигурации эта прослойка отсутствует.

Будем использовать первое предложенное в этом документе решение — поставим Metallb, который заменит нам внешний балансировщик.

Установка Metallb

Внесем изменения в configmap:

kubectl edit configmap -n kube-system kube-proxy

Нужно в mode записать ipvs, a strictARP активировать. Кусок конфигурации должен выглядеть так:

apiVersion: kubeproxy.config.k8s.io/v1alpha1 kind: KubeProxyConfiguration mode: "ipvs" ipvs: strictARP: true

Сохраняем и выходим, если сможете конечно, ибо запускается vim. Главное помните, в любой непонятной ситуации вводите:

Добавляем Helm-репозиторий:

helm repo add metallb https://metallb.github.io/metallb

Создаем файл с именем values.yaml и вставляем шаблон:

configInline: address-pools: - name: default protocol: layer2 addresses: - x.x.x.x/24

В поле addresses указываем пул IP-адресов, которые заберёт в своё пользование Metallb. Адреса нужно указать в виде CIDR. В моём пользовании всего две виртуальные машины, поэтому речи о пуле адресов не идёт. Маска подсети для одного-единственного IP-адреса записывается как «/32«.

В моем случае конфигурация выглядит так:

configInline: address-pools: - name: default protocol: layer2 addresses: - 194.87.253.108/32 - 87.247.157.40/32

Сохраняем и выходим. Теперь установим helm chart metallb с указанием файла конфигурации:

helm install metallb metallb/metallb -f values.yaml

После чего запросим сервисы:

kubectl get svc

Если вы всё сделали правильно, то в графе EXTERNAL-IP появится выделенный IP-адрес для этого сервиса. Проверим в браузере:

На этом настройка Куба на голом железе завершена.

Альтернативное решение

К любому из сервисов (с типом LoadBalancer), можно обратиться по NodePort. Например, Ingress-сервис будет доступен по адресу: http://*ip-master-node*:*NodePort*.

Номер NodePort и тип сервиса можно увидеть тут:

Это один из вариантов решения проблемы отсутствия внешнего балансировщика. Описано в той же документации.

Если мы возьмём доменное имя, например, example.com, и свяжем его с IP-адресом master-ноды, то сервис будет доступен по адресу: http://example.com:32128. Выглядит стремновато. Но как вариант, можно поставить на master-ноду nginx и проксировать трафик с порта 80 на порт 32128. И тогда Ingress будет доступен по адресу http://example.com.

Нашел даже интересный проект на Github, который кроме запуска nginx в Docker-контейнере еще и настраивает SSL-сертификаты. Надо лишь указать, на какие доменные имена нужно выпустить сертификаты, и всё остальное он сделает сам. Действительно, ставится буквально одной командой, что очень порадовало.

Но лично у меня с этой схемой начались проблемы, когда я начал разворачивать Kubernetes dashboard и настраивать авторизацию через Keycloak. Поэтому, этот вариант пусть останется в истории.

Настройка CI/CD в GitLab

Выбираем доменное имя для сервиса

Сначала нужно придумать название для своего сервиса и купить доменное имя. Чур, мой сервис будет называться awesomeservice, а доменную зону я выбрал модную и красивую — .tech. Где покупать, в принципе, не имеет значения.

Cвяжем external-ip Ingress-nginx с новым купленным доменным именем. Для этого создайте A-запись (awesomeservice.tech -> 194.87.253.108). Полное обновление DNS-сервера занимает не более трёх часов. После этого Ingress-nginx будет доступен уже по доменному имени.

Естественно архитектура, будет микросервисная, а это значит, что для проекта могут быть развернуты разные приложения со своим API. У нас будет вспомогательный микросервис coolapp. И кряхтеть он будет по адресу http://coolapp.awesomeservice.tech. Причем это production-окружение, которое будет использовать конечный пользователь. Еще понадобятся test- и staging-окружения. В итоге у нас будет развёрнуто сразу несколько приложений с такими поддоменными именами:

  1. http://coolapp.awesomeservice.tech

  2. http://staging.coolapp.awesomeservice.tech

  3. http://qa01.coolapp.awesomeservice.tech

  4. http://qa02.coolapp.awesomeservice.tech

Это значит, надо сделать ещё четыре A-записи для поддоменых имен: coolapp., staging.coolapp., qa01.coolapp., qa02.coolapp.. И да, все эти имена разрешаются на Ingress EXTERNAL-IP.

Создание проекта в GitLab

Создаём пустой проект. В 2к21 ребята из GitLab еще не завезли шаблон для Python-проекта ¯\_(ツ)_/¯.

Создадим вспомогательное приложение coolapp для сервиса awesomeservice. Использовать будем слегка модифицированный микросервис на FastApi из предыдущих статей. Иерархия:

coolapp
├── Dockerfile
├── README.md
├── main.py
└── requirements.txt
Dockerfile
FROM python:3.8 EXPOSE 8080 WORKDIR app COPY requirements.txt requirements.txt RUN pip install -r requirements.txt COPY . . CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
main.py
from fastapi import FastAPI, Cookie
from typing import Optional
from uuid import uuid4 app = FastAPI()
uuid = uuid4() @app.get("/api/v1/uuid")
async def root(key: Optional[str] = Cookie(None)): print(key) return {'uuid': uuid} @app.get("/healthz")
async def health_check(): return {}

Ручку /healthz будет использовать Kubernetes для проверки работоспособности поды.

requirements.txt
fastapi==0.63.0
uvicorn==0.13.3
requests==2.26.0

Сборка и запуск приложения:

docker build . -t coolapp docker run -p 8080:8080 -t coolapp

Проверяем работу:

Пушим проект в удаленный репозиторий.

Интеграция Kubernetes-кластера

На главной проекта нажимаем кнопку <Add Kubernetes cluster>.

С невозмутимым лицом проходим мимо предложения воспользоваться облачными сервисами GKE, AWS. Переходим во вкладку «Подключить существующий кластер».

В принципе, официальная документация довольно подробно расписывает, как заполнить каждое поле. Но для удобства продублирую инструкцию тут:

Здесь надо заполнить шесть полей:

  1. Kubernetes cluster name

  2. Environment scope

  3. API URL

  4. CA Certificate

  5. Service Token

  6. Project namespace prefix (optional, unique)

Галочки RBAC-enabled cluster, GitLab-managed cluster и Namespace per environment должны быть активны.

Kubernetes cluster name

Просто записываем имя кластера. У меня этоdevkube.

Environment scope

Оставляем *.

API URL

В консоли выполните:

kubectl cluster-info | grep -E 'Kubernetes master|Kubernetes control plane' | awk '/http/ {print $NF}'

Копируем вывод из консоли и вставляем.

CA Certificate

Выполните команду:

kubectl get secrets

Найдите название секрета, который начинается на default-token-xxxxx.

Вставьте это название секрета в следующую команду вместо secret name.

kubectl get secret "secret name" -o jsonpath="{['data']['ca\.crt']}" | base64 --decode

Скопируйте весь вывод консоли в поле CA Certificate.

Service Token

Создайте файл gitlab-admin-service-account.yaml и вставьте:

apiVersion: v1
kind: ServiceAccount
metadata: name: gitlab namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata: name: gitlab-admin
roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin
subjects: - kind: ServiceAccount name: gitlab namespace: kube-system

Выполните в той же папке с файлом:

kubectl apply -f gitlab-admin-service-account.yaml

После создания ресурсов выполните:

kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep gitlab | awk '{print $1}')

Скопируйте токен и вставьте в поле Service Token.

Project namespace

Введите префикс вашего пространства имён проекта. Пространство имен проекта будет составлено из двух частей: префикса и имени окружения. Если не вписать, то GitLab сам сгенерирует некрасивое название с циферками. У меня это coolapp-devkube. Префикс должен быть уникальным для каждого проекта.

Заполняем все поля и сохраняем. В итоге должно выглядеть примерно так:

После добавления кластера сразу будут показаны его настройки. В окне <Base domain> введите базовый адрес кластера. В моем случае это awesomeservice.tech.

Теперь активируем Auto Devops. На главной проекта нажмите кнопку <Enable Auto Devops>.

Откроются настройки проекта, где нужно тыкнуть галочку <Default to Auto DevOps pipeline> и выбрать стратегию развертывания приложения. Я выбрал третий вариант, с предварительным деплоем в staging-среде, после которой будет доступна постепенная (ступенчатая) выкатка в production-среду. Подробнее о стратегиях выкатки можно почитать тут.

Начиная с этого момента для проекта активен Auto Devops.

Настройка GitLab AutoDevops

Краткий обзор Auto Devops:

Auto DevOps покрывает весь цикл поставки: Просто сделайте коммит вашего кода в GitLab и позвольте Auto DevOps заняться остальным: эта система проведет сборкутестирование, проверку качества кодабезопасности и лицензийпакетированиетестирование производительностиразвертывание и мониторинг вашего приложения.

Звучит так классно, что даже утопично… Вам так не кажется?

Да. Вам не кажется.

После активации AutoDevops в master main-ветке сразу же запустится конвейер.

И конечно же, он зафейлится, разве могло быть иначе?

Сборка прошла успешно, что уже радует. В блоке тестирования прошли такие этапы, как проверка лицензии, качества кода и т.д. В общем, прошло всё то, что на этом этапе нас мало интересует. В итоге конвейер завершил работу на тестировании.

Быстрое гугление показало, что это проблема для всех питонистов: тестирование Python-проектов из коробки работать не будет. Но мы этап тестирования опустим, потому что наша цель — настроить автоматизированную поставку приложений. Выключать стадии можно несколькими способами.

1) Например, с помощью создания специальных переменных окружения в настройках проекта. Для этого перейдите в Settings -> CI/CD -> Variables и создайте переменную окружения TEST_DISABLED=true.

После этого нужно обязательно создать новый pipeline, текущий все равно будет запускать test.

Мы пропустили test, но ошибка возникла на этапе развертывания приложения в среду review (это аналог общепринятого окружения dev). Gitlab ожидает, что приложение отвечает по порту 5000, у нас же рабочий порт 8080. Можем, конечно, изменить на 5000, но так неинтересно. Хочется иметь возможность задавать порт самому. Решим эту проблему чуть позже.

2) Auto DevOps полностью изменяем, он работает по шаблону, который, по сути, является реализацией.gitlab-ci.yml.Мы можем в проекте определить свой .gitlab-ci.yml и добавить свои этапы, или переопределить стандартные из AutoDevops. Пойдём по второму пути.

Создадим ветку feature/autodevops(pipeline запустится автоматом).

Обратите внимание, что в ветке всё равно будет этап test, потому что я создал переменную среды TETEST_DISABLED как protect variable. Переменные такого типа доступны только в protected ветках, коей main и является.

Теперь переопределим стандартный шаблон AutoDevops. Мы оставим почти все стадии, уберём только test, deploy (это заглушка и так), dast и performance. Создайте файл .gitlab-ci.yml и вставьте следующие инструкции:

variables: ROLLOUT_RESOURCE_TYPE: deployment # без этого деплой фейлится stages: - build - review - staging - canary - production - incremental rollout 10% - incremental rollout 25% - incremental rollout 50% - incremental rollout 100% - cleanup include: - template: Jobs/Build.gitlab-ci.yml - template: Jobs/Deploy.gitlab-ci.yml 

В stages перечислены все стадии, которые будут выполняться, их реализация находится в подтягиваемых нами шаблонах (include). Например, шаблон деплоя, в котором описаны все стадии начиная с review до cleanup. Там же можно заметить, что review и cleanup выполняются только в ветках, а остальные — в main-ветке, это логично.

Если сейчас запушить, то будет ошибка во время деплоя, потому что GitLab ожидает приложение на порту 5000. Разумеется, и тут мы не ограничены и можем указать нестандартный порт. GitLab деплоит приложение с помощью Helm-чарта, настройки которого можно переопределить у себя в проекте. Для этого нужно создать папку .gitlab и файл auto-deploy-values.yaml. Чтобы приложение успешно развернулось, достаточно переопределить service.internalPort, service.externalPort и обязательно указать наш кастомный health check в разделе livenessProbe и readinessProbe. Также я явно указал, что TLS выключен, настройка сертификатов — это отдельная тема. Если этого не сделать, то, например, Postman будет ругаться на сертификат (через браузер же всё норм).

service: internalPort: 8080 externalPort: 8080 livenessProbe: path: /healthz readinessProbe: path: /healthz ingress: enabled: true tls: enabled: false

Иерархия проекта такая (меняться больше не будет):

coolapp
├── .gitlab
│   └── auto-deploy-values.yaml
├── .gitlab-ci.yml
├── Dockerfile
├── README.md
├── main.py
└── requirements.txt

Запушим изменения и посмотрим результат выполнения конвейера.

Конвейер успешно завершил работу, но не спешите радоваться, это ещё не всё. Зайдём и проверим, что и куда задеплоилось.

Детализация стадии review показывает, что сервис успешно развёрнут по адресу с интересным поддоменом 29857899-review-feature-cu-jfvpge. Естественно, этот URL нерабочий, мы же не делали A-запись для такого поддомена.

Дело в том, что для каждой ветки GitLab (согласно шаблону AutoDevops) генерирует уникальный URL и разворачивает под ним сервис. Именно такая система была на моем прошлом месте работы, это довольно удобно, когда можно для своей ветки раскатить сервис и тестировать его изолированно. Но годится это для больших компаний и проектов. К тому же, чтобы это заработало нужно для домена awesomeservice.tech уметь настраивать автоподдомены. Для текущей статьи и в целом для небольшого проекта это излишняя фича, поэтому для веток мы сделаем два окружения: qa01 и qa02. Почему именно два?

Начнем с того, что выключим стадии review и cleanup. Так как создаётся отдельный сервис для каждой ветки, ресурсы начинаются быстро плодиться и их надо чистить; для этого существует cleanup — она удаляет все созданные ресурсы, связанные с этой веткой. Просто взять и удалить review и cleanup из stages не получится. Если мы используем шаблон, то все описанные там стадии должны быть указаны в блоке stages нашего gitlab-ci.yml. Но стадии можно выключить, как я уже упоминал ранее, с помощью специальных переменных окружения. Переменная REVIEW_DISABLED отключает сразу обе стадии, это видно в реализации оных в шаблоне Jobs/Deploy.gitlab-ci.yml. Заодно включим стадию canary (она по умолчанию выключена), это нам понадобится для конвейера в main-ветке.

variables: ROLLOUT_RESOURCE_TYPE: deployment REVIEW_DISABLED: "true" CANARY_ENABLED: "true" stages: - build - review # off stage - staging - canary - production - incremental rollout 10% - incremental rollout 25% - incremental rollout 50% - incremental rollout 100% - cleanup # off stage include: - template: Jobs/Build.gitlab-ci.yml - template: Jobs/Deploy.gitlab-ci.yml 

После выключения этапа review создадим свою стадию с именем qa. За основу возьмем стадию staging из того же стандартного шаблона. Вот так она выглядит:

staging: extends: .auto-deploy stage: staging script: - auto-deploy check_kube_domain - auto-deploy download_chart - auto-deploy ensure_namespace - auto-deploy initialize_tiller - auto-deploy create_secret - auto-deploy deploy environment: name: staging url: http://$CI_PROJECT_PATH_SLUG-staging.$KUBE_INGRESS_BASE_DOMAIN rules: - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""' when: never - if: '$CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH' when: never - if: '$STAGING_ENABLED'

Нужно будет сделать небольшие правочки в блоках stage, environment и rules, а вот script останется нетронутым. Всё довольно просто: в stage записывается имя стадии, в environment.url указывается путь, под которым будет развёрнуто приложение (эта строчка просто пробрасывается в Ingress в spec.rules.host — кто знает, тот знает). В этом URL можно заметить странный префикс $CI_PROJECT_ID, если его оставить, то приложение развернётся с путем http://mopckou-coolapp-staging.awesomeservice.tech.

Выглядит стрёмно, поэтому для окружения qa01 и qa02 избавимся от этого префикса. И наконец, добавим ручной выбор, в какое окружение деплоить приложения в блоке rules. Вот так выглядят новенькие стадии qa01 и qa02:

qa01: extends: .auto-deploy stage: qa script: - auto-deploy check_kube_domain - auto-deploy download_chart - auto-deploy ensure_namespace - auto-deploy initialize_tiller - auto-deploy create_secret - auto-deploy deploy environment: name: qa01 url: http://qa01.coolapp.$KUBE_INGRESS_BASE_DOMAIN # убрал префикс $CI_PROJECT_PATH_SLUG- rules: - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""' when: never # не трогаем - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' when: never # эта стадия не запускается если ветка называется main (default branch) - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' when: manual # для ручного выбора куда деплоить qa02: # копирка предыдущего блока, просто заменил qa01 на qa02 extends: .auto-deploy stage: qa script: - auto-deploy check_kube_domain - auto-deploy download_chart - auto-deploy ensure_namespace - auto-deploy initialize_tiller - auto-deploy create_secret - auto-deploy deploy environment: name: qa02 url: http://qa02.coolapp.$KUBE_INGRESS_BASE_DOMAIN rules: - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""' when: never - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' when: never - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' when: manual

Эти стадии можно сделать более читаемыми с помощью якорей. Следующая запись полностью эквивалентна предыдущей:

.qa_env_setup: &qa_setup extends: .auto-deploy stage: qa script: - auto-deploy check_kube_domain - auto-deploy download_chart - auto-deploy ensure_namespace - auto-deploy initialize_tiller - auto-deploy create_secret - auto-deploy deploy rules: - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""' when: never - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' when: never - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' when: manual qa01: <<: *qa_setup environment: name: qa01 url: http://qa01.coolapp.$KUBE_INGRESS_BASE_DOMAIN qa02: <<: *qa_setup environment: name: qa02 url: http://qa02.coolapp.$KUBE_INGRESS_BASE_DOMAIN

А для остальных этапов (staging, canary, production) придется так же переопредилить URL, чтобы избавиться от некрасивого префикса.

staging: extends: .auto-deploy environment: name: staging url: http://staging.coolapp.$KUBE_INGRESS_BASE_DOMAIN .url: &production_url environment: name: production url: http://coolapp.$KUBE_INGRESS_BASE_DOMAIN production_manual: extends: .auto-deploy <<: *production_url production: extends: .auto-deploy <<: *production_url canary: extends: .auto-deploy <<: *production_url timed rollout 10%: extends: .auto-deploy <<: *production_url timed rollout 25%: extends: .auto-deploy <<: *production_url timed rollout 50%: extends: .auto-deploy <<: *production_url timed rollout 100%: extends: .auto-deploy <<: *production_url rollout 10%: extends: .auto-deploy <<: *production_url rollout 25%: extends: .auto-deploy <<: *production_url rollout 50%: extends: .auto-deploy <<: *production_url rollout 100%: extends: .auto-deploy <<: *production_url
Полный gitlab-ci.yml
variables: ROLLOUT_RESOURCE_TYPE: deployment # без этого деплой фейлится REVIEW_DISABLED: "true" CANARY_ENABLED: "true" stages: - build - review # off stage - qa - staging - canary - production - incremental rollout 10% - incremental rollout 25% - incremental rollout 50% - incremental rollout 100% - cleanup # off stage .qa_env_setup: &qa_setup extends: .auto-deploy stage: qa script: - auto-deploy check_kube_domain - auto-deploy download_chart - auto-deploy ensure_namespace - auto-deploy initialize_tiller - auto-deploy create_secret - auto-deploy deploy rules: - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""' when: never - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' when: never - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' when: manual .production_env_setup: &production_url environment: name: production url: http://coolapp.$KUBE_INGRESS_BASE_DOMAIN qa01: <<: *qa_setup environment: name: qa01 url: http://qa01.coolapp.$KUBE_INGRESS_BASE_DOMAIN qa02: <<: *qa_setup environment: name: qa02 url: http://qa02.coolapp.$KUBE_INGRESS_BASE_DOMAIN staging: extends: .auto-deploy environment: name: staging url: http://staging.coolapp.$KUBE_INGRESS_BASE_DOMAIN production_manual: extends: .auto-deploy <<: *production_url production: extends: .auto-deploy <<: *production_url canary: extends: .auto-deploy <<: *production_url timed rollout 10%: extends: .auto-deploy <<: *production_url timed rollout 25%: extends: .auto-deploy <<: *production_url timed rollout 50%: extends: .auto-deploy <<: *production_url timed rollout 100%: extends: .auto-deploy <<: *production_url rollout 10%: extends: .auto-deploy <<: *production_url rollout 25%: extends: .auto-deploy <<: *production_url rollout 50%: extends: .auto-deploy <<: *production_url rollout 100%: extends: .auto-deploy <<: *production_url include: - template: Jobs/Build.gitlab-ci.yml - template: Jobs/Deploy.gitlab-ci.yml 

Пушим изменения и наблюдаем за новеньким конвейером:

Приложение успешно развёрнуто в окружении qa01 и qa02. Посмотрим теперь детализацию этапа qa01:

Приложение развёрнуто с поддоменом qa01.coolapp. Проверим работу:

Приложение отвечает и отдает uuid по запросу. Chrome ругается на незащищенное HTTP-соединение, но настройка сертификатов выходит за рамки этой статьи.

В GitLab есть удобная борда для мониторинга под в каждом окружении. Перейдите в Deployments → Environments.

Но в данный момент она не работает. Панель включается только когда у каждой поды есть аннотация: app.gitlab.com/app=$CI_PROJECT_PATH_SLUG и app.gitlab.com/env=$CI_ENVIRONMENT_SLUG. Ребята из GitLab позаботились о нас и в Helm-чарте пробрасывают аннотацию подам, нам надо лишь переопределить эти параметры. Есть специальная переменная окруженияHELM_UPGRADE_EXTRA_ARGS, с помощью которой можно переопределить любой параметр из Helm-чарта.

Эту переменную я указал в gitlab-ci.yaml проекта в блоке variables, чтобы все переменные окружения были в одном месте. Но, конечно, все их можно указать на сайте в настройках вашего проекта в GitLab.

 HELM_UPGRADE_EXTRA_ARGS: "--set gitlab.env=$CI_ENVIRONMENT_SLUG,gitlab.app=$CI_PROJECT_PATH_SLUG"
Теперь уже точно полный gitlab-ci.yaml. Честно-честно!
variables: ROLLOUT_RESOURCE_TYPE: deployment REVIEW_DISABLED: "true" CANARY_ENABLED: "true" HELM_UPGRADE_EXTRA_ARGS: "--set gitlab.env=$CI_ENVIRONMENT_SLUG,gitlab.app=$CI_PROJECT_PATH_SLUG" stages: - build - review # off stage - qa - staging - canary - production - incremental rollout 10% - incremental rollout 25% - incremental rollout 50% - incremental rollout 100% - cleanup # off stage .qa_env_setup: &qa_setup extends: .auto-deploy stage: qa script: - auto-deploy check_kube_domain - auto-deploy download_chart - auto-deploy ensure_namespace - auto-deploy initialize_tiller - auto-deploy create_secret - auto-deploy deploy rules: - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""' when: never - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' when: never - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' when: manual .production_env_setup: &production_url environment: name: production url: http://coolapp.$KUBE_INGRESS_BASE_DOMAIN qa01: <<: *qa_setup environment: name: qa01 url: http://qa01.coolapp.$KUBE_INGRESS_BASE_DOMAIN qa02: <<: *qa_setup environment: name: qa02 url: http://qa02.coolapp.$KUBE_INGRESS_BASE_DOMAIN staging: extends: .auto-deploy environment: name: staging url: http://staging.coolapp.$KUBE_INGRESS_BASE_DOMAIN production_manual: extends: .auto-deploy <<: *production_url production: extends: .auto-deploy <<: *production_url canary: extends: .auto-deploy <<: *production_url timed rollout 10%: extends: .auto-deploy <<: *production_url timed rollout 25%: extends: .auto-deploy <<: *production_url timed rollout 50%: extends: .auto-deploy <<: *production_url timed rollout 100%: extends: .auto-deploy <<: *production_url rollout 10%: extends: .auto-deploy <<: *production_url rollout 25%: extends: .auto-deploy <<: *production_url rollout 50%: extends: .auto-deploy <<: *production_url rollout 100%: extends: .auto-deploy <<: *production_url include: - template: Jobs/Build.gitlab-ci.yml - template: Jobs/Deploy.gitlab-ci.yml 

Пушим изменения, деплоим в qa01 и qa02 и бежим в Deployments → Environments.

Появилась доска с информацией, сколько под развёрнуто и в каком окружении. Эти зеленые квадратики, кстати, кликабельные, так можно сразу провалиться к логам поды, они транслируются в режиме онлайн.

С тестовым окружением мы закончили, теперь сливаем ветку в master main. И наблюдаем за красивым пайпланчиком в главной ветке:

Приложение доступно по адресу:

http://staging.coolapp.awesomeservice.tech/api/v1/uuid — для staging-окружения

http://coolapp.awesomeservice.tech/api/v1/uuid — для prod-окружения

Масштабировать приложение очень просто. В переменных окружения проекта (Settings → CI/CD → Variables) создайте переменную PRODUCTION_REPLICAS с желаемым количеством под.

И запустите Re-deploy в панели:

Приложение успешно расплодилось до пяти под.

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

Разбираем канареечный деплой на практике

Суть канареечного деплоя в том, что мы направляем часть продуктового трафика на новую версию приложения (какую часть в процентах, мы выбираем сами). Наблюдаем, как ведёт себя новое приложение; если нет ошибок, то перебрасываем весь трафик на новую версию; если всё плохо, то просто откатываемся на предыдущую стабильную версию.

Достоинство: в случае ошибки страдает только небольшая часть пользователей, а не все, как при классическом деплое. Недостаток: надо понимать, что запрос от одного пользователя может попасть случайно как на стабильную версию, так и на новую канареечную. Это может привести к путанице и, возможно, к некоторым ошибкам. По идее, это можно решить настройкой sessionAffinity для Kubernetes services.

Продемонстрирую на практике. Изменим код приложения, чтобы по его ответу сразу было понятно, что это новая версия. Добавим в ответ ручки /api/v1/uuid ещё один ключ time:

from time import ctime from fastapi import FastAPI, Cookie
from typing import Optional
from uuid import uuid4 app = FastAPI()
uuid = uuid4() @app.get("/api/v1/uuid")
async def root(key: Optional[str] = Cookie(None)): print(key) return { 'uuid': uuid, 'time': ctime() } @app.get("/healthz")
async def health_check(): return {}

Пушим изменение и ждём, пока отработает deploy в staging-окружении:

Как только вы запустите canary stage, в том же пространстве имён запустится такое же количество под с новой версией приложения. В названии под есть специальный префикс -canary-:

Пока что весь трафик на старой версии приложения. Как только вы нажмете на конвейере на этап с процентом, то выбранная доля трафика станет поступать на канареечные поды.

Теперь взглянем на панель окружений.

Видно, что прибавилось ещё пять под с жёлтым кружочком, это обозначение канареечной поды. На панели можно менять соотношение трафика в любую сторону, и даже пустить весь поток на новую версию приложения.

Трафик поступает то на канареечную поду, то на стабильную версию приложения. Соотношение трафика 50/50
Трафик поступает то на канареечную поду, то на стабильную версию приложения. Соотношение трафика 50/50

В конвейере долю трафика можно менять как в большую сторону, так и в меньшую. Но как только вы нажмете на 100 %, весь трафик перейдёт на канареечные поды, в это время будут созданы поды с новой версией микросервиса и старое приложение начнёт удаляться.

Как только все новые будут запущены, а трафик на них переключится, канареечные поды начнут удаляться.

На этом процесc деплоя на новую версию закончен.

TL;DR

1. Установите кластер на голое железо.

2. Купите доменное имя (подробнее тут), у меня это awesomeservice.tech. И зарегистрируйте четыре поддомена: coolapp., staging.coolapp., qa01.coolapp, qa02.coolapp. В моем случае:

coolapp.awesomeservice.tech

staging.coolapp.awesomeservice.tech

qa01.coolapp.awesomeservice.tech

qa02.coolapp.awesomeservice.tech

Поддоменные имена легко меняются в gitlab-ci.yaml (см. следующий пункт).

3. Форкните готовый шаблон проекта на Python. Там уже есть настроенный gitlab-ci.yaml.

4. Подключите свой кластер Куба к проекту в GitLab. После чего в настройках укажите base domain. У меня это awesomeservice.tech.

5. Активируйте в проекте AutoDevops.

Создайте конвейер и наслаждайтесь:

Pipeline в любой дочерней ветке
Pipeline в любой дочерней ветке

Pipeline в главной ветке
Pipeline в главной ветке

В итоге мы создали свой небольшой Kubernetes кластер, состоящий из двух нод, и всё это на голом железе. И что важно — настроили в GitLab автодеплой приложения. На этой прекрасной ноте можно заканчивать.

Релизные практики. Best practice
Пссс… парень

Читайте так же: