JavaRush /Курси /Docker for Spring /Чиста схема конфігурації

Чиста схема конфігурації

Docker for Spring
Рівень 12 , Лекція 4
Відкрита

1. Чиста схема конфігурації: зміст

Якщо чесно, хаос у конфігурації зазвичай зʼявляється не тому, що люди «погані», а тому, що проєкт росте. Спочатку в нас один application.yml, потім зʼявляється PostgreSQL, потім Redis, потім RabbitMQ — і раптом кожен додає свої параметри, «аби швидше запрацювало». За кілька днів це перетворюється на музей: тут лежить application-final.yml, поруч application-docker.yml, а в README — чотири взаємовиключні команди запуску. У цей момент проєкт уже можна контейнеризувати, але підтримувати його стає незручно.

Чиста схема конфігурації — це коли ви можете відповісти на запитання «що буде, якщо я ввімкну профіль cache?» і «чому застосунок шукає базу за цією адресою?» без детективного розслідування. Ми хочемо досягти простого результату: кожен файл має одну відповідальність, а набір профілів складається в режим, як конструктор Lego, а не як «ящик із дротами, які краще не чіпати».

Важливо, що «чистота» тут не про красу заради краси. У світі Docker будь-який зайвий хаос у конфігурації автоматично перетворюється на дві неприємності. Перша — ви починаєте перезбирати image під різні випадки, порушуючи принцип same image, different runtime config. Друга — ви перестаєте розуміти, що саме було запущено в конкретному контейнері: чому він записує файли сюди, чому слухає порт там, чому підключається до Redis… і починається те саме: «а давайте ще одну змінну середовища додамо, раптом допоможе».

У цій лекції ми зафіксуємо канонічний набір файлів і профілів так, щоб він був лекційно зрозумілим, але при цьому дуже схожим на те, як це роблять у реальних командах: без сотні профілів, без копій одного й того самого YAML і без «профілю на кожен чих».

2. Матриця режимів: профілі та шари

Щоб конфігурація не розповзалася, спершу потрібно чесно домовитися: які осі відмінностей є в застосунку. У нашому навчальному сервісі це не «всі можливі опції Spring Boot», а лише кілька інфраструктурних контурів, бо проєкт навмисно простий. І це добра новина: що менше осей — то менше шансів отримати application-please-help.yml.

Уявіть таку картину: є базовий режим, у якому сервіс може жити сам (це standalone), і є режим, де зʼявляється зовнішня база (postgres). А вже поверх postgres ми можемо «докрутити» дві додаткові залежності — Redis (cache) і RabbitMQ (messaging). Тобто cache і messaging — це не самостійні «світи», а додаткові шари.

Для наочності зручно один раз побачити це у вигляді таблиці. Це не список команд і не «як запускати», а просто карта відмінностей:

Профіль(і) Сховище даних каталогу PostgreSQL Redis RabbitMQ Ідея режиму
standalone у памʼяті ні ні ні швидкий старт без інфраструктури
postgres JPA + БД так ні ні нормальна модель збереження
postgres,cache JPA + БД так так ні додали кеш для читання
postgres,cache,messaging JPA + БД так так так додали мінімальне налаштування messaging

Ключовий методичний момент: ми не намагаємося підтримувати всі комбінації. Скажімо, standalone,cache ми не вважаємо нормальним режимом. Теоретично можна, але це створює запитання «а cache чого, якщо немає бази?», «а як seed-дані?», «а що кешуємо?». Для курсу з Docker така комбінація приносить більше плутанини, ніж користі.

І ще один важливий момент: режим — це композиція профілів, а не «імʼя одного профілю, який усе вирішує». Тому ми спеціально обираємо короткі назви профілів за змістом, а не docker, prod2, mode_test. Профіль має відповідати на запитання «що змінюється». Якщо профіль називається postgres, ви не будете гадати, що він робить.

3. Правило “один ключ — один дім”

Зараз буде правило, яке виглядає нудно, але рятує від конфігураційного апокаліпсиса: кожна властивість має мати одне основне місце. Її можна перевизначити ззовні — це нормально, — але не слід одночасно записувати її як істину в трьох profile-файлах. Інакше ви отримаєте ефект last-wins там, де вам узагалі не хотілося гри у вгадування.

Уявіть, що конфігурація — це шафа. application.yml — це одяг на кожен день: джинси, футболка, куртка. Профілі — це «спецодяг»: каска для будівництва (postgres), рукавички для роботи з металом (cache), рація для переговорів (messaging). Проблема починається, коли ви починаєте зберігати джинси одночасно в касці й у рукавичках. Це вже не шафа, а квест.

