- Isolate Data
- Boost CI/CD speed
- Boost parallel development
- Increase deployment frequency
VS
Solution Overview: The Shift to Logical
Isolation
Strategy: Move complexity from infrastructure to
conventions and tooling.
● Isolation in Application Layer
● Multi tenancy
● Short-Lived Resources
● Baggage
Core Concept: Baggage and Context
Propagation
baggage: preview-env=payment-pr-162, user-tier=premium
HTTP: Headers carry context
Schema Context Bundle
● Schema from header → Extract tenant schema from baggage
● Central resolver → Global BaggageSchemaResolver
● Messaging support → Middleware injects & rehydrates schema/baggage
● HTTP propagation → Decorator adds baggage header
● Logging (optional) → Processor adds baggage to Monolog
Schema Context Bundle
https://github.com/MacPaw/schema-context-bundle
Schema Context Bundle
https://github.com/MacPaw/schema-context-bundle
Schema Context Bundle
https://github.com/MacPaw/schema-context-bundle
Schema Context Bundle
https://github.com/MacPaw/schema-context-bundle
Schema Context Bundle
https://github.com/MacPaw/schema-context-bundle
Service Mesh: Routing based 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/
Droids PR-49
namespace
Droids staging namespace
K8s
service
HTTPRoute
north/south
HTTPRoute
east/west
-H “baggage:
preview-
env=droids-
pr-49”?
Stormtroopers
namespace
HTTPRoute
east/west
(PR)
K8s
service
X-wing starfighter
view External
clients
How we configure it
Isolation Deep Dive: PostgreSQL
Row Level Security vs Separate Schema per env
PostgreSQL: Schemas separate data
https://github.com/MacPaw/postgres-schema-bundle
RabbitMQ: vhosts isolate messages
Redis: Keys are namespaced
Redis (Key-Value Store)
● Strategy: Automatic Redis key namespacing
● Mechanism: Keys are prefixed with the environment identifier (e.g., payment-pr-162.{key}).
Redis Schema Bundle
https://github.com/MacPaw/redis-schema-bundle
Isolation Deep Dive: Third-Party Webhooks
E2E tests and CI/CD
Observability: Tracing
The PHP Tooling Saving Us All
- Schema Context Bundle
- RabbitMQ (Symfony Messenger middleware)
- HTTP Client Decorator
- Custom integrations
- https://github.com/MacPaw/schema-context-bundle (public repository)
- Postgres Schema Bundle
- Dynamic tenant schema switching (PostgreSQL `search_path`)
- Doctrine integration
- https://github.com/MacPaw/postgres-schema-bundle (public repository)
- Redis Schema Bundle
- Automatic Redis key namespacing
- https://github.com/MacPaw/redis-schema-bundle (public repository)
Conclusion and Takeaway
● Trunk based development enabler
● Scalable parallel development
● E2E testing & dev environment debugging
● DORA metrics ++
● SDLC Discipline and Tooling

"Don’t clone infrastructure — isolate data: ephemeral PR environments in a shared environment", Oleksandr Buchek

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 простим і безпечним.
  • #13 Приклад логу
  • #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 політиками