<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[./deploy-thoughts --verbose]]></title><description><![CDATA[Особистий щоденник DevOps ентузіаста: від автоматизації релізів та інфраструктурних викликів до будь-яких технічних історій зі свого повсякдення.]]></description><link>https://ghaiklor.dev</link><generator>RSS for Node</generator><lastBuildDate>Mon, 20 Apr 2026 08:24:09 GMT</lastBuildDate><atom:link href="https://ghaiklor.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Як в мене налаштований macOS?]]></title><description><![CDATA[Всім привіт! Нещодавно я вирішив пройтись по своїм Apple пристроям та оновити на них все до останньої версії. У випадку з macOS вийшло оновитись до macOS Tahoe 26.2 (25C56).
Але пост не про те, що я оновив macOS. Річ у тім, що я цього разу оновлення ...]]></description><link>https://ghaiklor.dev/yak-v-mene-nalashtovanij-macos</link><guid isPermaLink="true">https://ghaiklor.dev/yak-v-mene-nalashtovanij-macos</guid><category><![CDATA[macOS]]></category><category><![CDATA[macOS Tips]]></category><dc:creator><![CDATA[Eugene Obrezkov]]></dc:creator><pubDate>Wed, 21 Jan 2026 15:43:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/IH3HiOTsM28/upload/71ebe04a15035a32125d65cc34ba6254.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Всім привіт! Нещодавно я вирішив пройтись по своїм Apple пристроям та оновити на них все до останньої версії. У випадку з macOS вийшло оновитись до macOS Tahoe 26.2 (25C56).</p>
<p>Але пост не про те, що я оновив macOS. Річ у тім, що я цього разу оновлення вирішив провести на чисту <em>(без всіляких бекапів чи іншого, форматування в нуль й встановлення на чистий ноутбук)</em> й ознайомитись з усіма налаштуваннями й застосунками, які там у нього є. Тобто, я рекурсивно заходив в кожен пункт, який я бачив, чи то в налаштуваннях системи, чи то в якомусь застосунку й знайомився що там взагалі відбувається.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768998711241/3e9c303a-0201-4bd4-b7f3-19b8febc3817.png" alt class="image--center mx-auto" /></p>
<p>Тож в цьому пості я б хотів поділитись налаштуваннями macOS, які, на мою думку, могли б бути корисними чи зручними. І звісно ж, в сучасному світі, без жирного дисклеймера не обійтись. Тож скажу, що це те, як мені зручно працювати за ноутбуком на macOS. Можливо, хтось дізнається звідси про якусь галочку в налаштуваннях, якої йому не вистачало, тож… все в цьому пості суто субʼєктивне.</p>
<p>Почнімо вже говорити про те, як в мене налаштований macOS.</p>
<h2 id="heading-terminal">Terminal</h2>
<h3 id="heading-profiles">Profiles</h3>
<p>За замовчуванням, я використовую профіль Homebrew:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768999350533/46308c90-f964-453e-8a68-f3df8e711e30.png" alt class="image--center mx-auto" /></p>
<p>І єдине, що я кардинально змінюю в профілі — це шрифт. Річ у тім, що я використовую Fish Shell, про який поговоримо згодом, з темами, яким потрібні додаткові гліфи. Цих гліфів немає в шрифтах за замовчуванням, а тому я додатково ставлю собі шрифт від GitHub, який називається Monaspace. Він же використовується і для роботи безпосередньо з кодом, а не лише в терміналі.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://monaspace.githubnext.com">https://monaspace.githubnext.com</a></div>
<p> </p>
<p>Але, не поспішайте ставити його по посиланню зверху :)</p>
<p>В шрифті від GitHub теж немає потрібних гліфів для теми в терміналі, а тому я хоч і ставлю Monaspace, але ставлю пропатчений від Nerd Fonts:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.nerdfonts.com">https://www.nerdfonts.com</a></div>
<p> </p>
<p>І вже цей шрифт йде в мене як основний шрифт для термінала: <code>Monaspice Neon Nerd Font Mono</code>. Його, до речі, можна і через <code>brew</code> поставити.</p>
<h3 id="heading-essentials">Essentials</h3>
<p>Довго не буду затримуватись на цій частині, бо кожен хто займається розробкою точно має ці інструменти. Особисто для себе я ставлю лише дві речі: Command Line Developer Tools та Homebrew.</p>
<p>Перша в мене йде суто для того, щоб <code>git</code> був, ну і Homebrew як пакетний менеджер для встановлення того ж Shell, про який я зараз трішки розкажу.</p>
<h3 id="heading-fish-shell">Fish Shell</h3>
<p>Чим мене купив <a target="_blank" href="https://fishshell.com">Fish Shell</a> свого часу, так це своїм автозаповненням команд по Tab. Раніше, я сидів і на всяких oh-my-zsh і потім ще й oh-my-fish був, грався багато де. Але в кінці — все одно залишився на Fish й досі ним користуюсь із коробки, ніяк не налаштовуючи його.</p>
<p>Напевно, найбільше що мені подобається у Fish й тим, чим я найчастіше користуюсь — це його автозаповненням по Tab та автозаповненням команд при їх наборі.</p>
<p>Майже з кожною розповсюдженою командою будуть йти автокомпліти. При роботі з Git, він буде вам пропонувати гілки обирати при <code>git switch</code> чи коміти при <code>git cherry-pick</code> й тому подібне, ну ви зрозуміли. І ще багато чого. Просто для прикладу, як це виглядає з <code>git</code>, якщо я, наприклад, не знаю, які в нього є команди та що вони роблять:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769001147072/075423df-89cd-42b4-b233-90f99fd60633.png" alt class="image--center mx-auto" /></p>
<p>Пошук в історії команд по Ctrl+R дає можливість знайти будь-що, що ви колись набирали. Він використовує fuzzy finder, тож ось я набирав, до прикладу <strong>co</strong>coa<strong>p</strong>od<strong>s</strong>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769001348524/13c614e2-41f7-4483-8e25-92380455a891.png" alt class="image--center mx-auto" /></p>
<p>Коротше… не буду довго на цьому зупинятись, бо краще про нього розповісти детальніше окремо. Знаю, що є прихильники й zsh і що “та це все є в zsh і набагато краще там зроблено”… Але на мою думку, те як це реалізовано та, що важливо, працює з коробки у Fish Shell, мені подобається найбільше.</p>
<p>Тож, якщо ви ще ніколи не пробували Fish Shell, то спробуйте, можливо вам сподобається.</p>
<h3 id="heading-theme">Theme</h3>
<p>За замовчуванням у Fish Shell стоїть доволі проста тема, яка мені не подобається. Тож я додатково встановлюю ще тему <code>bobthefish</code>.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/oh-my-fish/theme-bobthefish">https://github.com/oh-my-fish/theme-bobthefish</a></div>
<p> </p>
<p>Встановлюю я її за допомогою <a target="_blank" href="https://github.com/jorgebucaran/fisher">Fisher</a> - це такий типу “плагін-менеджер” для Fish Shell. Ну і не забуваю ввімкнути підтримку Nerd Fonts за допомогою <code>set --universal theme_nerd_fonts yes</code>. Всі ці нюанси з темою та шрифтами описані в документації теми.</p>
<p>І в результаті, я отримую зручний для себе термінал, в якому я відчуваю себе доволі продуктивно.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769004087521/22218495-87a5-46af-9a63-2411d1cbceec.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-time-machine">Time Machine</h2>
<p>Раніше, я недооцінював цей інструмент та не вважав вартим його налаштовувати. Напевно, це тягнеться в мене ще з часів Windows. Коли з часом, нагромаджувались записи в реєстрах, системні файли — все це змішувалось з твоїми власними даними й в результаті через рік ти отримував купу сміття змішаного з твоїм власним й вже не розбереш звідки куди йде. І що простіше вже було просто з нуля перевстановити систему та не тягнути за собою всю ту історію, яка б точно дала б якісь конфлікти налаштувань й так далі.</p>
<p>Проте, я трохи розібрався поверхнево з тим, як влаштовані в сучасній macOS системні розділи та користувацькі. І виявилось, що сама операційна система та системний розділ — це взагалі Read Only система, яка монтується із snapshot. Тобто все що стосується самої системи ви в принципі не можете ніяк змінити чи повпливати, а тобто і зламати. А ваш користувацький розділ — це взагалі окремий розділ, який монтується на <code>/Volumes/Data</code>. І от саме він і копіюється в Time Machine, ігноруючи системні файли.</p>
<p>Що це означає для мене, як для людини, яка постійно боролась зі сміттям після довгого використання Windows? А банально те, що можливо є сенс робити собі бекапи з Time Machine, та не перейматись, що воно буде тягнути за собою історію всіх попередніх версій системних файлів чи їх налаштувань та потенційно бути причинами конфліктів між різними версіями. Мої файли — це мої файли й вони копіюються, а все інше не попадає в копію.</p>
<p>Тому я також почав користуватись Time Machine, але подивимось як піде й чи мені сподобається.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769005373164/1567ebf4-a6ce-4a58-9b75-50c6666ee2b4.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-safari">Safari</h2>
<p>Тут багато не напишеш, але я все ж деякі невеличкі моменти змінюю в ньому.</p>
<p>Перш за все, я вмикаю попередження, коли мене намагаються перевести на сайт по HTTP:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769006194887/70636de9-fc1b-4e09-aebc-033bcca8cf81.png" alt class="image--center mx-auto" /></p>
<p>Потім, вимикаю взагалі всі AutoFill, окрім паролів. Мало того, що вони мене просто бісять завжди своїм віджетом, який питає що ти хочеш обрати, так це ще й з перспективи безпеки не дуже.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769006300485/180ecae4-2304-4d19-a482-e5a64343323a.png" alt class="image--center mx-auto" /></p>
<p>Ну і звісно, остання, але найбільш бісяча частина Safari за замовчуванням — відкривати нове вікно браузера з пустими вкладками. Я одразу ж це змінюю на “відкривати всі вкладки, які були в попередній сесії”:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769006405322/93e5e0bc-f2a5-4d82-a162-48b745d24f14.png" alt class="image--center mx-auto" /></p>
<p>Тому, по Safari в мене питань загалом немає, але є деякі бісячі моменти, які я змінюю в налаштуваннях.</p>
<h2 id="heading-system-settings">System Settings</h2>
<h3 id="heading-apple-account-sign-in">Apple Account Sign-In</h3>
<p>До цього питання я ставлюсь доволі серйозно, бо за моїм Apple Account-ом зберігаються в тому числі й паролі до всіх інших моїх сервісів. А тому, я вважаю, що ставлення до пароля на Apple Account має бути як до мастер-пароля. Спеціальні символи, числа, маленькі та великі букви — все це треба, щоб було в паролі.</p>
<p>Окрім цього, мій акаунт ще додатково захищений фізичним ключем YubiKey. Тому, навіть якщо мій пароль зламали, без фізичного ключа до акаунта не дістатись:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769006658595/a380ba3d-9749-4077-9de2-a1525de74d48.png" alt class="image--center mx-auto" /></p>
<p>Ну і на всяк випадок, я ще додаю контакти близьких, які можуть допомогти мені відновити акаунт, або отримати доступ до нього у випадку моєї смерті.</p>
<h3 id="heading-wi-fi">Wi-Fi</h3>
<p>Для тих, хто обожнює приватність в Інтернеті, якої не існує, в меню Wi-Fi можна ввімкнути ротацію MAC адрес та ховати свій IP. Для цього потрібно відкрити налаштування вашої конкретної мережі, до якої ви підʼєднались:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769007066486/b36e10b1-01fa-43ac-9264-220de51317aa.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-network">Network</h3>
<p>В меню Network я вмикаю Firewall й додатково вмикаю Stealth Mode в його налаштуваннях:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769007259607/c1845e56-b529-4a41-a368-9b5da96eaf28.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-three-finger-drag">Three Finger Drag</h3>
<p>Одна з моїх найулюбленіших можливостей трекпада на macOS - перетягувати вікна чи виділяти текст не тримаючи натиснутим його однією рукою, поки тягнеш іншою, а просто використовувати жест із трьома пальцями. Ви починаєте вести рукою по трекпаду трьома пальцями й macOS буде вважати це як натиснуту кнопку, яку ви утримуєте. Вмикається це в Accessibility → Pointer Control → Trackpad Options:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769007746807/35af8722-cd35-4a71-9601-893a5a88e6ac.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-desktop-amp-dock">Desktop &amp; Dock</h3>
<p>Док на macOS за замовчуванням це взагалі найбільш бісяча частина для мене. Мені зручніше, коли док використовується лише як менеджмент відкритих застосунків, а не як звалище всього що є і що потенційно тобі може знадобитись. Тому, я в налаштуваннях дока вимикаю взагалі все, що повʼязане з його Suggestions, викидую взагалі всі застосунки звідти, поки там не залишиться лише Finder та кошик.</p>
<p>Окрім цього, вимикаю ще один бісячий момент, коли macOS ховає всі відкриті вікна, коли ти натискаєш на робочий стіл. Все це можна вимкнути в Desktop &amp; Dock:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769008449243/1dcd9c88-891c-4624-8294-daf34b4644c4.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-night-shift">Night Shift</h3>
<p>Одна цікава деталь, яка особисто мені подобається, це Night Shift. Його можна налаштувати по графіку й коли буде вже ставати темніше, то температура кольорів на дисплеї буде ставати теплішою. Хочете вірте, хочете ні, але особисто я бачу різницю і ввечері за ноутбуком справді трохи легше сидиться в контексті очей.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769008639363/641b74ec-5be1-40b5-afc1-e3a5c172bf9e.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-touch-id-amp-password">Touch ID &amp; Password</h3>
<p>Ще одна цікава деталь, це можливість розблокувати свій MacBook своїм Apple Watch. Якщо ви це вмикаєте, то вже не потрібно навіть і палець чи пароль. Ваш годинник поруч і ноутбук автоматично розблоковується сам. А вам на Apple Watch приходить сповіщення, що ваш ноутбук було розблоковано.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769008824118/1e2d4e56-b79d-4ffd-af30-08f1d56fbd2b.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-icloud">iCloud</h3>
<p>Для того, щоб я не втрачав свої дані у випадку втрати ноутбука чи встановленні нового, я активно користуюсь iCloud-ом. В меню Saved to iCloud в мене ввімкнено взагалі все для всіх застосунків, щоб вони синхронізувались з iCloud.</p>
<p>А щоб воно було більш захищеним, я додатково налаштував Advanced Data Protection, який повністю шифрує всі ваші дані вашим ключем:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769009052030/871bb94d-21f0-4f2f-8680-a3710ea1a7dd.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-iphone-mirroring">iPhone Mirroring</h2>
<p>Це, напевно, зʼявилось з останнім оновленням macOS Tahoe, але виявилась дуже зручною штукою, якою я реально користуюсь.</p>
<p>Ви можете звʼязати свій ноутбук з вашим телефоном й керувати ним з ноутбука. Це буває корисно, коли телефон десь стоїть заряджається, або стоїть на станції тощо. Тут вам приходить сповіщення, на яке треба відреагувати, але вам не хочеться вставати та йти діставати свій телефон.</p>
<p>Відкриваєте цей застосунок та й керуєте телефоном як наче ви його в руках тримаєте:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769009339258/e36af11a-3b29-4a2a-aad1-122c0f3ce2f4.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-zakinchuyemo">Закінчуємо</h2>
<p>Ідея з цим постом мені прийшла доволі сумбурно сьогодні вдень. Я не знаю, чи було це взагалі комусь корисним, чи ні, але захотілось написати про те, що особисто мені доводиться налаштовувати додатково в macOS після її встановлення. Тобто, маю на увазі, що якщо щось працює із коробки так, як мені потрібно, то я його відповідно не налаштовую, а значить і в цей пост воно не потрапило.</p>
<p>Звісно, що я не писав тут взагалі про все, бо це було б нудно що мені, що вам. Тому тут лиш вибірка, яка, на мою думку, могла б бути корисною в певних ситуаціях.</p>
<p>Тож, закривши своє бажання про щось сьогодні написати, бажаю вам гарного вечора :)</p>
]]></content:encoded></item><item><title><![CDATA[Переваги інкрементальної збірки в монорепозиторії]]></title><description><![CDATA[В деяких своїх попередніх постах я писав про те, що наша команда може збирати весь монорепозиторій локально й при цьому не чекати, поки воно все знову збереться для всіх проєктів. Сьогодні в мене виникло бажання трохи пописати, тож розкажу вам деталь...]]></description><link>https://ghaiklor.dev/perevagi-inkrementalnoyi-zbirki-v-monorepozitoriyi</link><guid isPermaLink="true">https://ghaiklor.dev/perevagi-inkrementalnoyi-zbirki-v-monorepozitoriyi</guid><category><![CDATA[Node.js]]></category><category><![CDATA[rush]]></category><dc:creator><![CDATA[Eugene Obrezkov]]></dc:creator><pubDate>Mon, 13 Oct 2025 11:23:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/FDaCU3etvAc/upload/2f594c323f6e6d856e4e8a17e6c7d79c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>В деяких своїх попередніх постах я писав про те, що наша команда може збирати весь монорепозиторій локально й при цьому не чекати, поки воно все знову збереться для всіх проєктів. Сьогодні в мене виникло бажання трохи пописати, тож розкажу вам детальніше, що я мав на увазі. Почну з того, що опишу, чому це взагалі проблема.</p>
<h2 id="heading-v-chomu-problema">В чому проблема?</h2>
<p>На невеликих обсягах коду, проблеми ніякої нема. Викликали компілятор, той же TypeScript, а він вам виплюнув JavaScript кудись в <code>dist</code> й ви поїхали працювати далі. Коли за розмірами цей проєкт невеликий, то й проблем немає — це швидко.</p>
<p>Але довгострокові проєкти живуть достатньо довго, щоб обрости такою кількістю коду, компіляція якого починає займати багато часу. Це впливає на швидкість, з якою розробники отримують відповідь на питання “а чи скомпілювався мій код?”.</p>
<p>Додаємо сюди ще й той факт, що існують інші проєкти, які живуть в тому ж репозиторії. Тобто у вас один репозиторій не просто з одним TypeScript проєктом, а з десятками, а то й сотнями. Тож якщо ви викличете TypeScript компілятор в репозиторії для всіх цих проєктів, то чекати ви будете ще довше.</p>
<p>Так і зʼявляється необхідність надати розробникам можливість збирати лише частину репозиторію, а не весь. От подивимось на це з погляду розробника.</p>
<p>Ви клонуєте репозиторій, в якому працюють декілька команд і там зберігаються сотні окремих TypeScript проєктів. Бібліотеки, застосунки, допоміжні скрипти й так далі. Але працюєте ви зараз лише з одним конкретним застосунком.</p>
<p>Після впровадження вами змін у свій застосунок, ви викликаєте збірку репозиторію, а воно вам починає викликати TypeScript компілятор по всьому репозиторію й ви сидите чекаєте 5 хвилин, хоча вам потрібно було викликати лише збірку вашого застосунку та збірку всіх пакетів, від кого ви залежите, чи від вас. Незручно, правда?</p>
<h2 id="heading-vitayemo-koncepciyu-inkrementalnoyi-zbirki">Вітаємо концепцію інкрементальної збірки</h2>
<p>Для менеджменту нашого репозиторію ми використовуємо <a target="_blank" href="https://rushjs.io">Rush</a>. Одна із цікавих можливостей цього інструменту є те, що ви можете задавати свої власні команди, які потім стають командами самого <code>rush</code>. Ці налаштування живуть в окремому файлі <code>command-line.json</code>, де і відбувається опис команд.</p>
<p>До прикладу, скажімо, що ми хочемо зробити команду <code>build</code>, яка буде викликати скрипти збірки для всіх проєктів в репозиторії. Підемо в <code>command-line.json</code> та додамо нову команду <code>build</code>.</p>
<p><em>Я спеціально пропускаю деякі налаштування, щоб сфокусуватись саме на інкрементальній збірці, не беручи до уваги інші аспекти можливостей Rush.</em></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"commandKind"</span>: <span class="hljs-string">"bulk"</span>,
  <span class="hljs-attr">"enableParallelism"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"build"</span>
}
</code></pre>
<p>Цим описом ми кажемо Rush, що ми хочемо мати команду з назвою <code>build</code>, виклик якої має викликати скрипт <code>build</code> із <code>package.json</code> всіх проєктів в репозиторії (<code>commandKind</code> зі значенням <code>bulk</code> означає, що команда має викликатись в усіх проєктах). А <code>enableParallelism</code> дозволяє запускати їх паралельно.</p>
<p>Тепер, ми можемо зробити в консолі <code>rush build</code> і отримати запуск всіх <code>build</code> скриптів з усіх <code>package.json</code> файлів усіх проєктів в репозиторії, ще й паралельно. Ну а там зазвичай йде виклик TypeScript компілятора, тому де-факто це саме компіляція.</p>
<p>Але ладно якщо ви вперше склонували репозиторій й вперше викликали <code>rush build</code>. В цьому випадку, логічно було б зібрати всіх, бо ніхто ж не зібраний ще. Проте, я б очікував, що наступного разу, коли я викличу <code>rush build</code>, воно не буде наново викликати TypeScript компілятор, якщо в мене вже є зібраний проєкт, який я не чіпав. В Rush це питання одного прапорця <code>incremental</code>:</p>
<pre><code class="lang-diff">{
  "commandKind": "bulk",
  "enableParallelism": true,
<span class="hljs-addition">+ "incremental": true,</span>
  "name": "build"
}
</code></pre>
<p>Додавши всього цей прапорець для нашого <code>build</code>, ми вже маємо повністю інкрементальну команду <code>build</code>. Тепер, якщо я викличу цю команду вперше, то, звісно, буде збиратись весь репозиторій. Проте, якщо я викличу її ще раз, то воно взагалі нічого збирати не буде — змін же не було.</p>
<p>На прикладі нашого репозиторію, з яким ми зараз працюємо, це виглядає так. Перший виклик цієї команди на пустому репозиторії (<em>маю на увазі, що всі кеші холодні</em>), збірка займає приблизно хвилину. Найдовше йдуть проєкти на Next.js чи Svelte.</p>
<pre><code class="lang-bash">$ rush build
Rush Multi-Project Build Tool 5.153.2 - https://rushjs.io
Node.js version is 20.19.1 (LTS)