Що ми кладемо до базового application.yml? Усе, що однакове для всіх режимів і відображає «особистість» сервісу: порт або його значення за замовчуванням, базові налаштування Actuator, наш app.export-dir — тобто типовий шлях, — а також спільні логічні прапорці. А те, що залежить від конкретної інфраструктури, тобто URL бази, хост Redis і хост RabbitMQ, має жити в профільних файлах, бо за змістом це не «завжди», а «лише коли ввімкнено цей контур».

Окремо зафіксуємо важливий нюанс: профільні файли не мають бути копією application.yml. Їхнє завдання — бути короткими та точковими. Чим коротший application-postgres.yml, тим швидше ви розумієте, що саме вмикає режим postgres.

Щоб це не лишалося абстракцією, давайте один раз визначимо розподіл ключів по файлах. У вигляді таблиці це читається швидше, ніж довге моралізаторство:

Група налаштувань Де «дім» Приклад ключів
Загальні налаштування сервісу application.yml server.port, management.*, app.export-dir
Standalone-сховище application-standalone.yml app.storage-mode: memory (або аналогічна ознака)
PostgreSQL datasource application-postgres.yml spring.datasource.*, spring.jpa.*, spring.flyway.* (за потреби)
Підключення Redis application-cache.yml spring.data.redis.*, плюс ваш вузький прапорець на кшталт app.cache-enabled
Підключення RabbitMQ application-messaging.yml spring.rabbitmq.*, плюс app.messaging-enabled

Якщо ви помітили, що одну й ту саму властивість хочеться вписати в два профільні файли, це зазвичай сигнал: або властивість насправді спільна, і тоді її місце в application.yml, або ви намагаєтеся профілем переписати чужу відповідальність. Скажімо, файл cache намагається керувати datasource.

4. Структура ресурсів: п'ять файлів

До цього ми дивилися на властивості частинами. Нижче збираємо їх в один робочий набір файлів; саме його варто тримати в голові як базову схему проєкту, а не окремі фрагменти властивостей.

Ми багато говорили «тримайте файли короткими», але в реальному проєкті це починається з банальної дисципліни: рівно пʼять зрозумілих файлів у src/main/resources, без авторської поезії в назвах. Коли студент або колега відкриває resources, він має одразу побачити карту режимів, а не загадку з трьох літер.

У нашому наскрізному проєкті це виглядає так:

src/main/resources/
├── application.yml
├── application-standalone.yml
├── application-postgres.yml
├── application-cache.yml
└── application-messaging.yml

Чому це важливо саме для Docker-курсу? Бо Docker любить відтворюваність, а відтворюваність любить передбачуваність. Якщо конфіг розкладено чисто, ви можете запускати той самий image з SPRING_PROFILES_ACTIVE=postgres,cache і бути впевненими: Redis увімкнеться, а datasource залишиться привʼязаним до PostgreSQL, і нічого випадково не перетреться.

Ще одна маленька, але корисна дисципліна: в межах курсу ми не змішуємо YAML і .properties в одній локації. Технічно Spring Boot уміє і так, і так, але новачкові це створює зайвий сюрприз: в одній папці два файли з подібним змістом, і раптом пріоритет не той, який ви очікували. Нам потрібно, щоб last-wins було лише там, де ми свідомо це допускаємо, тобто в порядку профілів, а не тому, що хтось випадково додав application.properties.

І останнє: ми не кладемо spring.profiles.active всередину application-postgres.yml та подібних файлів. Профіль — це те, що приходить ззовні під час запуску. Файл профілю має описувати відмінності, а не намагатися ввімкнути сам себе. Це звучить бадьоро, але в підсумку ви або заплутаєтеся, або отримаєте неявну поведінку.

5. Приклад конфігів для Catalog Service

Тепер — повні варіанти packaged baseline, а не окремі фрагменти властивостей. Ми зробимо їх такими, щоб вони дружили із зовнішніми перевизначеннями через env vars, які ви можете передавати в контейнер.

application.yml — лише загальне

application.yml має бути нудним, і це добре. Якщо він виглядає як «енциклопедія всіх налаштувань Spring», ви програли.

# src/main/resources/application.yml

spring:
  profiles:
    default: standalone # Профіль за замовчуванням, коли під час запуску не вказано активних профілів

