kai.kzLab

CreateContainerConfigError: почему kubelet не может собрать конфигурацию контейнера

Команда KAI Lab · · 7 мин чтения

CreateContainerConfigError значит, что kubelet не смог подготовить конфигурацию контейнера перед запуском, поэтому контейнер даже не создан. Почти всегда причина — ссылка на Secret или ConfigMap, которого нет в пространстве имён пода, или на ключ, которого нет внутри них; реже — securityContext, например runAsNonRoot при образе, который запускается от root.

Хорошая новость: kubelet пишет причину прямым текстом в событии пода. Плохая: её легко спутать с падением приложения, если смотреть только на статус. Разберём, как это выглядит, где взять точный текст ошибки и как чинить каждую причину.

Что происходит при CreateContainerConfigError

Прежде чем создать контейнер, kubelet собирает его конфигурацию: подставляет переменные окружения из ConfigMap и Secret, готовит точки монтирования, применяет securityContext и проверяет, от какого пользователя запустится образ. Если любой шаг не удался, контейнер не создаётся. Он остаётся в состоянии Waiting с причиной CreateContainerConfigError, а kubelet пишет событие Failed с текстом ошибки и через некоторое время пробует снова.

Отсюда три отличия от CrashLoopBackOff:

  • RESTARTS остаётся 0: контейнер ни разу не запускался;
  • логов приложения нет, kubectl logs ничего не покажет;
  • фаза пода — Pending, хотя нода уже назначена.

Как выглядит ошибка

$ kubectl get pods -n shop
NAME                      READY   STATUS                       RESTARTS   AGE
billing-6c9d7f5b8-q2w4e   0/1     CreateContainerConfigError   0          2m
$ kubectl describe pod billing-6c9d7f5b8-q2w4e -n shop
...
    State:          Waiting
      Reason:       CreateContainerConfigError
    Ready:          False
    Restart Count:  0
    Environment:
      DB_HOST:      <set to the key 'DB_HOST' of config map 'app-config'>  Optional: false
      DB_PASSWORD:  <set to the key 'password' in secret 'db-credentials'>  Optional: false
...
Events:
  Type     Reason     Age                From               Message
  ----     ------     ----               ----               -------
  Normal   Scheduled  2m                 default-scheduler  Successfully assigned shop/billing-6c9d7f5b8-q2w4e to worker-2
  Normal   Pulled     11s (x12 over 2m)  kubelet            Container image "registry.example.kz/shop/billing:1.8.0" already present on machine
  Warning  Failed     11s (x12 over 2m)  kubelet            Error: secret "db-credentials" not found

Блок Environment сразу показывает, откуда контейнер ждёт каждую переменную и обязательна ли она (Optional: false). Событие Failed говорит, чего именно не хватило.

Какие бывают сообщения

Тексты ошибок формирует kubelet, и они почти дословно подсказывают решение:

  • Error: secret "db-credentials" not found — Secret с таким именем нет в пространстве имён пода;
  • Error: configmap "app-config" not found — то же для ConfigMap;
  • Error: couldn't find key password in Secret shop/db-credentials — объект есть, а ключа в нём нет;
  • Error: couldn't find key DB_HOST in ConfigMap shop/app-config — то же для ConfigMap;
  • Error: container has runAsNonRoot and image will run as root (pod: "…", container: billing) — политика запрещает root, а образ запускается от root;
  • Error: container has runAsNonRoot and image has non-numeric user (app), cannot verify user is non-root (pod: "…", container: billing) — в образе пользователь задан именем, и kubelet не может проверить, что это не root.

Для нескольких подов текст удобнее достать из статуса:

$ kubectl get pod billing-6c9d7f5b8-q2w4e -n shop -o jsonpath='{.status.containerStatuses[0].state.waiting.message}'
secret "db-credentials" not found

Пошаговая диагностика

Шаг 1. Проверить, есть ли объект и где он

$ kubectl get secret db-credentials -n shop
Error from server (NotFound): secrets "db-credentials" not found
$ kubectl get secrets -A --field-selector metadata.name=db-credentials
NAMESPACE   NAME             TYPE     DATA   AGE
default     db-credentials   Opaque   2      3d

Классика: секрет создали в default, а сервис живёт в shop. Документация Kubernetes прямо говорит, что под может ссылаться только на ConfigMap из своего пространства имён, и для Secret правило то же.

Шаг 2. Проверить ключи

kubectl describe secret показывает имена ключей и их размер, но не значения, поэтому команду можно спокойно запускать при коллегах:

$ kubectl describe secret db-credentials -n shop
Name:         db-credentials
Namespace:    shop
Labels:       <none>
Annotations:  <none>

Type:  Opaque

Data
====
DB_PASSWORD:  24 bytes
DB_USER:      7 bytes

В манифесте ждут ключ password, а в секрете он называется DB_PASSWORD. Ключи чувствительны к регистру. Для ConfigMap ключи и значения видно так:

$ kubectl get configmap app-config -n shop -o jsonpath='{.data}'
{"DB_HOST":"postgres.shop.svc","LOG_LEVEL":"info"}

Шаг 3. Создать недостающее или исправить ссылку

$ kubectl create secret generic db-credentials -n shop \
    --from-literal=DB_USER=billing \
    --from-literal=DB_PASSWORD="$DB_PASSWORD"

