Настройка приложений Kubernetes с помощью ConfigMap и Secret

KubernetesBeginner
Практиковаться сейчас

Введение

Образы контейнеров должны быть пригодны для повторного использования в разных окружениях. Kubernetes хранит несекретные параметры в ConfigMap, а учетные данные — в Secret, после чего передает оба типа объектов в контейнеры через переменные окружения или файлы.

Вы настроите одно приложение с использованием обоих механизмов, безопасно проверите результат, обновите конфигурацию и увидите, почему переменные окружения и смонтированные тома обновляются по-разному.

Цель этой работы — не просто создать два API-объекта. Вы проследите путь каждого значения: от хранилища Kubernetes до работающего контейнера, затем измените значения и увидите, как способ их передачи влияет на обновление.

Создание конфигурации приложения

На этом шаге создайте пространство имён и ConfigMap на основе литеральных значений. ConfigMap хранит строковые ключи в разделе data; эти значения не шифруются.

Сначала перейдите в рабочий каталог лаборатории. Пространство имён объединяет все объекты этого примера и не позволяет случайно выбрать одноимённую конфигурацию из другой лабораторной работы.

cd /home/labex/project/application-config
kubectl create namespace practice-config
kubectl create configmap app-settings -n practice-config --from-literal=APP_MODE=production --from-literal=LOG_LEVEL=info
kubectl get configmap app-settings -n practice-config -o yaml

Каждый параметр --from-literal=KEY=VALUE добавляет одну запись. Вывод в формате YAML показывает сохранённые ключи и подходит для проверки несекретной конфигурации.

Найдите в выводе раздел data: с ключами APP_MODE и LOG_LEVEL. Kubernetes сохраняет требуемые значения, но пока никто их не использует: само создание ConfigMap не изменяет Pod.

Соответствующая часть вывода намеренно содержит обычный открытый текст:

data:
  APP_MODE: production
  LOG_LEVEL: info
kind: ConfigMap

Такие поля, как uid, resourceVersion и creationTimestamp, генерируются Kubernetes и будут отличаться. На этом шаге анализируйте только раздел data, поскольку именно в нём находится переданная вами конфигурация.

Безопасное создание Secret

На этом шаге создайте Secret, не помещая его значение в YAML-файл. Данные Secret кодируются в API с помощью base64, но не шифруются автоматически только потому, что объект имеет тип Secret; поэтому по-прежнему важны RBAC и шифрование данных в состоянии покоя.

generic означает, что это обычный Secret в формате «ключ-значение», а не специализированный Secret для TLS или загрузки образа. Кавычки вокруг пароля не позволяют оболочке интерпретировать знаки пунктуации, если позже в значении появятся специальные символы.

kubectl create secret generic app-credentials -n practice-config --from-literal=username=appuser --from-literal=password='practice-pass'
kubectl get secret app-credentials -n practice-config
kubectl get secret app-credentials -n practice-config -o jsonpath='{.data.username}' | base64 --decode; echo

Последняя конвейерная команда декодирует только имя пользователя — это сделано для демонстрации. Не выводите пароли в обычных операционных журналах и результатах команд.

Символ вертикальной черты передаёт закодированный результат JSONPath в base64 --decode. Точка с запятой запускает завершающую команду echo, которая добавляет перевод строки после декодированного значения, чтобы следующий приглашение оболочки отображалось корректно. Значение appuser показывает принцип кодирования, а также демонстрирует, почему base64 нельзя считать границей безопасности.

Средняя команда выводит метаданные, не раскрывая ни одно из значений:

NAME              TYPE     DATA   AGE
app-credentials   Opaque   2      ...
appuser

DATA=2 означает, что Secret содержит два ключа, а не то, что его размер составляет два байта. Opaque — это тип API для обычного Secret в формате «ключ-значение».

Передача конфигурации в Pod

На этом шаге используйте значения ConfigMap как переменные окружения, а Secret смонтируйте в контейнер в виде файлов, доступных только для чтения.

Перед созданием Pod разделите в манифесте два способа передачи данных:

  • envFrom превращает каждый ключ ConfigMap в переменную окружения контейнера.
  • volumes подключает Secret к Pod, а volumeMounts выбирает каталог, в котором этот том будет доступен внутри конкретного контейнера.

Цикл внутри Pod не выполняет работу приложения — он лишь поддерживает контейнер запущенным, чтобы вы могли проверить оба способа передачи данных.

cd /home/labex/project/application-config
cat <<'EOF' > configured-app.yaml
apiVersion: v1
kind: Pod
metadata:
  name: configured-app
  namespace: practice-config
spec:
  containers:
    - name: app
      image: busybox:1.36
      imagePullPolicy: IfNotPresent
      command: ["sh", "-c", "while true; do sleep 30; done"]
      envFrom:
        - configMapRef:
            name: app-settings
      volumeMounts:
        - name: credentials
          mountPath: /etc/app-credentials
          readOnly: true
  volumes:
    - name: credentials
      secret:
        secretName: app-credentials
EOF

Примените манифест и дождитесь готовности Pod:

kubectl apply -f configured-app.yaml
kubectl wait --for=condition=Ready pod/configured-app -n practice-config --timeout=60s

Теперь проверьте каждый способ передачи данных изнутри контейнера. printenv читает переменные окружения процесса, а ls показывает файлы, предоставленные томом Secret:

