Personal (jesus) стенд — решаем проблему тестовых контуров в компании

228bbd03f9ab806ad55eee8f214090c1.jpg

Всем привет, меня зовут Захаров Антон, и я 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

Наши стенды — 2021

В рабочем чате творится вакханалия «Я занял такой-то стенд, а кто мне все сломал?! Это же был мой стенд». Сделали бота, через который сотрудники бронировали персональный стенд и отмечали, если стенд больше не использовали.

Все это время мы думали об использовании kubernetes. Даже были манифесты, позволяющие развернуть приложение в нем, но рук не хватало, и откладывали это все до подходящего момента. Момент наступил, когда расширилась команда DevOps.

Февраль 2022: подняли кластер kubernetes, написали helm chart и создали первые персональные стенды в кубере — ksb, каждый такой стенд — это отдельный неймспейс.

Первые 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

gitlab auto_stop

Если на стенд накатывали обновление, время сбрасывалось и отсчет начинался заново.

Со стендом можно работать:

  • pin — остановить auto_stop,  

  • view deployment — перейти по ссылке на сайт приложения,  

  • stop — запустить undeploy.

Environments

Environments

Стенды с docker-ом остались, их стало меньше, а высвобожденные ресурсы добавились в кластер.

Так мы и жили почти полтора года, поддерживали работу кластера, деплой персональных стендов, параллельно делали другие задачи. Количество стендов росло, добавились новые кластеры:

  • dev — основной кластер с персональными стендами,

  • regress — для проведения регрессионного тестирования,

  • stress — для нагрузочного тестирования и др.,

  • тестовый кластер — можем ломать его, как хотим.

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

Ноябрь 2023: персональных стендов в k8s — 60 штук. «МАЛО! — говорят люди, — нам не хватает!»

Дайте еще!!!

Дайте еще!!!

Декабрь 2023: вот к чему мы пришли.

Сейчас

Сейчас

Нам купили еще оборудование, и мы смогли увеличить ресурсы наших кластеров.

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

Наш итоговый pipeline (во время создания релиза добавляется еще один stage):

Родительский пайплайн

Родительский пайплайн

Заходим в sandboxes:

Sandboxes

Sandboxes

Еще глубже в to personal ksb 1, to personal k8s 1 manual — это возможность задеплоить различные наборы модулей приложения

Внутри to personal ksb 1

Внутри 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

Error

Dashboard — стандартный куберовский, авторизация через AD + keycloak.
Доступ к кластеру k8s такой же. Если выходит новый сотрудник, мы добавляем его в группу в AD, он получает возможность деплоить и пользоваться дашбордом, или скачать kubeconfig, работать с kubectl и различными приложениями для k8s (lens, k9s, и т.д).

Гитлабовская система auto_stop и запинивания сбоила, неймспейсы в неподходящий момент удалялись. Пришлось заменить на собственную:

pin/unpin

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

devops-api

Еще из плюшек:

  • Написали статью с решением основных проблем при деплое, чтобы люди сами выяснили, почему упало, и попробовали решить. Подумываем над созданием умного бота для поиска проблем.

  • pg_admin развернут в дефолтном неймспейсе. Любой может к нему подключиться и пользоваться, указав в настройках имя сервиса с базой из своего неймспейса.

  • Скоро реализуем создание бекапов и перенос данных из одного неймспейса в другой.

  • Инструкции для локальной установки kubernetes (k3s, deckhouse, использование telepresence). Напишем статью, как это все разворачивать и использовать.

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

  • ресурсы для поддержки стендов, обслуживание машин и кластеров,  

  • время на обучение сотрудников и поиск проблем при деплое,  

  • стресс на этапе создания и тестирования работы тестового кластера. Очень много вопросов от сотрудников: не работает то-то, как сделать то-то.

За несколько лет мы пришли к тому, что у нас уже ~200 человек в компании, кому может понадобиться персональный стенд. Для них у нас есть ~20 стендов с докером и 130 в kubernetes. 

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

© Habrahabr.ru