Starting <span class="hljs-string">"rush build"</span>
Analyzing repo state... DONE (0.06 seconds)

==[ SUCCESS: 43 operations ]===================================================
rush build (1 minute 2.2 seconds)
</code></pre>
<p>Тепер, якщо я одразу ж викликаю <code>rush build</code> ще раз, ми вже маємо зовсім інші числа.</p>
<pre><code class="lang-bash">$ rush build
Rush Multi-Project Build Tool 5.153.2 - https://rushjs.io
Node.js version is 20.19.1 (LTS)

Starting <span class="hljs-string">"rush build"</span>
Analyzing repo state... DONE (0.06 seconds)

==[ SKIPPED: 43 operations ]===================================================
rush build (0.10 seconds)
</code></pre>
<p>Як бачите, Rush взагалі пропустив виклик <code>build</code> скриптів в усіх проєктах, бо, логічно, вони не були змінені, а значить і сенсу їх наново збирати — немає.</p>
<p>Тож, як це взагалі працює?</p>
<h2 id="heading-yak-realizovana-inkrementalna-zbirka-v-rush">Як реалізована інкрементальна збірка в Rush?</h2>
<p>От уявімо, що станом на зараз в нас холодний репозиторій (<em>таке враження, наче я щойно термін вигадав, якого ніде в книгах не бачив, якісь репозиторії холодні, а не кеші… коротше я знову про те, що репозиторій ще ніколи не збирався</em>).</p>
<p>Ми викликаємо команду <code>rush build</code> і він починає лізти в кожен проєкт в репозиторії та викликати там скрипт <code>build</code> із <code>package.json</code>. Як тільки збірка одного з проєктів завершується, Rush робить доволі класичну річ — бере хеші всіх файлів цього проєкту.</p>
<p>Через те що Rush знає рамки вашого проєкту (<em>ви явно вказуєте шлях до нього при реєстрації</em>), то він знає і хеші яких файлів йому треба дістати. В результаті цієї операції він отримує JSON, де він записує, який файл і який хеш він мав. Це виглядає якось так:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"files"</span>: {
    <span class="hljs-attr">"repo/services/lookout/Dockerfile"</span>: <span class="hljs-string">"b6bbd8d808a7b0d26692ca8b2324e89e25296f11"</span>,
    <span class="hljs-attr">"repo/services/lookout/eslint.config.js"</span>: <span class="hljs-string">"c36008e6107cc988c554a8ffd5e8913c796bbe4a"</span>,
    <span class="hljs-attr">"repo/services/lookout/src/App.ts"</span>: <span class="hljs-string">"1784f1b34f84478a042a757702632f585a8ddc3c"</span>,
    <span class="hljs-comment">// and so on...</span>
  }
}
</code></pre>
<p>Окрім цього, він ще додає команду, яка викликалась на цих файлах. Це робиться для того, щоб у випадку зміни скрипту, він викликався, а не пропускався.</p>
<pre><code class="lang-diff">{
   "files": {
     "repo/services/lookout/Dockerfile": "b6bbd8d808a7b0d26692ca8b2324e89e25296f11",
     "repo/services/lookout/eslint.config.js": "c36008e6107cc988c554a8ffd5e8913c796bbe4a",
     "repo/services/lookout/src/App.ts": "1784f1b34f84478a042a757702632f585a8ddc3c",
     // and so on...
   }
<span class="hljs-addition">+  "arguments": "tsc --project tsconfig.build.json"</span>
}
</code></pre>
<p>Тепер, Rush знає, що раніше викликалась команда <code>tsc --project tsconfig.build.json</code> на файлах з ось такими хешами. Залишилось лише зберегти цю інформацію в тимчасовій теці, що він і робить — зберігає цей файл у теці з проєктом <code>repo/services/lookout/.rush/temp/package-deps_build.json</code>.</p>
<p>Наступний виклик <code>rush build</code> бачить, що вже є файл зі станом попередньої збірки, тож звіряємось… Якщо цього разу аргументи команди не змінились, тобто все та ж <code>tsc --project tsconfig.build.json</code> й при цьому хеші всіх файлів зараз такі ж самі, як і записані у файл — це значить, що проєкт не змінювався, як і його скрипт збірки. А тому виклик <code>tsc</code> можна повністю пропустити.</p>
<h2 id="heading-chi-obmezheni-mi-de-jogo-vikoristovuvati">Чи обмежені ми де його використовувати?</h2>
<p>На моєму досвіді — ні. В результаті, ми зробили три інкрементальні команди <code>build</code>, <code>lint</code> та <code>test</code>. Майже в усіх наших проєктах є TypeScript, тому <code>build</code> це в нас <code>tsc</code>. Аналогічно майже всі використовують ESLint, а тому <code>lint</code> це в нас <code>eslint</code>. Ну і <code>test</code> це в нас <code>jest</code> або <code>mocha</code> на старіших проєктах.</p>
<p>Кожна з них описана в Rush аналогічно до того, як я розповідав вище. Продублюю ще раз, але вже повний опис однієї із таких команд, наприклад <code>lint</code>:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"allowWarningsInSuccessfulBuild"</span>: <span class="hljs-literal">false</span>,
  <span class="hljs-attr">"commandKind"</span>: <span class="hljs-string">"bulk"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">"The `rush lint` command typically runs ESLint to check code quality across all packages in parallel. The linting process can be customized in each package's scripts."</span>,
  <span class="hljs-attr">"disableBuildCache"</span>: <span class="hljs-literal">false</span>,
  <span class="hljs-attr">"enableParallelism"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">"ignoreDependencyOrder"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">"ignoreMissingScript"</span>: <span class="hljs-literal">false</span>,
  <span class="hljs-attr">"incremental"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"lint"</span>,
  <span class="hljs-attr">"safeForSimultaneousRushProcesses"</span>: <span class="hljs-literal">false</span>,
  <span class="hljs-attr">"summary"</span>: <span class="hljs-string">"Lints all projects using ESLint."</span>,
  <span class="hljs-attr">"watchForChanges"</span>: <span class="hljs-literal">false</span>
}
</code></pre>
<p>З такою конфігурацією, якщо дуже коротко, поведінка команди <code>rush lint</code> наступна. Викликай <code>lint</code> команду з усіх <code>package.json</code> скриптів. Роби це паралельно на всіх доступних ядрах процесора, ігноруючи топологію залежностей (<em>для аналізу це не потрібно</em>). Якщо автор проєкту не надавав команду <code>lint</code> у своєму <code>package.json</code> - кинь помилку. Ну і, звісно, роби це інкрементально!</p>
<h2 id="heading-dodayemo-trohi-cukru-dlya-rozrobnikiv">Додаємо трохи цукру для розробників</h2>
<p>Я врешті додав ще одну команду <code>rush all</code>. Але вона дуже проста за своїм визначенням:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"commandKind"</span>: <span class="hljs-string">"global"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">"The `rush all` command is a convenience command that runs build, lint, and test in sequence across all packages, ensuring code is built, linted, and tested in one go."</span>,
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"all"</span>,
  <span class="hljs-attr">"safeForSimultaneousRushProcesses"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">"shellCommand"</span>: <span class="hljs-string">"rush build &amp;&amp; rush lint &amp;&amp; rush test"</span>,
  <span class="hljs-attr">"summary"</span>: <span class="hljs-string">"Runs build, lint, and test commands for all projects."</span>
}
</code></pre>
<p>Тобто тепер, замість того, щоб розробники викликали всі три команди окремо, коли вони хочуть переконатись, що все працює, тепер вони роблять це однією. Зручно ж!</p>
<p>До того, оскільки всі ці три команди мають інкрементальну збірку, команда <code>rush all</code> теж такою можна назвати. І тепер розробник під час своєї роботи, щоразу як йому треба щось зібрати, просто викликає <code>rush all</code>.</p>
<p>Запустімо <code>rush all</code> на холодному репозиторії:</p>
<pre><code class="lang-bash">$ rush all
Rush Multi-Project Build Tool 5.153.2 - https://rushjs.io
Node.js version is 20.19.1 (LTS)

Starting <span class="hljs-string">"rush all"</span>

Starting <span class="hljs-string">"rush build"</span>
Analyzing repo state... DONE (0.06 seconds)
==[ SUCCESS: 43 operations ]===================================================
rush build (1 minute 5.6 seconds)

Starting <span class="hljs-string">"rush lint"</span>
Analyzing repo state... DONE (0.07 seconds)
==[ SUCCESS: 43 operations ]===================================================
rush lint (40.22 seconds)

Starting <span class="hljs-string">"rush test"</span>
Analyzing repo state... DONE (0.07 seconds)
==[ SUCCESS: 43 operations ]===================================================
rush <span class="hljs-built_in">test</span> (31.00 seconds)
</code></pre>
<p>На холодному репозиторії повна збірка всього репозиторію зайняла приблизно дві хвилини. Але наступний виклик цієї ж команди, без змін в проєктах, вже набагато швидший:</p>
<pre><code class="lang-bash">$ rush all
Rush Multi-Project Build Tool 5.153.2 - https://rushjs.io
Node.js version is 20.19.1 (LTS)

