Предыдущие части серии:
В прошлой части мы разобрались с Gateway API и наконец собрали довольно приличную схему нашего мини-маркетплейса. У нас есть точка входа, маршруты, Service, Deployment и Pod. Трафик до приложений доходит, Kubernetes следит за количеством реплик.
Казалось бы, чего ещё не хватает?
Представим одну ситуацию.
Пятница, вечер. Наш shop-api отлично работает на тестовом стенде. Мы решили, что пора выпускать его в production. Образ собрали, Deployment применили, Pod запустились. Всё выглядит хорошо, пока не открываем логи.
А там приложение упорно пытается подключиться к тестовой базе данных.
Почему? Оказывается, где-то в настройках осталась строка:
DB_HOST=postgres-test
И теперь ради изменения одного адреса нам предлагают снова собирать Docker-образ.
Отличное занятие на вечер пятницы, не находите?
Сегодня разберёмся, как отделить само приложение от настроек, с которыми оно запускается. Заодно познакомимся с двумя объектами Kubernetes: ConfigMap и Secret.
Но сначала вопрос.
У нас один
shop-apiи три окружения: development, test и production. Нам действительно нужны три разных Docker-образа?
Пока не отвечайте. Давайте посмотрим, что между этими окружениями вообще меняется.
Один образ, три окружения
Предположим, наш shop-api читает три переменные окружения:
APP_MODE=development LOG_LEVEL=debug DB_HOST=postgres-dev
На тестовом стенде они выглядят так:
APP_MODE=test LOG_LEVEL=debug DB_HOST=postgres-test
А в production нам нужно:
APP_MODE=production LOG_LEVEL=info DB_HOST=postgres-service
Обратите внимание на одну важную деталь: сам код приложения мы не меняли. Мы не добавляли новый функционал, не исправляли ошибки, не меняли зависимости. Только сказали приложению, в каком режиме работать и куда обращаться за базой.
Получается довольно простое разделение:
Docker-образ = что запускаем Конфигурация = с какими настройками запускаем
И вот теперь можно ответить на вопрос из начала.
Правильнее использовать один и тот же образ с разной конфигурацией для разных окружений. Иначе придётся следить, чтобы shop-api:test и shop-api:prod не начали жить собственной жизнью, отличаясь не только настройками, но и версиями приложения.
Вообще, Kubernetes не запрещает указать переменные прямо в Deployment:
env: - name: LOG_LEVEL value: "info"
И для одной-двух настроек это вполне нормально.
Но представьте, что у нас десятки параметров, несколько окружений и не одно приложение, а целый маркетплейс. Каждый раз искать нужные строки внутри Deployment становится не самым приятным занятием.
Хочется хранить конфигурацию отдельно, а в Pod просто указывать, откуда её взять.

