open atlas
↑ К треку
Деплой и инфра DEP · 10 · 02

ConfigMap и секреты

Внедряй конфиг и секреты в рантайме, не запекай их в образ. ConfigMap и Secret питают env-переменные или примонтированные файлы — но base64 это кодирование, не шифрование, поэтому включай encryption-at-rest и RBAC.

DEP Middle ◷ 16 min
Уровень
ОсновыJuniorMiddleSenior

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 для любого, кто его клонирует.

СвойствоConfigMapSecret
НазначениеНеконфиденциальный конфигЧувствительные данные (токены, пароли, ключи)
Кодирование на проводеЧистый UTF-8 в database64 в data (кодирование, не шифрование)
Зашифровано в etcd at rest?НетНе по умолчанию — нужно включить encryption-at-rest
Кто может прочитатьЛюбой с доступом к API/etcdЛюбой с RBAC get secret — поэтому ограничивай жёстко
Потребляется какenv-переменные или файлы volumeenv-переменные или файлы 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 не подхватывают изменение. Почему?

Вспомните перед уходом
  1. 01
    Почему base64 в Kubernetes Secret — это не безопасность, и что реально надо сделать, чтобы защитить Secret?
  2. 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-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.