Starting <span class="hljs-string">"rush all"</span>

Starting <span class="hljs-string">"rush build"</span>
Analyzing repo state... DONE (0.06 seconds)
==[ SKIPPED: 43 operations ]===================================================
rush build (0.10 seconds)

Starting <span class="hljs-string">"rush lint"</span>
Analyzing repo state... DONE (0.07 seconds)
==[ SKIPPED: 43 operations ]===================================================
rush lint (0.09 seconds)

Starting <span class="hljs-string">"rush test"</span>
Analyzing repo state... DONE (0.07 seconds)
==[ SKIPPED: 43 operations ]===================================================
rush <span class="hljs-built_in">test</span> (0.09 seconds)
</code></pre>
<p>Всього забираючи лише дві секунди й ті, для того, щоб Node.js запустити :)</p>
<p>Наступного разу, коли розробник на повністю зібраному репозиторії, внесе деякі зміни в якісь застосунки чи пакети, то його <code>rush all</code> збере тільки необхідну для цього частку, пропускаючи будь-які процеси повʼязані з кодом, який він не змінив.</p>
<h2 id="heading-yaki-plyusi-dlya-rozrobnikiv">Які плюси для розробників?</h2>
<p>Маючи таку систему, команди розробників мають всі плюси монорепозиторіїв, щоб ділитись кодом між застосунками, лінкувати їх локально для пришвидшеної локальної розробки й тому подібне. Але при цьому, нівелюється проблема, коли треба було кожного разу збирати репозиторій весь й чекати довгий час лиш для того, щоб зрозуміти, чи мій рядок коду правильно написаний.</p>
<p>Тож тепер, це питання декількох секунд, можливо десятків секунд, якщо багато змінено, для того, щоб отримати повністю зібраний репозиторій з усіма проєктами локально + вашими змінами.</p>
]]></content:encoded></item><item><title><![CDATA[Тестуємо в проді, але не на користувачах]]></title><description><![CDATA[У Twitter я побачив ось цей твіт:
https://x.com/GithubProjects/status/1948652726297985270
 
Я ось цей “гумор” про пʼятничні деплої взагалі не розумію. Ну бо це не гумор, це якась хвора стигма в професіональному оточенні. Люди, щоб нормалізувати певні...]]></description><link>https://ghaiklor.dev/testuyemo-v-prodi-ale-ne-na-koristuvachah</link><guid isPermaLink="true">https://ghaiklor.dev/testuyemo-v-prodi-ale-ne-na-koristuvachah</guid><category><![CDATA[Node.js]]></category><category><![CDATA[  feature flags]]></category><dc:creator><![CDATA[Eugene Obrezkov]]></dc:creator><pubDate>Wed, 30 Jul 2025 12:00:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/U8XpuqGEO0I/upload/02e94ccdff2e1ddcd3b70a6076da91d8.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>У Twitter я побачив ось цей твіт:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://x.com/GithubProjects/status/1948652726297985270">https://x.com/GithubProjects/status/1948652726297985270</a></div>
<p> </p>
<p>Я ось цей “гумор” про пʼятничні деплої взагалі не розумію. Ну бо це не гумор, це якась хвора стигма в професіональному оточенні. Люди, щоб нормалізувати певні явища, інколи застосовують гумор до них. І через це, сприйняття змінюється з “щось, що можна поліпшити, бо це проблема” на “ха-ха, та це ж нормально, посміємось, це не проблема”. Але, хай там як, цей пост не про когнітивні пастки та викривлення. Про них можна написати й окремий пост, де розповісти про те, як наш мозок обдурює сам себе - дуже цікаві штуки насправді.</p>
<p>А в цьому пості я вирішив сфокусуватись на одній ідеї, яка безпосередньо й дозволяє в більшості випадків деплоїти в пʼятницю й не боятись нікого зламати. Але перед тим як я підійду конкретно до цієї теми, я зайду трішки здалеку. Чому? Та банально, тому що в мене зараз на це є настрій. Ну і, тому що вважаю, що донести цю ідею й пояснити чому вона працює - важливо, як мінімум для мене. Тож я почну трішки здалеку і пропоную почати з дуже простого кейсу.</p>
<h2 id="heading-cli-parametri">CLI параметри</h2>
<p>Ви всі у своїй роботі використовуєте CLI параметри. Чи то ви працюєте з <code>git</code>, чи то з <code>curl</code>, чи купа інших програм. Щоб скорегувати програму, аби вона виконувала ті дії, які вам потрібні, ви зазвичай вказуєте підкоманди чи параметри. Ну от візьмімо за простий приклад команду <code>curl</code>:</p>
<pre><code class="lang-bash">curl google.com
</code></pre>
<p>Викликавши цю команду ви очікувано отримаєте якийсь HTML у себе в терміналі:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753865517505/424b4386-87e9-49b2-9c6f-94751c22d451.png" alt class="image--center mx-auto" /></p>
<p>Але що ви робите, коли ви хочете змінити поведінку програми, не змінюючи саму програму? Ви передаєте параметри <em>(якщо програма їх підтримує звісно).</em> Ну от, наприклад, ми хочемо подивитись на детальнішу інформацію стосовно зʼєднання і додаємо параметр:</p>
<pre><code class="lang-bash">curl --verbose google.com
</code></pre>
<p>Додали прапорець <code>--verbose</code> і викликали програму:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753865680291/22905358-6ee1-4a66-b5ec-70e6987bd43a.png" alt class="image--center mx-auto" /></p>
<p>І ось ми вже бачимо зовсім іншу поведінку програми, яка не просто видає HTML, а ще й додатково виводить в термінал інформацію про зʼєднання.</p>
<p>Так от до чого я це все веду? От подумайте, у вас є одна і та ж сама програма, яку ми викликали вперше і вдруге. Той же самий <code>curl</code>, за тим же самим шляхом, той же самий файл. Але при цьому, ми отримали різні поведінки, залежно від вказаних параметрів від користувача.</p>
<p>Наведу ще один приклад.</p>
<h2 id="heading-zminni-seredovisha">Змінні середовища</h2>
<p>Я підозрюю, що багато з вас, хто читає цей пост, працює з вебом. Тож ви точно чули про environment variables. Це звичайні пари ключ\значення, які операційна система прокидає у ваш процес. І вже ваш процес може прочитати ці пари та дістати значення. Зазвичай їх часто використовують для довгоживучих серверів, але й не тільки. Так ось.</p>
<p>Скажімо, у вас є сервіс, який пише деякі логи, щоб краще розуміти що відбувається. Але ви, по класиці, зробили так, що у вас є рівні логів. Ну це зазвичай щось типу <code>error</code>, <code>warn</code> та <code>info</code>. І от, наприклад, <code>info</code> логів у вас багато і ви б хотіли їх вимкнути. Багато бібліотек надають цю можливість, вказуючи через <code>LOG_LEVEL</code> рівень логів, які ви б хотіли писати.</p>
<p>Тож ви запустили свій сервіс, вказали йому в змінних оточення <code>LOG_LEVEL=warn</code> і таким чином вимкнули інформаційні логи.</p>
<p>А тепер питання. Чи змінювали ви щось у своїй програмі для цього? Чи може у бібліотеці, яку ви використовуєте? Не думаю. Ви можете розгорнути такий же самий сервіс, вказати йому інший <code>LOG_LEVEL</code> і ви отримаєте два однакових сервіси, але які вже виконують різні шматки коду, скажімо так. І все це без зміни самого коду.</p>
<h2 id="heading-sho-mizh-nimi-spilnogo">Що між ними спільного?</h2>
<p>Думаю, цих двох прикладів достатньо, щоб передати ось цю концепцію - мати код, хід якого можна контролювати зовнішніми чинниками.</p>
<p>І це неважливо насправді як саме реалізовується ця концепція. Як я і показав на прикладах, в CLI це можуть бути параметри до самої програми. В сервісах це можуть бути змінні середовища.</p>
<p>Всіх їх обʼєднує одна риса - <strong>не змінюючи код програми, ми можемо контролювати її хід</strong>.</p>
<h2 id="heading-prosti-realizaciyi">Прості реалізації</h2>
<p>Ну от, познайомились з ідеєю. Але ідеї ідеями, але без прикладу буде неповноцінно. Як би ми могли дуже просто реалізувати ось цю концепцію з “зроби мені А, якщо X, а якщо ні, то зроби B”. Я буду частково використовувати для цього псевдокод, але дуже сильно відрізнятись від реальної реалізації воно не буде. Ну і як ви вже могли зрозуміти, звісно тут велику роль відіграє if statement.</p>
<p>Ну от візьмемо той же <code>curl</code>, як би це могло виглядати там?</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// These are some other functions somewhere else in the program</span>
<span class="hljs-keyword">declare</span> <span class="hljs-keyword">class</span> Connection {}
<span class="hljs-keyword">declare</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">doingAllTheStuffWith</span>(<span class="hljs-params">url: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">Connection</span></span>;
<span class="hljs-keyword">declare</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">outputVerboseInfoFor</span>(<span class="hljs-params">connection: Connection</span>): <span class="hljs-title">void</span></span>;
<span class="hljs-keyword">declare</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">outputResponseFrom</span>(<span class="hljs-params">connection: Connection</span>): <span class="hljs-title">void</span></span>;

<span class="hljs-keyword">import</span> { parseArgs } <span class="hljs-keyword">from</span> <span class="hljs-string">"node:util"</span>;

<span class="hljs-keyword">const</span> args = parseArgs({
  options: {
    verbose: { <span class="hljs-keyword">type</span>: <span class="hljs-string">"boolean"</span>, <span class="hljs-keyword">default</span>: <span class="hljs-literal">false</span> },
    url: { <span class="hljs-keyword">type</span>: <span class="hljs-string">"string"</span>, <span class="hljs-keyword">default</span>: <span class="hljs-string">""</span> },
  },
});

<span class="hljs-keyword">const</span> connection = doingAllTheStuffWith(args.values.url);

<span class="hljs-comment">// !!!</span>
<span class="hljs-comment">// This is the important part for this blog post</span>
<span class="hljs-keyword">if</span> (args.values.verbose === <span class="hljs-literal">true</span>) {
  outputVerboseInfoFor(connection);
}