И для этого в Kubernetes есть первый сегодняшний объект.
ConfigMap - настройки отдельно от приложения
ConfigMap - объект Kubernetes, предназначенный для хранения обычных, не секретных настроек.
Например, нашей конфигурации для production:
apiVersion: v1 kind: ConfigMap metadata: name: shop-config data: APP_MODE: "production" LOG_LEVEL: "info" DB_HOST: "postgres-service"
apiVersion, kind, metadata - всё это мы уже встречали, когда знакомились с YAML во второй части серии.
Теперь обратите внимание на:
kind: ConfigMap
Так мы сообщаем Kubernetes, какой именно объект создаём.
И на:
data: APP_MODE: "production" LOG_LEVEL: "info" DB_HOST: "postgres-service"
В data лежат пары «ключ - значение». В нашем примере это три строки.
Если сохранить всё в configmap.yaml и выполнить:
kubectl apply -f configmap.yaml
то в кластере появится объект shop-config. Проверить можно так:
kubectl get configmap shop-config
И тут важный момент, который легко пропустить.
Создание ConfigMap ещё не меняет настройки нашего приложения.
ConfigMap не запускает Pod, не пересобирает Docker-образ и вообще не знает, что конкретно означает LOG_LEVEL.
Мы пока просто сохранили данные в Kubernetes. Дальше нужно объяснить контейнеру, как ими пользоваться.
Как настройки попадают в Pod?
Вспомним наш Deployment. В нём мы уже описывали, сколько Pod должно работать и какие контейнеры нужно запустить.
Теперь к описанию контейнера добавим ещё одно поле:
containers: - name: shop-api image: registry.example.com/shop-api:1.0 envFrom: - configMapRef: name: shop-config
Здесь и дальше shop-api - учебный пример. Адрес образа условный, без настоящего приложения этот Deployment не предназначен для запуска.
Самое интересное находится в трёх строчках:
envFrom: - configMapRef: name: shop-config
Читается довольно просто: «Возьми значения из ConfigMap shop-config и передай их контейнеру как переменные окружения».
То есть внутри запущенного контейнера появятся:
APP_MODE=production LOG_LEVEL=info DB_HOST=postgres-service
Но давайте уточним ещё кое-что.
Kubernetes передаёт переменные окружения, а не решает за приложение, как ими пользоваться. Если наш backend вообще не читает LOG_LEVEL, то хоть десять раз передайте ему эту переменную - подробные логи сами собой не появятся.
А теперь небольшая проверка.
Вопрос: в ConfigMap лежат три параметра, но контейнеру нужен только
LOG_LEVEL. Обязательно забирать всё содержимое?
Нет. Можно обратиться к конкретному ключу:
env: - name: LOG_LEVEL valueFrom: configMapKeyRef: name: shop-config key: LOG_LEVEL
Здесь name указывает на ConfigMap, а key - на нужное значение внутри неё.
Получается, у нас есть два подхода:
envFrom- передать сразу набор подходящих ключей ConfigMap как переменные окружения.env+configMapKeyRef- выбрать одно конкретное значение.

Отлично. Режим приложения настроили, адрес базы вынесли, логирование тоже.
Но давайте вернёмся к нашей базе данных. Мы ведь указали только адрес. А как будем авторизовываться?
А пароль от базы куда положим?
Сейчас в конфигурации у нас есть:
DB_HOST=postgres-service
Но база наверняка попросит ещё хотя бы пароль.
И тут предлагаю выбрать один из трёх вариантов.
Куда запишем
DB_PASSWORD?А. Прямо в Docker-образ.
Б. В ту же ConfigMap, рядом с
DB_HOST.В. В отдельный объект для чувствительных данных.
Подумайте, прежде чем читать дальше.
С технической точки зрения положить пароль в ConfigMap можно. Kubernetes не скажет, что мы делаем что-то не то. Но здесь вопрос не в том, получится ли сохранить строку, а в том, где ей вообще место.
ConfigMap предназначена для обычных настроек. Для паролей, токенов и ключей есть другой объект.
Поэтому выбираем вариант В.
Secret - отдельный объект для чувствительных данных
Secret нужен для хранения небольших объёмов чувствительных данных: паролей, токенов, сертификатов и ключей.
На первый взгляд он очень похож на ConfigMap:
apiVersion: v1 kind: Secret metadata: name: shop-secret type: Opaque stringData: DB_PASSWORD: "not-a-real-password"
Разберём новые поля.
kind: Secret - сообщаем Kubernetes, что создаём Secret.
type: Opaque - универсальный тип Secret. Есть и специальные типы, например для TLS-сертификатов, но пока остановимся на обычном варианте.
stringData - позволяет записать значение привычной строкой. При обработке объекта Kubernetes преобразует его в поле data, где значения представлены в Base64.
Как передать пароль контейнеру? Принцип уже знакомый:
env: - name: DB_PASSWORD valueFrom: secretKeyRef: name: shop-secret key: DB_PASSWORD
В самом Deployment пароль мы не записываем. Вместо этого указываем, из какого Secret и по какому ключу его нужно взять.
Заметьте, насколько похоже на configMapKeyRef. Отличается источник данных, а идея та же.
Теперь попробуем собрать все части вместе.
Собираем конфигурацию нашего shop-api
Нам нужны два Pod shop-api, общие настройки из ConfigMap и пароль из Secret.
Вот как это может выглядеть в одном Deployment:
apiVersion: apps/v1 kind: Deployment metadata: name: shop-api spec: replicas: 2 selector: matchLabels: app: shop-api template: metadata: labels: app: shop-api spec: containers: - name: shop-api image: registry.example.com/shop-api:1.0 envFrom: - configMapRef: name: shop-config env: - name: DB_PASSWORD valueFrom: secretKeyRef: name: shop-secret key: DB_PASSWORD
На первый взгляд опять много YAML. Но если мысленно убрать знакомые поля Deployment, останутся всего две новые части.
envFrom берёт обычные настройки из shop-config.
secretKeyRef получает пароль из shop-secret.
А replicas: 2, как мы уже знаем, означает, что Kubernetes должен поддерживать два Pod нашего приложения.
Получается такая картина:
Deployment -> сколько Pod нужно и как их запускать ConfigMap -> обычные настройки приложения Secret -> чувствительные данные Приложение -> читает настройки и использует их
Обратите внимание: ConfigMap и Secret не управляют Pod. Они только предоставляют данные, на которые ссылается описание контейнера.
И ещё один практический нюанс. Если обязательной ConfigMap или Secret нет в кластере, контейнер не сможет нормально стартовать с такой конфигурацией. Поэтому сначала создаём нужные объекты, а уже затем запускаем Pod, который на них ссылается.

