ConfigMap и Secret

Уровень: beginner ~55 мин Практика: Vaultwarden (AGPL-3.0)

Что нужно знать перед уроком

  • M1.03 — Service и порты — нужен namespace/Service из предыдущего урока для проверки конфигурации

Что нужно знать перед уроком

M1 урок 1 — контейнер, его окружение и volumes на уровне понятий. В этом уроке разбираем, как передавать конфигурацию и секреты в контейнер без пересборки образа.

Теория

Зачем разделять код и конфигурацию

Образ контейнера — неизменяемый артефакт: одна и та же собранная версия myapp:1.4.0 должна работать одинаково и в тестовом, и в продакшен-окружении, отличаясь только конфигурацией (адрес БД, включённые фичи, секретные ключи). Если «зашивать» конфигурацию в образ — потребуется пересборка на каждое изменение окружения, что нарушает принцип «один артефакт — много окружений». ConfigMap и Secret — объекты Kubernetes для внешней передачи конфигурации в контейнер без изменения образа.

Два способа потребления: переменные окружения vs volume

Через переменные окружения — значение подставляется в env контейнера один раз при старте:

1
2
3
4
5
6
env:
  - name: DOMAIN
    valueFrom:
      configMapKeyRef:
        name: vaultwarden-config
        key: domain

Критичный момент: если ConfigMap изменится после старта контейнера — переменная окружения не обновится без пересоздания Pod’а (env читается только при запуске процесса, ОС не уведомляет процесс об изменении переменных на лету).

Через volume mount — ConfigMap/Secret монтируется как файл(ы) в файловую систему контейнера:

1
2
3
4
5
6
7
8
9
volumes:
  - name: config-vol
    configMap:
      name: vaultwarden-config
containers:
  - name: vaultwarden
    volumeMounts:
      - name: config-vol
        mountPath: /etc/vaultwarden

В этом случае kubelet действительно обновляет файлы на диске при изменении ConfigMap (с задержкой синхронизации, обычно до минуты) — но приложение должно само уметь перечитывать файл (через inotify или периодический опрос), Kubernetes не заставляет процесс перезапуститься автоматически.

subPath — монтирование одного конкретного ключа ConfigMap как отдельного файла, без замещения всей директории:

1
2
3
4
volumeMounts:
  - name: config-vol
    mountPath: /etc/vaultwarden/config.env
    subPath: config.env
Важный побочный эффект subPath
При использовании subPath файл не обновляется при изменении ConfigMap (в отличие от монтирования целой директории) — это осознанный trade-off Kubernetes ради предсказуемости при частичном монтировании.

Secret — это не шифрование

Ключевое заблуждение новичков: Secret не защищает данные шифрованием. Значения в Secret хранятся в кодировке base64 (это просто представление, не шифрование — декодировать может кто угодно, у кого есть доступ к чтению объекта). Реальная защита Secret — не в самом объекте, а в (а) более строгой RBAC-политике по умолчанию на чтение Secret по сравнению с ConfigMap, и (б) шифровании данных at rest в etcd на уровне кластера (EncryptionConfiguration, настраивается администратором кластера, не гарантировано «из коробки» на всех дистрибутивах).

1
kubectl get secret vaultwarden-secret -o jsonpath='{.data.admin-token}' | base64 -d

Типизированные Secret

Помимо обычного Opaque (произвольные ключи-значения), Kubernetes определяет специализированные типы с зарезервированными полями, которые понимают отдельные компоненты платформы:

  • kubernetes.io/tls — сертификат + приватный ключ (tls.crt, tls.key), используется Ingress/Gateway API для TLS-терминации.
  • kubernetes.io/dockerconfigjson — креды для приватного registry, единственный ключ .dockerconfigjson.
1
2
3
kubectl create secret generic regcred \
  --from-file=.dockerconfigjson=./config.json \
  --type=kubernetes.io/dockerconfigjson

Такой Secret указывается в imagePullSecrets Pod’а/ServiceAccount, чтобы kubelet мог скачать образ из приватного registry — это станет актуально начиная с M3, когда мы начнём публиковать собственные образы не в публичный registry.

immutable: true

Начиная с определённой версии Kubernetes и ConfigMap, и Secret поддерживают флаг immutable: true:

1
2
3
4
5
6
7
apiVersion: v1
kind: ConfigMap
metadata:
  name: vaultwarden-config
immutable: true
data:
  domain: "https://vault.example.com"

Эффект двойной: (1) защита от случайных правок «на живую» (попытка kubectl edit вернёт ошибку — нужно явно удалить и создать новый объект), (2) снижение нагрузки на kube-apiserver, потому что для иммутабельных объектов не нужно поддерживать watch на изменения — компоненты, которые их читают, могут кэшировать значение бессрочно. Побочный эффект: если конфигурация должна меняться без пересоздания Pod’а (как в примере с volume mount выше) — immutable: true этому противоречит, нужно выбирать одно из двух осознанно.

Частые ошибки и подводные камни

  • Ожидать, что изменение ConfigMap мгновенно обновит переменную окружения запущенного контейнера. Не обновит никогда, без пересоздания Pod’а — переменные окружения читаются только при старте процесса.
  • Хранить пароли в ConfigMap «для простоты». Даже без шифрования, Secret имеет более строгий RBAC по умолчанию — использование ConfigMap для чувствительных данных фактически снижает уровень защиты.
  • Путать subPath с обычным монтированием директории — при subPath теряется автообновление файла при изменении ConfigMap, о чём легко забыть.
  • Считать Secret «безопасным просто по факту существования объекта такого типа» — без включённого шифрования etcd at rest и грамотного RBAC, Secret в base64 практически равносилен ConfigMap по уровню защищённости данных на диске etcd.

