Kube/Pythonproduction course
Курс · RU03-configuration-and-secrets.md

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, маскируйте телеметрию и аудитируйте чтения.

Самопроверка

  1. Меняется ли окружение после изменения ConfigMap?
  2. Почему смонтированный файл может измениться без перезапуска?
  3. Защищает ли base64 от чтения?
  4. Что проверять при CreateContainerConfigError?
  5. Что даёт шифрование хранимых данных и чего оно не даёт?
  6. Почему автоматическая ротация требует контракта приложения?

Ответы: 1) нет; 2) kubelet обновляет проецируемый том, а приложение должно перечитать файл; 3) нет; 4) Events и точные объект, ключ и namespace; 5) байты etcd и резервные копии при правильной настройке, но не раскрытие во время исполнения; 6) соединения, кеш и обновление должны пережить смену учётных данных.

Резюме и источники

Конфигурация имеет контракт доставки и обновления. Secret — чувствительный объект, а не волшебным образом зашифрованное значение. Далее: Service, DNS и Ingress.