outputResponseFrom(connection);
</code></pre>
<p>Зверніть увагу, що наш код з самого початку може видавати <code>verbose</code> логи для нашого псевдо <code>Connection</code>. Але! Робитиме він це лише за умови, що йому прокинули параметр <code>--verbose</code>. В усіх інших випадках, програма банально не зайде в ту гілку, вона буде пропущена.</p>
<p>Аналогічно можна було б зробити й за допомогою змінних оточення. Додамо альтернативний спосіб ввімкнути наші verbose логи:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">if</span> (args.values.verbose === <span class="hljs-literal">true</span> || <span class="hljs-keyword">typeof</span> process.env[<span class="hljs-string">"CURL_VERBOSE"</span>] !== <span class="hljs-string">"undefined"</span>) {
  outputVerboseInfoFor(connection);
}
</code></pre>
<p>І ось в нас вже є програма, хід якої ми можемо змінювати лише вказуючи додаткові параметри чи змінні оточення.</p>
<p>Дочитали ви до цього місця і такі думаєте “Жека, ну це ж смішно, ми й так це розуміємо”. І матимете рацію. Ви ж знаєте, я полюбляю йти від простого до складного, тож зараз я почну ускладнювати вам задачку.</p>
<h2 id="heading-praporec-abo-vvimknenij-abo-vimknenij">Прапорець або ввімкнений, або вимкнений</h2>
<p>Основний великий мінус цього підходу, про який я так довго пишу, в тому, що у нього бінарна логіка. Ну тобто, ви або можете ввімкнути це у програмі, або вимкнути. І з цим, особливо якщо ми говоримо про прод і про користувачів, є дуже велика проблема.</p>
<p>Така бінарна логіка може працювати в короткострокових програмах по типу CLI утиліт чи щось подібне. Для одного сценарію ви викликали програму з одним набором параметрів, а для іншого сценарію - з іншими.</p>
<p>Якщо ж ми починаємо говорити про довгострокові сервери, які постійно працюють, то ми ж не будемо для кожного запиту підіймати окремий сервіс з іншим набором змінних оточення. Правда ж?</p>
<p>Ось це і є проблема. Маючи описаний вище підхід, ми то, звісно, можемо підняти сервіс з одним набором конфігурацій, але цей набір буде працювати або для всіх, або ні для кого.</p>
<p>Що як, я б хотів підняти сервіс, але щоб той код, який я написав, працював лише для мене. А у мого колеги він би не відпрацьовував? І тим паче не відпрацьовував би для наших користувачів? Хоч він вже і на проді. Іншими словами, наш if має вміти робити щось типу такого:</p>
<pre><code class="lang-diff"><span class="hljs-deletion">- if (args.values.verbose === true || typeof process.env["CURL_VERBOSE"] !== "undefined") {</span>
<span class="hljs-addition">+ if (thisIsMineRequestAndNoOneElse) {</span>
  outputVerboseInfoFor(connection);
}
</code></pre>
<p>Ось тут ми й приходимо до того, що я так люблю - ускладнення!</p>
<h2 id="heading-prijnyattya-rishennya-v-run-time">Прийняття рішення в run-time</h2>
<p>Для того, щоб ми могли реалізувати цей умовний <code>thisIsMineRequestAndNoOneElse</code>, нам вже недостатньо мати ту статичну інформацію, яку ми маємо, коли передаємо її через прапорці чи через змінні оточення. Бо цієї інформації банально недостатньо, щоб зрозуміти хто є хто у запиті.</p>
<p>Тому наший if має кудись сходити з якимись даними й щоб цей хтось нам сказав чи це має бути <code>true</code>, чи <code>false</code> <em>(дуже спрощено)</em>. І в принципі зі збором даних на нашій стороні все відносно легко. Ми можемо просто підготувати контекст з важливою інформацією та передати її тому, хто буде приймати рішення.</p>
<p>Для простого прикладу, нехай у нас буде якийсь Express сервер, в якому вже є налаштовані middleware, які роблять аутентифікацію й засовує ваші дані в <code>req.user</code>. Тоді, ми могли б викликати функцію, яка скаже нам, а чи можна для цього користувача ввімкнути цей шлях, чи ні:</p>
<pre><code class="lang-diff"><span class="hljs-deletion">- if (thisIsMineRequestAndNoOneElse) {</span>
<span class="hljs-addition">+ if (featureEnabledFor(req.user)) {</span>
  outputVerboseInfoFor(connection);
}
</code></pre>
<p>Перенесення цих рішень у динамічну площину та додавання контекстів, перед тим як приймати рішення, й робить можливим реалізацію прапорців на більш гранулярному рівні. Тепер ми вже можемо прапорці вмикати не лише для конкретної програми, а і для конкретних користувачів чи інших обʼєктів, які містяться в тій програмі на момент обробки.</p>
<p>Проблема тепер лише в тому, щоб правильно побудувати потрібний контекст для прийняття рішення. А там ви вже що вирішите, так і буде, обмежень майже немає, майже. Захотіли зробити, щоб прапорець був ввімкнений тільки коли <code>context.date == Feb 30 2222</code>? Та робіть, запихайте в контекст дату, ну і налаштуйте того, хто прийматиме рішення, що якщо дата <code>Feb 30 2222</code> в контексті, то прапорець треба ввімкнути.</p>
<p>Але я оце говорю постійно про оцього “хтось”, та й так не сказав, про кого ж мова. Що ж, тепер поговоримо про тих, хто приймає рішення, й місце, де ви це все налаштовуєте.</p>
<h2 id="heading-feature-management-platform">Feature Management Platform</h2>
<p>Ці інструменти називають по різному в різних компаніях, бо хтось будує свої власні платформи, як от Wix, й називає по своєму. Хтось бере вже готові COTS <em>(Component-Of-The-Shelf)</em>. Тому і терміни вигадують різні. Мені ж ближче більш таке загальне використання терміну - Feature Management Platform.</p>
<p>Що ж це за комбайни? Це платформи, які надають вам, якщо дуже грубо:</p>
<ul>
<li><p>Базу даних для зберігання налаштованих правил. Для кого, коли, в який час, як довго й тому подібне, вся ця інформація зберігається в цих базах даних і потім використовується для прийняття рішення. <em>(Маю на увазі, що це не їх власні бази даних, а просто що вони використовують БД для зберігання налаштованих прапорців з усіма їх правилами)</em>.</p>
</li>
<li><p>Web інтерфейс для зручного налаштування правил. Через нього ви, як розробники, й кажете, що має бути ввімкнено, а що вимкнено, для кого, як надовго й т.і. Зазвичай це процес по типу “Створити новий прапорець → Вмикати лише для користувачів з email, який закінчується на company.com → Вмикати прапорець лише один раз із десяти випадків” ну і тому подібне.</p>
</li>
<li><p>Рушій для прийняття рішень, який безпосередньо й бере участь в тому, що потім повертається у ваш код у вигляді <code>true</code> чи <code>false</code>. Ви даєте йому свій контекст, про який ми поговорили трішки раніше, а він на основі переданих йому даних, порівнює це з налаштованими правилами із бази даних й каже вам - так, прапорець ввімкнено для цього користувача, або ні, не ввімкнено.</p>
</li>
</ul>
<p>Ось це все і є тим, що потрібно, щоб ви могли змінювати хід програми залежно від того, хто нею користується - ваш користувач чи ви.</p>
<ol>
<li><p>Налаштували прапорець через Web-інтерфейс, дали йому назву, вказали правила по типу “для цих email” та “на ці дати” й т.д.</p>
</li>
<li><p>У своєму коді зібрали контекст для прапорця, в який запхали потрібні дані, як от пошта чи унікальний ідентифактор користувача чи ще щось.</p>
</li>
<li><p>Відправили контекст на Feature Management Platform й отримали відповідь чи для цього контексту прапорець ввімкнено чи ні.</p>
</li>
</ol>
<h2 id="heading-pro-yaki-platformi-ya-chuv">Про які платформи я чув?</h2>
<p>Щоб ви могли детальніше пошукати та почитати про всі ці платформи, накидаю сюди список, який я принаймні чув, бачив та читав щось про них.</p>
<h3 id="heading-unleash">Unleash</h3>
<p>Наскільки я зрозумів, то вважається одним із найбільших платформ цього типу з відкритим вихідним кодом. Є і документація й приклади, можна їх і на on-prem розгортати, наскільки памʼятаю. Наявність клієнтів під більшість мов програмування. Ось їх сторінка - <a target="_blank" href="https://www.getunleash.io">Unleash</a>.</p>
<p>Їх інтерфейс, як на мене, доволі гарний, що теж плюс:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753873390509/7271739f-e8d5-4b65-a477-8c62c282440e.png" alt class="image--center mx-auto" /></p>
<p>Є в них і всілякі залежності, сегменти, стратегії активацій - але то все вже більш для досвідчених і можна про то окремий пост зробити.</p>
<h3 id="heading-gitlab-feature-toggles">GitLab Feature Toggles</h3>
<p>Наш проєкт хоститься на GitLab і ми там використовуємо й реєстри для контейнерів й всіляких пакетів для npm, Helm і так далі. Так ось у них <a target="_blank" href="https://docs.gitlab.com/operations/feature_flags/">також є і прапорці</a>. Єдине що, інтерфейс у них набагато гірший й мені важко сказати, що в тій реалізації вони нам якось корисні. Все доволі просто й, наскільки я зрозумів, під капотом в них там unleash. Тому можна починати з GitLab, а потім переїхати на Unleash, коли масштаби виростуть.</p>
<h3 id="heading-openfeature">OpenFeature</h3>
<p>Не зовсім платформа, а скоріш спроби стандартизувати все що повʼязане з прапорцями. Можете використати як <a target="_blank" href="https://openfeature.dev">джерело додаткової інформації</a>, щоб детальніше з ними познайомитись.</p>
<h2 id="heading-prostij-praporec-v-unleash">Простий прапорець в Unleash</h2>
<p>Ну от вирішили ви таки спробувати це все і встановили собі Unleash, наприклад. Як буде виглядати ваш код з усіма цими модними прапорцями?</p>
<p>В першу чергу, вам потрібно буде налаштувати прапорець в інтерфейсі, де вказуєте стратегії активації, сегменти й тому інше, якщо воно вам, звісно, потрібно.</p>
<p>Через те, що я постійно розповідав про активацію прапорця для конкретного користувача в системі, то зробімо прапорець, якому ми скажемо, що ввімкни наш if для мене і мого колеги <em>(ID випадкові)</em>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753875764778/e5870f66-ec7d-4b69-9496-461a42bbef44.png" alt class="image--center mx-auto" /></p>
<p>Зберігши цю стратегію активації в нашому прапорці з іменем <code>demoApp.step2</code>, в нас тепер є місце, куди ми можемо піти спитати, чи можна нам в той закритий для користувачів if чи ні. <em>Так, я використовую Demo App від Unleash, ну бо а чого ні, в мене зараз немає ніде серверів з ним.</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753875982817/d5192d1c-50cc-403c-a8ca-6c0e1a639e91.png" alt class="image--center mx-auto" /></p>
<p>Але як це виглядатиме в коді? Налаштувати прапорець зі своїми власними User ID в системі ми то налаштували, а як передати? Ну, грубо кажучи, от як я розповідав раніше в прикладі з Express, так і передавати. Це просто обʼєкт з контекстом, в якому можуть лежати й інші значення, наприклад:</p>
<pre><code class="lang-diff"><span class="hljs-addition">+ const context = {</span>
<span class="hljs-addition">+   userId: req.user.id,</span>
<span class="hljs-addition">+   sessionId: req.session.id,</span>
<span class="hljs-addition">+   remoteAddress: req.ip,</span>
<span class="hljs-addition">+   properties: {</span>
<span class="hljs-addition">+     region: 'EMEA',</span>
<span class="hljs-addition">+   },</span>
<span class="hljs-addition">+ };</span>

<span class="hljs-deletion">- if (featureEnabledFor(req.user)) {</span>
<span class="hljs-addition">+ if (unleash.isEnabled('demoApp.step2', context)) {</span>
  outputVerboseInfoFor(connection);
}
</code></pre>
<p>Unleash SDK зробить запит до свого рушія, передасть туди, в тому числі, User ID із вашої бази даних. На сервері Unleash відбудеться порівняння, він побачить User ID в списку тих, кому прапорець можна ввімкнути й поверне вам в if <code>true</code>. Якщо ж User ID не співпаде, то буде <code>false</code>.</p>
<p>Ось таким чином ми і зробили можливість умовно не робочий код деплоїти в прод і при цьому не ламати наших користувачів. Бо ми буквально цей код викликаємо тільки для конкретних ситуацій, конкретних користувачів, тенантів й тому інше.</p>
<p>За допомогою цих прапорців великі компанії роблять в тому числі й A/B тестування, підписки по типу Premium, Ultra й так далі, з різним набором функціоналу.</p>
<h2 id="heading-pidsumovuyuchi">Підсумовуючи</h2>
<p>Концепція прапорців вже дуже давно як не нова. Про неї говорили ще чуть не з 2010-их, а то й раніше. Швидкий пошук показав, що і Fowler в 2017 щось там про нього писав, й на DOU вже проскакували статті про них в 2019. Звісно, що до нас воно прийшло здалеку, тому є деяка затримка, але так, якось так - концепція перевірена часом.</p>
<p>Основна ідея в тому, що ви, як розробник, змінюєте хід програми, не змінюючи код програми. Ви деплоїте в пʼятницю ввечері неробочий код в прод, але ховаєте його за прапорцем зі стратегією активації “тільки для мене”. Тому якщо хтось і зламається в пʼятницю ввечері - то це тільки ви.</p>
]]></content:encoded></item><item><title><![CDATA[Як ми збираємо Docker контейнери в Rush монорепозиторії]]></title><description><![CDATA[Вирішив розповісти про те, як ми, працюючи в монорепозиторії, збираємо наші контейнери. Впевнений, що кожен із вас знає про docker build й може поставити собі питання “А що там такого цікавого?”. Так ось про це і поговоримо, бо цікавого там трохи є й...]]></description><link>https://ghaiklor.dev/yak-mi-zbirayemo-docker-kontejneri-v-rush-monorepozitoriyi</link><guid isPermaLink="true">https://ghaiklor.dev/yak-mi-zbirayemo-docker-kontejneri-v-rush-monorepozitoriyi</guid><category><![CDATA[Docker]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[rush]]></category><dc:creator><![CDATA[Eugene Obrezkov]]></dc:creator><pubDate>Tue, 08 Jul 2025 11:51:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/3jtEN0ZxT4Y/upload/c0e75ae1c60cc09ccb20688c5c70d6a1.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Вирішив розповісти про те, як ми, працюючи в монорепозиторії, збираємо наші контейнери. Впевнений, що кожен із вас знає про <code>docker build</code> й може поставити собі питання “А що там такого цікавого?”. Так ось про це і поговоримо, бо цікавого там трохи є й це не просто <code>docker build</code>. І почну я з опису проблеми, починаючи від класичних сценаріїв, щоб вам було легше прослідкувати за ускладненням.</p>
<h2 id="heading-odin-servis-odin-repozitorij">Один сервіс — один репозиторій</h2>
<p>Майже завжди, принаймні з тими командами й проєктами з якими я працював, все починається з одного репозиторію й монолітного сервісу. Тобто у вас буквально один проєкт на весь репозиторій і ви цей проєкт можете зібрати однією командою, щось типу <code>docker build --tag my-awesome-app path/to/context</code>.</p>
<p>Все настільки просто, що ви навіть можете робити це в себе локально і не робити ніяких CI/CD. Інколи, за певних умов, я б навіть і сказав, що вам не потрібен CI/CD. Але сьогодні ми не про це.</p>
<p>Якщо ви все ж таки зробили собі якийсь простий CD, то у вас могло б це бути якимось простим скриптом типу:</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>

<span class="hljs-built_in">set</span> -euo pipefail

pnpm install
pnpm run build
docker build --tag my-awesome-app:latest --file path/to/Dockerfile path/to/context
</code></pre>
<p>Цей скрипт ви загорнули б в якийсь YAML для GitHub чи GitLab й справу закрито. Воно собі там щось робить, контейнер збирає й кудись завантажує. Але… ускладнимо задачу.</p>
<h2 id="heading-bagato-servisiv-odin-repozitorij">Багато сервісів — один репозиторій</h2>
<p>З цим сценарієм вже стикаюсь рідше, але він все одно є. Зазвичай, такий сценарій починають втілювати організації, в яких достатньо велика кількість розробників та сервісів, які між собою повʼязані, але операційно є окремими сутностями. Детальніше про монорепозиторії я б розповів іншим разом, а зараз продовжимо з контейнерами.</p>
<p>І, до речі, а скільки це “багато” сервісів? Ну, звісно це число може плавати від одного до другого в різних компаніях, тому я просто скажу скільки сервісів зараз у нас, з якими я прямо працюю. І в нас їх небагато — близько 30 з чимось, щось таке. Доводилось працювати в монорепозиторіях і з сотнями сервісів, тому вважаю що 30 - це небагато.</p>
<p>Так ось, всі ці сервіси лежать в одному репозиторії, просто що в різних теках. Структура вашого репозиторію з цими сервісами виглядала б якось так <em>(всі назви вигадані, хочу підкреслити що ось таких Dockerfile може бути багато)</em>:</p>
<pre><code class="lang-plaintext">monorepo/
├─ packages/
│  ├─ logger/
│  ├─ eventually/
├─ services/
│  ├─ orchestrator/
│  │  ├─ Dockerfile
│  ├─ lookout/
│  │  ├─ Dockerfile
│  ├─ purger/
│  │  ├─ Dockerfile
</code></pre>
<p>Тепер, якщо ви спробуєте зробити класичний <code>docker build</code> в корні репозиторію, то, звісно, що ви не отримаєте ніякого корисного результату. Але ми ж розумні люди, правда? Чом би просто не взяти цей <code>docker build</code> та й не почати його викликати для кожного <code>Dockerfile</code> в його окремих теках. Робимо якесь рішення в лоб зі скриптом, який буде нам збирати всі наші контейнери:</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>

<span class="hljs-built_in">set</span> -euo pipefail

find . -<span class="hljs-built_in">type</span> f -name <span class="hljs-string">'Dockerfile'</span> | <span class="hljs-keyword">while</span> <span class="hljs-built_in">read</span> -r dockerfile; <span class="hljs-keyword">do</span>
  dir=$(dirname <span class="hljs-string">"<span class="hljs-variable">$dockerfile</span>"</span>)
  tag=$(basename <span class="hljs-string">"<span class="hljs-variable">$dir</span>"</span> | tr <span class="hljs-string">'[:upper:]'</span> <span class="hljs-string">'[:lower:]'</span>)
  docker build --tag <span class="hljs-string">"<span class="hljs-variable">$tag</span>"</span> <span class="hljs-string">"<span class="hljs-variable">$dir</span>"</span>
<span class="hljs-keyword">done</span>
</code></pre>
<p><em>P.S. Писав цей скрипт по памʼяті, не перевіряв що він працює, але я хочу передати тут саму ідею, що ми ітеруємо по всім знайденим Dockerfile та збираємо їх.</em></p>
<p>І от, здавалось б, в нас вже є підтримка збірки контейнерів в нашому монорепозиторії. Зробили зміни, завантажили їх на GitHub, а воно вже вам всі контейнери зібрало — кошерно. Ні, не кошерно, давайте я трішки ускладню проблему.</p>
<h2 id="heading-servisi-povyazani-z-bibliotekami-z-inshih-tek">Сервіси повʼязані з бібліотеками з інших тек</h2>
<p>Сама суть монорепозиторіїв полягає в тому, що це дозволяє розробникам локально повʼязувати кодові бази з різних бібліотек чи сервісів. Для демонстрації візьмемо те ж вигадане дерево, що я зробив раніше.</p>
<pre><code class="lang-plaintext">monorepo/
├─ packages/
│  ├─ logger/
│  ├─ eventually/
├─ services/
│  ├─ orchestrator/
│  │  ├─ Dockerfile
│  ├─ lookout/
│  │  ├─ Dockerfile
│  ├─ purger/
│  │  ├─ Dockerfile
</code></pre>
<p>Пакети <code>logger</code> та <code>eventually</code> - це звичайні npm пакети. Але ці пакети через <code>dependencies</code> використовуються, наприклад, в сервісі <code>lookout</code>. Так, операційно, вони окремі сутності, які приходять через npm реєстр під час <code>npm install</code>. Але для того, щоб локально розробка була трішки легшою, вони повʼязуються локально за допомогою hardlink/softlink. Тож ми маємо сервіс <code>lookout</code>, який у своїх <code>node_modules</code> має посилання на теки в інших частинах монорепозиторію.</p>
<p>Якщо ми просто викличемо <code>docker build</code> для кожного <code>Dockerfile</code> в репозиторії, вказавши йому за контекст збірки теку з Dockerfile, то зібраний сервіс просто не матиме всіх залежностей, які знаходяться за рамками цього контексту. Тобто, він банально не запрацює. Ви отримаєте <code>Cannot find module …</code> чи як там той текст помилки йде.</p>
<p><em>Тут варто зазначити, що ми використовуємо підхід зі збіркою контейнерів із самого монорепозиторію. Якщо вибудувати робочий процес таким чином, що ви спочатку публікуєте все в реєстри із репозиторію, а потім із реєстрів вже тягнете через</em> <code>npm install</code> <em>під час збірки контейнерів, то цієї проблеми там не буде.</em></p>
<p>Тож нам, щоб успішно зібрати контейнер, потрібно не лише викликати <code>docker build</code>, вказавши теку з <code>Dockerfile</code> як контекст збірки, а і впевнитись, що в тому контексті збірки будуть всі необхідні залежності. А саме — сам сервіс і та частина монорепозиторію, яка необхідна цьому сервісу як залежність.</p>
<h2 id="heading-gotuyemo-kontekst-dlya-zbirki">Готуємо контекст для збірки</h2>
<p>Для менеджменту нашого монорепозиторію ми використовуємо <a target="_blank" href="https://rushjs.io">Rush</a>. Так от, нам пощастило, що в Rush є саме команда, яка цю проблему й вирішує. До речі, в <a target="_blank" href="https://pnpm.io/cli/deploy">pnpm теж є аналог цієї команди</a> - <code>pnpm deploy</code>.</p>
<p>Коли ми викликаємо команду <code>rush deploy</code>, ми вказуємо проєкт, який ми хочемо проаналізувати й викинути в готовий до споживання артефакт. Що мається на увазі під “готовий до споживання”? Я маю на увазі, що ця тека буде містити в собі все необхідне для того, щоб сервіс запрацював.</p>
<p>Наведу невеликий приклад, на тому ж дереві, що ми бачили раніше:</p>
<pre><code class="lang-plaintext">monorepo/
├─ packages/
│  ├─ logger/
│  ├─ eventually/
├─ services/
│  ├─ orchestrator/
│  │  ├─ Dockerfile
│  ├─ lookout/
│  │  ├─ Dockerfile
│  ├─ purger/
│  │  ├─ Dockerfile
</code></pre>
<p>Я наводив приклад з <code>lookout</code>, який використовує залежності з <code>packages/logger</code> та <code>packages/eventually</code>. Так ось, якщо ми тепер викличемо команду <code>rush deploy</code> та вкажемо, що ми хочемо зібрати <code>lookout</code>:</p>
<pre><code class="lang-bash">rush deploy --project lookout --target-folder /tmp/somewhere/<span class="hljs-keyword">in</span>/the/abyss
</code></pre>
<p>То ми отримаємо теку <code>/tmp/somewhere/in/the/abyss</code>, але, що цікаво, вже з такою структурою теки:</p>
<pre><code class="lang-plaintext">monorepo/
├─ packages/
│  ├─ logger/
│  ├─ eventually/
├─ services/
│  ├─ lookout/
│  │  ├─ Dockerfile
</code></pre>
<p>Зверніть увагу, що в цій теці присутня тільки та частина монорепозиторію, яка необхідна для нормальної працездатності сервісу. Відповідно всі hardlink/softlink, які були створені, будуть вказувати на реальні файли, а значить всі залежності будуть знайдені. А все що зайве — було викинуто.</p>
<p>На додаток, <code>rush deploy</code> не тільки викидає частину монорепозиторію, яка не стосується цільового сервісу. Він також ще й перебирає <code>node_modules</code> і залишає тільки ті, які справді потрібні цільовому сервісу. Тому в результаті ми отримаємо теку, в якій є необхідний мінімум для виклику Node.js.</p>
<pre><code class="lang-bash">node /tmp/somewhere/<span class="hljs-keyword">in</span>/the/abyss/services/lookout/dist/index.js
</code></pre>
<p>Маючи ось такий subset основного монорепозиторію, ми вже не маємо проблему з локально повʼязаними бібліотеками й сервісами. А тому можемо й запакувати його в контейнер:</p>
<pre><code class="lang-bash">docker build \
    --tag lookout:latest
    --file /tmp/somewhere/<span class="hljs-keyword">in</span>/the/abyss/services/lookout/Dockerfile
    /tmp/somewhere/<span class="hljs-keyword">in</span>/the/abyss
</code></pre>
<p>Єдине що я б тут додав, що цей процес починає виглядати дуже непривабливо для розробників. Оце піди, скажи через <code>rush deploy</code> який проєкт треба підготувати до збірки, а потім піди ще сам якісь <code>docker build</code> пороби з якимись незрозумілими контекстами. А тому я всю цю рутину запакував в скрипт до Rush.</p>
<h2 id="heading-rozshiryuyemo-komandi-rush">Розширюємо команди Rush</h2>
<p>Ще одна річ, яка мені дуже подобається в Rush, це те що ви можете розширювати його команди. Ну тобто писати свої власні скрипти, які ви можете підʼєднати до основного набору команд. Це робиться через <code>command-line.json</code> файл у його теці з конфігураціями.</p>
<p>Тобто, якщо раніше команда розробників вже навчилась збирати монорепозиторій через <code>rush build</code>, або тестувати його через <code>rush test</code>, то точно так само можна було б зробити й інтерфейс для збірки контейнерів!</p>
<p>Так я і зробив і надав командам скрипт, який доступний через <code>rush build-docker</code> команду. Основна суть цього скрипту саме в автоматизації тієї рутини, про яку я розповідав розділами вище. Це звичайний TypeScript скрипт, який використовує Rush API <em>(так, в нього є API)</em>, для того, щоб зібрати контейнер для вказаного проєкту. Але я його ще більше розширив інтеграціями саме з нашим пропрієтарним стеком, а тому в результаті ми отримали команду, яку можна використати ось так:</p>
<pre><code class="lang-bash">rush build-docker --project lookout --push
</code></pre>
<p>І в результаті отримати:</p>
<ul>
<li><p>Пошук по репозиторію саме цього проєкту й підготовка його метаданих <em>(через Rush API)</em></p>
</li>
<li><p>Виклик <code>rush deploy</code>, який в тимчасову теку скидає той самий контекст для збірки <em>(через child process)</em></p>
</li>
<li><p>Підготовка всіх тегів, версій, назв для контейнера — всього, що необхідно для GitLab, щоб він прийняв до себе в реєстр наш образ <em>(через версії із package.json та маніпуляції із рядками)</em></p>
</li>
<li><p>Безпосередньо сама збірка контейнера <em>(через Docker API, з яким я по сокету комунікую із TypeScript через</em> <code>dockerode</code><em>)</em></p>
</li>
<li><p>Ну і саме завантаження зібраного контейнера на GitLab Container Registry <em>(теж через</em> <code>dockerode</code> <em>із TypeScript коду)</em></p>
</li>
</ul>
<p>Таким чином, все те, про що ми щойно говорили, я приховав від команд розробників. Якщо їм потрібно зібрати контейнер для якогось зі своїх сервісів, то вони просто викликають то й же інструмент, яким вони й інші операції з репозиторієм роблять - Rush. Вони не те що про контексти збірок не думають, а навіть про версії та про те як там те завантажувати на реєстр.</p>
<p>Проте, якщо з погляду розробника, цього достатньо <em>(розробник часто працює в рамках одного сервісу за одиницю часу)</em>, то з погляду DevOps є один незручний нюанс — а що, як нам треба зібрати всі контейнери? Ми ж не будемо бігати для кожного з них викликати <code>rush build-docker</code>?</p>
<h2 id="heading-zbirayemo-pachku-kontejneriv">Збираємо пачку контейнерів</h2>
<p>Як щойно я зрозумів, що нам не вистачає команди, яка може працювати з сукупністю контейнерів, я одразу ж реалізував ще одну - <code>rush build-docker-all</code>.</p>
<p>Початкова реалізація була доволі проста — ітерація по всім Rush проєктам, які зазначені як сервіси, та виклик <code>rush build-docker</code> для них через дочірні процеси. Але я і тут стикнувся зі складнощами.</p>
<p>Річ у тім, що дуже часто розробники не змінюють ж всі сервіси у PR. Ну, це очевидно вже, так, але тоді я про це не подумав. І коли у вас в монорепозиторії вже більш ніж 30 сервісів, а змінився один, то… всі просто чекають поки зберуться всі 30 сервісів.</p>
<p>Тому я розширив свою команду ще одним прапорцем - <code>--changed-only</code>. Коли цей прапорець вмикається, <code>rush build-docker-all</code> після того, як збере всі сервіси на збірку, відфільтрує з них й залишить тільки ті сервіси, які реально були змінені. А тому <code>rush build-docker</code> вже викликався тільки для тих сервісів із монорепозиторія, які були змінені.</p>
<h2 id="heading-yak-ce-viglyadaye-na-ci">Як це виглядає на CI</h2>
<p>Через те, що вся логіка реалізована на TypeScript, як звичайний скрипт, який використовує Rush API та Docker API, то там це все виглядає дуже просто. Ба більше, цей скрипт, він же оформлений як ще одна команда Rush. А тому, якщо дуже коротко, то в моїх скриптах збірки, процес виглядає приблизно ось так <em>(спрощено)</em>:</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>

<span class="hljs-built_in">set</span> -euo pipefail

rush build <span class="hljs-comment"># Calls TypeScript</span>
rush <span class="hljs-built_in">test</span> <span class="hljs-comment"># Tests the changes</span>
rush build-docker-all --push --changed-only <span class="hljs-comment"># Build and push Docker images for changed services</span>
</code></pre>
<p>Справді, це все, тут більше нічого додати. Це буквально, спрощено звісно, частина CI, яка відповідає за збірку контейнерів — просто викликати <code>rush build-docker-all --push --changed-only</code> десь там у YAML файлах.</p>
<p>Ось так ми розвʼязали проблеми з локально повʼязаними бібліотеками, автоматизували збірку контейнерів через власний скрипт й зробили YAML трішки легшим для розуміння. Пишіть про свої досвіди збірки контейнерів. Як у вас побудований цей процес? :)</p>
]]></content:encoded></item><item><title><![CDATA[Мій CI/CD: огляд одного з процесів очима розробника]]></title><description><![CDATA[Disclaimer
Цей пост виявився більшим, аніж я собі представляв, а тому, перед тим як почати розповідати про цікаве, хотілось би трохи задати контексту. Про що ми будемо говорити, а про що не будемо.

Перш за все, скажу, що говоритиму я сьогодні про що...]]></description><link>https://ghaiklor.dev/mij-cicd-oglyad-odnogo-z-procesiv-ochima-rozrobnika</link><guid isPermaLink="true">https://ghaiklor.dev/mij-cicd-oglyad-odnogo-z-procesiv-ochima-rozrobnika</guid><category><![CDATA[CI/CD]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Eugene Obrezkov]]></dc:creator><pubDate>Mon, 30 Jun 2025 09:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/QckxruozjRg/upload/3c5957cc799251c39f69b18c635fc389.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-disclaimer">Disclaimer</h2>
<p>Цей пост виявився більшим, аніж я собі представляв, а тому, перед тим як почати розповідати про цікаве, хотілось би трохи задати контексту. Про що ми будемо говорити, а про що не будемо.</p>
<ul>
<li><p>Перш за все, скажу, що говоритиму я сьогодні про щоденну роботу розробника його очима. Тобто, як це взагалі, працювати розробнику в тому, що я там настворював.</p>
</li>
<li><p>По друге, хочу сказати, що жодних тонкощів реалізації, вихідних кодів, посилань безпосередньо на реалізацію, й інше - не буде. Але це не заважатиме нам говорити про концепції самого процесу як такого.</p>
</li>
<li><p>Ну і, вся ця тема з CI/CD доволі обʼємна. Тож, я не намагаюсь втиснути в один пост взагалі все, що відбувалось. Є дуже багато питань, які я свідомо пропускаю в цьому пості, але які, можливо, ми піднімемо в наступних постах, коли буде ясно, про що саме можна було б поговорити.</p>
</li>
<li><p>Також, майте на увазі, все про що ми будемо говорити, реалізувалось виключно мною протягом року. Сюди входить як і початкова фаза дизайну й намагань продавити його в бюджет й інше, так і сама реалізація, включно з допомогою командам мігрувати в нове середовище, розвʼязуючи їх проблеми на шляху до нього. Тому хоч я і кажу про великий обсяг роботи, який я робив майже рік, але сюди входить не тільки CI/CD, а і купа іншого супроводу.</p>
</li>
</ul>
<p>Але який все-таки результат ми отримали? Почнемо з розробника, який прокинувся і взяв задачу в роботу.</p>
<h2 id="heading-dobrogo-ranku-devchik">Доброго ранку, девчик!</h2>
<p>Розробник з команди, який береться за задачу після ранкового созвону, першим ділом підтягує собі останні зміни основної гілки - нічого незвичного. Стягнув собі через <code>git pull</code>, чи що він там використовує, останній commit з гілки, зробив йому <code>rush install</code> та <code>rush all</code>, аби переконатись, що станом на сьогодні, основна його гілка збирається та з нею все добре.</p>
<p>Якщо з <code>git pull</code> все зрозуміло, то, думаю, з <code>rush</code> треба трішки розповісти. Ми його використовуємо для менеджменту нашого моно репозиторію.</p>
<p>І от, <code>rush install</code> встановлює на робочій станції розробника залежності тих версій, які зараз знаходяться в основній гілці, та лінкує між собою всі наші пакети та сервіси, щоб локально воно все відчувалось як одне спільне під час розробки. Він також робить й інші речі, але не будемо про це в цьому пості.</p>
<p>Після успішного <code>rush install</code>, розробник, опціонально, може викликати <code>rush all</code>. Це просто сахар поверх інших команд: <code>rush build</code>, <code>rush lint</code>, <code>rush test</code>. Він, відповідно, викликає всі білд скрипти в усіх сервісах та пакетах, потім лінтери й врешті решт тести. Але, він не робить це для всього репозиторію. Не потрібно чекати, поки вся велика монорепа збереться. Він аналізує ваш локальний стан, дивиться, що було змінено за час відсутності розробника, і викликає ці команди тільки в тих місцях, які були змінені відносно вашого попереднього білда репозиторію. Тож, якщо викликати <code>rush all</code> на чистому репозиторії, то це білд всього, а вже наступний виклик <code>rush all</code> пропускає всі скрипти і закінчує свою роботу за менш ніж секунду. Дуже зручно для локальної роботи - дозібрати тільки те, що змінилось.</p>
<p>І ось розробник працює собі з таскою в його IDE, воно йому там все підсвічує, автоматично чинить помилки, форматує й так далі. Зміни в сервісі чи пакеті одразу поширюються на суміжні сервіси, які з ними повʼязані, і розробник може одразу ж локально все це запускати й перевіряти що воно працює.</p>
<p>В певний момент, він такий дивиться й каже: “Ну, наче можна відкривати PR”.</p>
<h2 id="heading-stvoryuyemo-pull-request">Створюємо Pull Request</h2>
<p>Ми використовуємо GitLab для нашого коду, але це не дуже відрізняється від того, що на GitHub. Розробник бере гілку, в якій він працював, пушить на GitLab та створює новий Pull Request. Знову ж, нічого незвичного.</p>
<p>На цьому етапі, він може зробити паузу, піти попити чаю чи кави, але недовго :)</p>
<p>Одразу після створення PR, білд ферма починає робити свою роботу. Встановлюються залежності для репозиторію через <code>rush install</code> <em>(воно, звісно, закешоване)</em> та починаються запуски <code>rush build</code> <em>(в основному, це виклик TypeScript компілятора, щоб зібрати dist)</em>, <code>rush lint</code> <em>(тут, зазвичай, просто ESLint запускається)</em> та <code>rush test</code> <em>(залежно від видів тестів, які розробники пишуть, можуть бути різні інструменти, але переважно це mocha або jest)</em>.</p>
<p>І, здавалось б, наче нічого такого, але є там одна цікава деталь, яка допомагає робити це дуже швидко.</p>
<p>Я вже писав трішки вище, що, коли розробник робить <code>rush all</code>, воно здатне зрозуміти, що було змінено в порівнянні з попереднім станом, та на основі цієї інформації вивести список сервісів й пакетів, які змінились.</p>
<p>Так ось, такий самий алгоритм застосовується і на білд фермах. Береться PR та порівнюється з основною гілкою. Все що було змінено в PR, мапиться на список сервісів, які нам потрібно зібрати. Але і тут є нюанс.</p>
<p>Окрім того, що ми користуємось цим алгоритмом для розуміння де нам потрібно прогнати скрипти, нам також потрібно не забувати і про топологію. Якщо, скажімо, змінився сервіс А, який не працюватиме без пакету В <em>(який не змінився, наприклад)</em>, то щоб зібрати цей сервіс А, нам потрібно також зібрати і пакет В, навіть якщо він не був змінений.</p>
<p>І ось тут ми приходимо до класичної проблеми графів та вибору його під-дерев <em>(не знаю, як subtrees правильніше написати)</em>. І ми зробили наступним чином:</p>
<ul>
<li><p><code>rush build</code> - обирає частину репозиторію, де явно були змінені файли. Включно з їх залежностями, щоб воно могло взагалі зібратись, та всіма, хто залежить від цих змінених файлів, щоб перевірити, що ми не зламали downstream, когось другого, хто від нас залежить.</p>
</li>
<li><p><code>rush lint</code> - обирає частину репозиторію, де явно були змінені файли і більше нічого. Перевіряти на best practices код, який не змінився, немає сенсу. Так само як і перевіряти downstream. Це повністю атомарна й ізольована задача, яка просто перевірить, що ваші зміни не порушують наші практики.</p>
</li>
<li><p><code>rush test</code> - обирає частину репозиторію, де явно були змінені файли. Включно з усіма, хто залежить від цих файлів. Таким чином, ми перевіряємо, що безпосередньо зміни не зламали нічого в сервісі. Але, також, перевіряємо і всіх, хто залежить від цих змін, на випадок, якщо хтось щось зламав для іншої команди чи іншого сервісу.</p>
</li>
</ul>
<p>Ось такі, здавалось б, прості задачі, а під ними багато всякого цікавого відбувається :)</p>
<p>У випадку, якщо всі ці процеси пройдуть успішно, CI почне перевіряти, а чи описали ви зміни, які ви зробили. Це потрібно для наступної фази нашого CI процесу.</p>
<h2 id="heading-versionuyemo-nashi-zmini">Версіонуємо наші зміни</h2>
<p>Питання версіонування це завжди складно. Потрібно враховувати канали дистрибуції, чи потрібно нам підтримувати старі версії, чи потрібно нам бекпорти підтримувати й багато іншого. Це прям окремий пост можна писати.</p>
<p>В нашому випадку, в нас версіонування відносно просте, а тому ми вирішили його зробити повністю асинхронним та автоматичним. Але ж виникає тоді питання, а хто взагалі приймає рішення про те, яка має бути наступна версія, якщо це повністю автоматичне та асинхронне?</p>
<p>Так ось, ми вирішили це наступним чином. Якщо розробник забуде описати свої зміни, то CI завалить йому білд і напише список сервісів, які він змінив, але не описав ці зміни. В такому випадку, це означатиме, що розробник просто забув викликати ще одну команду <code>rush</code> - <code>rush change</code>. Якщо ж він не забуде її викликати перед створенням PR, то звісно що і білд валитись не буде. Що ж це за команда така?</p>
<p><code>rush change</code> аналізує сервіси та пакети, які були змінені, та які піддаються політикам версіонування <em>(ми можемо вказувати різні політики версіонування та розділяти внутрішні пакети на зовнішні й так далі)</em>. На основі цієї інформації, в інтерактивному режимі, розробник має описати, що він змінив, та вказати яка має бути наступна версія: <code>patch</code> чи <code>minor</code> <em>(важливо зрозуміти, що ми вказуємо не саму версію, а лиш стратегію, за якою оновлювати версію)</em>.</p>
<p>Результатом цього інтерактивного опитування, яке відбувається виключно на локальному компʼютері розробника і не потребує нічого складного, стають JSON файли, які коммітяться разом з іншими змінами.</p>
<p>Тож тепер, CI бачить не тільки зміни і що вони працюють, а ще й має інформацію про те, який сервіс на яку версію потрібно буде оновити - ця інформація є частиною PR.</p>
<p>Це дає нам змогу не синхронізувати взагалі жодним чином інших розробників та команди. Кожен розробник, створюючи новий Pull Request, просто вказує стратегію оновлення версії через <code>rush change</code> і коммітить це разом зі своїми змінами.</p>
<p>Тому цей процес я і називаю асинхронним - тут відсутня координація між розробниками. Хоч у вас буде 200+ PR-ів, але якщо в кожному із них буде вказано розробником, що, наприклад, наступна версія має бути patch, то хто б там що не робив, але після мержу в основну гілку, наступна версія буде - теперішня версія з основної гілки + patch.</p>
<p>Якщо весь цей процес відбувається в рамках Pull Request, то окрім того, що CI робить нові версії згідно опису автора цього PR, він ще додає до нього <code>dev-$COMMIT</code> суфікс. Тож, якщо процес запустився на PR, то ми отримали дев збірку, яку ми можемо задеплоїти на тестові кластери та потестувати. Ну і, відповідно, якщо воно відбувається в основній гілці, то нова версія буде вже без <code>dev-$COMMIT</code> суфіксу.</p>
<p>Реалізувавши асинхронне <em>(автор PR не координується з іншими)</em> та автоматичне <em>(CI бере теперішню версію й просто інкрементує згідно опису автора)</em> версіонування, ми вже можемо зібрати артефакти з новими версіями, та завантажити їх по всім нашим реєстрам.</p>
<h2 id="heading-deliverimo-artefakti">Деліверимо артефакти</h2>
<p>Про артефакти я довго розповідати не буду. В нас тут все доволі класично і, на мою думку, нічого цікавого не відбувається.</p>
<p>Маючи нові версії пакетів та сервісів, наш CI прописує їх в <code>package.json</code> всіх тих пакетів, які підпадають під нову версію. Оновлені <code>package.json</code>, з новими <code>version</code> полями, використовуються для того, щоб вивести всі версії для всіх артефактів в подальших кроках.</p>
<p>Якщо це npm пакет, то використовується звичайний <code>pnpm publish</code> <em>(звісно, що з врахуванням всіх тих нюансів, але в кінці це просто</em> <code>publish</code><em>)</em>. Пакети публікуються в закритий реєстр на GitLab Package Registry, який знаходиться в тому ж проєкті, що і сам репозиторій.</p>
<p>Якщо це Docker образ, то ми його збираємо з використанням наших кастомних скриптів, які через програмне API комунікує з Docker Engine. Це нам дозволяє побудувати місток між Rush API та Docker Engine, що, своєю чергою, дозволяє нам реалізувати збірку контейнерів частиною команд <code>rush</code>. Що, своєю чергою…, дозволяє нам використати всі ті корисні речі, про які я розповідав раніше, для збірки саме контейнерів.</p>
<p>Ми розуміємо, де контейнер треба збирати, де не треба і тому подібне. Тегом контейнера виступає той ж <code>version</code> із <code>package.json</code> проєкту, який раніше був оновлений на CI. Результатом цього всього є команда <code>rush build-docker-all --push --changed-only</code>, яку ми викликаємо на CI, а він вже розбирається, що змінилось, й зібрав контейнери з новими версіями та запушив їх на GitLab Container Registry.</p>
<p>Ну і з Helm аналогічна ситуація. Окрім самого Helm, поруч з ним зберігається такий ж <code>package.json</code>, але він використовується лише для того, щоб зберігати там версію й інтегрувати в спільний workflow монорепозиторію. Ми і тут розуміємо, коли змінився Helm, і коли він змінився, то розробник описує зміну й стратегію для нової версії, а все інше - те саме. А, ну і сховищем для Helm Charts у нас є GitLab Package Registry.</p>
<p>Тож, на цьому етапі, в нас вже всі нові версії є, всі артефакти зібрані й всі розкидані по всім реєстрам. Вони задеплоїлись на тестові кластера, прогнались e2e тести і розробник отримує зелену галочку на свої <code>dev-$COMMIT</code> версії.</p>
<h2 id="heading-dayemo-dobro-na-merzh">Даємо добро на мерж</h2>
<p>Всі ці процеси, описані вище, відбуваються в Pull Request, а тому вони повністю безпечні. Мало того, ми не просто запускаємо весь цей CI процес на окремій гілці розробника, а робимо ефемерні комміти, які є результатом автоматичного злиття гілки розробника з основною гілкою. Це дозволяє нам розуміти, чи буде мерж успішним, чи ні, ще до того, як ми його змержимо. Тобто в нас немає ситуацій, коли ми постфактум дізнаємось, що після мержа основна гілка вже не працює. Ми про це дізнаємось ще на етапі Pull Request-а.</p>
<p>А ще, ми використовуємо Merge Train, як додатковий спосіб перевірки, чи буде мерж успішним, чи ні. Ці Merge Train-и здатні автоматично мержити декілька гілок в один комміт та запускати наш CI процес на комбінаціях декількох ПР-ів з основною гілкою. Як на мене, про це варто і окремий пост зробити, а тут не буду вдаватись в деталі.</p>
<p>Що б там не було, які б комбінації мержів там не тасувались, але ось, нарешті, розробник отримав ту зелену галочку і отримав дозвіл на мерж. Після чого це все вливається в основну гілку. Причому, важливо, що вливається як squash commit. Тобто що б там розробник собі не робив у своєму PR, то його пісочниця і він може робити що завгодно. Але в основній гілці це буде один єдиний комміт, з посиланням на PR розробника, де можна прослідкувати всю історію.</p>
<p>Таким чином, ми страхуємо себе від ситуації, коли недобросовісний розробник наробив коммітів по типу <code>asdf</code> та <code>bla-lba</code>, та які попали в основну гілку - такі ситуації ми забороняємо, але надаємо можливість робити що завгодно у гілці розробника, поки це залишається там.</p>
<p>Після змерженого Pull Request в основну гілку, запускаємо знову CI. Але цього разу, викидаємо з його роботи деякі моменти, які вже й так були виконані ще на етапі Merge Train. Через те, що ось той ефемерний комміт, про який я казав раніше, вже і так містить в собі комбінацію кодових баз розробника з його гілки та основної гілки - ми можемо не запускати той ж лінтер чи e2e тести - бо ми вже і так їх прогнали на цьому комміті, перед тим як мержитись.</p>
<p>А тому, CI на основній гілці викликає тільки <code>rush build</code> для змінених сервісів, щоб отримати артефакти в <code>dist</code>. Ми їх одразу ж оновлюємо на нові версії згідно опису автора PR та пакуємо в реєстри - CI закінчив свою роботу - ми отримали нові production артефакти.</p>
<h2 id="heading-deployimos-v-dev">Деплоїмось в dev</h2>
<p>Як тільки ми отримали нові production артефакти, тобто з версіями типу <code>1.5.12</code>, а не дев збірки, ми їх одразу ж деплоїмо на dev кластера. Так, я знаю, що для деяких команд, автоматичний деплой це щось страшне і штибу в “пʼятницю нічого не чіпайте”. Але я проти такої культури і, більш того, вважаю її неефективною. Потрібно працювати над створенням процесів, які дозволяють деплоїтись, коли вам завгодно, а не лякати людей і стримувати їх мантрами “працює - не чіпай”. Бо коли настане час справді чіпати - може бути вже пізно.</p>
<p>Так ось, для деплоїв на наші кластера, ми використовуємо Helm. Це доволі великий чарт, в якому прописані всі наші мікросервіси разом з їх базами даних й іншими операційними залежностями. Для того, щоб цей деплой був автоматичний, я реалізував скрипти, які беруть інвентар зібраних Docker образів та оновлює ними потрібний Helm chart. Це банальний обхід дерева YAML файлу та оновлення всіх збігів по Docker-у на нові версії. Тобто відкрили YAML, розпарсили його, отримали дерево, по якому пробіглись рекурсивно, та оновили всі збіги образів на нові версії.</p>
<p>Змінене дерево я зберігаю назад у файл і роблю новий комміт в основну гілку. Всі ці файли ми зберігаємо в текі <code>.gitops</code> й використовуємо Flux, для того, щоб застосовувати нові зміни на наші кластера. Тобто, таким чином, скрипт оновив та закоммітив нові версії в <code>.gitops</code>, а Flux уже прийшов й зробив reconcilation loop.</p>
<p>В результаті, ми отримали процес, в якому розробник після мержу свого Pull Request, може одразу бачити свої зміни на дев кластерах. Це, по відгуках, особливо приємно для frontend розробників. Бо вони не чекають когось, хто задеплоїть їх зміни. Вони змержились й хвилин через 5 вже бачать свої зміни на живому дев кластері. Ну, а для backend розробників, це теж корисно, бо вони одразу бачать чи не зламали вони щось після мержу свого PR.</p>
<h2 id="heading-deployimos-v-production">Деплоїмось в production</h2>
<p>Ось розробники пописали коду, перемержили PR-и, і такі сидять, і думають - може в прод? І це, я вважаю, і є те, заради чого, всі ці речі потрібно робити. Я вважаю, що деплоїтись в прод має бути виключно політичне рішення. Чи то маркетингове, неважливо. Не має бути жодних труднощів для команд задеплоїтись в production. Це має бути відбуватись за бажанням в будь-яку годину <em>(ну, майже в будь-яку, в рамках розумного)</em>.</p>
<p>Для цього, команда йде на пайплайни репозиторію, та просто запускає ще одну задачу - deploy to production. І цей скрипт робить все те саме, що робить і скрипт для деплою в dev. Бабільше, використовується той ж скрипт, просто з іншими аргументами. Він пішов у YAML файл, але який стосується вже проду, оновив там всі версії й закомітив назад в <code>.gitops</code>, щоб Flux застосував зміни на кластери.</p>
<p>Єдиний, правда, момент в тому, що ми вирішили себе трохи таки застрахувати. Коли ми деплоїмось в прод, то версії для проду ми беремо виключно із версій теперішнього дев кластеру. Таким чином, ми хочемо себе застрахувати від моментів, коли щось могло б трапитись лише після довгої роботи кластера.</p>
<p>А тому, спочатку команда деплоїться в dev автоматично після мержу PR і, якщо на dev-і все добре, то ми можемо вважати, що і на проді теж буде норм, а тому версії можна перекласти в прод з деву.</p>
<h2 id="heading-rezultati">Результати</h2>
<p>На новому CI/CD процесі, команда вже працює в режимі деплою щоранку. Раніше, це було чи то раз у два тижні, чи щось таке, не памʼятаю. Звісно, тут ще багато було роботи, в тому числі культурної та менторської: розповісти, показати, зруйнувати міфи і тому подібне, але вже зараз вони відчувають покращення в порівнянні з минулою системою.</p>
<p>Станом на сьогодні, весь цей процес, я б сказав, знаходиться вже у формі публічної бети всередині компанії. Це рішення масштабується та дозволяє інтегрувати більше команд в наш репозиторій, та одразу отримати для себе всі ці автоматизації безкоштовно.</p>
<p>Звісно, ми ще знаходимо баги, чинимо їх, але загалом - маємо монорепозиторій, в який можна набирати більше команд, та який надає весь необхідний інструментарій для повного SDLC.</p>
<p>На цьому ми й закінчимо це велике полотно. Дякую вам, якщо дочитали до цього місця, та допоможіть мені лайком й розповсюдженням, якщо вам сподобалось. Тема велика, а тому пишіть коментарі, ставте питання та пропонуйте, про що б хотілось почитати далі.</p>
]]></content:encoded></item><item><title><![CDATA[Як зменшити розмір Docker образу з Node.js]]></title><description><![CDATA[Всім привіт! В Twitter спитав чи було б цікаво почитати про Docker з Node.js та чи буде цікавим історія про те, як я зменшував розмір образу до 40-50 Мб. На твіт відгукнулись, тож вирішив, що можна й погратись трохи з цією історією (тим паче, що зара...]]></description><link>https://ghaiklor.dev/yak-zmenshiti-rozmir-docker-obrazu-z-nodejs</link><guid isPermaLink="true">https://ghaiklor.dev/yak-zmenshiti-rozmir-docker-obrazu-z-nodejs</guid><category><![CDATA[Node.js]]></category><category><![CDATA[Docker]]></category><dc:creator><![CDATA[Eugene Obrezkov]]></dc:creator><pubDate>Wed, 25 Jun 2025 12:58:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1750856022245/4c4ab17b-fa84-4729-a58d-3f97e755629a.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Всім привіт! В Twitter спитав чи було б цікаво почитати про Docker з Node.js та чи буде цікавим історія про те, як я зменшував розмір образу до 40-50 Мб. На твіт відгукнулись, тож вирішив, що можна й погратись трохи з цією історією (тим паче, що зараз трішки часу вільного зʼявилось).</p>
<h2 id="heading-disclaimer">Disclaimer</h2>
<p>Проте одразу залишу парочку дисклеймерів, хоча я знаю, що ніколи ці дисклеймери не захищали від занудних тіпків, які хочуть показати що вони все знають.</p>
<p>По перше, я пишу цей текст от як від себе до людини, яка сидить поруч і я їй розповідаю історію. В мене немає зараз часу й бажання займатись редагуванням текстів і оце все, тому читайте це з чайочком там, чи з кавою, не напинайтесь лишній раз 🙂</p>
<p>По друге, я не пропагую ніяких best practices в цій статті. Це просто цікава історія (або ні), що можна зробити, на що повпливати й так далі й тому подібне. Можливо у вас є певні обставини, за яких вам необхідно зменшувати розмір образів, можливо ні. В будь-якому разі, якщо ви із цієї статті дізнаєтесь щось нове, не забувайте, що у всього є контекст і потрібно аналізувати й дивитись, чи варто робити щось, чи не варто. Ви всі великі молодці й у вас є свої голови на плечах, для того, щоб подумати, чи варто робити речі із цієї статті, чи не варто.</p>
<h2 id="heading-pochnemo-z-rishennya-v-lob">Почнемо з рішення “в лоб”</h2>
<p>Я трішки подумав як би то можна було організувати, але чесно, зараз щось такий стан трохи втомлений, що я вирішив зробити це просто як інкрементальний процес від роздутого образу до чогось такого, до чого прийдемо згодом.</p>
<p>Я ще навіть не знаю куди ми прийдемо. Я пишу зараз цей текст без жодного контент-плану, імпровізую 🙂</p>
<p>Почнемо з того, що зазвичай можна знайти в просторах інтернету, простий й примітивний Dockerfile:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> node:lts
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>

<span class="hljs-keyword">COPY</span><span class="bash"> dist dist</span>
<span class="hljs-keyword">COPY</span><span class="bash"> package.json package.json</span>

<span class="hljs-keyword">RUN</span><span class="bash"> npm install</span>

<span class="hljs-keyword">CMD</span><span class="bash"> [ <span class="hljs-string">"node"</span>, <span class="hljs-string">"dist/index.js"</span> ]</span>
</code></pre>
<p>Зібравши цей образ через <code>docker build</code> й запустивши його через <code>docker run</code> ми вже маємо сервер, який готовий приймати наші запити:</p>
<pre><code class="lang-bash">Server is listening on http://69bfecfb40ae:36199
</code></pre>
<p>Сам сервер це звичайний Hapi сервер, ніякої екзотики.</p>
<p>Глянувши в <code>docker images</code> ми побачимо, що розмір цього образу <strong>1.8GB</strong> 🤯</p>
<p>І тут буде цілком очікуваним позадавати питання штибу “ви шо тут, вообще всі такі чи шо?”.</p>
<p>Тож давайте потроху розбиратись.</p>
<h2 id="heading-viddilyayemo-run-time-zalezhnosti-vid-compile-time-zalezhnostej">Відділяємо run-time залежності від compile-time залежностей</h2>
<p>Один із факторів який впливає на розмір вашого Docker образу - <strong>node_modules</strong>.</p>
<p>Я бачив різні команди й різні підходи до розв'язання цієї проблеми, дехто їх і взагалі не вирішував, як ось <code>npm install</code> просто та і все.</p>
<p>Але загалом, правилом гарного тону все ж є використання <code>--production</code> при встановленні тільки тих залежностей, які вам критично необхідні для працездатності сервісу.</p>
<p>Коли ви вказуєте у своїх <code>package.json</code> залежності, то дотримуйтесь правила, що в <code>dependencies</code> ви вказуєте тільки ті залежності, які вам <strong>потрібні</strong> в runtime. В моєму випадку це <code>@hapi/hapi</code>, бо на ньому в мене сервер написаний. Без нього, мій сервер просто не запуститься. Це робить цю залежність необхідною в runtime.</p>
<p>Все інше, що вам потрібно щоб прогнати ESLint, чи може тести прогнати, чи ще щось — якщо це робиться під час розробки й не потрібно в runtime - виносьте в <code>devDependencies</code>.</p>
<p>Таким чином, у вас локально <code>npm install</code> буде встановлювати все що треба для розробки. А коли ви будете готувати ваш сервіс, який вже зібраний, до роботи в runtime, то використовуйте там <code>npm install --production</code>:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> node:lts
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>

<span class="hljs-keyword">COPY</span><span class="bash"> dist dist</span>
<span class="hljs-keyword">COPY</span><span class="bash"> package.json package.json</span>

<span class="hljs-keyword">RUN</span><span class="bash"> npm install --production</span>

<span class="hljs-keyword">CMD</span><span class="bash"> [ <span class="hljs-string">"node"</span>, <span class="hljs-string">"dist/index.js"</span> ]</span>
</code></pre>
<p>Зробили знову <code>docker build</code> і подивились скільки тепер займає образ - <strong>1.69GB</strong>. Ще дуже далеко до 40 Мб 😆</p>
<p>Давайте тепер поговоримо про різницю між різними образами від яких ви можете наслідуватись!</p>
<h2 id="heading-lts-lts-slim">lts, lts-slim</h2>
<p>Коли ви обираєте який базовий образ використовувати, памʼятайте, що неправильно підібраний образ може мати різні наслідки починаючи від проблем з розміром і закінчуючи несумісністю архітектур й того що компілюється, коли ви встановлюєте якість нативні аддони під Node.js.</p>
<p>От почав я з того, що обрав <code>node:lts</code> як базовий образ, але що там, в цьому образі?</p>
<p>Якщо піти глянути на Docker Hub, то ми можемо побачити там, з яких інших образів був створений цей образ, які шари застосовані й т.п.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750854190855/cf9fdea4-0be2-4bd7-ad61-810eed6e2747.png" alt class="image--center mx-auto" /></p>
<p>Ми бачимо що <code>node:lts</code> побудований з <code>debian:12</code> до якого потім додали <code>buildpack-deps</code> і пішло поїхало по ланцюжку.</p>
<p>Тобто наш образ <code>node:lts</code> насправді має в собі не тільки Node.js, який ми запустили й поїхали. Також, в нашому образі знаходиться велика купа різних бінарників, бібліотек, які насправді не потрібні нам для того, щоб запустити наш сервіс. Це можуть бути всякі <code>git</code>, <code>curl</code>, <code>ca-certificates</code> і що там ще можна згадати, мені ліньки 🙂</p>
<p>Чи потрібні нам всі ці пакети при збірці наших сервісів, компіляції, тестування — звісно! Чи потрібні вони нам, коли ми вже запускаємо це в runtime - ні!</p>
<p>Тому правилом гарного тону вважається, що у вас є оточення, яке ви використовуєте для збірки ваших сервісів, а є оточення, яке ви використовуєте вже безпосередньо для запуску цього сервісу.</p>
<p>І в нашому випадку, ми можемо позбутись цього роздутого образу з <code>buildpack-deps</code>, використавши <code>lts-slim</code>.</p>
<p>Якщо ми поглянемо на інвентар того, що є в цьому образі, то ми побачимо наступне:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750854236364/928a195f-709d-4cb8-8f2f-9d9016b7d050.png" alt class="image--center mx-auto" /></p>
<p>По перше, тепер у нас два базових образи, з яких будувався <code>lts-slim</code>. По друге, в шарах вже немає всіляких <code>apt-get update</code> та <code>apt-get install</code>.</p>
<p>Тобто ці образи хоч і містять меншу кількість різних бінарників та інструментів, що заважає розробці, але для production нам це і не потрібно. Тому тут мінус в тому, що ми втрачаємо всілякі зручності повноцінних ОС, але виграємо в потенційно захищенішому середовищі, бо менше потенційних векторів атаки, та виграємо в зменшеному розмірі нашого власного образу. Тому замінимо <code>lts</code> на <code>lts-slim</code>:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> node:lts-slim
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>

<span class="hljs-keyword">COPY</span><span class="bash"> dist dist</span>
<span class="hljs-keyword">COPY</span><span class="bash"> package.json package.json</span>

<span class="hljs-keyword">RUN</span><span class="bash"> npm install --production</span>

<span class="hljs-keyword">CMD</span><span class="bash"> [ <span class="hljs-string">"node"</span>, <span class="hljs-string">"dist/index.js"</span> ]</span>
</code></pre>
<p>Зібрали знову образ і подивились, а скільки він тепер займає місця - <strong>429MB</strong>.</p>
<p>Ну це вже прям непогано. Принаймні GB замінились на MB 😄</p>
<p>Але поговорім про ще один варіант, більш радикальний.</p>
<h2 id="heading-alpine-linux">Alpine Linux</h2>
<p>По всіляких Linux дистрибутивам можна дуже довго розмовляти. Починаючи від “BTW, I’m using Arch”, “вічно дивитись <s>як тече вода</s>, збираються портажі на Gentoo”, а потім сісти за макбук і на Notion поток сознанія видавать.</p>
<p>Так от Alpine Linux це один із дистрибутивів Linux, але в якого є кардинальні відмінності від більш house-hold дистрибутивів Linux, таких як Debian (пробачте мені молодого, я не хотів називати їх house-hold).</p>
<p>Цей дистрибутив спеціально заточений, по дизайну, бути малим за розміром. Але досягає він цього дещо “радикальними” методами (хоча рівень радикальності тут можна задавати відносно окремо взятих систем координат).</p>
<p>По перше, він використовує musl замість glibc. Якщо хтось із читачів писав програми на С, то він знає, що в C є стандартна бібліотека. А чи то musl, чи то glibc - це вже реалізації цих стандартних бібліотек.</p>
<p>Так от, вважається, чи то вважалось, тут мої знання можуть бути вже не актуальні, але вважалось, що Node.js більше дружить з glibc і вірогідність що якийсь нативний аддон чи C-шний код не відпрацює з glibc набагато нижча, аніж якби це був musl. Можливо зараз там стан речей зовсім інший, але хай там як, потрібно усвідомлювати, що такі речі можуть принести багато цікавих проблем на вашу голову, якщо вам не пощастить.</p>
<p>По друге, він використовує BusyBox. От якщо ви звикли до того, що у всіляких Debian-ах у вас купа бінарників, де кожен надає якийсь свій інструментарій, то BusyBox це все один бінарник, в якому купа цих інструментів. Ну добре добре, не купа, менше 🙂</p>
<p>Ну і ще там OpenRC замість systemd… короче… ви зрозуміли — змін багацько. Радикальних змін.</p>
<p>Тож чим більше у вас всілякої екзотики у вашому сервісі, можливо кастомних нативних аддонів, абощо, тим більше уваги вам треба приділяти перевірці, що воно точно під Alpine заведеться.</p>
<p>Але у нас це звичайний сервер на Hapi, так шо нам норм, поїхали 🙂</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> node:lts-alpine
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>

<span class="hljs-keyword">COPY</span><span class="bash"> dist dist</span>
<span class="hljs-keyword">COPY</span><span class="bash"> package.json package.json</span>

<span class="hljs-keyword">RUN</span><span class="bash"> npm install --production</span>

<span class="hljs-keyword">CMD</span><span class="bash"> [ <span class="hljs-string">"node"</span>, <span class="hljs-string">"dist/index.js"</span> ]</span>
</code></pre>
<p>Зібрали цей образ і отримали розмір - <strong>308MB</strong>. Що ж можна придумати ще?</p>
<h2 id="heading-vikidayemo-npm-z-procesu-zbirki">Викидаємо npm з процесу збірки</h2>
<p>Якщо нам обставини дозволяють, або можливо це навіть архітектурно прописано у вашій компанії, то ми можемо зрізати ще в одному місці 😉</p>
<p>Суть дуже проста. Ми коли викликаємо <code>npm install</code> то при встановленні залежностей, npm тягне то всьо з інтернету, завантажує це у своєму кеші й так далі й тому подібне. Ми то, звісно, можемо зробити потім <code>npm cache prune</code> чи як воно там, але особисто мені воно якесь meh.</p>
<p>Окрім цього, він ще й компілює нативні аддони, якщо залежності цього потребують, і компілює він їх під ту архітектуру, на якій ви запустили процес збірки. Тобто якщо ви запустили <code>npm install</code> на макі (<code>darwin-arm64v</code>, якщо не помиляюсь) то <code>node_modules</code> будуть містити в собі код, який працюватиме тільки на таких же маках з архітектурою <code>darwin-arm64v</code>. Якщо ви <code>node_modules</code> скопіюєте в образ і запустите його на <code>linux-amd64</code> то я сумніваюсь що воно у вас запрацює, але ви можете спробувати, звісно.</p>
<p><strong>Але! 😉</strong></p>
<p>Якщо ми впевнені в тому, що на CI пайплайнах використовується ферма із нод з такою ж архітектурою, яка використовується і в проді, то ми можемо зробити фінт вухами.</p>
<p>Замість того, щоб робити <code>npm install</code> під час збірки образу, ми можемо зробити його в контексті звичайного процесу збірки вашого репозиторію. Якщо архітектури збігаються, то тоді <code>node_modules</code> вашого репозиторію можна буде просто перенести в контейнер і не тягнути туди <code>npm install</code>.</p>
<p>А якщо ще й застосувати, як я їх називаю, екстрактори в пакетних менеджерах, то це взагалі можна зробити так, що <code>node_modules</code> будуть містити ну прям дуже гарний layout, ну прям 🤌</p>
<p>Наприклад у pnpm є команда <code>pnpm deploy</code>. Або ж якщо ви використовуєте підхід з моно репозиторієм і ви використовуєте Rush як інструмент для менеджменту вашого репозиторію, то в нього теж є <code>rush deploy --project PROJECT --target-folder path/to/folder</code>.</p>
<p><strong>В чому їх особливість?</strong></p>
<p>Ці команди аналізують ваші пакети, які ви хочете винести й аналізують залежності, які вашим пакетам потрібні. Маючи цю інформацію, ці команди здатні зробити вам окремо теку, де будуть лежати тільки ваші production файли разом з <code>node_modules</code>, де лежать тільки те, що справді потрібно в runtime - production залежності.</p>
<p>Воно настільки самодостатнє, що ви навіть можете зробити <code>rush deploy --project my-package --target-folder /app</code> і одразу викликати <code>node /app/dist/index.js</code> - воно спрацює. Бо всі <code>node_modules</code> вже там, всі <code>files</code> із вашого <code>package.json</code> вже теж там.</p>
<p>Хочете зробити просто архів з цим і десь передати щоб потім запустити через <code>node</code> - кидайте.</p>
<p>Хочете зробити якийсь свій дуже кастомний процес delivery цього на якісь ноди - робіть.</p>
<p>А ми ж зробимо наступним чином…</p>
<h2 id="heading-novij-build-context-dlya-obrazu">Новий build context для образу</h2>
<p>Ми дізнались що в пакетних менеджерах є команда, яка дозволяє нам виокремити із нашого репозиторію з купою коду й залежностей теку, яка буде містити в собі мінімальний набір необхідного для запуску в проді.</p>
<p>Якщо ми застосуємо це як новий build context для докера, то нам, насправді, вже й не треба ніякі <code>npm install</code> чи то <code>COPY dist dist</code> чи ще щось - в нас готовий артефакт, самодостатній, який працює і без докера. Тобто докер в цьому контексті починає виступати просто як, буквально, контейнер 🙂</p>
<p>Тож що ми робимо…</p>
<p>Ми беремо, викликаємо, скажімо <code>rush deploy --project hapi-server --target-folder common/deploy</code>.</p>
<p>Якщо ми тепер викличемо <code>node common/deploy/dist/index.js</code>, то побачимо, що наш сервер все ще працює й слухає порт:</p>
<pre><code class="lang-bash">Server is listening on http://ghaiklor---MacBook-Pro.local:52215
</code></pre>
<p>А по структурі файлів там просто <code>dist</code> з моїми JS файлами та й <code>node_modules</code>. Все.</p>
<p>Як же це тепер запакувати в образ? Та насправді дуже просто 🙂</p>
<p>Якщо ми враховуємо, що цей <code>common/deploy</code> стає нашим новим build context для докера, то нам вже не потрібні вибіркові <code>COPY</code>, ми можемо тупо зробити <code>COPY . .</code>:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> node:lts-alpine
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>

<span class="hljs-keyword">COPY</span><span class="bash"> . .</span>

<span class="hljs-keyword">CMD</span><span class="bash"> [ <span class="hljs-string">"node"</span>, <span class="hljs-string">"dist/index.js"</span> ]</span>
</code></pre>
<p>Можна навіть залишити коментар нашим наступникам, щоб вони не подумали, що цей <code>COPY . .</code> копіює взагалі весь репозиторій:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> node:lts-alpine
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>

<span class="hljs-comment"># The COPY command here copies the entire prepared folder into the Docker image.</span>
<span class="hljs-comment"># This works because we're using the `rush deploy` command.</span>
<span class="hljs-comment"># Which prepares a minimized subset of the monorepository containing only the necessary files and dependencies.</span>
<span class="hljs-comment"># The prepared folder acts as the build context on CI.</span>
<span class="hljs-comment"># So it's already optimized to exclude unnecessary files, such as development tools.</span>
<span class="hljs-comment"># As a result, copying the entire folder is safe since everything included in the build context is essential for the service’s operation.</span>
<span class="hljs-comment"># And there's no risk of including extraneous files from the broader monorepo.</span>
<span class="hljs-keyword">COPY</span><span class="bash"> . .</span>

<span class="hljs-keyword">CMD</span><span class="bash"> [ <span class="hljs-string">"node"</span>, <span class="hljs-string">"dist/index.js"</span> ]</span>
</code></pre>
<p>І, як бачимо, ми вже не викликаємо тут <code>npm install</code>, бо <code>node_modules</code> вже теж там 🙂</p>
<p>Що ж, почнімо збірку з врахуванням цього трюку:</p>
<pre><code class="lang-bash">rush deploy --project hapi-server --target-folder common/deploy
docker build --file common/deploy/Dockerfile --tag hapi-server common/deploy
</code></pre>
<p>З таким Dockerfile та цією комбінацією команд, ми, по великому рахунку, всю збірку вже зробили ще на самому хості. Тут же, ми просто прокидуємо вже зібране в сам образ - от і все.</p>
<p>Гляньмо, що в нас там зараз з місцем виходить - <strong>192MB</strong>! Ну це вже не майже 2Gb, правда ж? 😄</p>
<p>Щоб бути гарними хлопчиками, вкажімо ще конкретну версію Node.js рантайма, щоб не вистрілити собі в ногу випадково, коли <code>lts-alpine</code> неочікувано зміниться на іншу версію:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> node:<span class="hljs-number">20.17</span>.<span class="hljs-number">0</span>-alpine
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>

<span class="hljs-comment"># The COPY command here copies the entire prepared folder into the Docker image.</span>
<span class="hljs-comment"># This works because we're using the `rush deploy` command.</span>
<span class="hljs-comment"># Which prepares a minimized subset of the monorepository containing only the necessary files and dependencies.</span>
<span class="hljs-comment"># The prepared folder acts as the build context on CI.</span>
<span class="hljs-comment"># So it's already optimized to exclude unnecessary files, such as development tools.</span>
<span class="hljs-comment"># As a result, copying the entire folder is safe since everything included in the build context is essential for the service’s operation.</span>
<span class="hljs-comment"># And there's no risk of including extraneous files from the broader monorepo.</span>
<span class="hljs-keyword">COPY</span><span class="bash"> . .</span>

<span class="hljs-keyword">CMD</span><span class="bash"> [ <span class="hljs-string">"node"</span>, <span class="hljs-string">"dist/index.js"</span> ]</span>
</code></pre>
<p>Не те, щоб я очікував, що тут щось зміниться з розміром, але як правило гарного тону…</p>
<h2 id="heading-distroless">Distroless</h2>
<p>Ще один варіант, який можна застосувати, це distroless:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/GoogleContainerTools/distroless">https://github.com/GoogleContainerTools/distroless</a></div>
<p> </p>
<p>Конкретно в нашому випадку, в тому що я зробив до цього, то ми багато не виграємо, насправді. Я навіть не знаю, може ми нічого не виграємо, давайте перевіримо 😅</p>
<p>Специфіка цих образів в тому, що в них відсутнє все те, що зазвичай присутнє в дистрибутивах Linux. Ви в них не знайдете ніяких shell-ів, пакетних менеджерів - нічого.</p>
<p>В них є тільки необхідне для того, щоб запустити Node.js, в нашому випадку.</p>
<p>Спробуймо замінити наш <code>FROM</code> на distroless образ й подивитись, скільки ми отримаємо:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> gcr.io/distroless/nodejs20-debian12:nonroot
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>

<span class="hljs-comment"># The COPY command here copies the entire prepared folder into the Docker image.</span>
<span class="hljs-comment"># This works because we're using the `rush deploy` command.</span>
<span class="hljs-comment"># Which prepares a minimized subset of the monorepository containing only the necessary files and dependencies.</span>
<span class="hljs-comment"># The prepared folder acts as the build context on CI.</span>
<span class="hljs-comment"># So it's already optimized to exclude unnecessary files, such as development tools.</span>
<span class="hljs-comment"># As a result, copying the entire folder is safe since everything included in the build context is essential for the service’s operation.</span>
<span class="hljs-comment"># And there's no risk of including extraneous files from the broader monorepo.</span>
<span class="hljs-keyword">COPY</span><span class="bash"> . .</span>

<span class="hljs-keyword">CMD</span><span class="bash"> [ <span class="hljs-string">"dist/index.js"</span> ]</span>
</code></pre>
<p>Зверніть, до речі, увагу, що ми прибрали <code>node</code> із <code>CMD</code>. Через те що в distroless образах відсутній shell, то ми не можемо це запускати вже як раніше. Але ми можемо вказати прямо шлях до нашого файлу. В distroless це спрацює.</p>
<p>Гляньмо, скільки тепер займає місця - <strong>189MB</strong>. Не густо щось 😂</p>
<p>Із цих 189 Мб, сам тільки Node.js займає майже 100MB, але чому так багато?</p>
<h2 id="heading-sho-mi-mozhemo-vikinuti-z-nodejs">Що ми можемо викинути з Node.js?</h2>
<p>Зараз вже майже восьма година вечора. Я приїхав з офісу додому й хочу вже поскоріш закінчити цю, і так довгу, історію 🙃</p>
<p>Тому на цьому етапі ми закінчимо з практичною частиною, проте, пофантазуємо, що ми можемо зробити ще, якщо це справді буде необхідно.</p>
<p>Річ у тім, що заздалегідь зібрані бінарники Node.js під різні платформи містять в собі ну-у-у-у занадто багато всього. Наприклад?</p>
<p>Наприклад Internationalization - <a target="_blank" href="https://nodejs.org/api/intl.html">https://nodejs.org/api/intl.html</a></p>
<p>Всі ці API, яка надає вам Node.js, по типу <code>Intl</code> обʼєкту чи <code>.localeCompare()</code> й тому подібні несуть за собою доволі відчутний розмір.</p>
<p>І що ми могли б зробити, насправді, це зібрати власний бінарник Node.js, в якому повикидати те що нам не потрібно. Ну, принаймні, те що ми можемо викинути.</p>
<p>У випадку з інтернаціоналізацією, якщо ми розуміємо, що ми ці API не використовуємо, то давайте зберемо Node.js без ICU. Зклонували собі репозиторій з вихідними кодами Node.js й еге-ге-й:</p>
<p><em>Останній раз коли я контрібʼютив в Node.js, це робилось так, як зараз, я, чесно, не знаю, можливо вони щось змінили, тому take it with a grain of salt.</em></p>
<pre><code class="lang-bash">./configure --without-intl
make
</code></pre>
<p>Тобто ми налаштовуємо білд систему й кажемо їй, що ми хочемо скомпілювати Node.js, але без використання ICU. Отриманий <code>node</code> бінарник буде вже займати, по грубим прикидкам, на 20-40 МБ менше, в залежності від купи факторів.</p>
<p>Також можна почати думати щось цікаве з zlib чи openssl, можливо викинути їх теж, або скомпілювати як з shared libraries. Загалом, можна буде подумати. Детальніше про те, як зібрати Node.js у себе вдома, можна почитати ось тут:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/nodejs/node/blob/HEAD/BUILDING.md">https://github.com/nodejs/node/blob/HEAD/BUILDING.md</a></div>
<p> </p>
<p>До прикладу, ось залежності в збірці Node.js, яка в мене стоїть, встановлена через nvm, та яка займає 90MB:</p>
<pre><code class="lang-json">{
  node: '<span class="hljs-number">20.17</span><span class="hljs-number">.0</span>',
  acorn: '<span class="hljs-number">8.11</span><span class="hljs-number">.3</span>',
  ada: '<span class="hljs-number">2.9</span><span class="hljs-number">.0</span>',
  ares: '<span class="hljs-number">1.32</span><span class="hljs-number">.3</span>',
  base64: '<span class="hljs-number">0.5</span><span class="hljs-number">.2</span>',
  brotli: '<span class="hljs-number">1.1</span><span class="hljs-number">.0</span>',
  cjs_module_lexer: '<span class="hljs-number">1.2</span><span class="hljs-number">.2</span>',
  cldr: '<span class="hljs-number">45.0</span>',
  icu: '<span class="hljs-number">75.1</span>',
  llhttp: '<span class="hljs-number">8.1</span><span class="hljs-number">.2</span>',
  modules: '<span class="hljs-number">115</span>',
  napi: '<span class="hljs-number">9</span>',
  nghttp2: '<span class="hljs-number">1.61</span><span class="hljs-number">.0</span>',
  nghttp3: '<span class="hljs-number">0.7</span><span class="hljs-number">.0</span>',
  ngtcp2: '<span class="hljs-number">1.1</span><span class="hljs-number">.0</span>',
  openssl: '<span class="hljs-number">3.0</span><span class="hljs-number">.13</span>+quic',
  simdutf: '<span class="hljs-number">5.3</span><span class="hljs-number">.0</span>',
  tz: '<span class="hljs-number">2024</span>a',
  undici: '<span class="hljs-number">6.19</span><span class="hljs-number">.2</span>',
  unicode: '<span class="hljs-number">15.1</span>',
  uv: '<span class="hljs-number">1.46</span><span class="hljs-number">.0</span>',
  uvwasi: '<span class="hljs-number">0.0</span><span class="hljs-number">.21</span>',
  v8: '<span class="hljs-number">11.3</span><span class="hljs-number">.244</span><span class="hljs-number">.8</span>-node<span class="hljs-number">.23</span>',
  zlib: '<span class="hljs-number">1.3</span><span class="hljs-number">.0</span><span class="hljs-number">.1</span>-motley<span class="hljs-number">-209717</span>d'
}
</code></pre>
<p>Якщо буде стояти питання порізати Node.js, щоб воно вмістилось на якусь embedded приблуду, то можна буде подумати в напрямку збірки свого власного бінарника.</p>
<p>Буде це питання того, щоб повністю викинути якісь шматки, чи буде це питання вивчення конкретного дистрибутива, які бібліотеки в тому дистрибутиві є і чи можемо ми скомпілювати Node.js, який буде динамічно лінкуватись до цих бібліотек - то вже, як кажуть, треба буде розбирати конкретний кейс.</p>
<p>Просто для прикладу, наскільки я знаю, то Arch Linux має системний ICU, тож можна було б скомпілювати Node.js з <code>--with-intl=system-icu</code> і не тягнути його з бінарником Node.js.</p>
<p>Короче, гратись можна було б довго з цим й пробувати різні комбінації.</p>
<p>Я знаю випадки, коли такими танцями витягували Node.js бінарник до 15-20MB. Але, знову ж таки, вони всі зачіпають збірку Node.js з його вихідних кодів з різними тюнінгами 🙂</p>
<h2 id="heading-kompresiya-obraziv">Компресія образів</h2>
<p>Весь цей час, всю цю історію, ми розглядали розмір образів як образів без компресії. Тобто це все розміри вже “розпакованих” образів.</p>
<p>Але ж ми, коли пушимо образ в реєстр, Docker нам додатково ще застосовує алгоритми компресії. Тому давайте глянемо, а скільки ж, такий образ, який ми створили на Alpine Linux, буде займати місця безпосередньо в реєстрі? Скільки трафіку буде бігати по мережі, коли буде завантажуватись образ?</p>
<p>Я запушив цей образ на GitLab Container Registry та глянув, скільки місця він займає в реєстрі - <strong>45.40 MiB.</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750854670190/8204b2fd-640e-4fed-bba8-ccf80b9d558c.png" alt class="image--center mx-auto" /></p>
<p>Тобто, після компресії, наш образ займає 45MB в реєстрах, а значить, по мережі буде йти 45MB, а не 192MB.</p>
<h2 id="heading-zakinchuyemo">Закінчуємо</h2>
<p>На годиннику вже девʼята вечора, тож буду йти відпочивати.</p>
<p>Сподіваюсь, в цьому потоці сознанія, ви віднайшли для себе щось нове, можливо абсурдне, можливо креативне, то вже як буде. Я поділився історією, яку я сьогодні в себе на ноуті провів, а вам вже вирушівати для себе, що воно дає і чи дає взагалі.</p>
<p>Всіх благ і гарного вечора ❤️</p>
]]></content:encoded></item></channel></rss>