Тестуємо в проді, але не на користувачах


У Twitter я побачив ось цей твіт:
Я ось цей “гумор” про пʼятничні деплої взагалі не розумію. Ну бо це не гумор, це якась хвора стигма в професіональному оточенні. Люди, щоб нормалізувати певні явища, інколи застосовують гумор до них. І через це, сприйняття змінюється з “щось, що можна поліпшити, бо це проблема” на “ха-ха, та це ж нормально, посміємось, це не проблема”. Але, хай там як, цей пост не про когнітивні пастки та викривлення. Про них можна написати й окремий пост, де розповісти про те, як наш мозок обдурює сам себе - дуже цікаві штуки насправді.
А в цьому пості я вирішив сфокусуватись на одній ідеї, яка безпосередньо й дозволяє в більшості випадків деплоїти в пʼятницю й не боятись нікого зламати. Але перед тим як я підійду конкретно до цієї теми, я зайду трішки здалеку. Чому? Та банально, тому що в мене зараз на це є настрій. Ну і, тому що вважаю, що донести цю ідею й пояснити чому вона працює - важливо, як мінімум для мене. Тож я почну трішки здалеку і пропоную почати з дуже простого кейсу.
CLI параметри
Ви всі у своїй роботі використовуєте CLI параметри. Чи то ви працюєте з git
, чи то з curl
, чи купа інших програм. Щоб скорегувати програму, аби вона виконувала ті дії, які вам потрібні, ви зазвичай вказуєте підкоманди чи параметри. Ну от візьмімо за простий приклад команду curl
:
curl google.com
Викликавши цю команду ви очікувано отримаєте якийсь HTML у себе в терміналі:
Але що ви робите, коли ви хочете змінити поведінку програми, не змінюючи саму програму? Ви передаєте параметри (якщо програма їх підтримує звісно). Ну от, наприклад, ми хочемо подивитись на детальнішу інформацію стосовно зʼєднання і додаємо параметр:
curl --verbose google.com
Додали прапорець --verbose
і викликали програму:
І ось ми вже бачимо зовсім іншу поведінку програми, яка не просто видає HTML, а ще й додатково виводить в термінал інформацію про зʼєднання.
Так от до чого я це все веду? От подумайте, у вас є одна і та ж сама програма, яку ми викликали вперше і вдруге. Той же самий curl
, за тим же самим шляхом, той же самий файл. Але при цьому, ми отримали різні поведінки, залежно від вказаних параметрів від користувача.
Наведу ще один приклад.
Змінні середовища
Я підозрюю, що багато з вас, хто читає цей пост, працює з вебом. Тож ви точно чули про environment variables. Це звичайні пари ключ\значення, які операційна система прокидає у ваш процес. І вже ваш процес може прочитати ці пари та дістати значення. Зазвичай їх часто використовують для довгоживучих серверів, але й не тільки. Так ось.
Скажімо, у вас є сервіс, який пише деякі логи, щоб краще розуміти що відбувається. Але ви, по класиці, зробили так, що у вас є рівні логів. Ну це зазвичай щось типу error
, warn
та info
. І от, наприклад, info
логів у вас багато і ви б хотіли їх вимкнути. Багато бібліотек надають цю можливість, вказуючи через LOG_LEVEL
рівень логів, які ви б хотіли писати.
Тож ви запустили свій сервіс, вказали йому в змінних оточення LOG_LEVEL=warn
і таким чином вимкнули інформаційні логи.
А тепер питання. Чи змінювали ви щось у своїй програмі для цього? Чи може у бібліотеці, яку ви використовуєте? Не думаю. Ви можете розгорнути такий же самий сервіс, вказати йому інший LOG_LEVEL
і ви отримаєте два однакових сервіси, але які вже виконують різні шматки коду, скажімо так. І все це без зміни самого коду.
Що між ними спільного?
Думаю, цих двох прикладів достатньо, щоб передати ось цю концепцію - мати код, хід якого можна контролювати зовнішніми чинниками.
І це неважливо насправді як саме реалізовується ця концепція. Як я і показав на прикладах, в CLI це можуть бути параметри до самої програми. В сервісах це можуть бути змінні середовища.
Всіх їх обʼєднує одна риса - не змінюючи код програми, ми можемо контролювати її хід.
Прості реалізації
Ну от, познайомились з ідеєю. Але ідеї ідеями, але без прикладу буде неповноцінно. Як би ми могли дуже просто реалізувати ось цю концепцію з “зроби мені А, якщо X, а якщо ні, то зроби B”. Я буду частково використовувати для цього псевдокод, але дуже сильно відрізнятись від реальної реалізації воно не буде. Ну і як ви вже могли зрозуміти, звісно тут велику роль відіграє if statement.
Ну от візьмемо той же curl
, як би це могло виглядати там?
// These are some other functions somewhere else in the program
declare class Connection {}
declare function doingAllTheStuffWith(url: string): Connection;
declare function outputVerboseInfoFor(connection: Connection): void;
declare function outputResponseFrom(connection: Connection): void;
import { parseArgs } from "node:util";
const args = parseArgs({
options: {
verbose: { type: "boolean", default: false },
url: { type: "string", default: "" },
},
});
const connection = doingAllTheStuffWith(args.values.url);
// !!!
// This is the important part for this blog post
if (args.values.verbose === true) {
outputVerboseInfoFor(connection);
}
outputResponseFrom(connection);
Зверніть увагу, що наш код з самого початку може видавати verbose
логи для нашого псевдо Connection
. Але! Робитиме він це лише за умови, що йому прокинули параметр --verbose
. В усіх інших випадках, програма банально не зайде в ту гілку, вона буде пропущена.
Аналогічно можна було б зробити й за допомогою змінних оточення. Додамо альтернативний спосіб ввімкнути наші verbose логи:
if (args.values.verbose === true || typeof process.env["CURL_VERBOSE"] !== "undefined") {
outputVerboseInfoFor(connection);
}
І ось в нас вже є програма, хід якої ми можемо змінювати лише вказуючи додаткові параметри чи змінні оточення.
Дочитали ви до цього місця і такі думаєте “Жека, ну це ж смішно, ми й так це розуміємо”. І матимете рацію. Ви ж знаєте, я полюбляю йти від простого до складного, тож зараз я почну ускладнювати вам задачку.
Прапорець або ввімкнений, або вимкнений
Основний великий мінус цього підходу, про який я так довго пишу, в тому, що у нього бінарна логіка. Ну тобто, ви або можете ввімкнути це у програмі, або вимкнути. І з цим, особливо якщо ми говоримо про прод і про користувачів, є дуже велика проблема.
Така бінарна логіка може працювати в короткострокових програмах по типу CLI утиліт чи щось подібне. Для одного сценарію ви викликали програму з одним набором параметрів, а для іншого сценарію - з іншими.
Якщо ж ми починаємо говорити про довгострокові сервери, які постійно працюють, то ми ж не будемо для кожного запиту підіймати окремий сервіс з іншим набором змінних оточення. Правда ж?
Ось це і є проблема. Маючи описаний вище підхід, ми то, звісно, можемо підняти сервіс з одним набором конфігурацій, але цей набір буде працювати або для всіх, або ні для кого.
Що як, я б хотів підняти сервіс, але щоб той код, який я написав, працював лише для мене. А у мого колеги він би не відпрацьовував? І тим паче не відпрацьовував би для наших користувачів? Хоч він вже і на проді. Іншими словами, наш if має вміти робити щось типу такого:
- if (args.values.verbose === true || typeof process.env["CURL_VERBOSE"] !== "undefined") {
+ if (thisIsMineRequestAndNoOneElse) {
outputVerboseInfoFor(connection);
}
Ось тут ми й приходимо до того, що я так люблю - ускладнення!
Прийняття рішення в run-time
Для того, щоб ми могли реалізувати цей умовний thisIsMineRequestAndNoOneElse
, нам вже недостатньо мати ту статичну інформацію, яку ми маємо, коли передаємо її через прапорці чи через змінні оточення. Бо цієї інформації банально недостатньо, щоб зрозуміти хто є хто у запиті.
Тому наший if має кудись сходити з якимись даними й щоб цей хтось нам сказав чи це має бути true
, чи false
(дуже спрощено). І в принципі зі збором даних на нашій стороні все відносно легко. Ми можемо просто підготувати контекст з важливою інформацією та передати її тому, хто буде приймати рішення.
Для простого прикладу, нехай у нас буде якийсь Express сервер, в якому вже є налаштовані middleware, які роблять аутентифікацію й засовує ваші дані в req.user
. Тоді, ми могли б викликати функцію, яка скаже нам, а чи можна для цього користувача ввімкнути цей шлях, чи ні:
- if (thisIsMineRequestAndNoOneElse) {
+ if (featureEnabledFor(req.user)) {
outputVerboseInfoFor(connection);
}
Перенесення цих рішень у динамічну площину та додавання контекстів, перед тим як приймати рішення, й робить можливим реалізацію прапорців на більш гранулярному рівні. Тепер ми вже можемо прапорці вмикати не лише для конкретної програми, а і для конкретних користувачів чи інших обʼєктів, які містяться в тій програмі на момент обробки.
Проблема тепер лише в тому, щоб правильно побудувати потрібний контекст для прийняття рішення. А там ви вже що вирішите, так і буде, обмежень майже немає, майже. Захотіли зробити, щоб прапорець був ввімкнений тільки коли context.date == Feb 30 2222
? Та робіть, запихайте в контекст дату, ну і налаштуйте того, хто прийматиме рішення, що якщо дата Feb 30 2222
в контексті, то прапорець треба ввімкнути.
Але я оце говорю постійно про оцього “хтось”, та й так не сказав, про кого ж мова. Що ж, тепер поговоримо про тих, хто приймає рішення, й місце, де ви це все налаштовуєте.
Feature Management Platform
Ці інструменти називають по різному в різних компаніях, бо хтось будує свої власні платформи, як от Wix, й називає по своєму. Хтось бере вже готові COTS (Component-Of-The-Shelf). Тому і терміни вигадують різні. Мені ж ближче більш таке загальне використання терміну - Feature Management Platform.
Що ж це за комбайни? Це платформи, які надають вам, якщо дуже грубо:
Базу даних для зберігання налаштованих правил. Для кого, коли, в який час, як довго й тому подібне, вся ця інформація зберігається в цих базах даних і потім використовується для прийняття рішення. (Маю на увазі, що це не їх власні бази даних, а просто що вони використовують БД для зберігання налаштованих прапорців з усіма їх правилами).
Web інтерфейс для зручного налаштування правил. Через нього ви, як розробники, й кажете, що має бути ввімкнено, а що вимкнено, для кого, як надовго й т.і. Зазвичай це процес по типу “Створити новий прапорець → Вмикати лише для користувачів з email, який закінчується на company.com → Вмикати прапорець лише один раз із десяти випадків” ну і тому подібне.
Рушій для прийняття рішень, який безпосередньо й бере участь в тому, що потім повертається у ваш код у вигляді
true
чиfalse
. Ви даєте йому свій контекст, про який ми поговорили трішки раніше, а він на основі переданих йому даних, порівнює це з налаштованими правилами із бази даних й каже вам - так, прапорець ввімкнено для цього користувача, або ні, не ввімкнено.
Ось це все і є тим, що потрібно, щоб ви могли змінювати хід програми залежно від того, хто нею користується - ваш користувач чи ви.
Налаштували прапорець через Web-інтерфейс, дали йому назву, вказали правила по типу “для цих email” та “на ці дати” й т.д.
У своєму коді зібрали контекст для прапорця, в який запхали потрібні дані, як от пошта чи унікальний ідентифактор користувача чи ще щось.
Відправили контекст на Feature Management Platform й отримали відповідь чи для цього контексту прапорець ввімкнено чи ні.
Про які платформи я чув?
Щоб ви могли детальніше пошукати та почитати про всі ці платформи, накидаю сюди список, який я принаймні чув, бачив та читав щось про них.
Unleash
Наскільки я зрозумів, то вважається одним із найбільших платформ цього типу з відкритим вихідним кодом. Є і документація й приклади, можна їх і на on-prem розгортати, наскільки памʼятаю. Наявність клієнтів під більшість мов програмування. Ось їх сторінка - Unleash.
Їх інтерфейс, як на мене, доволі гарний, що теж плюс:
Є в них і всілякі залежності, сегменти, стратегії активацій - але то все вже більш для досвідчених і можна про то окремий пост зробити.
GitLab Feature Toggles
Наш проєкт хоститься на GitLab і ми там використовуємо й реєстри для контейнерів й всіляких пакетів для npm, Helm і так далі. Так ось у них також є і прапорці. Єдине що, інтерфейс у них набагато гірший й мені важко сказати, що в тій реалізації вони нам якось корисні. Все доволі просто й, наскільки я зрозумів, під капотом в них там unleash. Тому можна починати з GitLab, а потім переїхати на Unleash, коли масштаби виростуть.
OpenFeature
Не зовсім платформа, а скоріш спроби стандартизувати все що повʼязане з прапорцями. Можете використати як джерело додаткової інформації, щоб детальніше з ними познайомитись.
Простий прапорець в Unleash
Ну от вирішили ви таки спробувати це все і встановили собі Unleash, наприклад. Як буде виглядати ваш код з усіма цими модними прапорцями?
В першу чергу, вам потрібно буде налаштувати прапорець в інтерфейсі, де вказуєте стратегії активації, сегменти й тому інше, якщо воно вам, звісно, потрібно.
Через те, що я постійно розповідав про активацію прапорця для конкретного користувача в системі, то зробімо прапорець, якому ми скажемо, що ввімкни наш if для мене і мого колеги (ID випадкові):
Зберігши цю стратегію активації в нашому прапорці з іменем demoApp.step2
, в нас тепер є місце, куди ми можемо піти спитати, чи можна нам в той закритий для користувачів if чи ні. Так, я використовую Demo App від Unleash, ну бо а чого ні, в мене зараз немає ніде серверів з ним.
Але як це виглядатиме в коді? Налаштувати прапорець зі своїми власними User ID в системі ми то налаштували, а як передати? Ну, грубо кажучи, от як я розповідав раніше в прикладі з Express, так і передавати. Це просто обʼєкт з контекстом, в якому можуть лежати й інші значення, наприклад:
+ const context = {
+ userId: req.user.id,
+ sessionId: req.session.id,
+ remoteAddress: req.ip,
+ properties: {
+ region: 'EMEA',
+ },
+ };
- if (featureEnabledFor(req.user)) {
+ if (unleash.isEnabled('demoApp.step2', context)) {
outputVerboseInfoFor(connection);
}
Unleash SDK зробить запит до свого рушія, передасть туди, в тому числі, User ID із вашої бази даних. На сервері Unleash відбудеться порівняння, він побачить User ID в списку тих, кому прапорець можна ввімкнути й поверне вам в if true
. Якщо ж User ID не співпаде, то буде false
.
Ось таким чином ми і зробили можливість умовно не робочий код деплоїти в прод і при цьому не ламати наших користувачів. Бо ми буквально цей код викликаємо тільки для конкретних ситуацій, конкретних користувачів, тенантів й тому інше.
За допомогою цих прапорців великі компанії роблять в тому числі й A/B тестування, підписки по типу Premium, Ultra й так далі, з різним набором функціоналу.
Підсумовуючи
Концепція прапорців вже дуже давно як не нова. Про неї говорили ще чуть не з 2010-их, а то й раніше. Швидкий пошук показав, що і Fowler в 2017 щось там про нього писав, й на DOU вже проскакували статті про них в 2019. Звісно, що до нас воно прийшло здалеку, тому є деяка затримка, але так, якось так - концепція перевірена часом.
Основна ідея в тому, що ви, як розробник, змінюєте хід програми, не змінюючи код програми. Ви деплоїте в пʼятницю ввечері неробочий код в прод, але ховаєте його за прапорцем зі стратегією активації “тільки для мене”. Тому якщо хтось і зламається в пʼятницю ввечері - то це тільки ви.
Subscribe to my newsletter
Read articles from Eugene Obrezkov directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Eugene Obrezkov
Eugene Obrezkov
I write in Ukrainian about DevOps, architecture, and everything that makes code (and teams) flow better. Markdown, diagrams, and real-world messes included.