1. Профілі та стратегія оточень
Коли ви починаєте працювати з профілями, дуже легко потрапити в пастку: здається, що раз є механізм local/dev/prod, то все автоматично стане охайним і красивим. На практиці профілі — це як кухонні ножі: річ корисна, але якщо розкласти їх по всій квартирі, жити стає тривожно. Тому нам потрібна environment strategy — домовленість про те, які профілі існують, навіщо вони потрібні й які відмінності їм дозволено мати.
Найчастіша причина хаосу — відсутність правил. Один розробник створює профіль oleg-laptop, інший — dev2, третій — prod-new, а четвертий просто пише application.properties, бо «так звичніше». У підсумку застосунок стартує, але ніхто не може впевнено сказати, чому він стартує саме так. Стратегія оточень — це спосіб заздалегідь відповісти на запитання: “які відмінності між запусками вважаються нормальними, а які — ознакою безладу?”
Якщо пов’язати це з нашим catalog-service, то думка проста: сервіс має запускатися на вашій машині, у спільному dev-оточенні команди та в «умовному проді» (навіть якщо поки що це просто запуск jar), причому без правок Java-коду. Відмінності мають бути зрозумілими, мінімальними й передбачуваними — щоб будь-яка людина могла відкрити конфіги й сказати: “ага, ось це local, ось це dev, а ось це prod”.
2. База: local, dev, prod
Коли говорять про “оточення”, мозок одразу малює страшну матрицю: dev, dev2, dev-eu, staging, preprod, prod, prod-hotfix, і ще зверху “профіль для понеділка”. Для навчального проєкту (і для багатьох реальних сервісів на старті) така матриця не потрібна. Нам потрібен мінімальний, але стійкий набір, який покриває типові сценарії й не створює зайвого когнітивного навантаження. І зазвичай це три профілі: local, dev, prod.
Важливо, що це не просто “три файли”, а три ролі.
local — це запуск на вашій машині. Тут допустимі зручності: інший порт (щоб не конфліктувати з чимось ще), більш «балакучі» налаштування (у розумних межах), можливо, трохи зручніша поведінка для ручної перевірки. Це профіль “щоб мені було зручно”.
dev — це спільна development-конфігурація, максимально схожа на командну базову. Це не “мій ноутбук”, а “наше спільне середовище”. Якщо local можна змінювати під себе, то dev має бути стабільним і відтворюваним для всіх.
prod — це строгий, охайний режим, де немає локальних послаблень. Навіть якщо в нас немає справжнього продакшена, профіль prod корисний як дисципліна: він змушує тримати конфігурацію “в формі” і не тягнути в неї випадкові зручності розробника.
Щоб зафіксувати зміст без довгих філософських лекцій, ось компактна таблиця. Вона не замінює мислення, але допомагає швидко перевірити: “ми точно складаємо туди налаштування?”
| Профіль | Хто запускає | Головна мета | Що зазвичай відрізняється | Що не повинно туди потрапляти |
|---|---|---|---|---|
| local | конкретний розробник | зручний запуск на своїй машині | порт, зручні прапорці, локальні значення | командні/продові секрети, «загальні налаштування проєкту» |
| dev | команда / спільний стенд | спільна базова конфігурація для розробки | налаштування, що допомагають у dev-роботі, але не особисті | “індивідуальний порт Петі”, випадкові експерименти |
| prod | реальний деплой / jar-run | строгість і безпека за замовчуванням | мінімальні та безпечні значення | dev-зручності, «тимчасово вимкнемо фільтр», “відкриємо все назовні” |
Якщо коротко, local — це “зручно”, dev — це “спільно”, prod — це “строго”. І це вже половина успіху, тому що багато проблем із профілями починаються саме там, де люди плутають ці ролі.
3. catalog-service: що розрізняти за оточеннями
Коли у вас уже є local/dev/prod, наступна спокуса — почати “розфарбовувати” профілями взагалі все. На цьому етапі корисно поставити собі одне просте запитання: це справді різниця оточень чи просто параметр, який інколи хочеться змінити? Якщо це параметр, то часто достатньо override через env var або CLI, а профіль не потрібен.
У нашому catalog-service зараз (на стадії до @ConfigurationProperties) є кілька властивостей, які дуже природно сприймаються як відмінності між середовищами. Наприклад, server.port — у local часто хочеться запускатися не на 8080, бо на 8080 уже сидить щось (або минулий “hello world” у вас забув померти). Ще один приклад — app.catalog.title: у local корисно бачити в заголовку “(local)”, щоб за браузером або логами не плутатися, що саме запущено.
Є й більш «поведінкові» налаштування. Наприклад, app.catalog.default-published-only. У prod логічно показувати лише опубліковані курси за замовчуванням. У local і інколи в dev зручно тимчасово бачити й неопубліковані — щоб тестувати дані та фільтрацію. Це не “feature flag на п’ять хвилин”, а доволі стабільна різниця: у проді ми обережні, локально — досліджуємо й перевіряємо.
А ось що краще залишити спільним у базовому application.yaml: spring.application.name, структуру app.catalog.* і значення за замовчуванням, які є нормою проєкту. Спільні значення — це фундамент, на якому стоять усі профілі. Якщо фундамент дублювати в трьох місцях, ви рано чи пізно забудете оновити один із файлів і отримаєте класичний баг “чому в dev одне, а в prod інше?” (спойлер: бо ви самі собі це влаштували).
Невеликий орієнтир, який допомагає не перевантажити профілі: base config — це “як улаштований застосунок”, профілі — це “які точкові відмінності в цьому середовищі”. Профільний файл має виглядати як “дельта”, а не як “копія роману з правкою одного речення”.
4. Profile groups без магії
Профілі зручні, але вручну писати --spring.profiles.active=dev,local щоразу — задоволення приблизно як вводити пароль від Wi‑Fi на телевізорі пультом. Ось тут і з’являються profile groups. Вони дозволяють сказати: “коли активний профіль X, автоматично активуй ще ось ці профілі”. Тобто ви задаєте псевдопрофіль, який вмикає набір реальних.
Одразу важлива межа: group — це зручний псевдонім, а не обов’язкова частина проєктної бази. Якщо вам вистачає явного local або dev, група взагалі не потрібна. Вона корисна лише там, де одна й та сама комбінація профілів повторюється знову й знову.
У Spring Boot для цього використовується spring.profiles.group.*. З погляду mental model це просто “аліас” (псевдонім). Жодного окремого всесвіту: правила last wins нікуди не зникають, порядок профілів усе ще важливий, а конфліктні налаштування все ще конфліктуватимуть — просто ви тепер вмикаєте набір профілів одним словом.
І ось тут дуже важливо не перетворити групи на спосіб приховати проблеми. Якщо ви робите групу chaos = [dev, local, prod], то це не стратегія оточень, а самознищення із затримкою. Група хороша, коли вона описує стійке й зрозуміле поєднання. Наприклад: “у нашій локальній розробці ми хочемо взяти спільну dev-базу, а поверх неї накласти особисті локальні відмінності”. У такому сценарії група справді економить сили й зменшує кількість помилок під час запуску.
У YAML це виглядає охайно й читабельно:
spring:
profiles:
group:
# Псевдонім: одним ім'ям вмикаємо набір профілів
full-dev:
# Порядок важливий: більш загальний профіль має йти раніше
- dev
# Більш специфічний профіль — останнім, щоб він міг перекривати значення
- local
Зверніть увагу на порядок: ми перелічили dev, а потім local. Це зроблено не заради краси, а заради змісту: якщо один і той самий ключ є і там, і там, то local має право перекривати dev, бо local є більш специфічним. Інакше ви отримаєте “локальний профіль, який локально нічого не вирішує”, а це трохи прикро.
Щоб візуально зафіксувати, як думати про групу, ось проста схема:
flowchart TD
A["spring.profiles.active=full-dev"] --> B["full-dev (група)"]
B --> C["dev"]
B --> D["local"]
C --> E["application-dev.yaml"]
D --> F["application-local.yaml"]
G["application.yaml (base)"] --> E
G --> F
Тобто full-dev — це не окремий світ. Це команда: “увімкни dev і local, а далі Boot зробить звичайне завантаження base + profile overrides”.
5. catalog-service: група full-dev
Давайте зберемо невеликий, але життєвий сценарій. full-dev тут — саме необов’язковий зручний псевдонім: базова схема local/dev/prod і без нього вже робоча. Група потрібна лише для того, щоб однією командою вмикати спільну dev-базу й локальні відмінності поверх неї. Коли ролі профілів уже узгоджені, далі залишається лише акуратно зібрати YAML без дублювань.
Базовий application.yaml тримаємо коротким і спільним:
spring:
application:
# Ім'я застосунку зазвичай однакове в усіх оточеннях
name: "catalog-service"
app:
catalog:
# Базовий title — спільне значення за замовчуванням
title: "Course Catalog"
# Базова політика: у prod це значення має бути безпечним
default-published-only: true
Локальні відмінності — порт і більш явний title:
# application-local.yaml
server:
# Локально часто потрібно піти з 8080, щоб не конфліктувати з іншими сервісами
port: 8081
app:
catalog:
# Позначка (local), щоб не плутатися за браузером і логами
title: "Course Catalog (local)"
dev-відмінності — наприклад, теж title, але без особистого порту (бо порт у dev має бути командно передбачуваним):
# application-dev.yaml
app:
catalog:
# Позначка (dev), щоб за логами/метриками було видно, де ви перебуваєте
title: "Course Catalog (dev)"
# У dev зручно бачити й неопубліковані дані під час перевірок
default-published-only: false
prod зазвичай узагалі мінімальний: він не має копіювати базовий файл, а має фіксувати строгі відмінності, якщо вони є. У нашому прикладі можна навіть залишити application-prod.yaml порожнім, але інколи корисно явно продублювати “строге значення”, щоб воно читалося як policy:
# application-prod.yaml
app:
catalog:
# Явно фіксуємо policy для prod (навіть якщо це збігається з base)
default-published-only: true
Якщо база вже тримає safe default, такий продовий шар легко скоротити до порожньої дельти. Тут він потрібен лише для того, щоб було видно саму ідею строгого prod-поведінки.
Тепер додаємо profile group у базовий application.yaml. Зазвичай її тримають саме там, щоб група була частиною “головної конфігураційної карти” застосунку:
spring:
profiles:
group:
# full-dev = dev baseline + локальні відмінності поверх нього
full-dev:
- dev
- local
Як тепер запускати? Так само, як звичайний профіль — просто в spring.profiles.active кладемо ім'я групи:
# Запуск з активованою групою профілів (dev + local)
./gradlew bootRun --args="--spring.profiles.active=full-dev"
Якщо вам такий alias не потрібен, нічого не ламається: звичайні local, dev, prod уже повністю закривають стратегію оточень.
А щоб не гадати, “що реально ввімкнулося”, корисно додати маленький діагностичний вивід на старті. У нас уже є StartupSummaryRunner, тому можна в його run() зробити найпростіший знімок стану. Приклад нижче — саме фрагмент, який можна вставити всередину run() (решта класу у вас уже є):
import java.util.Arrays;
import org.springframework.core.env.Environment;
// environment — це Environment, який ви отримуєте через DI (наприклад, у конструкторі/полі)
System.out.println("профілі=" + Arrays.toString(environment.getActiveProfiles()));
// Ключові властивості краще друкувати з дефолтом, щоб не отримувати null і зайві NPE в діагностиці
System.out.println("title=" + environment.getProperty("app.catalog.title", "n/a"));
Якщо ви запустили з full-dev, вивід буде приблизно таким (може відрізнятися порядком, але зміст той самий):
profiles=[full-dev, dev, local]
title=Course Catalog (local)
І це чудовий момент істини. Ми бачимо, що group-профіль теж активний (він — “ім'я комбінації”), а також активні dev і local. І ми бачимо, що title у підсумку взявся локальний, бо local перекрив dev. Тобто правило last wins продовжує працювати, просто тепер порядок профілів задається не вручну в CLI, а вашою групою.
6. Профільна гігієна
Коли профілі починають приносити користь, з’являється нова спокуса: “а давайте додамо ще один профіль… і ще один… і ось цей теж…”. У якийсь момент у вас уже 14 профілів, які ніхто не розуміє, і одна група, яка включає половину з них, бо “інакше не заводиться”. Щоб цього не сталося, корисно тримати в голові кілька простих принципів — без пафосу, суто щоб не страждати.
Якщо відмінність нестійка й живе кілька днів, профіль майже завжди зайвий. У такі моменти краще зробити override через env var або CLI, а потім прибрати. Профіль — це не “перемикач настрою”, а опис стабільного середовища.
Якщо ви помітили, що application-dev.yaml став таким само великим, як application.yaml, це сигнал тривоги. Отже, ви або дублюєте спільне, або у вас справді немає спільного базового конфига (а це вже архітектурна проблема). Нормальний профільний файл виглядає коротко: він нагадує “патч”, а не “копію репозиторію”.
Якщо ви використовуєте profile groups, ставтеся до них як до аліасів. Вони мають спрощувати запуск і робити його менш помилковим. Група не повинна бути “місцем, де ховається конфлікт налаштувань”. Якщо вам довелося створити групу fix-it, щоб “воно хоч якось запустилося”, краще зупинитися й розібратися, чому конфігурація конфліктує.
І ще один принцип, який особливо важливий у схемі local/dev/prod: за замовчуванням корисно мислити “один environment-профіль за раз”. Тобто prod не повинен вмикатися разом із dev. І якщо ви робите групу, яка включає і dev, і local, ставтеся до цього як до одного логічного “режиму розробки”, а не як до двох середовищ, які випадково запустилися разом.
7. Типові помилки під час роботи з профілями
Помилка №1: копіювання конфігів між local, dev, prod.
Так зазвичай народжуються три однакові файли, які через місяць перестають бути однаковими випадково й без вашої згоди. Хороша стратегія — навпаки: базовий application.yaml зберігає майже все, а профільні файли містять маленьку дельту. Тоді зміни в проєкті робляться в одному місці, і ви не граєте в “знайди відмінність у трьох копіях”.
Помилка №2: особисті налаштування розробника потрапили в dev.
Це трапляється частіше, ніж здається. Хтось поставив server.port: 8099 “бо в мене зайнято”, закомітив — і вся команда раптом намагається зрозуміти, чому в них не відкривається сервіс на 8080. Особисті відмінності — це зона local. Профіль dev має бути командним і максимально передбачуваним.
Помилка №3: profile group використовується як маскування конфліктів.
Групи іноді починають жити дивним життям: “увімкни full-dev, інакше не працює”, а чому не працює — ніхто вже не пам’ятає. Група має бути прозорою: ви повинні легко пояснити, які профілі вона включає і чому саме такий порядок. Якщо пояснення перетворюється на легенду, значить стратегія оточень уже десь дала тріщину.
Помилка №4: неправильний порядок профілів усередині групи.
Це особливо підступно, бо все “ніби працює”, але не так. Якщо в групі ви перелічили local, а потім dev, то dev-значення перекриватимуть local-значення. У підсумку ви змінюєте щось у application-local.yaml, а воно “не застосовується”, і починаються ворожіння. Ліки прості: більш специфічний профіль має йти останнім, щоб він міг перекривати більш загальний.
Помилка №5: очікування замість перевірки.
Профілі легко перетворюються на гру “мені здається, що активний dev”. Але Boot не читає думки. Він читає property sources і збирає підсумкову картину. Тому корисно мати бодай один маленький діагностичний вивід (через Environment.getActiveProfiles() і пару ключових properties), щоб бачити не “як задумано”, а “як реально вийшло”. Це особливо рятує, коли профілі починають приходити ззовні через env vars або CLI.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