Практика в кластере

🧩 Практика урока: Vaultwarden · лицензия AGPL-3.0

Vaultwarden — неофициальный, но широко используемый self-hosted сервер, совместимый с клиентами Bitwarden (менеджер паролей), написан на Rust. Конфигурация почти целиком через переменные окружения и файлы — идеальный пример для этого урока.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
kubectl create namespace course-m1-configsecret
kubectl create configmap vaultwarden-config -n course-m1-configsecret \
  --from-literal=domain="http://localhost:8080" \
  --from-literal=signups_allowed="true"
kubectl create secret generic vaultwarden-secret -n course-m1-configsecret \
  --from-literal=admin-token="$(openssl rand -base64 32)"

kubectl apply -n course-m1-configsecret -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vaultwarden
spec:
  replicas: 1
  selector: { matchLabels: { app: vaultwarden } }
  template:
    metadata: { labels: { app: vaultwarden } }
    spec:
      containers:
        - name: vaultwarden
          image: vaultwarden/server:latest
          ports: [{ containerPort: 80 }]
          env:
            - name: DOMAIN
              valueFrom: { configMapKeyRef: { name: vaultwarden-config, key: domain } }
            - name: SIGNUPS_ALLOWED
              valueFrom: { configMapKeyRef: { name: vaultwarden-config, key: signups_allowed } }
            - name: ADMIN_TOKEN
              valueFrom: { secretKeyRef: { name: vaultwarden-secret, key: admin-token } }
          resources:
            requests: { cpu: "100m", memory: "128Mi" }
            limits: { cpu: "300m", memory: "256Mi" }
---
apiVersion: v1
kind: Service
metadata: { name: vaultwarden }
spec:
  selector: { app: vaultwarden }
  ports: [{ port: 80, targetPort: 80 }]
EOF
  1. Проверьте, что переменные окружения реально попали в контейнер:
1
kubectl exec -n course-m1-configsecret deploy/vaultwarden -- env | grep -E "DOMAIN|SIGNUPS|ADMIN_TOKEN"
  1. Измените ConfigMap и убедитесь, что переменная окружения в уже запущенном контейнере не поменялась:
1
2
3
4
kubectl patch configmap vaultwarden-config -n course-m1-configsecret --type=merge -p '{"data":{"signups_allowed":"false"}}'
kubectl exec -n course-m1-configsecret deploy/vaultwarden -- env | grep SIGNUPS   # всё ещё "true"!
kubectl rollout restart deployment/vaultwarden -n course-m1-configsecret
kubectl exec -n course-m1-configsecret deploy/vaultwarden -- env | grep SIGNUPS   # теперь "false"
  1. Убедитесь, что значение Secret — просто base64, не шифрование:
1
kubectl get secret vaultwarden-secret -n course-m1-configsecret -o jsonpath='{.data.admin-token}' | base64 -d; echo
  1. Попробуйте отредактировать Secret «на живую» после того, как сделаете его immutable:
1
2
kubectl patch secret vaultwarden-secret -n course-m1-configsecret --type=merge -p '{"immutable":true}'
kubectl edit secret vaultwarden-secret -n course-m1-configsecret   # попытка изменить data — ошибка валидации

Практика разработки

Используется официальный образ vaultwarden/server; собственная сборка появится в M3.

Шпаргалка команд урока

1
2
3
4
kubectl create configmap <name> --from-literal=<k>=<v> [--from-file=<path>]
kubectl create secret generic <name> --from-literal=<k>=<v> [--type=<type>]
kubectl get secret <name> -o jsonpath='{.data.<key>}' | base64 -d
kubectl patch configmap|secret <name> --type=merge -p '{"data":{...}}'

Вопросы для самопроверки

Что произойдёт с переменной окружения контейнера, если изменить связанный ConfigMap после старта Pod'а?

env.valueFrom.configMapKeyRef подставляется единожды при старте контейнера; последующие изменения ConfigMap не отражаются без пересоздания Pod'а.

Источник: Kubernetes — configmap

Почему Secret считается более защищённым, чем ConfigMap, если оба хранят данные в открытом (base64) виде?

base64 — не шифрование; защита Secret строится на более строгом RBAC по умолчанию и опциональном шифровании etcd at rest, настраиваемом администратором.

Источник: Kubernetes — secret

Какой побочный эффект у монтирования ключа ConfigMap через subPath?

subPath — известный trade-off Kubernetes: точечное монтирование одного файла отключает механизм автообновления содержимого при изменении источника.

Источник: Kubernetes — configmap

Для чего предназначен тип Secret kubernetes.io/dockerconfigjson?

Единственное поле .dockerconfigjson содержит креды registry, на которые ссылается imagePullSecrets Pod'а/ServiceAccount для скачивания приватных образов.

Источник: Kubernetes — secret

Что даёт immutable: true у ConfigMap/Secret помимо защиты от случайных правок?

Иммутабельные ConfigMap/Secret позволяют компонентам кэшировать значение бессрочно без подписки на изменения, что снижает нагрузку на kube-apiserver в больших кластерах.

Источник: Kubernetes — configmap

Рекомендуемая литература

Официальная документация

Статьи и блоги

Книги

  • Marko Lukša. Kubernetes in Action. 2nd ed., 2022. ISBN 978-1617297618.
  • Kelsey Hightower, Brendan Burns, Joe Beda. Kubernetes: Up and Running. 3rd ed., 2022. ISBN 978-1098110201.
  • Michael Hausenblas, Stefan Schimanski. Programming Kubernetes. 2019. ISBN 978-1492047107.

Связанные материалы