03. Конфигурация и секреты
Незнакомый термин? Откройте словарь Kubernetes и терминов курса.
Назначение, предварительные знания и результаты обучения
Нужны главы 00–02 и образ. Здесь отделяем артефакт выпуска от конфигурации окружения и не выдаём base64 за защиту.
После главы вы сможете:
- выбрать переменные окружения, аргументы команды или смонтированный файл;
- предсказать обновление ConfigMap/Secret;
- создать Secret-заглушку без утечки реального значения в Git или историю;
- выполнить управляемый rollout после изменения конфигурации;
- диагностировать
CreateContainerConfigError; - объяснить внешнюю систему секретов и шифрование хранимых данных как разные уровни защиты.
Ментальная модель: доставка, видимость и обновление
ConfigMap хранит несекретные пары ключ-значение. Secret имеет специальный API,
семантику RBAC и аудита и несколько типов, но его data — кодировка base64,
не шифрование. Оба объекта находятся в namespace и доступны только тем Pods,
которые явно
ссылаются на них (если admission не мутирует Pod).
| Способ передачи | Представление в приложении | Семантика обновления | Риск |
|---|---|---|---|
env.valueFrom / envFrom | окружение процесса | слепок при запуске контейнера | раскрытие окружения при диагностике процесса |
| проецируемый том | файлы | kubelet обновляет с задержкой | приложение должно перечитывать файл или следить за ним |
монтирование subPath | один путь к файлу | автоматических обновлений нет | легко ошибочно ожидать обновление |
| Клиент API | динамическое чтение | кеш, повторы и RBAC приложения | токен и зависимость от API |
Kubernetes обновляет проецируемый том не мгновенно: синхронизация и кеш дают задержку. Окружение не меняется до замены контейнера. Поэтому изменение конфигурации само по себе не создаёт rollout. Helm/Kustomize часто добавляют хеш в шаблон Pod: изменение хеша создаёт новую ревизию Deployment.
Полные ConfigMap, Secret и ресурс-потребитель
apiVersion: v1
kind: ConfigMap
metadata:
name: python-api-config
namespace: kube-course
labels:
app.kubernetes.io/name: python-api
data:
APP_MESSAGE: "hello from ConfigMap"
FEATURE_COLOR: "blue"
message: "mounted message"
---
apiVersion: v1
kind: Secret
metadata:
name: python-api-secret
namespace: kube-course
labels:
app.kubernetes.io/name: python-api
type: Opaque
stringData:
API_KEY: "replace-before-use"
stringData — удобное поле только для записи: сервер API преобразует его в
base64 data, а чтение вернёт data. Никогда не сохраняйте настоящий секрет в
Git даже через stringData.
Полный ресурс-потребитель:
apiVersion: apps/v1
kind: Deployment
metadata:
name: python-api-config-lab
namespace: kube-course
labels:
app.kubernetes.io/name: python-api
app.kubernetes.io/instance: config-lab
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: python-api
app.kubernetes.io/instance: config-lab
template:
metadata:
labels:
app.kubernetes.io/name: python-api
app.kubernetes.io/instance: config-lab
annotations:
course.example.com/config-revision: "1"
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: api
image: kube-python-course:1.0.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8000
env:
- name: APP_MESSAGE
valueFrom:
configMapKeyRef:
name: python-api-config
key: APP_MESSAGE
- name: API_KEY
valueFrom:
secretKeyRef:
name: python-api-secret
key: API_KEY
volumeMounts:
- name: config
mountPath: /config
readOnly: true
- name: data
mountPath: /data
- name: tmp
mountPath: /tmp
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 250m
memory: 192Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumes:
- name: config
configMap:
name: python-api-config
items:
- key: message
path: message
- name: data
emptyDir: {}
- name: tmp
emptyDir: {}
optional: true у ссылки меняет контракт немедленного отказа: Pod запускается
без ключа. Для обязательной конфигурации это обычно ухудшает корректность.
Практическое упражнение: слепок против обновления
Подготовка
kubectl apply -f manifests/base/configmap.yaml
kubectl apply -f manifests/base/secret.example.yaml
kubectl apply -f /tmp/python-api-config-lab.yaml
kubectl rollout status deployment/python-api-config-lab \
-n kube-course --timeout=120s
kubectl port-forward -n kube-course deployment/python-api-config-lab 8000:8000
В другом терминале:
curl --fail --silent http://127.0.0.1:8000/config | python3 -m json.tool
Ожидается message_from_env: hello from ConfigMap,
message_from_mounted_file: this mounted value...,
api_key_configured: true. Значение Secret не возвращается.
Изменение и доказательства
kubectl patch configmap python-api-config -n kube-course --type merge \
-p '{"data":{"APP_MESSAGE":"env-v2","message":"file-v2"}}'
kubectl get configmap python-api-config -n kube-course \
-o jsonpath='{.data.APP_MESSAGE}{" / "}{.data.message}{"\n"}'
Подождите до 90 секунд и повторяйте /config. Ожидаемая семантика:
message_from_envостаётся старым;- смонтированный файл через некоторое время станет
file-v2; - UID Pod и число перезапусков не изменятся.
Если файл ещё старый, это не доказательство отказа: проверьте фактическое монтирование:
POD="$(kubectl get pod -n kube-course \
-l app.kubernetes.io/instance=config-lab \
-o jsonpath='{.items[0].metadata.name}')"
kubectl exec -n kube-course "$POD" -- cat /config/message
Управляемый rollout:
kubectl patch deployment python-api-config-lab -n kube-course --type merge \
-p '{"spec":{"template":{"metadata":{"annotations":{"course.example.com/config-revision":"2"}}}}}'
kubectl rollout status deployment/python-api-config-lab \
-n kube-course --timeout=120s
curl --fail --silent http://127.0.0.1:8000/config
Новый процесс видит env-v2. В эксплуатационной среде аннотация обычно содержит
детерминированный хеш отрендеренной конфигурации, а не ручной счётчик.
Безопасное создание Secret вне манифеста
read -r -s -p 'Lab API key: ' LAB_API_KEY
printf '\n'
kubectl create secret generic python-api-secret \
-n kube-course \
--from-literal=API_KEY="$LAB_API_KEY" \
--dry-run=client -o yaml |
kubectl apply -f -
unset LAB_API_KEY
Литеральное значение всё ещё может попасть в средства просмотра процессов,
аудит или историю командной оболочки; для эксплуатационной среды используйте одобренный
способ доставки секретов, стандартный ввод или файл и маскирование. Вывод
kubectl get secret ... -o yaml не включайте в заявку.
Очистка:
kubectl delete deployment python-api-config-lab -n kube-course
kubectl apply -f manifests/base/configmap.yaml
kubectl apply -f manifests/base/secret.example.yaml
Внешние системы секретов и шифрование
Kubernetes Secret решает задачу распространения через API, но не управляет жизненным циклом источника. External Secrets Operator, Secrets Store CSI Driver или интеграция поставщика — дополнения, а не базовые примитивы. Они синхронизируют или монтируют секрет из Vault, облачного KMS или менеджера секретов и требуют собственного RBAC, ротации и контракта поведения при отказе и кешировании.
Шифрование хранимых данных защищает байты Secret в etcd через сервер API
EncryptionConfiguration, желательно с провайдером KMS. Это операция администратора кластера:
существующие объекты нужно переписать после включения или ротации; резервную
копию тоже необходимо защищать. Шифрование хранимых данных не защищает секрет:
- после передачи в окружение или файл контейнера;
- от субъекта с
get secrets; - в журналах приложения и дампах аварии;
- в артефакте CI или Git.
Самостоятельная задача
Добавьте FEATURE_COLOR одновременно как смонтированный файл и переменную
окружения. Обновите ConfigMap, измерьте фактическое время обновления файла,
затем выполните rollout. Запишите UID Pod и restartCount на каждом шаге.
Критерий приёмки: объяснены слепок окружения, проекция с согласованием через
некоторое время и замена Pod.
Сломанный манифест: отсутствующий ключ
apiVersion: apps/v1
kind: Deployment
metadata:
name: broken-config
namespace: kube-course
labels:
app.kubernetes.io/name: broken-config
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: broken-config
template:
metadata:
labels:
app.kubernetes.io/name: broken-config
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: api
image: kube-python-course:1.0.0
env:
- name: REQUIRED_SETTING
valueFrom:
configMapKeyRef:
name: python-api-config
key: KEY_DOES_NOT_EXIST
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
Ожидаемый результат:
kubectl apply -f /tmp/broken-config.yaml
kubectl get pods -n kube-course -l app.kubernetes.io/name=broken-config
kubectl describe pod -n kube-course \
-l app.kubernetes.io/name=broken-config
Причиной ожидания Pod будет CreateContainerConfigError; Event назовёт
отсутствующий ключ. Журналов приложения нет, потому что процесс не запустился.
Исправление: исправить ключ или осознанно добавить его, после чего повтор
контроллер запустит контейнер. Очистка:
kubectl delete deployment broken-config -n kube-course.
Дерево диагностических решений
Конфигурация выглядит неверно
├─ Pod не стартовал
│ ├─ Event сообщает `not found` → имя объекта/namespace
│ ├─ `couldn't find key` → написание ключа/обязательный контракт
│ └─ `admission denied` → политика/тип Secret/безопасность
└─ Pod Running
├─ старое окружение → ожидаемый снимок; требуется rollout
├─ старый подключённый файл
│ ├─ subPath? → автоматического обновления нет
│ ├─ объект действительно обновлён? → проверить поля `resourceVersion` и `data`
│ └─ задержка проекции/кэш приложения → проверить подключение, затем перезагрузку приложения
└─ Secret раскрыт → сначала сменить/отозвать, затем расследовать/проверять аудит
Типичные ошибки: считать base64 шифрованием; хранить секрет в Git; широкое
envFrom с конфликтующими именами; rollout каждую секунду при внешней ротации;
журналирование окружения; один Secret для несвязанных приложений; неизменяемый
ConfigMap без версионного имени и плана rollout.
Эксплуатационные компромиссы, безопасность и стоимость
Большие или часто меняющиеся ConfigMaps создают нагрузку на API и наблюдение; у
объектов API есть ограничения размера, и они не заменяют хранилище артефактов.
Версионная неизменяемая конфигурация повышает воспроизводимость, но требует
сборки мусора. Более частая ротация сокращает окно компрометации, но увеличивает
нагрузку на зависимости и плоскость управления. Для минимальных привилегий рабочей
нагрузке обычно не нужен get secrets через API, если kubelet уже проецирует
точный ключ. Ограничьте команды debug/exec, маскируйте телеметрию и аудитируйте чтения.
Самопроверка
- Меняется ли окружение после изменения ConfigMap?
- Почему смонтированный файл может измениться без перезапуска?
- Защищает ли base64 от чтения?
- Что проверять при
CreateContainerConfigError? - Что даёт шифрование хранимых данных и чего оно не даёт?
- Почему автоматическая ротация требует контракта приложения?
Ответы: 1) нет; 2) kubelet обновляет проецируемый том, а приложение должно перечитать файл; 3) нет; 4) Events и точные объект, ключ и namespace; 5) байты etcd и резервные копии при правильной настройке, но не раскрытие во время исполнения; 6) соединения, кеш и обновление должны пережить смену учётных данных.
Резюме и источники
Конфигурация имеет контракт доставки и обновления. Secret — чувствительный объект, а не волшебным образом зашифрованное значение. Далее: Service, DNS и Ingress.