Кажется, всё хорошо. Настройки отделили от образа, пароль положили в специальный объект.
И тут возникает закономерный вопрос.
Раз Secret называется Secret, значит пароль зашифрован?
Вот здесь Kubernetes может немного обмануть ожидания. Не специально, конечно, но название действительно звучит надёжно.
Посмотрим на пример:
data: DB_PASSWORD: "cGFzc3dvcmQ="
Набор символов выглядит непонятно. Можно подумать, что пароль теперь надёжно спрятан.
На самом деле cGFzc3dvcmQ= - это слово password, просто записанное в кодировке Base64. Превратить его обратно в обычный текст не составляет труда.
Base64 - это кодирование, а не шифрование.
Иными словами, если человек получил доступ к содержимому Secret, сама кодировка его не остановит.
Более того, без соответствующей настройки Kubernetes может хранить Secret в etcd без шифрования при хранении.
Значит ли это, что Secret бесполезен? Конечно нет. Он позволяет отделить чувствительные данные и применять к ним специальные правила доступа. Но название объекта само по себе не обеспечивает полную безопасность.
Поэтому для реального кластера важно:
Ограничивать доступ к Secret через RBAC.
Включать шифрование Secret при хранении (encryption at rest).
Не сохранять реальные пароли в открытом GitHub и не печатать их без необходимости в логах.
При необходимости использовать внешние хранилища секретов.
Сейчас углубляться в настройку RBAC и шифрования не будем. Здесь нам нужно запомнить главное: Secret предназначен для чувствительных данных, но безопасность не появляется автоматически только потому, что объект так называется.