Или исправить key: в манифесте, если ключ просто назван иначе. Пересоздавать под после появления секрета не нужно: kubelet сам повторяет попытку, и на следующей контейнер запустится. Если же поменялся шаблон Deployment, контроллер создаст новые поды.

Шаг 4. Если секрет создаёт кто-то другой

Часто Secret появляется не из вашего манифеста: его создаёт оператор вроде External Secrets из хранилища секретов, отдельный шаг пайплайна или Helm-чарт другой команды. Тогда CreateContainerConfigError — симптом, а причина — в этом инструменте: он ещё не успел синхронизировать секрет, у него нет доступа к хранилищу или изменилось имя. Проверьте его статус и логи, прежде чем создавать секрет руками: иначе оператор перезапишет или удалит вашу копию.

runAsNonRoot и пользователь образа

Если в securityContext стоит runAsNonRoot: true, kubelet перед запуском проверяет, от кого запустится контейнер. Образ без инструкции USER запускается от root, и kubelet откажется его запускать. Образ с USER app задаёт пользователя именем, и проверить UID kubelet не может. Решение — указать числовой UID:

securityContext:
  runAsNonRoot: true
  runAsUser: 10001

Или прописать USER 10001 в Dockerfile. Убедитесь, что у этого пользователя есть права на файлы, которые приложение читает и пишет, иначе следующей ошибкой станет permission denied уже в логах приложения.

Похожая ошибка: Secret подключён как том

Если отсутствующий Secret или ConfigMap подключён не как переменные окружения, а как том, статус будет другим. Под зависнет в ContainerCreating, а в событиях появится FailedMount:

Warning  FailedMount  18s (x9 over 2m)  kubelet  MountVolume.SetUp failed for volume "tls" : secret "api-tls" not found

Лечится так же: создать секрет в пространстве имён пода или исправить имя. Если это TLS-секрет с сертификатом, заодно проверьте срок его действия: как не пропустить истечение SSL-сертификата, мы разобрали отдельно. А если не хватает секрета для скачивания образа, будет событие FailedToRetrieveImagePullSecret и статус ImagePullBackOff.

Причины CreateContainerConfigError: таблица

ПричинаКак узнатьКак исправить
Нет SecretError: secret "…" not foundсоздать Secret в пространстве имён пода или исправить имя
Нет ConfigMapError: configmap "…" not foundсоздать ConfigMap или исправить имя
Нет ключаcouldn't find key … in Secret … или in ConfigMap …добавить ключ или исправить key: с учётом регистра
Объект в другом пространстве имёнkubectl get secrets -A находит его в чужом namespaceсоздать объект рядом с подом
Секрет ещё не создан оператором или пайплайномобъекта нет, у оператора ошибки синхронизациичинить оператор или порядок деплоя
runAsNonRoot и root в образеcontainer has runAsNonRoot and image will run as rootrunAsUser с числовым UID или USER 10001 в образе
Пользователь образа задан именемimage has non-numeric userчисловой UID в runAsUser или в Dockerfile

Если настройка действительно необязательна, отметьте ссылку optional: true. По документации, при отсутствии ConfigMap или ключа переменная тогда просто останется пустой, и под запустится:

env:
- name: FEATURE_FLAGS
  valueFrom:
    configMapKeyRef:
      name: feature-flags
      key: FLAGS
      optional: true

Частые вопросы

Чем CreateContainerConfigError отличается от CrashLoopBackOff? При CreateContainerConfigError контейнер ни разу не запускался: kubelet не смог собрать его конфигурацию. При CrashLoopBackOff контейнер запускается и падает. Отличить просто: у первого RESTARTS 0 и нет логов, у второго счётчик растёт, а в kubectl logs --previous есть вывод приложения.

Нужно ли пересоздавать под после того, как создал Secret? Нет. kubelet повторяет попытки сам и на следующей запустит контейнер. Пересоздание понадобится, только если нужно поменять сам манифест: имя секрета, ключ или securityContext.

Поменял значение в ConfigMap, а приложение видит старое. Почему? Переменные окружения читаются один раз при старте контейнера. Документация Kubernetes: ConfigMap в переменных окружения не обновляются автоматически, нужен перезапуск пода, например kubectl rollout restart deployment/billing -n shop. Подключённые томом ConfigMap обновляются сами, кроме тех, что смонтированы через subPath.

Почему kubectl describe не показывает значение секрета? Так задумано: kubectl describe secret выводит только ключи и их размер в байтах, чтобы значения не попадали на экран и в логи. Посмотреть значение можно через kubectl get secret -o jsonpath с декодированием из base64, но делайте это осознанно.

Работают ли ConfigMap в статических подах? Нет. Документация отдельно оговаривает, что kubelet не поддерживает ConfigMap для static pods: такие поды не могут ссылаться на объекты API. Конфигурацию для них кладут в файлы на ноде. Этот и соседние вопросы полезно повторить перед собеседованием по Kubernetes.

Потренироваться на практике

Такие ошибки проще всего запомнить, когда разбираешь их сам. В симуляторе DevOps KAI Lab месяц про Kubernetes построен на тикетах от коллег: поды, которые не стартуют, конфигурация, выкатки и ноды на обслуживании. Консоль в браузере отдаёт реальные выводы kubectl. Начать можно с бесплатного теста навыков DevOps.

Источники

Читать дальше