Всем привет.
Статья написана в простом стиле "DevOps для домохозяек" от таких же домохозяек. В ней будет описано с какими неожиданностями можно столкнуться при настройке проекта в TeamCity. Также приведу рекомендации как эти проблемы можно обойти.
Нижеописанное основано на моём двухлетнем опыте настройке TeamCity сборок, чтению баг репортов и обмене мнений с коллегами по цеху. Не претендую на истину в последней инстанции, так как в работе в основном использовался подход SDD (Stackoverflow Driven Development).
Небольшая справка:
- TeamCity — CI (Continous Integration) инструмент. "Аналог" Gitlab CI, Github Actions с прицелом на возможность полной настройки автоматизации из графического интерфейса.
- Проект (Project) — агрегирующая сущность, в неё связываются несколько сборок. В TeamCity древовидная структура проект->подпроект->сборка с частичным наследованием настроек.
- Сборка (Build) — Атомарная сущность автоматизации. Примеры сборок "Запуск автотестов", "Установка конфигурации", "Сборка дистрибутива". Каждая сборка состоит из нескольких шагов.
- Шаг (Build step) — Описание "а что нужно делать" используя разные "runner type". Это может быть как простой Bash скрипт, так и запуск Docker контейнера.
Вкратце по проекту TeamCity с которым я работаю:
- ~30 сборок, шаги сборок состоят из вызовов Bash, Ansible и Python.
- Никаких сборок Android приложений, Web проектов, Docker, k8s и прочего. Просто заказ облачных серверов, подъем базы данных из дампа, установка программ и конфигурации.
- Настройка ведётся из графического интерфейса, без Kotlin DSL (переход на него в планах).
Про достоинства инструмента я думаю можно почитать в официальной документации и маркетинговых материалах, нет смысла их здесь повторять. Перейдём сразу к "нюансам".
1 Нельзя изменить параметры сборки при запуске по триггеру
Сборки можно запускать по триггеру (внешнему событию). Триггеры могут быть разные: была завершена другая сборка, обновилась git ветка, cron задача. При этом в сборке можно задать параметры по умолчанию.
Так вот: запуск по триггеру можно сделать только с параметрами по умолчанию. На эту проблему есть нерешённая задача 2008 года.
Опишу простой пример, на котором можно понять масштаб проблемы: у нас есть сборка по поднятию стенда и сборка по запуску автотестов на стенде. Вторая сборка получает на вход имя стенда и прогоняет автотесты. Первую сборку нужно запустить 1 раз, чтобы получить 1 стенд. Вторую сборку нужно запускать часто, чтобы понимать в каком состоянии стенд. Ииии такую простую штуку нельзя сделать.
Но мне возразят, что нужно использовать build chain (цепочку сборок). Окей, давайте посмотрим что там не так.
2 Build chain or not — просто скопируй ещё сборки
В случае вышеописанного кейса (заказ стенда + прогон автотестов) мы настраиваем build chain. В таком случае если нам нужен стенд, то мы его получаем из сборки запуска автотестов. Интуитивно понятно, не правда ли?
Но тут приходит другая проблема: а как ещё раз запустить сборку автотестов (мы же её запускаем периодически для каждого стенда). Иииии никак. Ну точнее можно, но тогда будет заказан ещё один стенд (если у вас не решён полностью вопрос с идемпотентностью). Или нужно указать во вкладке зависимостей, что не запускай зависимую сборку ещё раз.
Но как тогда брать имя стенда, на котором запускать автотесты? Если у нас две сборки отдельных, то мы указываем имя в параметрах. Если цепочка сборок — то проще всего использовать артефакт. А теперь если мы желаем запускать и отдельно, и вместе, то нужен специальный огород, который будет это всё обрабатывать. Осталось только представить, как будет "интересно" настраивать такой огород, когда у нас появится две разные сборки на заказ стенда (заказ идёт в разных облаках, разные программы), а сборка с запуском автотестов будет одна.
К сожалению, я не смог найти "пруфов" решения, которое предлагают разработчики, но если мне не изменяет память оно было такое: для каждого случая создавайте отдельную сборку. По мне это решение приводит к дублированию кода и больше похоже на костыль. Итого можно констатировать: нельзя сделать так, чтобы одну сборку можно было запустить и в цепочке и отдельно.
3 Переопределение параметров зависимой сборки
Рассмотрим другой пример: у нас есть три зависимые последовательные сборки, которые связаны в build chain. Скажем заказ в облаке стенда, установка дампа базы данных и установка программ. Мы "интуитивно" запускаем сборку по установке программ и возникает вопрос: а как пробросить размер заказываемой машины в зависимой сборке? И тут нам на помощь приходит переопределение параметров.
Теперь у нас в одном диалоговом окне по запуску сборке возникает 3*N параметров, которые никак не отличаются друг от друга. Ещё стоит учитывать, что описание и параметры по умолчанию таких параметров не копируются, их нужно копировать отдельно. Особенно это "радует", когда эти описания и значения меняются. Их тогда нужно будет обновлять в N местах, если сборки можно вызывать из разных мест как в нашем случае. Например, нужен человеку только стенд с дампом базы данных, он тогда вправе заказать его со второй сборки. А там в параметрах версия дампа устаревшая, в отличие от последней сборки цепочки, и пойдёт разбор полётов на полдня почему дамп кривой.
И конечно тут не будет никакой валидации, что вы не ошиблись в имени переопределяемого параметра, всё в лучших традициях YAML Developer'ов.
4 Конфигурация сборки может быть только одна
Проблема не относится к Kotlin DSL (у меня нет опыта его использования, не могу сказать насколько эта проблема действительно решается). Если мы настраиваем сборки в графическом интерфейсе как настоящие домохозяйки, то сталкиваемся со следующей проблемой: а как плавно поменять настройки проекта, так чтобы это не коснулось пользователей?
Первый и самый простой вариант: объявить технологические работы и править "на горячую". Второй вариант: скопировать сборку в отдельное место и делать изменения в ней (наш вариант).
Потом нужно после внесения изменения в основную ветку кода и в сборке как-то проинформировать всех пользователей, чтобы они обновили свою ветку кода. В общем не будьте как мы — используйте Kotlin DSL.
Рекомендация: не архивируйте/удаляйте старые сборки, а обновляете их после внесения изменений в основную ветку кода. Всегда найдутся люди, у которых ссылки на сборки хранятся где-то в закладках и если вы поменяется ID сборки, то все ссылки побьются.
5 TeamCity API
Я думаю многие здесь знакомы с "главными" 4 метриками DevOps. И TeamCity кажется идеальным местом, чтобы собрать всю информацию хотя бы о половине из них ("Deployment Frequency" и "Lead Time for Changes").
Иии вот нельзя в API быстро узнать процент упавших сборок, частоту запуска и причины падения. Да, там есть какие-то дашборды, но информация в них именно та, которая не нужна. То есть вот начали у нас падать все сборки на основной ветке, и мы можем только вручную "Assign investigation", либо придумывать как реализовывать такой инструмент в каждой сборке. Не очень удобно.
Однако, в чём хорош API — так это в формировании build chain из-за вышеперечисленных недостатков с "нативным" способом. В таком случае можно сборки создавать для независимого запуска. А их связывание делать в отдельной сборке с запуском тупого Python скрипта. И много кто так обходит проблему.
6 При запуске Bash скрипта проверяется только результат последней команды
Есть у нас простой скрипт, написанный в интерфейсе:
./command_1.sh # always fail
ls # always success
В таком случае этот шаг сборки будет всегда зелёный. Но если мы добавим мантру:
./command_1.sh # always fail
if [ $? -ne 0 ]; then
echo "##teamcity[buildProblem description='Build failed']"
fi
ls # always success
И тогда уже шаг будет красным, и сборка дальше не пойдёт (тут уже как выставлен "Execute step"). Иными словами, всегда нужно учитывать особенность работы Bash скриптов.
7 Работа с шифрованными параметрами сборки
В TeamCity можно добавить параметры-секреты, такие как пароли и API ключи. Чтобы такие данные не утекли, в логах сборки ищутся значения таких параметров и заменяются на символы *
. И возникает следующий нюанс: а если нам эти параметры нужно записать в другой файл. Команда echo
в таком случае не сработает — фильтр перехватит. В итоге мы пришли к следующему варианту:
cat > constants.json <<- EOM
{
"key": "%value%"
}
EOM
Задача, в которой это понадобилась, была следующая. Есть Python скрипт, который выполняет запросы к нескольким системам, и вся его конфигурация сохранена в JSON. Как к нему подать секреты? Можно через командную строку, но тогда у нас растекается конфигурация сразу по четырём местам: JSON, командная строка, значение по умолчанию в скрипте и ещё значение по умолчанию в параметре TeamCity. Поскольку скрипт должен быть тупым и одноразовым, то решили максимально упростить: всю конфигурацию втащить в JSON. JSON прям с вписанными именами параметров TeamCity сохраняем в репозиторий и копируем как есть в шаг сборки. В итоге мы сразу формируем нужный JSON и не разбираемся какой параметр, где нужен.
8 Скорость прогрузки графического интерфейса
Это будет очень субъективный пункт. Поскольку основная работа по настройке проводится в интерфейсе (для тех кто не пользуется связкой Kotlin DSL + TeamCity API), то производительность работы пропорционально скорости работы этого интерфейса. А он ооочень медленный. Я постарался в меру своих интеллектуальных способностей замерить скорость прогрузки и вот какие цифры получил (был использован браузер Firefox и инструмент Network).
- Загрузка окно проекта со всеми сборками и вызов окна запуска сборки
- load: 9.87 s
- DOMContentLoaded: 4.92 s
- Finish: 34.39 s
- Size/transferred size of all requests: 10.69 MB / 2.42 MB
- Requests: 345
- Загрузка окна отдельной сборки и вызов окна запуска сборки
- load: 4.59 s
- DOMContentLoaded: 1.27 s
- Finish: 27.42 s
- Size/transferred size of all requests: 11.53 MB / 2.23 MB
- Requests: 120
Время Finish — до момента как у меня появляется возможность запустить сборку. Так как после этого ещё в фоне прогружается страница и идут запросы. Полминуты чтобы просто запустить одну сборку, неплохо?
9 Информация по упавшей сборке
Это будет ещё один субъективный пункт. Для каждой сборки есть вкладка Overview. В ней в случае падения сборки указывается ошибка. И эта ошибка в 99% случаев определяется неправильно. В нашем случае (может у кого по-другому) — ошибка определяется как "первое, что попало в stderr
", хотя было бы логичнее сделать "последнее, что попало в stderr
". В случае Ansible это всегда будет какой-нибудь "WARNING: Deprecation setting...". И в итоге это вызывает стабильный поток вопросов у людей слабо знакомых с TeamCity. Пусть лучше вообще там ничего не писалось.
10 Вечные проблемы с агентами
Здесь я бы хотел написать про все проблемы, которые связанные с агентами сборки (Build agents). Как известно в TeamCity есть master сервер, который выполняет роль планировщика, а сами сборки запускаются на отдельных серверах. И нужно уметь правильно с этими агентами работать (обслуживать), TeamCity тут мало что может сделать.
Первая проблема — удаление агентов. У агентов чаще всего есть какой-то срок жизни, их лучше не делать долгоживущими, иначе потом появляется дрейф конфигурации. Например, кто-то поменял JAVA_HOME
и понеслось. И вот это удаление может прилетать вообще неожиданно. Планировщик выдал агента для тяжеловесной ночной десятичасовой сборки. Но ночью чаще всего проводятся все работы по обслуживанию и какой-то сервис, не проверив что там с агентом просто его грохает. Мы так и не победили эту проблему (администрированием TeamCity занимается отдельная "команда").
Вторая проблема — закончившееся место на агенте, либо неработающая базовая утилита. Пересекается с первым пунктом, но тут "веселее". Запускаем сборку, на самом первом шаге это всё падает и агент снова бодро готов браться за новую работу. Мы раздражённо запускаем сборку заново и по великому везению попадается тот самый агент и по новой. "Но у нас же есть великая настройка по выбору агента!" — скажете вы и будете правы. Только один нюанс: а если у нас build chain? Тутуту руру тутуту, а оказывается, что выбрать агент можно только на текущую сборку, на зависимые может браться какой угодно и мы знаем какой будет выбран по закону подлости. Но это в случае, если вы не выставили при установке зависимой сборки "Run build on the same agent". Но вы же выставили, правда?
Третья проблема — дрейф конфигурации на агентах. На агентах должно быть неизменяемое окружение (то, что не должно быть root
прав, я думаю объяснять не надо). Кто-то поменял переменную окружения, поменял локальные пути до утилит и понеслось. Начинаешь после каждого такого случая перестраховываться и потом у тебя 90% сборки — это подготовка агента к твоему print("Hello, World!")
.
И конечно зайти на агент и интерактивно разбираться в чём там проблема не получится (в моём конкретном случае). Ещё лучше, когда на локальном линуксе всё отрабатывает. В общем "кто дебажил непонятную проблему на билд агентах — тот в цирке не смеётся".