server:
  port: ${SERVER_PORT:8080} # Порт можна перевизначити через змінну середовища

app:
  export-dir: ${APP_EXPORT_DIR:/app/exports} # Базовий шлях для експорту; його теж можна перевизначити ззовні

Тут видно три речі: є зрозумілий профіль за замовчуванням, порт можна перевизначити ззовні, каталог експорту теж можна перевизначити. Ми не говоримо, де фізично розташовано /app/exports — це окрема файлова механіка, а сьогодні ми тримаємо фокус на схемі конфігурації.

Якщо хочеться додати Actuator exposure, можна, але тримайте це коротко й лише тоді, коли це справді спільне для всіх режимів. Це все ще той самий application.yml:

# src/main/resources/application.yml

management:
  endpoints:
    web:
      exposure:
        # Список endpointʼів можна перевизначити ззовні — це зручно для різних середовищ
        include: ${MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE:health,info,metrics}

application-standalone.yml — відмінності in-memory режиму

Standalone має вмикатися швидко й не вимагати зовнішніх сервісів. Тому в профільному файлі ми описуємо лише те, що відрізняє цей режим.

# src/main/resources/application-standalone.yml

app:
  storage-mode: memory # У standalone використовуємо in-memory сховище

Так, файл вийшов «смішно маленький». І це ідеальний комплімент. Якщо режим відрізняється одним ключем, значить, ви добре спроєктували схему.

application-postgres.yml — datasource і все, що пов'язано з БД

У postgres-режимі зʼявляється база, а отже — datasource. Тут логічно тримати datasource «домом» саме в цьому файлі.

# src/main/resources/application-postgres.yml

app:
  storage-mode: postgres # Перемикаємо режим зберігання на БД

spring:
  datasource:
    # Усі ключові параметри можна перевизначати через env vars — це важливо для контейнерів і Compose
    url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/catalog}
    username: ${SPRING_DATASOURCE_USERNAME:catalog}
    password: ${SPRING_DATASOURCE_PASSWORD:catalog}

Зверніть увагу на важливий психологічний ефект: навіть коли ви поки запускаєте не в Compose, а просто в контейнері «у вакуумі», дім datasource лишається тут. Не треба переносити spring.datasource.* у application.yml «тому що так швидше». Це саме те «швидше», яке потім перетворюється на «а чому standalone намагається підключитися до бази?».

application-cache.yml — Redis як додатковий шар

Профіль cache має бути саме «додатком», а не «другою версією postgres». Ми не дублюємо datasource й не змінюємо порт сервера. Ми просто додаємо налаштування Redis і, якщо хочете, один маленький прапорець для власних бінів.

# src/main/resources/application-cache.yml

app:
  cache-enabled: true # Увімкнюємо контур кешу; це зручно для умовної реєстрації бінів

spring:
  data:
    redis:
      # Параметри Redis очікуємо ззовні: у контейнерах host майже ніколи не localhost
      host: ${SPRING_DATA_REDIS_HOST:localhost}
      port: ${SPRING_DATA_REDIS_PORT:6379}

Навіть коли ви поки не реалізували складний cache-сценарій, цей файл усе одно корисний: він наперед фіксує, що Redis — це окремий контур, який вмикається окремим профілем.

application-messaging.yml — RabbitMQ як додатковий шар

Те саме для messaging: додаток, а не «новий світ».

# src/main/resources/application-messaging.yml

app:
  messaging-enabled: true # Увімкнюємо messaging-контур; це умовна реєстрація бінів і слухачів

spring:
  rabbitmq:
    # Параметри RabbitMQ зручно передавати через env vars або Compose
    host: ${SPRING_RABBITMQ_HOST:localhost}
    port: ${SPRING_RABBITMQ_PORT:5672}

Зверніть увагу: усі параметри «заточені» під зовнішні перевизначення. Це і є та схема, яка робить контейнеризацію чесною: всередині jar є розумні defaults, а ззовні ви можете їх перевизначити, не переписуючи артефакт.

6. Корисні нюанси

Профілі в режимі: default, active і last-wins

У цій схемі spring.profiles.default: standalone живе в application.yml і дає запасний старт без зовнішньої інфраструктури. Коли запуск нічого явно не сказав про профілі, сервіс підніметься в in-memory режимі й не почне раптом шукати PostgreSQL, Redis чи RabbitMQ.

Конкретний режим усе одно задається ззовні через spring.profiles.active: postgres, postgres,cache або postgres,cache,messaging. Тобто packaged baseline залишається тим самим, а потрібна комбінація контурів обирається на старті контейнера або java -jar.

