"Don’t clone infrastructure — isolate data: ephemeral PR environments in a shared environment", Oleksandr Buchek
I’ll show practical approaches, common pitfalls, and ready-to-use templates with concise PHP snippets that let you run dozens of parallel features in one cluster, cut costs, and speed up CI.
Solution Overview: TheShift to Logical
Isolation
Strategy: Move complexity from infrastructure to
conventions and tooling.
● Isolation in Application Layer
● Multi tenancy
● Short-Lived Resources
● Baggage
Service Mesh: Routingbased on Baggage
North/South - handles ingress traffic from external clients to
in-cluster workloads, replaces Nginx Ingress controller
East/West - manages in-cluster L4/L7 transparent proxies for service-
to-service communication, enhances Kubernetes native service
discovery
DACI
DACI
An official Kubernetes project focused on L4 and L7 routing in Kubernetes. This
project represents the next generation of Kubernetes Ingress, Load Balancing,
and Service Mesh APIs. https://gateway-api.sigs.k8s.io/
Conclusion and Takeaway
●Trunk based development enabler
● Scalable parallel development
● E2E testing & dev environment debugging
● DORA metrics ++
● SDLC Discipline and Tooling
Editor's Notes
#2 Відносно класичний git flow:
Фіча бранчі релізились на стейдж
У нас монорепо
Багато інженерів ведуть розробку -> багато PR
Е2е тести та проблеми зі стабільністю
Дані почали конфліктувати
Довго відпрацьовував CI/CD
Довго дебажити
Інженери довго накопичували “фіча бранчі” і через це набагато рідше релізили
#3 Ізолювати дані, щоб e2e тести завжди писались на “чистому” листі
Пришвидшити CI/CD
Зробити так, щоб паралельні PR не блокували інших
Зробити так, щоб інженери частіше деплоїли на прод
#4 Dedicated Infrastructure vs. Shared Environment
Infra per env:
Дорого
Довгий CI/CD
Ми прийшли до ефемерних енвів в рамках одного кластеру
Що якщо ми поруч будемо піднімати лише нові версії сервісів поруч з існуючими версіями і перевикористовувати депенденсі
Shared environment
Деплоїти сервіси поруч з іншими на стейджі, але забезпечити ізоляцію даних
Дешево
Швидко
#5 Strategy: Move complexity from infrastructure to conventions and tooling.
Isolation Pushed to Application Layer:
Логіка по ізоляції даних переходить на application рівень, де у нас з’являється нова абстракція preview-env
Multi tenancy: Імплементувати ізоляцію використовуючи уже реалізовані механізми ізоляції/namespacing-у або multi-tenancy в залежностях (postgres, redis, rabbit, temporal)
Short-Lived Resources: By design все має бути garbage-collected by PR close or nightly TTL job. Легко рестартанути воркфлоу, оскільки він “легкий” та атомарний
Core Principle: Baggage. Вам завжди на кожному рівні потрібно пам’ятати, що у вас є baggage, котрий потрібно пропагейтити через всі рівні вашого application layer-a
#6 What is Baggage?
– Baggage — це офіційний стандарт W3C (Web Consortium), який визначає, як передавати довільні key-value пари разом із HTTP-запитами в розподілених системах
На відміну від traceparent (що передає тільки ідентифікатор трейсингу для distributed tracing), Baggage дозволяє передавати будь-які корисні дані для ізоляції або бізнес-логіки
Це стандарт, а не кастомний хак → легко інтегрувати з OpenTelemetry, Service Mesh, проксі.
Забезпечує єдиний канал передачі контексту:
HTTP → Middleware
Queue → Message headers
Logs/Tracing → Monolog / OpenTelemetry
#7 Все що потрібно для того, щоб використати preview-env - це вказати baggage з його назвою
#8 Що таке Schema Context Bundle
Це Symfony-бандл, який допомагає динамічно витягувати й передавати schema / контекст (baggage) по всьому стеку (HTTP → Messenger → логування)
Він інтегрується з HTTP клієнтами, Symfony Messenger, Monolog, і дозволяє “пронести” обрану схему / контекст автоматично через ввесь стек
Основні можливості / фічі
Витягування schema з HTTP заголовку (з W3C Baggage чи іншого header)
Глобальний резольвер (BaggageSchemaResolver), що зберігає поточну схему і baggage
Інтеграція з Messenger:
Додає BaggageSchemaStamp до повідомлень при відправці
Відновлює schema / baggage при обробці повідомлення
Decorator для HTTP-клієнтів — щоб автоматично додавати baggage header до вихідних запитів
Processor для Monolog — щоб включити інформацію з baggage у лог (додає до extra поля log записів)
#9 Що тут відбувається
У контролері ми інжектуємо BaggageSchemaResolver, і вже у методі index отримуємо схему (getSchema()) та весь baggage (усі контекстні дані).
Після цього можна просто використовувати $schema у своїй логіці — наприклад, встановити search_path, створити SQL запити або звертатися до кешу з префіксом.
Значення для архітектури
Це ілюструє головну ідею: розробник не повинен думати про те, як витягнути контекст або додати його до кожного компонента — SchemaContextBundle вже під капотом робить це за нас.
Контекст передається довкола всієї обробки запиту (HTTP, черги, логи) послідовно і без додаткового коду в бізнес-логіці.
#10 Що ми робимо
Беремо звичайний HTTP-клієнт (наприклад, для платежів) і декоруємо його BaggageAwareHttpClient.
Це означає, що кожен вихідний HTTP-запит автоматично отримає baggage-заголовок із потрібним контекстом (preview-env, schema тощо).
Чому це важливо
Нам не треба вручну прокидати заголовки в кожному запиті.
Розробник пише звичайний код paymentHttpClient->request(...), а middleware вже сам додає baggage.
Головний меседж
Це робить передачу контексту прозорою та дисциплінованою — немає “людського фактора”, немає ризику забути заголовок.
#11 Проблема
У синхронному HTTP-запиті ми легко передаємо baggage через хедери.
Але коли повідомлення йде у чергу (RabbitMQ, Kafka, Redis Streams), контекст може загубитися.
Що робить bundle
Middleware автоматично додає BaggageSchemaStamp до кожного відправленого повідомлення.
А коли повідомлення обробляється — він відновлює baggage та schema, щоб хендлер бачив той самий контекст, що й оригінальний запит.
Що це дає
Розробник пише звичайний handler, і не думає про те, як прокинути preview-env.
Логіка завжди виконується у правильній схемі (наприклад, preview_123), незалежно від того, через HTTP чи через Messenger прийшов запит.
#12 Проблема
У нас є десятки схем / preview-env. Коли дивимося логи — важко зрозуміти, яке повідомлення належить до якого PR чи середовища.
Що робить інтеграція
Bundle дає Monolog Processor, який автоматично додає baggage (наприклад, preview-env=payment-pr-162) у extra поле кожного запису логу.
Тобто в логах завжди є тег з контекстом.
Приклад
{
"message": "User created",
"extra": {
"baggage": {
"preview-env": "payment-pr-162"
}
}
}
Що це дає
Тепер можна швидко фільтрувати логи по preview-env.
Це значно спрощує діагностику: якщо тести впали у конкретному preview env, відкриваєш Kibana / Graylog / ELK і одразу бачиш усі пов’язані логи.
Головний меседж
Навіть у логах ми гарантуємо ізоляцію — кожен запис має свій контекст. Це робить troubleshooting у shared environment простим і безпечним.
#14 The Service Mesh enhances Kubernetes networking to utilize the propagated baggage for granular traffic routing.
North/South (Ingress)
* Handles ingress traffic from external clients to in-cluster workloads.
* Allows external clients to hit the public API endpoint (e.g., api.macpaw.dev/resellers-account) while setting the baggage header, routing the request to the correct isolated preview environment.
East/West (Service-to-Service)
* Manages in-cluster L4/L7 transparent proxies.
* The Mesh utilizes HTTPRoute definitions, allowing the system to direct traffic between services based on the presence of the baggage header.
* This ensures that internal service calls maintain isolation, even if they share the same backend service endpoint (e.g., routing based on -H “baggage: preview-env=d roids-pr-49”).
Benefit: This approach enables unified observability, as the same W3C Baggage mechanism is used to propagate fields for OpenTelemetry tracing.
#15 - Istio проксі роутить запити витягуючи значення preview-env з хедера
-Таким чином на application layer-і нам не потрібно мати якусь хитру логіку і підставляти різні сабдомени чи path при відправці http запитів, цю логіку забирає на себе Service Mesh
#17 “Ми обрали schema per env, бо вона дає простішу ментальну модель, менший ризик витоків і легше інтегрується з нашими PHP-бібліотеками. Так, потрібно менеджити cleanup і міграції, але виграш у безпеці та простоті перекриває це.”
Проста модель мислення
Легше пояснити розробнику: “У тебе є окрема схема з власними таблицями”, ніж розбиратися з політиками RLS.
Debugging простіший: можна підключитися в psql і бачити тільки дані цього PR.
Надійність / Hard walls
Навіть якщо забули десь про tenant_id, дані не перетечуть.
Schema ізольована фізично.
Продуктивність
RLS додає runtime overhead для кожного запиту (фільтрація).
Schema switching (SET search_path) — це дешевше, ніж перевіряти policy на кожному рядку.
Сумісність з інструментами
Doctrine / Symfony легше інтегрувати зі схемами (через search_path), ніж переписувати політики під ORM.
Багато сторонніх тулз (pg_dump, аналізатори) працюють прозоріше.
Зручність cleanup
Легко видалити schema цілком, коли PR закрився → все дані preview-env пропадають одним DROP SCHEMA.
#18 Стратегія: для кожного PR створюємо окрему схему в одному й тому ж Postgres інстансі.
Переваги:
Нуль додаткових кластерів, усе в одному Postgres.
Дуже швидке створення/видалення.
Бекап простий, бо єдина база.
Обмеження:
Потрібні тулзи для автоматизації міграцій і cleanup.
Якщо хочеш робити join між схемами — треба явно це прописувати.
#19 Lifecycle:
1. Drop schema - на випадок якщо це rerun
2. CI creates schema. - (для всіх subdependency сервісів, ініціюючи preview-env по всьому дереву залежностей)
3. CI runs migrations - для сервісу який деплоїться ми ранимо міграції
4. TTL or PR close drops the schema.
Benefit:
Швидкий create/drop
#20 Цей бандл бере на себе роботу з PostgreSQL:
коли ми знаємо, яка схема потрібна (скажімо, із baggage), він автоматично налаштовує search_path для Doctrine-з’єднання.
Якщо схема відсутня — ми отримаємо помилку рано.
Завдяки цьому бізнес-логіка працює як завжди, але з правильною ізоляцією в БД
Перевикористовується наша бібліотека BaggageSchemaResolver
#21 Стратегія: Isolation by /vhost
Ми не розділяємо черги по іменах чи тегах — ми робимо окремий vhost у RabbitMQ для кожного preview environment. Це створює “тверду стіну” між середовищами.
Архітектурна передумова
Важливо: Producer і Consumer завжди йдуть у парі й деплояться разом в одному CI/CD пайплайні. Інакше зв’язок зламається.
Механіка роботи
Коли створюється нове preview environment, під час CI/CD ми автоматично створюємо новий vhost (наприклад /service-a-pr-1).
Всередині нього живуть свої черги й ексченджі. Це дозволяє повторно використовувати ті самі імена queue/exchange в кожному environment, без ризику конфліктів.
Роль Baggage Propagation
Тут немає додаткової магії: ми просто беремо environment ID із baggage-заголовка HTTP і прокидаємо його далі у RabbitMQ message header (Symfony Messenger middleware).
Таким чином контекст не губиться і завжди потрапляє у правильний vhost.
Переваги
Hard walls — дані фізично ніколи не перетинають межі між env.
Це частина стандартного bootstrap-флоу у CI/CD.
Коли PR закривається або спливає TTL, vhost видаляється, щоб уникнути сміття.
“Vhost на кожен environment — це найпростіший і найнадійніший спосіб ізолювати повідомлення у RabbitMQ. Жодних хитрих naming-конвенцій, тільки чіткі hard walls.”
#23 Проблема
Redis — це key-value storage. Якщо ми запускаємо багато preview environment в одному Redis, то ключі легко можуть конфліктувати між собою.
Наприклад, у staging та у PR-оточенні одночасно може існувати ключ user.123.
Що робить Redis Schema Bundle
Він автоматично додає префікс до всіх ключів на основі поточного schema/context (наприклад, preview_123.user.123).
Тобто один і той самий код з однаковими ключами працює в різних ізольованих просторах.
Як це виглядає
$this->cache->get('user.123', fn() => 'value');
Якщо schema = client_a, ключ збережеться як client_a.user.123.
Якщо schema = payment-pr-162, то payment-pr-162.user.123.
Переваги
Проста інтеграція: працює через стандартний Symfony CacheInterface / AdapterInterface.
Нуль змін у бізнес-логіці: розробник працює з тими ж ключами, але система сама ставить префікс.
Жорстка ізоляція: жоден ключ із staging не перетне preview env.
Головний меседж
“Redis Schema Bundle робить для Redis те саме, що search_path для Postgres — прозоро додає контекст ізоляції, щоб уникнути витоків даних між середовищами.”
#24 Проблема: що робити з 3d party сервісами, які не можуть прокинути нам baggage в хедері
Зазвичай сервіси, з яких прям потрібно ізолювати дані мають механізми прокидування даних через metadata (Paddle: passthrough, Stripe: metadata)
Прийдеться інвивідуально вирішувати це з кожним подібним кейсом
#25 Проблема: у нас полірепо і деплойменти знаходяться в різних репозиторіях. Постає питання політики доступів сервіс акаунтів та токенів з GH Actions
У нас створення схем, /vhost та накатування фікстур відбувається саме в QA репозиторії де пишуться е2е тести та готуються preconditions для сценаріїв
#26 Дебажити чому не спрацював тест в такому випадку досить важко, тому без Tracing (OpenTelemetry) - ніяк
Дякуючи, тому що ми вже слідкуємо за пропагацією метаданих, ми вже заклали гарний фундамент та дисципліну, яка заодно нам дає надійні трейси
#27 Кудос Олексію Тупіченкову та Артуру Ніколаєнко за написання бандлів та опублікування для коммюніті
Запрошую користовуватись
Contributions are welcomed
#28 Trunk based development enabler
Швидкі та легкі preview-environments, швидкі CI/CD
Scalable parallel development
Завдяки легкії Data Isolation і preview-envs, можна створювати десятки і сотні PRs які будуть паралельно ранитись швидко
E2E testing & dev environment debugging
Тестувальники мають ізольоване та передбачуване середовище для тест кейсів
DORA metrics ++
Deployment Frequency >>
Deployment Time <<
SDLC Discipline and Tooling
Все когнітивне навантаження нівелюється тулінгом та SDLC політиками