С этим разобрались. Но у нас остаётся ещё один любопытный момент, причём уже про саму работу приложения.
Изменили ConfigMap - приложение сразу увидит новые настройки?
Представьте, что два Pod нашего shop-api уже работают.
В ConfigMap записано:
LOG_LEVEL=info
Оба контейнера получили это значение через envFrom, и приложение спокойно пишет обычные логи.
Но нам понадобилось подробнее разобраться в какой-то ошибке. Меняем настройку:
LOG_LEVEL=debug
Применяем обновлённую ConfigMap. Kubernetes принял изменение.
А теперь попробуйте предсказать результат.
Что произойдёт с работающими Pod?
А. Они сразу начнут использовать
LOG_LEVEL=debug.Б. ConfigMap обновится, но у запущенных контейнеров останется
LOG_LEVEL=info.В. Kubernetes автоматически пересоберёт Docker-образ.
Правильный ответ - Б.
Дело в том, что переменные окружения задаются при запуске контейнера. Если потом изменить ConfigMap, уже работающий процесс автоматически новые значения не получит.
А вот новые контейнеры, которые стартуют с этой конфигурацией, прочитают обновлённое значение.
Как быть, если мы хотим, чтобы оба Pod начали работать с LOG_LEVEL=debug? Для Deployment можно запустить пересоздание Pod командой:
kubectl rollout restart deployment/shop-api
Deployment постепенно заменит Pod, а новые контейнеры получат актуальную конфигурацию при запуске.
В процессе обновления вполне возможна такая ситуация:
ConfigMap: LOG_LEVEL=debug Старый Pod: LOG_LEVEL=info Новый Pod: LOG_LEVEL=debug
И тут важно не перепутать две вещи: настройка в Kubernetes уже обновилась, но приложение ещё не обязательно начало её использовать.
А если приложение читает настройки из файла?
До этого мы передавали настройки через переменные окружения. Указали LOG_LEVEL=info, Kubernetes передал значение контейнеру, приложение его прочитало. Всё хорошо
Но есть приложения, которым переменные вообще не интересны. Они привыкли искать готовый файл с настройками в определённом месте.
Допустим, наш shop-api при запуске открывает:
/etc/shop/config.yaml
И ожидает увидеть внутри вот это:
log_level: info features: recommendations: true
Вопрос: как этот файл должен оказаться внутри контейнера?
Можно добавить его в Docker-образ. Но тогда ради смены log_level придётся снова собирать образ. А мы ведь всю статью пытаемся уйти именно от этого
Можно попробовать передать путь к файлу на нашем компьютере. Только вот контейнер работает в Kubernetes и не видит файлы на нашем ноутбуке сам по себе. Да и завтра Pod может запуститься уже на другой ноде.
Значит, нам нужно сделать две вещи:
Передать Kubernetes содержимое файла, чтобы оно не зависело от того, где лежит файл на нашем компьютере.
Указать, где этот файл должен появиться внутри контейнера, чтобы приложение смогло его найти.
Вот здесь ConfigMap нам и пригодится. Давайте пройдём весь путь на простом примере.
Шаг 1. Берём обычный файл с настройками
Представьте, что у нас уже есть config.yaml в папке проекта. Самый обычный файл, который мы открываем в редакторе:
log_level: info features: recommendations: true
Переписывать его содержимое ещё раз в большой манифест совсем не обязательно.
Откроем терминал в той же папке и выполним:
kubectl create configmap shop-file-config --from-file=config.yaml
Прочитаем команду по частям:
create configmap shop-file-config- создаём ConfigMap с именемshop-file-config.--from-file=config.yaml- берём данные из нашего файлаconfig.yaml.
Всё. Kubernetes получил содержимое файла и сохранил его в ConfigMap. Проверить, что объект появился, можно командой:
kubectl get configmap shop-file-config
А теперь важное уточнение. Сам файл на компьютере никуда не переехал, и Kubernetes не будет постоянно заглядывать в нашу папку. Мы передали ему содержимое файла на момент выполнения команды.
То есть пока ConfigMap - это, грубо говоря, сохранённая копия наших настроек. Но внутри Pod её ещё нет
Шаг 2. Говорим, куда положить настройки внутри контейнера
Вспомним, что приложение ищет файл строго по пути:
/etc/shop/config.yaml
Kubernetes уже знает, что должно быть внутри файла. Осталось объяснить ему, где приложение должно этот файл увидеть.
Для этого используются volumes и volumeMounts. Звучит громоздко, но идея довольно простая.
Посмотрите на фрагмент описания Pod:
spec: containers: - name: shop-api image: registry.example.com/shop-api:1.0 volumeMounts: - name: app-config mountPath: /etc/shop readOnly: true volumes: - name: app-config configMap: name: shop-file-config
Это фрагмент spec Pod. Если используем Deployment, он должен находиться внутри spec.template.spec. Образ shop-api по-прежнему условный.
Не спешите разбирать всё сразу. Здесь всего два новых элемента, и у каждого своя задача.
Сначала посмотрим на нижний блок:
volumes: - name: app-config configMap: name: shop-file-config
Здесь мы говорим Kubernetes: «У нас будет том app-config. Данные для него возьми из ConfigMap shop-file-config».
app-config - просто придуманное нами имя тома. Оно нужно, чтобы дальше на него сослаться.
Теперь верхняя часть:
volumeMounts: - name: app-config mountPath: /etc/shop readOnly: true
А это уже инструкция для контейнера: «Подключи том app-config в папку /etc/shop и не разрешай контейнеру изменять файлы в нём».
И вот вся цепочка:
Наш компьютер Kubernetes Контейнер shop-api config.yaml --from-file--> ConfigMap shop-file-config | volume app-config | mountPath: /etc/shop | /etc/shop/config.yaml
Откуда взялось имя config.yaml в конце пути?
Из команды --from-file=config.yaml. Kubernetes сохранил содержимое под ключом config.yaml, а при подключении ConfigMap как тома этот ключ становится именем файла.
Теперь всё сошлось: приложение открывает /etc/shop/config.yaml и получает наши настройки. При этом исходный Docker-образ менять не пришлось.
Проверим, разобрались ли мы
Допустим, в volumeMounts мы заменили:
mountPath: /etc/shop
на:
mountPath: /app/settings
Где теперь появится файл?
Правильно, по пути /app/settings/config.yaml. Мы изменили папку, куда подключается том, а имя файла осталось прежним.
Ещё один момент. Когда мы подключаем том в /etc/shop, уже существующие файлы образа в этой папке на время монтирования оказываются скрыты. Поэтому не стоит наугад подключать ConfigMap поверх папки, где лежит что-то важное.

