Предыдущие части серии:
В прошлой части мы разобрались с 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=developmentLOG_LEVEL=debugDB_HOST=postgres-dev
На тестовом стенде они выглядят так:
APP_MODE=testLOG_LEVEL=debugDB_HOST=postgres-test
А в production нам нужно:
APP_MODE=productionLOG_LEVEL=infoDB_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: v1kind: ConfigMapmetadata: name: shop-configdata: 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=productionLOG_LEVEL=infoDB_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: v1kind: Secretmetadata: name: shop-secrettype: OpaquestringData: 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/v1kind: Deploymentmetadata: name: shop-apispec: 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: infofeatures: recommendations: true
Вопрос: как этот файл должен оказаться внутри контейнера?
Можно добавить его в Docker-образ. Но тогда ради смены log_level придётся снова собирать образ. А мы ведь всю статью пытаемся уйти именно от этого
Можно попробовать передать путь к файлу на нашем компьютере. Только вот контейнер работает в Kubernetes и не видит файлы на нашем ноутбуке сам по себе. Да и завтра Pod может запуститься уже на другой ноде.
Значит, нам нужно сделать две вещи:
-
Передать Kubernetes содержимое файла, чтобы оно не зависело от того, где лежит файл на нашем компьютере.
-
Указать, где этот файл должен появиться внутри контейнера, чтобы приложение смогло его найти.
Вот здесь ConfigMap нам и пригодится. Давайте пройдём весь путь на простом примере.
Шаг 1. Берём обычный файл с настройками
Представьте, что у нас уже есть config.yaml в папке проекта. Самый обычный файл, который мы открываем в редакторе:
log_level: infofeatures: 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-apiconfig.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 заставить его использовать их правильно?
ссылка на оригинал статьи https://habr.com/ru/articles/1096930/