kubectl exec configured-app -n practice-config -- printenv APP_MODE LOG_LEVEL
kubectl exec configured-app -n practice-config -- ls -l /etc/app-credentials

envFrom импортирует все ключи ConfigMap. Том Secret создаёт отдельный файл для каждого ключа и подключается внутри контейнера в режиме только для чтения.

Ожидаемый вывод переменных окружения — сначала production, затем info. В каталоге должны находиться файлы username и password; имена файлов соответствуют ключам Secret.

В работающем контейнере список выглядит примерно так:

production
info
total 0
lrwxrwxrwx ... password -> ..data/password
lrwxrwxrwx ... username -> ..data/username

Эти элементы являются символическими ссылками, которыми управляет Kubernetes. Благодаря такой косвенной структуре kubelet может переключить смонтированные данные на новую версию, не перезаписывая файл в момент, когда приложение его читает. При этом приложение по-прежнему обращается к привычным путям /etc/app-credentials/username и /etc/app-credentials/password.

Обновление данных смонтированного Secret

На этом шаге обновите Secret и проверьте содержимое смонтированного файла. Команда kubectl create ... --dry-run=client -o yaml | kubectl apply -f - локально формирует требуемый YAML и передаёт его напрямую в apply; символ - означает стандартный ввод.

Такой подход удобен для обновления объекта, созданного императивно, без его удаления. Левая часть команды выводит полный манифест Secret, а правая передаёт этот манифест API-серверу.

kubectl create secret generic app-credentials -n practice-config --from-literal=username=serviceuser --from-literal=password='rotated-pass' --dry-run=client -o yaml | kubectl apply -f -

Поскольку первоначально Secret был создан с помощью kubectl create, перед строкой успешного выполнения при первом обновлении может появиться такое предупреждение:

Warning: resource secrets/app-credentials is missing the kubectl.kubernetes.io/last-applied-configuration annotation ...
secret/app-credentials configured

Предупреждение означает, что у apply не было предыдущего декларативного снимка, с которым можно было бы сравнить изменения. Во время этого обновления Kubernetes автоматически добавит аннотацию, поэтому сообщение configured подтверждает успех; удалять и создавать Secret заново не требуется.

Kubelet обновляет содержимое проецируемого тома Secret асинхронно. Выполняйте проверку, пока не появится новое имя пользователя:

current_username=""
for attempt in $(seq 1 24); do
  current_username=$(kubectl exec configured-app -n practice-config -- cat /etc/app-credentials/username)
  if [ "$current_username" = "serviceuser" ]; then
    break
  fi
  sleep 5
done
echo "$current_username"
test "$current_username" = "serviceuser"

Цикл повторяет проверку до двух минут, поскольку проецирование томов согласуется не мгновенно. В тестовом окружении объект API изменился сразу, однако для обновления смонтированного файла потребовалось больше минуты.

Конструкция $(...) называется подстановкой команды: оболочка выполняет seq 1 24 и использует полученные числа в качестве значений цикла. На каждой итерации содержимое текущего файла сохраняется в current_username; break останавливает цикл при первом совпадении, а sleep 5 не позволяет выполнять непрерывный опрос API. Последняя команда test возвращает ошибку, если значение так и не изменилось, поэтому выполнение не продолжится незаметно со старыми данными. При успешном выполнении выводом будет serviceuser.

Обновление конфигурации переменных окружения

На этом шаге измените LOG_LEVEL и сравните его значение с тем, что видит работающий Pod. Переменные окружения фиксируются при запуске контейнера, поэтому изменение ConfigMap не изменяет окружение уже работающего процесса.

Патч слияния изменяет только одну запись в разделе data. Немедленная проверка работающего процесса демонстрирует важное отличие от смонтированного тома:

kubectl patch configmap app-settings -n practice-config --type=merge -p '{"data":{"LOG_LEVEL":"debug"}}'
kubectl exec configured-app -n practice-config -- printenv LOG_LEVEL

Pod по-прежнему выведет info. Чтобы получить новое значение, пересоздайте его из сохранённого манифеста:

configmap/app-settings patched
info

Это ожидаемый результат, а не признак неудачного обновления: ConfigMap уже содержит debug, но работающий процесс всё ещё использует окружение, созданное при запуске.

cd /home/labex/project/application-config
kubectl delete pod configured-app -n practice-config --wait=true
kubectl apply -f configured-app.yaml
kubectl wait --for=condition=Ready pod/configured-app -n practice-config --timeout=60s
kubectl exec configured-app -n practice-config -- printenv LOG_LEVEL

Новый Pod выведет debug. Именно поэтому в рабочих окружениях после изменения конфигурации, передаваемой через переменные окружения, обычно запускают новый rollout.

pod "configured-app" deleted
pod/configured-app created
pod/configured-app condition met
debug

Сам манифест по-прежнему ссылается на ConfigMap по имени; значение не копировалось в configured-app.yaml. При пересоздании Pod kubelet снова разрешает эту ссылку и передаёт последнюю версию данных.

Итоги

Вы отделили настройки приложения и учетные данные от образа, использовали данные ConfigMap как переменные окружения, смонтировали данные Secret в виде файлов, обновили проецируемый Secret и пересоздали Pod, чтобы обновить конфигурацию, передаваемую через переменные окружения.