Кстати, Secret можно подключить похожим образом, например когда приложению нужны сертификаты или ключи в виде файлов.
И напоследок - тонкость, связанная с нашим экспериментом выше. Когда ConfigMap подключена как том, содержимое файла может обновляться в работающем Pod спустя некоторое время, без пересоздания контейнера. Но это ещё не гарантирует, что само приложение автоматически перечитает файл и начнёт использовать новые настройки.
А если мы поменяли config.yaml только у себя на компьютере, Kubernetes вообще об этом не узнает, пока мы отдельно не обновим ConfigMap.
Вот теперь у нас есть оба варианта: приложению нужны переменные - передаём переменные, приложению нужен файл - подключаем ConfigMap как том. Главное, чтобы приложение понимало выбранный способ настройки
Что в итоге?
Сегодня мы добрались до той части Kubernetes, которая не занимается ни маршрутизацией трафика, ни созданием новых Pod, но при этом нужна практически каждому приложению.
ConfigMap хранит обычные настройки: режим работы, уровень логирования, адреса других сервисов и так далее.
Secret предназначен для чувствительных значений: паролей, токенов, ключей. При этом Base64 - не шифрование, а значит, о безопасности данных нужно думать отдельно.
Оба объекта позволяют не зашивать настройки в Docker-образ. Мы можем передать их через переменные окружения или подключить как файлы.
Но самое главное, на мой взгляд, вот что:
Docker-образ определяет, что мы запускаем. ConfigMap и Secret помогают указать, с какими настройками. А само приложение должно уметь эти настройки прочитать и использовать.
И теперь, когда наш мини-маркетплейс научился не только принимать трафик, но и получать конфигурацию отдельно от образа, мы стали ещё на шаг ближе к тому, как приложения действительно разворачивают в Kubernetes.
Проверьте себя
Как обычно, несколько вопросов, чтобы убедиться, что всё уложилось в голове.
Почему мы не стали делать отдельный Docker-образ для development, test и production?
Чем отличается
envFromотconfigMapKeyRef?Почему пароль от базы лучше не класть в ConfigMap?
Если в Secret значение выглядит как Base64, можно ли считать его зашифрованным?
Почему работающий Pod не увидит обновление переменной окружения после изменения ConfigMap?
При каком способе передачи конфигурации изменения могут появиться в файле без пересоздания Pod?
И главный вопрос: если приложение не умеет читать переданные ему настройки, сможет ли Kubernetes заставить его использовать их правильно?
dplsoft
#offtop
хм... где-то же уже такое было.... ах да... трехтомник "Как научиться писать на С++ за 21 день"...
но спасибо за контент, почитаем под пиво.
Meedoeed Автор
Ахахахпхпх, спасибо!