Рассматривая паттерн EnvVar Configuration (Конфигурация в переменных окружения), мы познакомимся с самым простым способом настройки приложений. Самый простой способ определить конфигурацию, когда количество настраиваемых параметров невелико, — поместить их в переменные окружения. Мы уже видели разные способы определения переменных окружения в Kubernetes, а также познакомились с некоторыми ограничениями, препятствующими их использованию для сложных конфигураций.
Всякое нетривиальное приложение требует определения параметров для доступа к источникам данных и внешним службам или для тонкой настройки поведения в промышленном окружении. Еще до появления манифеста «Двенадцать факторов» мы знали, что хранить конфигурацию внутри приложения неправильно. Конфигурацию следует вынести за рамки приложения, чтобы ее можно было изменить даже после его сборки. Это еще больше увеличивает ценность контейнерных приложений, которые позволяют использовать неизменяемые артефакты и способствуют этому. Но как это сделать в контейнерном мире?
Для хранения конфигураций приложений манифест «Двенадцать факторов» рекомендует использовать переменные окружения. Этот подход прост и может использоваться в любых окружениях и на любых платформах. Любая операционная система позволяет определять переменные окружения и передавать их в приложениях, и каждый язык программирования поддерживает простые средства доступа к этим переменным. Можно смело утверждать, что переменные окружения — это универсальный механизм. Обычно при использовании переменных окружения во время сборки определяются их значения по умолчанию, а во время выполнения они изменяются. Давайте рассмотрим некоторые конкретные примеры, как это реализовать в Docker и Kubernetes.
В образах Docker переменные окружения можно определить прямо в файлах Dockerfile, с помощью директивы ENV, по одной переменной в строке, как показано в листинге 18.1.
Листинг 18.1. Файл Dockerfile с определениями переменных окружения
FROM openjdk:11
ENV PATTERN "EnvVar Configuration"
ENV LOG_FILE "/tmp/random.log"
ENV SEED "1349093094"
# Альтернативный вариант:
ENV PATTERN="EnvVar Configuration" LOG_FILE=/tmp/random.log SEED=1349093094
...
Приложение на Java, действующее в таком контейнере, может легко получить доступ к переменным с помощью стандартной библиотеки Java, как показано в листинге 18.2.
Листинг 18.2. Чтение переменных окружения в Java
public Random initRandom() {
long seed = Long.parseLong(System.getenv("SEED"));
return new Random(seed);
}
Инициализация генератора случайных чисел начальным значением из переменной окружения SEED.
о значениях по умолчанию
Значения по умолчанию упрощают жизнь, потому что избавляют от бремени выбора значения для параметра конфигурации, о существовании которого вы можете даже не подозревать. Они также играют важную роль в парадигме преобладания соглашений перед настройками. Однако не всегда желательно определять значения по умолчанию. А иногда такой подход может даже быть антипаттерном.
Причина в том, что ретроспективное изменение значений по умолчанию — сложная задача. Во-первых, чтобы изменить значение по умолчанию, нужно изменить код, а в этом случае придется выполнить повторную сборку приложения. Во-вторых, люди, полагающиеся на значения по умолчанию (осознанно или нет), могут быть неприятно удивлены, если эти значения изменятся. Мы должны явно сообщать о таких изменениях, чтобы пользователи нашего приложения смогли изменить вызывающий код.
С другой стороны, значения по умолчанию часто приходится менять, потому что трудно выбрать достаточно хорошие значения с самого начала. Поэтому изменение значений по умолчанию следует рассматривать как существенную модификацию, и если используется семантическое управление версиями, такие изменения должны вызывать увеличение основного номера версии. Если не удается найти удовлетворительное значение по умолчанию, часто лучше вообще удалить его и генерировать ошибку, если пользователь не определил свое значение параметра. Это по крайней мере явно нарушит работу приложения и не приведет к неожиданным и труднодиагностируемым проблемам.
Учитывая все эти проблемы, часто лучше с самого начала отказаться от значений по умолчанию, если вы не уверены хотя бы на 90%, что выбрали разумное значение по умолчанию. Пароли или параметры подключения к базе данных — вот яркие примеры настроек, для которых не должно быть значений по умолчанию, потому что они сильно зависят от окружения и часто не имеют разумных значений. Кроме того, в отсутствие значений по умолчанию мы должны явно представить информацию о настройке, что также послужит дополнительной документацией.
Если запустить этот образ как есть, он будет использовать жестко заданные значения по умолчанию. Но часто бывает желательно переопределить их извне образа.
Сделать это можно, добавив новые значения для переменных окружения в команду, запускающую контейнер Docker, как показано в листинге 18.3.
Листинг 18.3. Переопределение переменных окружения при запуске контейнера Docker
docker run -e PATTERN="EnvVarConfiguration" \
-e LOG_FILE="/tmp/random.log" \
-e SEED="147110834325" \
k8spatterns/random-generator:1.0
В Kubernetes переменные окружения можно переопределить непосредственно в спецификации пода, в контроллере Deployment или ReplicaSet (как показано в листинге 18.4).
Листинг 18.4. Переопределение переменных окружения в контроллере Deployment
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
env:
- name: LOG_FILE
value: /tmp/random.log
- name: PATTERN
valueFrom:
configMapKeyRef:
name: random-generator-config
key: pattern
- name: SEED
valueFrom:
secretKeyRef:
name: random-generator-secret
key: seed
Фактическое значение для переменной окружения.
Значение извлекается из карты конфигураций ConfigMap.
Имя карты конфигураций ConfigMap.
Ключ внутри карты конфигураций ConfigMap для поиска значения для переменной окружения.
Значение извлекается из Secret (поиск выполняется так же, как при использовании карты конфигураций ConfigMap).
При таком подходе можно определять не только фактические значения переменных окружения (как для LOG_FILE), но и ссылаться на ресурсы Secret (предназначенные для хранения конфиденциальных данных) и ConfigMap (если настройки можно хранить в открытом виде). Преимущество использования ресурсов Secret и ConfigMap заключается в том, что они позволяют управлять переменными окружения независимо от определений подов. Подробнее о Secret и ConfigMap, а также об их плюсах и минусах рассказывается в главе 19 «Конфигурация в ресурсах».
В предыдущем примере значение для переменной SEED извлекается из ресурса Secret. В общем и целом это верное решение, но важно отметить, что переменные окружения не являются безопасными хранилищами. Конфиденциальная информация, помещенная в переменные окружения, становится доступной для чтения и может даже просочиться в журналы.
Переменные окружения просты в использовании и хорошо знакомы. Идея их применения хорошо отображается на контейнеры и поддерживается всеми платформами времени выполнения. Но переменные окружения небезопасны и хороши только для небольшого числа параметров. Но когда параметров слишком много, управление переменными окружения превращается в утомительную задачу.
В таких случаях многие используют дополнительный уровень косвенности и помещают свои конфигурации в различные файлы, по одному для каждого окружения, а затем используют единственную переменную для выбора одного из этих файлов. Этот подход, например, используется в профилях Spring Boot. Поскольку файлы профилей обычно хранятся в самом приложении внутри контейнера, они оказываются тесно связанными с приложением. Это часто приводит к тому, что конфигурации для разработки и эксплуатации включаются в один образ Docker с приложением, что требует перестройки образа при любом изменении в любом окружении. Все это лишь доказывает, что переменные окружения подходят только для небольших наборов конфигураций.
Паттерны Configuration Resource (Конфигурация в ресурсах), Immutable Configuration (Неизменяемая конфигурация) и Configuration Template (Макет конфигурации), описанные в следующих главах, являются более удачными альтернативами, когда возникает потребность в более сложных конфигурациях.
Переменные окружения — это универсальный механизм, и их можно определять на разных уровнях. Это может приводить к фрагментированию настроек и затруднять выявление источника значения для заданной переменной. Из-за отсутствия централизованного места, где определяются все переменные окружения, часто бывает трудно отладить проблемы, связанные с ошибками в конфигурации.
Еще один недостаток переменных окружения состоит в том, что определить их можно только перед запуском приложения и нельзя изменить позже. С одной стороны, невозможность оперативного изменения конфигурации во время выполнения приложения можно считать недостатком. Однако многие видят в этом преимущество, потому что это способствует неизменности конфигурации. В данном случае неизменность подразумевает необходимость остановки действующего контейнера и запуска новой копии с измененной конфигурацией, например, с помощью стратегии развертывания, такой как непрерывное развертывание обновлений. При таком подходе вы всегда будете иметь приложение с четко определенным состоянием конфигурации.
Переменные окружения просты в использовании, но в основном могут применяться только для хранения простых конфигураций и имеют ограничения при наличии более сложных требований. Паттерны, описываемые далее, показывают, как преодолеть эти ограничения.
• Пример паттерна Конфигурация в переменных окружения (http://bit.ly/2YcUtJC).
• Методология «Двенадцать факторов» (https://12factor.net/ru/).
• Статья Кифа Морриса (Kief Morris) «Immutable Server» (https://martinfowler.com/bliki/ImmutableServer.html).
• Использование профилей Spring Boot для хранения конфигураций (http://bit.ly/2YcSKUE).