Якщо активні кілька профілів, файли накладаються в їхньому порядку, і пізніше вказаний профіль може перекрити ранніший. Саме тому ми й розклали ключі за ролями: postgres відповідає за datasource, cache — за Redis, messaging — за RabbitMQ. Тоді last-wins залишається рідкісним усвідомленим випадком, а не повсякденною рулеткою.

Зовнішні перевизначення та packaged baseline

З цією пʼятифайловою схемою сервіс має нормально стартувати й без зовнішніх файлів: packaged baseline всередині jar самодостатній. Зовнішній конфіг тут не «друга правда», а тонкий override-шар, який потрібен тільки тоді, коли конкретній машині або конкретному запуску потрібне інше значення.

Якщо ззовні лежать звичайні application.yml / application-{profile}.yml у стандартному місці, Spring Boot може підхопити їх сам. Якщо ви обираєте довільну назву на кшталт runtime.yml або нестандартний шлях, такий файл підключають явно через spring.config.import, spring.config.additional-location або spring.config.location. Важлива не механіка заради механіки, а правило: змінюється runtime-шар, а не jar і не image.

Перевірка активного режиму без endpointʼів

Після складання схеми корисно перевіряти не «нібито працює», а два конкретні сигнали на старті: які профілі активні й яке підсумкове значення отримали один-два характерні ключі на кшталт app.export-dir. Така швидка перевірка показує, чи зійшлися packaged defaults, зовнішнє перевизначення й env vars саме в тому порядку, на який ви розраховували.

Це можна вивести у стартовий лог будь-яким простим способом: через ApplicationRunner, звичайний логер, хоч тимчасовий System.out.println у навчальному проєкті. Сенс не в красі реалізації, а в тому, щоб не вгадувати, стартували ви в standalone чи в postgres,cache, і чи не тягнете ви випадково старе значення app.export-dir.

7. Типові помилки в фінальній схемі конфігурації

У цій темі помилки майже завжди виглядають однаково: застосунок «нібито має працювати так», але запускається «нібито інакше». І майже завжди причина не в Docker і не в Spring Boot як у «чорній магії», а в тому, що конфігурація втратила структуру. Нижче — найчастіші граблі, які варто впізнавати в обличчя.

Помилка №1: application-{profile}.yml перетворюється на копію application.yml.
Це виглядає як «надійніше»: ніби кожен профіль самодостатній. На практиці ви отримуєте пʼять майже однакових файлів, у яких відмінності важко помітити, а зміни потрібно робити одразу в пʼяти місцях. У підсумку хтось забуває змінити одне місце, і починається «чому лише в postgres-режимі порт інший?».

Помилка №2: один і той самий ключ живе в кількох файлах «просто тому, що так сталося».
Скажімо, spring.datasource.url опинився і в application.yml, і в application-postgres.yml. Це небезпечно тим, що ви перестаєте розуміти «дім» властивості. У якийсь момент ви змінюєте одне значення, а застосунок продовжує брати інше, і ви починаєте підозрювати містицизм. Насправді це просто конфлікт джерел.

Помилка №3: профіль cache або messaging починає дублювати налаштування postgres.
Додаткові профілі мають додавати відмінності, а не бути «другою версією postgres-режиму». Якщо application-cache.yml раптом починає містити datasource, ви втрачаєте ідею композиції: вмикання cache має зачіпати лише контур кешу, а не переписувати базовий режим роботи з даними.

Помилка №4: спроба «ввімкнути профіль ізсередини профілю».
Іноді хочеться написати spring.profiles.active: postgres всередині application-postgres.yml. Це виглядає логічно, але на практиці ламає передбачуваність: профіль має приходити ззовні під час запуску, інакше ви отримуєте дивні каскади та не можете очима зрозуміти, чому активувалося саме те, що активувалося.

Помилка №5: зовнішній конфіг стає обовʼязковим і без нього сервіс не стартує.
Коли зовнішній файл не позначений як optional: (або ви використовуєте spring.config.location так, що стандартні місця пошуку вимкнені), ви легко отримуєте ситуацію «на моїй машині працює, у тебе — ні». Для курсу й для шаблону важливо, щоб базовий packaged path лишався робочим: завантажили проєкт, запустили — працює.

1
Опитування
Spring конфіг, рівень 12, лекція 4
Недоступний
Spring конфіг
Профілі та налаштування конфігурації
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