1. Профили и стратегия окружений
Когда начинаешь работать с профилями, очень легко попасть в ловушку: кажется, что раз есть механизм local/dev/prod, то всё автоматически станет аккуратно и красиво. На практике профили — это как кухонные ножи: вещь полезная, но если разложить их по всей квартире, жить становится тревожно. Поэтому нам нужна environment strategy — договорённость, какие профили существуют, зачем они существуют и какие различия им разрешены.
Самая частая причина хаоса — отсутствие правил. Один разработчик делает профиль oleg-laptop, другой — dev2, третий — prod-new, а четвёртый просто пишет application.properties, потому что «так привычнее». В итоге приложение стартует, но никто не может уверенно сказать, почему оно стартует именно так. Стратегия окружений — это способ заранее ответить на вопрос: “какие различия между запусками считаются нормальными, а какие — признак беспорядка?”
Если связать это с нашим catalog-service, то мысль простая: сервис должен запускаться на вашей машине, в общей дев‑среде команды и в «условном проде» (пусть даже пока это просто запуск jar), причём без правок Java-кода. Различия должны быть понятными, минимальными и предсказуемыми — чтобы любой человек мог открыть конфиги и сказать: “ага, вот это local, вот это dev, а вот это prod”.
2. База: local, dev, prod
Когда говорят “окружения”, мозг сразу рисует страшную матрицу: dev, dev2, dev-eu, staging, preprod, prod, prod-hotfix, и ещё сверху “профиль для понедельника”. Для учебного проекта (и для многих реальных сервисов на старте) такая матрица не нужна. Нам нужен минимальный, но устойчивый набор, который покрывает типичные сценарии и не создаёт лишней когнитивной нагрузки. И обычно это три профиля: local, dev, prod.
Важно, что это не просто “три файла”, а три роли.
local — это запуск на вашей машине. Здесь допустимы удобства: другой порт (чтобы не конфликтовать с чем-то ещё), более «болтливые» настройки (в разумных пределах), возможно, чуть более удобное поведение для ручной проверки. Это профиль “чтобы мне было удобно”.
dev — это общая development‑конфигурация, максимально похожая на командный baseline. Это не “мой ноутбук”, а “наша общая среда”. Если local можно менять под себя, то dev должен быть стабильным и воспроизводимым для всех.
prod — это строгий, аккуратный режим, где нет локальных послаблений. Даже если у нас нет настоящего продакшена, профиль prod полезен как дисциплина: он заставляет держать конфигурацию “в форме” и не таскать в неё случайные удобства разработчика.
Чтобы зафиксировать смысл без долгих философских лекций, вот компактная таблица. Она не заменяет мышление, но помогает быстро проверить: “мы точно туда складываем настройки?”
| Профиль | Кто запускает | Главная цель | Что обычно отличается | Что не должно туда попадать |
|---|---|---|---|---|
| local | конкретный разработчик | удобный запуск на своей машине | порт, удобные флаги, локальные значения | командные/продовые секреты, «общие настройки проекта» |
| dev | команда / общий стенд | общий baseline для разработки | настройки, помогающие 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 удобно временно видеть и неопубликованные — чтобы тестировать данные и фильтрацию. Это не “фича-флаг на пять минут”, а довольно стабильная разница: в проде мы осторожны, локально — исследуем и проверяем.
А вот что лучше оставить общим в базовом 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 — это convenience alias, а не обязательная часть проектного baseline. Если вам хватает явного local или dev, группа вообще не нужна. Она полезна только там, где одна и та же комбинация профилей повторяется снова и снова.
В Spring Boot для этого используется spring.profiles.group.*. С точки зрения mental model это просто “алиас” (псевдоним). Никакой отдельной вселенной: правила last wins никуда не исчезают, порядок профилей всё ещё важен, а конфликтующие настройки всё ещё будут конфликтовать — просто вы теперь включаете набор профилей одним словом.
И вот здесь очень важно не превратить группы в способ спрятать проблемы. Если вы делаете группу chaos = [dev, local, prod], то это не стратегия окружений, а самоуничтожение с задержкой. Группа хороша, когда она описывает устойчивое и понятное сочетание. Например: “в нашей локальной разработке мы хотим взять общий dev‑baseline, а поверх него наложить личные локальные отличия”. В таком сценарии группа действительно экономит силы и снижает количество ошибок при запуске.
В YAML это выглядит аккуратно и читаемо:
spring:
profiles:
group:
# Алиас: одним именем включаем набор профилей
full-dev:
# Порядок важен: более общий профиль раньше
- dev
# Более специфичный профиль последним, чтобы он мог перекрывать значения
- local
Обратите внимание на порядок: мы перечислили dev, а потом local. Это сделано не ради красоты, а ради смысла: если один и тот же ключ есть и там, и там, то local должен иметь право перекрыть dev, потому что local более специфичный. Иначе вы получите “локальный профиль, который локально ничего не решает”, а это слегка обидно.
Чтобы визуально зафиксировать, как думать о группе, вот простая схема:
flowchart TD
A["spring.profiles.active=full-dev"] --> B["full-dev (group)"]
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 здесь — именно optional convenience alias: базовая схема local/dev/prod и без него уже рабочая. Группа нужна только затем, чтобы одной командой включать общий dev‑baseline и локальные отличия поверх него. Когда роли профилей уже договорены, дальше остаётся только аккуратно собрать YAML без дублей.
Базовый application.yaml держим коротким и общим:
spring:
application:
# Имя приложения обычно одинаковое во всех окружениях
name: "catalog-service"
app:
catalog:
# Базовый title — общее значение по умолчанию
title: "Course Catalog"
# Базовая политика: в проде это значение должно быть безопасным
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() сделать простейший snapshot. Пример ниже — именно фрагмент, который можно вставить внутрь run() (остальной класс у вас уже есть):
import java.util.Arrays;
import org.springframework.core.env.Environment;
// environment — это Environment, который вы получаете через DI (например, в конструкторе/поле)
System.out.println("profiles=" + 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.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