Паттерн Immutable Configuration (Неизменяемая конфигурация) предполагает упаковку конфигурационных данных в неизменяемый образ контейнера и его подключение к приложению во время выполнения. Это не только дает возможность использовать разные и неизменяемые версии конфигурационных данных, но и позволяет преодолеть ограничения на объем этих данных, свойственные переменным окружения и ресурсам ConfigMap.
Как рассказывалось в главе 18 «Конфигурация в переменных окружения», переменные окружения предлагают самый простой способ настройки контейнерных приложений. Но несмотря на простоту и универсальность, управлять переменными окружения становится крайне сложно, как только их число превысит определенный порог.
Эту сложность отчасти можно преодолеть с помощью Configuration Resources (Конфигурация в ресурсах). Однако эти паттерны не обеспечивают неизменности самих конфигурационных данных. Под неизменностью здесь подразумевается невозможность изменить конфигурацию после запуска приложения, которая гарантирует, что конфигурационные данные всегда находятся в четко определенном состоянии. Кроме того, неизменяемую конфигурацию можно хранить в системе управления версиями и следовать за процессом контроля изменений.
Для решения упомянутых проблем все конфигурационные данные для конкретного окружения можно поместить в один пассивный образ и распространять его как обычный образ контейнера. Во время выполнения приложение и образ с данными связываются друг с другом, и приложение получает возможность извлекать конфигурацию из этого образа. Такой подход позволяет легко создавать различные образы с конфигурационными данными для различных окружений. Эти образы объединяют всю информацию о конфигурации для конкретных окружений и могут храниться в системе управления версиями, как и любые другие образы контейнеров.
Создаются такие образы данных легко и просто, так как являются обычными образами контейнеров, которые хранят только данные. Сложность заключается лишь в том, чтобы связать контейнеры во время запуска. Для этого можно использовать различные подходы, в зависимости от платформы.
Прежде чем рассматривать решение для Kubernetes, отступим на шаг назад и рассмотрим случай с обычными контейнерами Docker. В Docker контейнер может экспортировать том с данными. С помощью директивы VOLUME в Dockerfile можно указать каталог, который позже можно сделать общим. Во время запуска содержимое этого каталога в контейнере копируется в общий каталог. Как показано на рис. 20.1, такое связывание томов
Рис. 20.1. Реализация паттерна Immutable configuration (Неизменяемая конфигурация) с использованием тома Docker
является отличным способом передачи конфигурационной информации из контейнера с конфигурацией в контейнер приложения.
Рассмотрим пример. Создадим для окружения разработки образ Docker, который содержит ее конфигурацию и создает том /config. Создать такой образ можно с помощью Dockerfile-config, как показано в листинге 20.1.
Листинг 20.1. Dockerfile, определяющий образ с конфигурацией
FROM scratch
ADD app-dev.properties /config/app.properties
VOLUME /config
Добавить заданное свойство.
Создать том и скопировать свойство в него.
Теперь, с помощью клиента командной строки Docker CLI, создадим сам образ и контейнер Docker, как показано в листинге 20.2.
Листинг 20.2. Сборка образа Docker с конфигурацией
docker build -t k8spatterns/config-dev-image:1.0.1 -f
Dockerfile-config
docker create --name config-dev k8spatterns/config-dev-image:1.0.1 .
Заключительный шаг: запуск контейнера с приложением и связывание его с конфигурационным контейнером (листинг 20.3).
Листинг 20.3. Запуск контейнера с приложением и связывание его с конфигурационным контейнером
docker run --volumes-from config-dev k8spatterns/welcome-servlet:1.0
Образ с приложением ожидает найти файлы конфигурации в каталоге /config, в томе, смонтированном контейнером с конфигурацией. После перемещения приложения из окружения разработки в промышленное окружение вам останется только изменить команду запуска. Менять сам образ приложения не придется, вы просто свяжете контейнер приложения с контейнером, содержащим конфигурацию промышленного окружения, как показано в листинге 20.4.
Листинг 20.4. Использование другой конфигурации в промышленном окружении
docker build -t k8spatterns/config-prod-image:1.0.1 -f
Dockerfile-config
docker create --name config-prod k8spatterns/
config-prod-image:1.0.1 .
docker run --volumes-from config-prod k8spatterns/
welcome-servlet:1.0
Совместное использование томов контейнерами в поде идеально подошло бы для связывания контейнеров приложений с конфигурационными контейнерами в Kubernetes. Но если мы решим перенести эту технику из Docker в мир Kubernetes, то обнаружим, что в настоящее время в Kubernetes нет поддержки контейнерных томов. Учитывая, насколько давно обсуждается необходимость этой поддержки, сложность ее реализации и ограниченность преимуществ, представляется маловероятным, что контейнерные тома появятся в ближайшее время.
Поэтому контейнеры могут совместно использовать внешние тома, но пока не могут напрямую использовать общие каталоги, находящиеся в самих контейнерах. Однако для реализации паттерна Immutable Configuration (Неизменяемая конфигурация) в Kubernetes можно использовать паттерн Init Containers (Init-контейнеры), описанный в главе 14, который может инициализировать пустой общий том во время запуска.
В примере с контейнерами Docker мы создали пустой образ с конфигурацией, не содержащий никаких файлов операционной системы. Нам ничего не нужно иметь в этом контейнере, кроме конфигурационных данных, передаваемых через тома Docker. Однако init-контейнеру в Kubernetes нужна некоторая помощь от базового образа, чтобы скопировать конфигурационные данные в общий том в поде. busybox — хороший выбор на роль такого базового образа, потому что он достаточно мал и позволяет использовать простую Unix-команду cp.
Но как происходит инициализация общих томов с конфигурациями? Рассмотрим пример. Для начала снова создадим образ с конфигурацией с помощью Dockerfile, как показано в листинге 20.5.
Листинг 20.5. Образ с конфигурацией для окружения разработки
FROM busybox
ADD dev.properties /config-src/demo.properties
ENTRYPOINT [ "sh", "-c", "cp /config-src/* $1", "--" ]
Здесь для интерпретации шаблонных символов используется командная оболочка.
Единственное отличие от примера 20.1 со стандартным образом Docker заключается в наличии базового образа и директивы ENTRYPOINT с кодом, который копирует файл свойств в каталог, указанный в аргументе команды запуска образа Docker. Теперь на этот образ можно сослаться в init-контейнере, внутри раздела .template.spec с определением развертывания Deployment (листинг 20.6).
Листинг 20.6. Определение развертывания, копирующего конфигурацию из init-контейнера в указанный каталог
initContainers:
- image: k8spatterns/config-dev:1
name: init
args:
- "/config"
volumeMounts:
- mountPath: "/config"
name: config-directory
containers:
- image: k8spatterns/demo:1
name: demo
ports:
- containerPort: 8080
name: http
protocol: TCP
volumeMounts:
- mountPath: "/config"
name: config-directory
volumes:
- name: config-directory
emptyDir: {}
Определение развертывания Deployment включает один том и два контейнера:
• Том volume config-directory имеет тип emptyDir, то есть на узле, где находится под, создается пустой каталог.
• Init-контейнер, вызываемый фреймворком Kubernetes во время запуска, конструируется из образа, который мы только что создали. Мы также определили единственный аргумент /config, который передается в ENTRYPOINT образа. Этот аргумент сообщает init-контейнеру, куда следует скопировать его содержимое. Каталог /config монтируется из тома config-directory.
• Контейнер приложения монтирует том config-directory, чтобы получить доступ к конфигурации, скопированной из init-контейнера.
Схема на рис. 20.2 иллюстрирует, как контейнер приложения получает доступ к конфигурационным данным, созданным init-контейнером в общем томе.
Теперь, чтобы заменить конфигурацию для окружения разработки конфигурацией для промышленного окружения, нужно лишь заменить образ init-контейнера. Для этого можно исправить определение YAML или произвести обновление с помощью команды kubectl. Однако было бы слишком утомительно править описание ресурса для каждого окружения. Если вы используете Red Hat OpenShift, корпоративный дистрибутив Kubernetes, паттерны OpenShift могут помочь в решении этой проблемы. Паттерны OpenShift дают возможность создавать разные описания ресурсов для разных окружений из одного паттерна.
Рис. 20.2. Реализация паттерна Immutable Configuration (Неизменяемая конфигурация) с помощью init-контейнера
Паттерны — это обычные описания ресурсов, которые допускают параметризацию. Как показано в листинге 20.7, в качестве такого параметра можно использовать образ конфигурации.
Листинг 20.7. Паттерн OpenShift, параметризуемый образом с конфигурацией
apiVersion: v1
kind: Template
metadata:
name: demo
parameters:
- name: CONFIG_IMAGE
description: Name of configuration image
value: k8spatterns/config-dev:1
objects:
- apiVersion: v1
kind: DeploymentConfig
// ....
spec:
template:
metadata:
// ....
spec:
initContainers:
- name: init
image: ${CONFIG_IMAGE}
args: [ "/config" ]
volumeMounts:
- mountPath: /config
name: config-directory
containers:
- image: k8spatterns/demo:1
// ...
volumeMounts:
- mountPath: /config
name: config-directory
volumes:
- name: config-directory
emptyDir: {}
Объявление параметра паттерна CONFIG_IMAGE.
Использование параметра паттерна.
Здесь показана только часть полного описания, но мы с легкостью сможем распознать параметр CONFIG_IMAGE в объявлении init-контейнера. Если создать этот паттерн в кластере OpenShift, мы сможем создать его экземпляр командой oc, как показано в листинге 20.8.
Листинг 20.8. Применение паттерна OpenShift для создания нового приложения
oc new-app demo -p CONFIG_IMAGE=k8spatterns/config-prod:1
Как обычно, инструкции по опробованию этого примера, а также полное определение контроллера Deployment можно найти в репозитории Git с примерами для этой книги.
Использование отдельных контейнеров с данными в паттерне Immutable Configuration (Неизменяемая конфигурация) — не самая простая задача. Однако этот паттерн предлагает некоторые уникальные преимущества:
• Конфигурация для конкретного окружения заключена в контейнер, а значит, ее можно хранить в системе управления версиями, как любые другие образы контейнеров.
• Конфигурацию, созданную таким способом, можно распространять через реестр контейнеров. Кроме того, такую конфигурацию можно проверить, даже не имея доступа к кластеру.
• Конфигурация считается неизменяемой, потому что хранится в образе контейнера: изменение конфигурации требует обновления версии и создания нового образа контейнера.
• Образы с конфигурационными данными удобнее, когда конфигурации слишком сложны или содержат слишком большие объемы конфигурационных данных, чтобы их можно было поместить в переменные окружения или в ресурсы ConfigMap.
Однако этот паттерн имеет и свои недостатки:
• Он сложнее в использовании, потому что приходится создавать и распространять через реестры дополнительные образы контейнеров.
• Он не решает проблемы безопасности конфиденциальных конфигурационных данных.
• В случае с Kubernetes требуется дополнительная обработка init-контейнеров, а значит, придется управлять различными объектами Deployment для разных окружений.
В любом случае нужно тщательно оценить, действительно ли необходим такой сложный подход. Если неизменность не требуется, может быть, будет достаточно простого ресурса ConfigMap, как описано в главе 19 «Конфигурация в ресурсах».
Еще одно решение, когда необходимо работать с большими конфигурационными файлами, незначительно отличающимися в разных окружениях, описано в следующей главе, где обсуждается паттерн Configuration Template (Макет конфигурации).
• Пример неизменяемой конфигурации (http://bit.ly/2HL95dp).
• Как имитировать --volumes-from в Kubernetes (http://bit.ly/2YbRhhy).
• Предложение по улучшению: поддержка томов образов в Kubernetes (http://bit.ly/2Wf0pjt).
• docker-flexvol: драйвер для Kubernetes, поддерживающий тома Docker (https://github.com/dims/docker-flexvol).
• Паттерны OpenShift (https://red.ht/2Ohh7vO).