Personal (jesus) стенд — решаем проблему тестовых контуров в компании
Всем привет, меня зовут Захаров Антон, и я DevOps-инженер в компании Bimeister! Весь свой опыт я получил в этой компании, за 5 лет прошел путь от эникея до того, кем я работаю сейчас (большое спасибо коллегам!). Я расскажу, как на базе своих серверов и внутренних ресурсов мы создаем персональные стенды для разработки и тестирования нашего приложения.
Тогда
Ноябрь 2018 года: нас 20 человек, бэк-офис, три команды разработчиков, три собственных сервера и желание сделать работу коллег удобной. Один сервер под бэк-офис, два остальных отдали нам. Мы только-только переехали в gitlab (был bitbucket и jenkins) и начали настраивать наш ci/cd.
С этого все начиналось
Вот так он выглядел вначале. Билд, создание docker-compose.yml для локального деплоя и деплой нашего приложения на стенды с докером.
Каждый такой стенд с докером — виртуальная машина, на которую любой разработчик или тестировщик может зайти и посмотреть логи, что-то изменить:
dev — для программистов,
test — для QA,
prod — на тот момент это деплой стабильной ветки,
demo — для демонстраций заказчикам,
sandbox — песочницы или персональные стенды, общие стенды. Кто успел, тот и использует, про них сегодня и пойдет речь.
Отдавать приложение еще никому не надо, идет разработка. Основной проект лежит в монорепозитории.
Октябрь 2020: добавились новые люди, микросервисы и еще пару персональных стендов, их уже пять.
Апрель 2021: прибавился еще сервер, обновили старые, и вот у нас уже 13 персональных стендов. Стоит сказать, что не многие могут позволить себе развернуть локально наше приложение, оно уже достаточно объемное. Да и удобнее, когда ты нажимаешь кнопку и у тебя все готово. Около 30 различных контейнеров в одном приложении.
Ноябрь 2021: 19 sb стендов. Одна виртуалка на 24 GB RAM, 8 CPU, 300 GB disk.
Наши стенды — 2021
В рабочем чате творится вакханалия «Я занял такой-то стенд, а кто мне все сломал?! Это же был мой стенд». Сделали бота, через который сотрудники бронировали персональный стенд и отмечали, если стенд больше не использовали.
Все это время мы думали об использовании kubernetes. Даже были манифесты, позволяющие развернуть приложение в нем, но рук не хватало, и откладывали это все до подходящего момента. Момент наступил, когда расширилась команда DevOps.
Февраль 2022: подняли кластер kubernetes, написали helm chart и создали первые персональные стенды в кубере — ksb, каждый такой стенд — это отдельный неймспейс.
Первые ksb
to personal k8s — установить
from personal k8s — удалить
Задумка была такая: если кому-то надо что-то протестировать, он в своей ветке нажимает деплой, разворачивается неймспейс с именем того, кто нажал (ksb-firstname-lastname) все это тестируется самим разработчиком, отдается в QA или вливается в главную ветку. После, стенд удаляется или берется следующая задача. Дали возможность иметь одновременно два личных персональных стенда.
Адрес, по которому доступен стенд : https://$ENV_NAME.dev.bimeister.io
ENV_NAME="ksb-$(echo ${GITLAB_USER_LOGIN::45} | sed -r 's/\./-/g;s/\-$//')-$ENV_NUM"
В итоге получается ksb-anton-zakharov-1.dev.bimeister.io — деплой на первый стенд,
ksb-anton-zakharov-2.dev.bimeister.io — на второй.
Количество стендов ограничено. Перед началом подсчитываются уже развернутые, и если превышено определенное значение, возникает ошибка «извините, приходите попозже».
ST_KUBE_ENV_NUM=$(kubectl get ns --no-headers -o custom-columns=":metadata.name" | grep -c "^ksb" ) || ST_KUBE_ENV_NUM=0 ;
ENV_EXISTS=$(kubectl get ns --no-headers -o custom-columns=":metadata.name" | grep -c -x "$ENV_NAME") || ENV_EXISTS=0 ;
echo -e "${TXT_CYAN}Number of deployed kubernetes environments now is ${ST_KUBE_ENV_NUM}${TXT_CLEAR}" ;
if ([ "$ST_KUBE_ENV_NUM" -ge "$MAX_KUBE_ENV_NUM" ] && [ "$ENV_EXISTS" -eq 0 ]); then echo -e "${TXT_RED}The number of kubernetes environments exceeded ($MAX_KUBE_ENV_NUM is maximum)!${TXT_CLEAR}" && exit 1; fi ;
Чтобы удалять неиспользуемые стенды, срок их жизни ограничили на сутки с момента деплоя. Срок отслеживает гитлаб с помощью функции auto_stop:
gitlab auto_stop
Если на стенд накатывали обновление, время сбрасывалось и отсчет начинался заново.
Со стендом можно работать:
pin — остановить auto_stop,
view deployment — перейти по ссылке на сайт приложения,
stop — запустить undeploy.
Environments
Стенды с docker-ом остались, их стало меньше, а высвобожденные ресурсы добавились в кластер.
Так мы и жили почти полтора года, поддерживали работу кластера, деплой персональных стендов, параллельно делали другие задачи. Количество стендов росло, добавились новые кластеры:
dev — основной кластер с персональными стендами,
regress — для проведения регрессионного тестирования,
stress — для нагрузочного тестирования и др.,
тестовый кластер — можем ломать его, как хотим.
Несколько раз наш dev кластер умирал безвозвратно из-за отключения электричества, в основном. Думали держать резервный мастер в облаке, но затрат на это больше, проще подготовить скрипты, чтобы быстро поднять кластер с нуля.
Ноябрь 2023: персональных стендов в k8s — 60 штук. «МАЛО! — говорят люди, — нам не хватает!»
Дайте еще!!!
Декабрь 2023: вот к чему мы пришли.
Сейчас
Нам купили еще оборудование, и мы смогли увеличить ресурсы наших кластеров.
Монорепозиторий изрядно похудел, сейчас это больше сборочный проект для всех остальных модулей.
Наш итоговый pipeline (во время создания релиза добавляется еще один stage):
Родительский пайплайн
Заходим в sandboxes:
Sandboxes
Еще глубже в to personal ksb 1, to personal k8s 1 manual — это возможность задеплоить различные наборы модулей приложения
Внутри to personal ksb 1
Еще глубже, лог успешной джобы:
Лог джобы
В конце джобы нам доступен адрес стенда, ссылка на kube-dashboard, мониторинг вместе с логами.
Отдельно про мониторинг и логирование можете почитать в нашей статье:
Оптимизация DevOps: Как персональные стенды и Grafana улучшают разработку и мониторинг
Debug-proxy запускается при необходимости (поэтому not found), это tcp-proxy. На одном external-ip со своими портами доступны те сервисы, которые необходимы, — так разработчики и QA могут подключиться к подам своих персоналок с локальных машин. Минус — заканчиваются внешние ip).
Очень много проблем возникает во время деплоя: инфра, ошибки приложения, автомиграции, невезение. Чтобы хоть как-то их выявить и показать в джобе, мы выполняем:
helm upgrade --install ${RELEASE_NAME} "${CHART_FOLDER}/" \
--namespace "$ENV_NAME" \
-f ${CHART_FOLDER}/values.yaml ${PRODUCT_NAME:+-f ${CHART_FOLDER}/}${PRODUCT_NAME}${PRODUCT_NAME:+-values.yaml} $ADDITIONAL_ENV_VALUES \
--set image.tag="${REWRITE_CONTAINER_VERSION:-$CONTAINER_VERSION}" \
--set global.ingress.enabled="true" \
--set global.ingress.hosts[0]="$ENV_URL" \
--set global.image.pullPolicy="$HELM_IMAGE_PULL_POLICY" \
--set debug_proxy.enabled=true \
--wait \
--timeout ${TIMEOUT}m ${HELM_EXTRA_FLAGS} \
&& mkdir -p "${DEPLOYED_STANDS_FOLDER}" \
&& echo ${DYNAMIC_ENVIRONMENT_URL} > "${DEPLOYED_STANDS_FOLDER}/$ENV_NAME" \
&& echo -e "${TXT_CYAN}Environment API address - https://$ENV_URL${TXT_CLEAR}" \
&& echo -e "${TXT_CYAN}Kubernetes Dashboard - $KUBE_DASH_URL${TXT_CLEAR}" \
) || ( \
export JOB_EXIT_CODE="$?" \
&& echo -e "${TXT_RED}========================
Что-то пошло не так!!!
========================${TXT_CLEAR}" \
&& export RELEASE_NAME=${RELEASE_NAME} \
&& export ENV_NAME=${ENV_NAME} \
&& export TIMEOUT=${TIMEOUT} \
&& ./kubectl_show_error.sh \
&& echo -e "${TXT_RED}Helm завершился с ошибкой.${TXT_CLEAR}" \
&& echo -e "${TXT_GREEN}Логи всех контейнеров также доступны по ссылке:
${TXT_CYAN}https://monitoring.bimeister.io/d/o6-BGgnnk/loki-kubernetes-logs?orgId=1&var-query=&var-namespace=${ENV_NAME}&var-stream=All&var-container=All&from=${TIME_IN_UTC_MILLISECONDS_HELM_STARTED_AT}&to=now${TXT_CLEAR}" \
где kubectl_show_error.sh:
#!/bin/sh
export RELEASE_NAME=${RELEASE_NAME:-"bimeister"}
export ENV_NAME=${ENV_NAME:-"bimeister"}
export TAIL=${TAIL:-20}
export TIMEOUT=$(( ${TIMEOUT:-20} + 5 ))
PROBLEM_PODS="$(kubectl get pods -n $ENV_NAME -o jsonpath='{range .items[*]}{.status.containerStatuses[*].ready} {.metadata.name}
{end}' | grep "false" | rev |cut -d ' ' -f1 | rev )"
for POD in $PROBLEM_PODS
do
JS=$(kubectl get pod -n $ENV_NAME $POD -o json)
# init-container не отработал. Находим его, выводим последние 3 строки лога
if [[ "true" == "$(echo $JS | jq '.status.initContainerStatuses[].state | any(paths; .[] == "running")' | grep true | head -n1)" ]] ; then
echo -e "======================
${TXT_RED}init-container в поде ${TXT_GREED}${POD}${TXT_RED} не отработал${TXT_CLEAR}" ;
kubectl logs -n $ENV_NAME $POD --timestamps=true --prefix=true --all-containers=true --tail=3 ;
# CrahLoopBackOff
elif [[ "" != "$(echo $JS | jq '.status.containerStatuses[] | select(.state.waiting.reason == "CrashLoopBackOff")')" ]] ; then
echo -e "=====================
${TXT_RED}Под ${TXT_GREED}${POD}${TXT_RED} не может корректно стартовать.
Количество рестартов: ${TXT_GREED}$(echo $JS | jq '.status.containerStatuses[].restartCount')${TXT_RED}
Лог последней попытки: ${TXT_CLEAR}" ;
kubectl logs -n $ENV_NAME $POD --timestamps=true --prefix=true --since=${TIMEOUT}m ;
# imagePullBackOff
elif [[ "true" == "$(echo $JS | jq '.status.containerStatuses[].state | any(paths; .[-1] == "waiting")')" ]] ; then
echo -e "======================
${TXT_RED}Возможно не найден образ в поде ${TXT_GREED}${POD}${TXT_RED}
или под не может быть назначен ни на одну ноду.
Подробности ниже:${TXT_CLEAR}" ;
echo $JS | jq '.status.containerStatuses[].state.waiting' ;
echo -e "${TXT_RED}Лог последней попытки:${TXT_CLEAR}" ;
kubectl logs -n $ENV_NAME $POD --timestamps=true --prefix=true --since=${TIMEOUT}m ;
# контейнер unready
elif [[ "false" == "$(echo $JS | jq '.status.containerStatuses[].ready')" ]] ; then
echo -e "======================
${TXT_RED}Под ${TXT_GREED}${POD}${TXT_RED} не имеет статус ready. Проверка healthcheck'a не проходит.
Количество рестартов: ${TXT_GREED}$(echo $JS | jq '.status.containerStatuses[].restartCount')${TXT_RED}
Лог последней попытки: ${TXT_CLEAR}" ;
kubectl logs -n $ENV_NAME $POD --timestamps=true --prefix=true --since=${TIMEOUT}m ;
# Проблемы с контейнером (выводим логи)
else
echo -e "======================
${TXT_RED}Проблемы в поде ${TXT_GREED}${POD}${TXT_RED} неочевидны. Ниже его статусы и лог: ${TXT_CLEAR}"
echo $JS | jq '.status.conditions' ;
kubectl logs -n $ENV_NAME $POD --timestamps=true --prefix=true --since=${TIMEOUT}m ;
fi
done
echo -e "${TXT_CYAN}Список незапущенных подов:${TXT_CLEAR}"
kubectl get pods $PROBLEM_PODS -n $ENV_NAME -o=custom-columns=POD_NAME:.metadata.name,STATUS:.status.phase,INIT_CONTAINERS:.spec.initContainers[*].name
Вывод ошибки примерно такой:
Error
Dashboard — стандартный куберовский, авторизация через AD + keycloak.
Доступ к кластеру k8s такой же. Если выходит новый сотрудник, мы добавляем его в группу в AD, он получает возможность деплоить и пользоваться дашбордом, или скачать kubeconfig, работать с kubectl и различными приложениями для k8s (lens, k9s, и т.д).
Гитлабовская система auto_stop и запинивания сбоила, неймспейсы в неподходящий момент удалялись. Пришлось заменить на собственную:
pin/unpin
При первом деплое или обновлении в неймспейс по умолчанию добавляется лейбл с датой создания или обновления. Джоба pin добавляет дополнительный лейбл bimeister/stand.pinned=true, unpin удаляет его.
На мастере по крону запускается скрипт, который проверяет эти лейблы. Если стенд живет больше суток и у него нет лейбла bimeister/stand.pinned=true, запускается удаление. Если bimeister/stand.pinned=true, он не удалится до момента, пока не снимешь этот лейбл, или не удалишь неймспейс вручную. После авто удаления приходит письмо «извини, но стенда больше нет». Если запиненый стенд живет 5 дней, приходит письмо с предупреждением «а ты точно его используешь или может пора его уничтожить?»
#!/bin/bash
email_on_namespace_deletion(){
LABELS=$(kubectl get ns $1 -o json | jq -r '.metadata.labels' | grep 'bimeister/' | sed 's/"//g' | sed 's/,//g' | sed
':a;N;$!ba;s/\n/\\n/g')
curl --header "Content-Type: application/json" --request POST --data "{\"namespace\":\"$1\",\"whoami\":\"Скрипт авто
очистки незапиненых неймспейсов delete_not_pinned_namespace.sh\", \"additional_info\": \"Время удалени
я: $(date)\", \"labels\":\"$LABELS\"}" https://devops-api.dev.bimeister.io/namespace_deleted
}
# Define the current time and the time threshold (24 hours ago)
THRESHHOLD_TO_DELETE_IN_DAYS=${THRESHHOLD_TO_DELETE_IN_DAYS:-1}
THRESHHOLD_TO_DELETE_IN_SECONDS=$(( 27 * 3600 * $THRESHHOLD_TO_DELETE_IN_DAYS ))
current_time=$(date +%s)
threshold_time=$((current_time - "$THRESHHOLD_TO_DELETE_IN_SECONDS")) # 86400 seconds in a day
echo $THRESHHOLD_TO_DELETE_IN_SECONDS
# List all namespaces and iterate through them
for namespace in $(kubectl get ns -l bimeister/stand.updated.time -o jsonpath='{.items[*].metadata.name}' | tr ' ' '\n' |
grep -E 'ksb-[a-z]+-[a-z]+' | sort)
do
# Get the stand.started_time label value
started_time_label=$(kubectl get ns "$namespace" -o jsonpath='{.metadata.labels.bimeister\/stand\.updated\.time}')
# Convert label time to Unix timestamp
# Convert YYYY-MM-DDTHH-MM to YYYY-MM-DD HH:MM
formatted_time_label="${started_time_label:0:10} ${started_time_label:11:2}:${started_time_label:14:2}"
# Use 'date' to convert to Unix timestamp
label_time=$(date -d "$formatted_time_label" +%s)
# Check if stand.pinned label exists
pinned_label=$(kubectl get ns "$namespace" -o jsonpath='{.metadata.labels.bimeister\/stand\.pinned}')
# If label time is older than the threshold and pinned label is not true, delete the namespace
if [[ $label_time -le $threshold_time ]] && [[ $pinned_label != "true" ]]; then
echo label_time=$label_time threshold_time=$threshold_time
echo "Deleting namespace: $namespace"
email_on_namespace_deletion $namespace
kubectl delete ns $namespace
fi
done
https://devops-api.dev.bimeister.io — самописное api на питоне.
devops-api
Еще из плюшек:
Написали статью с решением основных проблем при деплое, чтобы люди сами выяснили, почему упало, и попробовали решить. Подумываем над созданием умного бота для поиска проблем.
pg_admin развернут в дефолтном неймспейсе. Любой может к нему подключиться и пользоваться, указав в настройках имя сервиса с базой из своего неймспейса.
Скоро реализуем создание бекапов и перенос данных из одного неймспейса в другой.
Инструкции для локальной установки kubernetes (k3s, deckhouse, использование telepresence). Напишем статью, как это все разворачивать и использовать.
Минусы использования персональных стендов:
ресурсы для поддержки стендов, обслуживание машин и кластеров,
время на обучение сотрудников и поиск проблем при деплое,
стресс на этапе создания и тестирования работы тестового кластера. Очень много вопросов от сотрудников: не работает то-то, как сделать то-то.
За несколько лет мы пришли к тому, что у нас уже ~200 человек в компании, кому может понадобиться персональный стенд. Для них у нас есть ~20 стендов с докером и 130 в kubernetes.
Разрабатывать и тестировать стало проще и быстрее, отдавать во внедрение — спокойнее. На этом мы не останавливаемся и продолжаем делать работу нашего RND комфортнее, о чем узнаете в следующих статьях.