ConfigMap и секреты
Внедряй конфиг и секреты в рантайме, не запекай их в образ. ConfigMap и Secret питают env-переменные или примонтированные файлы — но base64 это кодирование, не шифрование, поэтому включай encryption-at-rest и RBAC.
Staging-сборку промоутят в продакшен, и она тут же начинает писать в staging-базу. Образ собрали с DATABASE_URL=postgres://staging..., запечённым в строку ENV, поэтому «промоут образа» молча протащил staging-конфиг вместе с ним. Хуже того, git log показывает, что в прошлом релизе так же был вшит API-токен — он теперь в каждом реестре, в каждом docker pull разработчика и в истории слоёв навсегда. У обоих багов одна корневая причина: конфигурация, принадлежащая окружению, была заморожена в артефакте.
Один образ, много окружений: разделение по 12-factor
Спроси себя: если образ собирается в CI и затем промоутится в продакшен, что меняется между окружениями? Всё, что меняется, принадлежит снаружи образа — а не заморожено внутри него.
Образ контейнера задуман так, чтобы собираться один раз и запускаться где угодно — dev, staging, prod — одинаково. Как только ты запекаешь в него значение, специфичное для окружения (URL базы, фича-флаг, API-ключ), ты ломаешь это обещание: один образ больше не может работать в двух окружениях, а всё секретное, что ты заморозил, теперь навсегда в истории слоёв. Правило 12-factor — держать конфиг в окружении, а не в коде, и внедрять его в рантайме.
Kubernetes даёт для этого два объекта. ConfigMap хранит несекретную конфигурацию в формате ключ/значение — «API-объект для хранения неконфиденциальных данных в парах ключ-значение». Secret хранит те же формы для чувствительных данных. Оба развязаны с Pod: можно менять конфиг без пересборки образа и запускать один и тот же образ в staging и prod, направив его на разные ConfigMap и Secret.
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
LOG_LEVEL: "info"
FEATURE_NEW_CHECKOUT: "true"
app.properties: |
cache.ttl=300
cache.size=2000
---
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
data:
# base64 от "postgres://app:s3cr3t@db:5432/app" — кодирование, НЕ шифрование
DATABASE_URL: cG9zdGdyZXM6Ly9hcHA6czNjcjN0QGRiOjU0MzIvYXBwДва способа доставки: env-переменные vs примонтированные файлы
Pod потребляет ConfigMap или Secret в одной из двух форм, и выбор имеет последствие в рантайме, которое большинство узнаёт на собственных шишках. Можно проецировать значения как env-переменные (envFrom для всей карты, или valueFrom.configMapKeyRef / secretKeyRef для одного ключа), либо монтировать их как файлы в volume, где каждый ключ становится файлом. Env-переменные удобны для плоских скаляров; volume — правильный выбор для целых файлов конфигурации (TLS-сертификат, application.yaml) и для всего, что хочешь обновлять без редеплоя.
Последствие: примонтированные файлы обновляются вживую; env-переменные — нет. Когда ConfigMap, потребляемый в volume, обновляется, «проецируемые ключи в итоге тоже обновляются» — kubelet освежает их на своей периодической синхронизации. Но «ConfigMap, потребляемые как env-переменные, не обновляются автоматически и требуют рестарта пода». То же разделение справедливо для Secret. Поэтому если ты меняешь значение, которое приложение читает из env-переменной, ничего не происходит до рестарта Pod; меняешь значение, примонтированное как файл, — файл обновляется на месте, хотя приложению всё равно надо перечитать его.
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 3
selector:
matchLabels: { app: app }
template:
metadata:
labels: { app: app }
spec:
containers:
- name: app
image: registry.example.com/app:1.4.2 # один образ, каждое окружение
envFrom:
- configMapRef:
name: app-config # весь ConfigMap как env-переменные
env:
- name: DATABASE_URL # один ключ секрета как env-переменная
valueFrom:
secretKeyRef:
name: app-secrets
key: DATABASE_URL
volumeMounts:
- name: config-files
mountPath: /etc/app # app.properties появляется как файл; обновляется вживую
readOnly: true
volumes:
- name: config-files
configMap:
name: app-config▸Почему это работает
«Обновляется вживую» — не вся история. Kubelet освежает примонтированные volume только на своей периодической синхронизации, так что задержка может быть «вплоть до периода синхронизации kubelet + задержки распространения кэша» — секунды до минуты, не мгновенно. И смена файла на диске ничего не даёт, пока приложение не следит за файлом и не перечитывает его; большинство приложений читают конфиг один раз на старте. Так что монтирование как volume необходимо, но недостаточно для живой ротации — приложение должно участвовать.
Сеньорская оговорка: base64 это не безопасность
Это провал, на котором команды получают breach. В поле data секрета значения закодированы в base64, а не зашифрованы — а base64 тривиально обратим (echo ... | base64 -d). По умолчанию «секреты Kubernetes по умолчанию хранятся незашифрованными в нижележащем хранилище API-сервера (etcd)». Так что объект Secret сам по себе не секретен: «любой с доступом к API может получить или изменить Secret, как и любой с доступом к etcd». Secret отличается от ConfigMap в основном намерением и инструментарием (он типизирован, держится вне некоторых логов, может быть ограничен RBAC) — но не какой-либо криптографией.
Чтобы Secret стали реально безопасны, Kubernetes говорит «предпринять как минимум следующие шаги»: включить Encryption at Rest, чтобы байты в etcd были зашифрованы, и настроить RBAC с наименьшими привилегиями, чтобы только те рабочие нагрузки и люди, которым нужен Secret, могли сделать get. Сверху — ограничение доступа к Secret конкретными контейнерами, а для настоящего управления секретами тянись к внешним хранилищам секретов — HashiCorp Vault, External Secrets Operator или облачному secret-менеджеру (AWS/GCP/Azure) — которые держат источник истины вне кластера, поддерживают аудит и ротацию и синхронизируют в Kubernetes по запросу. Кардинальный грех, которого избегать: считать base64-манифест Secret безопасным и коммитить его в git. base64 в репозитории — это plaintext для любого, кто его клонирует.
| Свойство | ConfigMap | Secret |
|---|---|---|
| Назначение | Неконфиденциальный конфиг | Чувствительные данные (токены, пароли, ключи) |
| Кодирование на проводе | Чистый UTF-8 в data | base64 в data (кодирование, не шифрование) |
| Зашифровано в etcd at rest? | Нет | Не по умолчанию — нужно включить encryption-at-rest |
| Кто может прочитать | Любой с доступом к API/etcd | Любой с RBAC get secret — поэтому ограничивай жёстко |
| Потребляется как | env-переменные или файлы volume | env-переменные или файлы volume (те же две формы) |
Иммутабельность и ротация
Когда ты меняешь значение конфига в проде, что должно происходить с уже запущенными pod-ами? Ответ зависит от того, за какой из двух рычагов ты берёшься — и у каждого свой сценарий отказа при неправильном применении.
Два операционных рычага завершают картину. Помечай ConfigMap или Secret immutable (добавь верхнеуровневое immutable: true), когда его значение не должно меняться на месте. Это даёт два эффекта: защиту от случайных правок, которые молча сломали бы работающие Pod, и выигрыш в производительности — kubelet перестаёт следить за иммутабельными объектами, что заметно снижает нагрузку на API-сервер в больших кластерах. Трейдофф в том, что иммутабельный объект нельзя редактировать; чтобы изменить его, создаёшь новый (часто с суффиксом-хэшем) и катишь Deployment, чтобы он указывал на него.
Это естественно ведёт к ротации. Поскольку значения env-переменных заморожены на старте Pod, ротация Secret, потребляемого как env-переменная, ничего не делает, пока ты не перезапустишь Pod — и осознанный kubectl rollout restart честный способ это применить. Secret, примонтированный как файл, может ротироваться без рестарта, но только если приложение следит за файлом и перезагружается. Поэтому многие команды стандартизируются либо на «монтирование + приложение следит» для настоящей ротации без даунтайма, либо на «immutable + имена с хэш-суффиксом + rollout», чтобы смена конфига была обычным, аудируемым, откатываемым деплоем.
Прод-приложение должно ротировать пароль базы без даунтайма. Как доставить credential в Pod?
Коллега говорит, что Secret безопасен, потому что значения закодированы в base64. Какая корректная поправка?
Ты правишь значение ConfigMap, которое приложение читает из env-переменной. Работающие Pod не подхватывают изменение. Почему?
- 01Почему base64 в Kubernetes Secret — это не безопасность, и что реально надо сделать, чтобы защитить Secret?
- 02Когда смена значения ConfigMap или Secret реально вступает в силу в работающем Pod, и как форма потребления это решает?
Конфигурацию, принадлежащую окружению, надо внедрять в рантайме, а не запекать в образ — запечёшь, и тот же артефакт уже не сможет перемещаться между окружениями, а любой секрет, что ты заморозил, живёт в истории слоёв навсегда. Теперь, когда встретишь secretKeyRef, ссылающийся на Secret, задай себе вопрос: зашифрован ли этот Secret at rest (зашифровано ли содержимое в хранилище etcd), ограничен ли доступ к нему RBAC с наименьшими привилегиями и хранится ли источник истины вне git? Если нет — у тебя строка в base64 с ложным ощущением безопасности. Kubernetes даёт ConfigMap для неконфиденциального конфига ключ/значение и Secret для чувствительных данных, оба развязаны с Pod, так что один образ работает везде, указывая на разные объекты. Каждый потребляется в одной из двух форм — env-переменные или файлы в примонтированном volume — и форма определяет ротацию: env-переменные заморожены на старте контейнера и требуют рестарта Pod, тогда как примонтированные файлы освежаются на синхронизации kubelet, но вступают в силу, только если приложение их перечитает. Сеньорская ловушка в том, что base64 в Secret — это кодирование, не шифрование: Secret по умолчанию лежат незашифрованными в etcd, и любой с RBAC get-secret или доступом к etcd может их декодировать, поэтому надо включить encryption-at-rest, ограничить доступ RBAC с наименьшими привилегиями, никогда не коммитить манифест Secret в git и тянуться к внешнему secret-менеджеру для всего, что реально важно. Наконец, помечай конфиг иммутабельным, чтобы предотвратить случайные правки и срезать нагрузку слежения kubelet, принимая, что ротация тогда означает новый объект с хэш-именем плюс rollout.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.