Чому CI/CD Python розвалюється на залежностях
Класичний сценарій: розробник проганяє тести локально, усе зелене, гілка вирушає в загальний конвеєр — і збірка залежностей падає на встановленні пакетів. Причина не в коді, а в оточенні розробника: воно накопичувалося місяцями й не описане в жодному файлі проєкту.
Перше джерело — відсутність зафіксованого набору версій. Якщо залежності оголошені діапазонами, а файл блокування не оновлюється за кожної зміни, CI/CD Python встановлює те, що на момент запуску лежить в індексі.
Два запуски на різних машинах дають різні дерева залежностей, і конфлікт версій пакетів спливає вже в чужій гілці.
Другий — змішування інструментів. pip, Poetry і Conda розв’язують залежності по-різному: Conda тягне бінарні збірки зі своїх каналів, pip — з індексів, Poetry фіксує власне дерево. Один пакет, поставлений в обхід основного менеджера, робить збірку невідтворюваною.
Окремий рядок болю — версія інтерпретатора: розбіжність між локальною машиною та образом у конвеєрі змінює правила розв’язання залежностей і ламає бінарні пакети, зібрані під іншу версію.
Ціна такої нестабільності — не одна збірка, що впала. Падіння збірки на етапі встановлення з’їдає час усієї команди, привчає ігнорувати червоні прогони й переносить пошук справжньої помилки на день релізу. Розбираємо, як це закривається.
Що змінюється в команді після налагодженої збірки й випуску
Ефект від налагодженого конвеєра видно в щоденних цифрах: скільки триває збірка, скільки займає повернення до попереднього релізу, яка частка кроків випуску робиться руками.
Нижче — чотири зрушення, які замовник помічає вже в перші тижні, без окремого проєкту з «упровадження метрик».
Збірка — хвилини замість десятків хвилин
Кеш залежностей і шарів, зафіксований базовий образ, розділення встановлення пакетів і прогону тестів — збірка перестає заново завантажувати й компілювати те саме.
Розробник отримує зворотний зв’язок щодо свого коміту, а не до кінця дня, і правки перестають накопичуватися в очікуванні прогону.
Відтворюваність на всіх стендах
Poetry із зафіксованим lock-файлом, хеші пакетів і один образ для локальної машини, тестового стенда та бойового оточення прибирають розбіжності «у мене працює». Набір залежностей однаковий усюди, тому збій відтворюється локально, а не лише на сервері за п’ять хвилин до випуску.
Повернення до попереднього релізу — хвилини
Артефакт збирається один раз і просувається середовищами без перезбирання: у бойове оточення потрапляє рівно те, що пройшло перевірки. Якщо реліз пішов не так, повернення — це перемикання на попередній тег, а не повторна збірка й пошук потрібної гілки в поспіху.
Клас відмов, який зникає
Розбіжність оточень, забута залежність, несумісна версія бібліотеки, крок, пропущений вручну, — ці відмови відловлюються на етапі перевірок і не доходять до користувачів.
Частка ручних операцій у випуску падає, а разом із нею зникає варіативність: сценарій виконується однаково і людиною, і за розкладом.
Способи збирання оточень для Python-проєктів
У Python немає вбудованої фіксації оточення: інтерпретатор один, а набір бібліотек у системній гілці спільний для всіх проєктів. Варто підняти поруч другий сервіс із несумісною мажорною версією спільної залежності — і падає вже працюючий.
Віртуальне оточення закриває ізоляцію, але не відтворюваність: без фіксації версій збірка на іншій машині підтягне свіжі транзитивні залежності, і поведінка коду зміниться.
Вибір менеджера залежностей визначає, що саме потрапляє в ізоляцію і наскільки повторювано це відтворюється у збірці пакетів.
| Варіант | Коли підходить | Обмеження |
|---|---|---|
venv + pip + requirements.txt |
Невеликі сервіси, залежності перелічені вручну | Немає фіксації транзитивних версій — повторне встановлення дає інший набір |
pip-tools |
Уже склався requirements.txt, потрібна фіксація версій |
Прямі й транзитивні списки ведуться роздільно, конфлікти розв’язуються перезбиранням |
| Poetry / PDM | Бібліотеки й сервіси з групами залежностей під тести та документацію | Власний алгоритм розв’язання версій, розбіжності з pip під час збирання артефактів |
| Conda | Науковий стек і бінарні залежності поза PyPI | Важкі канали, оточення погано стискається в мінімальний образ |
| Образ контейнера як оточення | Сервіси, де середовище збірки й рантайм мають збігатися | Без шаруватого кешу час збірки зростає кратно на кожній правці коду |
Практично вибір іде за двома осями: хто розв’язує конфлікти версій — pip чи власний механізм менеджера, і де проходить межа оточення — у віртуальному каталозі чи в образі.
Для сервісу, який викочується контейнером, розумно фіксувати версії файлом блокування й ставити залежності окремим шаром: тоді правка коду не перетягує бібліотеки, а незбіг версій спливає на збірці, а не в рантаймі.
Етапи впровадження: від розбору репозиторіїв до передачі
Порядок робіт однаковий для будь-якого стеку: спочатку розібратися, що вже є в репозиторіях, і лише потім будувати конвеєр. Пропущений аудит призводить до того, що збірка заводиться в одному проєкті й розвалюється в решті, а розбіжність оточень спливає вже на викочуванні. Нижче — послідовність, за якою ми проходимо впровадження.
- Аудит репозиторіїв. Дивимося структуру проєктів і те, чим оголошені залежності: requirements.txt з точними версіями, pyproject.toml із Poetry або окреме середовище Conda. Відзначаємо, де оточення збирається руками і скільки це віднімає часу в команди.
- Схема конвеєра. Узгоджуємо стадії та їхній порядок: підготовка оточення, перевірки коду, тести, збірка артефакту, публікація, доставка на стенди. Для кожної стадії одразу визначаємо, що вважається блокуючою помилкою.
- Налаштування оточень. Приводимо версію інтерпретатора й склад залежностей до єдиного вигляду, вмикаємо кеш пакетів між запусками — повторна збірка більше не тягне все з нуля.
- Тестовий прогін. Запускаємо конвеєр на одному репозиторії або гілці, заміряємо час стадій і шукаємо падіння на порожньому місці: плаваючі залежності, відсутність ізоляції тестів.
- Приймальна перевірка. Проходимо сценарії, критичні для замовника: падіння тестів блокує доставку, відкат на попередній артефакт, паралельні збірки не заважають одна одній.
- Передача й навчання. Віддаємо схему конвеєра, інструкцію з правки стадій і розбір типових збоїв, проходимо їх із командою — далі підтримку ведуть власними силами.
Конфігурації, схеми й регламенти: що залишається в проєкті
Налаштований конвеєр без документації живе до першого звільнення інженера, який його збирав: за місяць уже ніхто не пам’ятає, чому залежності фіксуються саме так і що станеться під час відкату.
Тому разом із робочою збіркою передаємо комплект артефактів у репозиторій проєкту та в базу знань команди — за ними процес відтворюється без автора.
- Файли конфігурації конвеєра з коментарями: де стадії збірки й тестів, які умови запуску, що кешується між прогонами.
- Файли керування залежностями для вибраного інструмента (pip, Poetry або Conda): зафіксовані версії, розділення наборів для розробки й для релізу.
- Схема оточень: які контури є, що в кожному розгортається, чим відрізняються параметри, як оточення піднімається з нуля.
- Політики доступу до секретів: де зберігаються ключі, хто їх видає, як проходить ротація і що гарантовано не потрапляє в логи.
- Регламент випуску: шлях від гілки до продуктивного середовища, хто ухвалює рішення, у яких точках можливий відкат і що блокує випуск.
- Інструкції для чергового інженера: типові збої збірки, порядок дій при падінні прогону й відкаті, точки ескалації.
Комплект знімає залежність від усних домовленостей: новий інженер входить у проєкт за документами, а перевіряльник бачить, хто і на якій підставі змінював доступи та випуски.
Регламент і схема оточень залишаються робочими документами — їх правлять разом із кодом, а не відновлюють після збою.
Кейс: конвеєр випуску для розподіленої команди
У розподіленій команді збірка одного сервісу займала від 18 до 25 хвилин, і цей час майже не залежав від обсягу правок.
Причина була не в тестах: половину кроку з’їдало встановлення залежностей, яке кожен контур вирішував по-своєму — сервіси тягнули свіжі версії із загального індексу, частина проєктів збиралася Poetry, аналітичний контур жив в окремому Conda-оточенні. У результаті виходило головне: збирач і машина розробника давали різні набори пакетів.
Що змінили
Залежності звели до єдиного джерела істини: версії фіксуються файлом блокування, встановлення йде з нього, а не з діапазонів.
Кеш шарів налаштований так, що перезбирання оточення запускається лише за зміни цього файлу, а сама збірка в GitLab CI іде в незмінному образі, прив’язаному до конкретної ревізії коду. Перевірки розділили на швидкий набір на кожну зміну й повний прогін перед випуском.
До: близько 20 хвилин на збірку, зривався кожен четвертий випуск — розбіжність оточень, падіння на кроці встановлення, ручні правки на сервері.
Після: збірка вкладається в 6–7 хвилин, зривається один випуск із двадцяти, а відкат повертає попередній образ за хвилини замість ручного розбору.
Чим збірка бібліотеки відрізняється від збірки сервісів?
Бібліотека й сервіс різняться не вмістом репозиторію, а тим, що вважається результатом збірки. У бібліотеки це незмінний пакет, який публікується в приватний реєстр і далі живе в чужих залежностях.
У сервісу — образ, який розгортається в конкретному оточенні й залишається там працювати місяцями. Звідси розходяться вимоги до версіонування, публікації артефактів і відкату.
Збірка бібліотек перевіряється матрицею: кілька версій інтерпретатора, обидва підтримувані менеджери залежностей, окремо — встановлення пакета зі зібраного артефакту, а не з вихідників.
Схема версіонування тут семантична й жорстка: зміна, що ламає зворотну сумісність, має підняти мажорну частину. Від пакета залежать інші команди, і вони оновлюються за своїм графіком, а не одночасно з вами.
Публікація артефактів для бібліотеки — операція незворотна за змістом: одна версія випускається один раз, перезапис заборонений, інакше у двох різних збірок буде однакове ім’я.
Якщо у випущеній версії знайшлася помилка, випускають нову patch-версію; відкат виконується на боці споживача, який закріплює попередній номер у своєму файлі залежностей.
Збірка сервісів розв’язує іншу задачу: отримати відтворюваний образ, який однаково поводиться на стенді та в робочому контурі. Тут перевіряють запуск застосунку, міграції схеми даних, читання конфігурації й секретів, поведінку під навантаженням.
Версіонування прив’язують до стану репозиторію й мітки випуску, а не до номера пакета, — важлива трасування «яка ревізія коду зараз працює».
Відкат сервісу й відкат бібліотеки — теж різні процедури. Пакет не можна повернути назад у реєстрі: можна лише позначити проблемну версію як непридатну, а залежні проєкти переведуть свої закріплення.
Сервіс відкочується перемиканням на попередній образ — швидко, але тільки якщо міграції схеми сумісні з минулим кодом. Тому зміни даних готують так, щоб один крок випуску допускав роботу обох версій.
Технічні сумніви перед стартом робіт
Питання, які найчастіше спливають перед початком робіт, стосуються не інструментів, а ризиків: що буде з уже працюючим конвеєром, як розв’язуються конфлікти залежностей і що відбувається після передачі. Нижче — короткі відповіді на кожне.
У нас уже налаштований конвеєр збірки — доведеться переписувати все з нуля?
Ні. Спочатку знімаємо інвентаризацію: які оточення, звідки тягнуться залежності, де кроки дублюються. Зазвичай перевикористовується більша частина — змінюються лише етапи встановлення залежностей і кешування. Перехід робимо поетапно, щоб основна гілка залишалася робочою.
Що робити, якщо залежності двох сервісів конфліктують за версіями?
Розводимо їх за ізольованими оточеннями й фіксуємо несумісні пари в окремих цілях збірки. Конфлікт, який не можна зняти оновленням, локалізується в одному сервісі й не блокує збірку решти. Розбір конфліктів ведемо за деревом залежностей, а не підбором навмання.
Чи можна обійтися без власного сховища пакетів?
Так, але із застереженнями: за кожної збірки тягнемо із зовнішніх індексів і тримаємо локальний кеш. Власне сховище варто піднімати, коли потрібні закриті пакети, суворий контроль версій або збірка без доступу до зовнішньої мережі. Рішення ухвалюємо за кількістю сервісів і вимогами до ізоляції.
Як забезпечується відтворюваність збірки?
Фіксуємо точні версії, а не діапазони, і зберігаємо файл блокування поруч із вихідниками. Збірка йде в чистому оточенні, із закріпленими базовими образами. Це дає однаковий результат на машині розробника й на складальному агенті, а також дає змогу повторити реліз через місяці.
Хто супроводжує конвеєр після передачі?
Передаємо конфігурації, схеми етапів і опис типових збоїв із порядком дій. Команда замовника може вести конвеєр сама — документація розрахована на це.
Якщо потрібна підтримка, залишаємося на супроводі: правимо збої збірки, оновлюємо залежності, доопрацьовуємо етапи під нові сервіси.
Обговоримо задачу й повернемося зі схемою та планом
Щоб повернутися зі схемою конвеєра, а не із загальними словами, нам достатньо одного обговорення задачі. Ми розбираємо, як зараз збирається й публікується код, де найчастіше падають перевірки та які кроки перед випуском версії роблять руками.
Від вас потрібен мінімум вступних даних:
- репозиторій або його структура: гілки, теги, порядок випуску версій;
- файли залежностей — що використовуєте: pip, Poetry або Conda, чи є приватні дзеркала пакетів;
- перелік середовищ і чим вони відрізняються за доступами та змінними оточення;
- слабкі місця за останні місяці: збірки, що впали, ручні кроки, відкати;
- обмеження: закриті мережі, вимоги до доступів і регламенти безпеки;
- бажаний час збірки й випуску — цифри, які вважаєте прийнятними.
У відповідь надсилаємо схему конвеєра цілком: етапи, умови запуску, кеш залежностей, розділення перевірок і публікації артефактів.
До неї — склад робіт із межами «що входить і що ні», план робіт за етапами та оцінку термінів у робочих днях, окремо на налаштування й на передачу в експлуатацію. Якщо на якомусь кроці бачимо ризик, говоримо про нього до старту, а не в середині.
Опишіть задачу у вільній формі — повернемося з оцінкою та планом.







