imagePullPolicy и imagePullSecrets
Когда Pod планируется на node, kubelet должен где-то взять образ: либо использовать уже скачанный, либо тянуть из registry. Решение принимает поле imagePullPolicy. Если registry приватный — нужны credentials, которые передаются через imagePullSecrets. Эти две темы дают второй по частоте класс ошибок в production: ImagePullBackOff и ErrImagePull. Разбираемся в политиках и в том, как привязывать registry-credentials к Pod через сам Pod или через ServiceAccount.
Теги и digests: mutable label vs immutable id
Три значения imagePullPolicy
spec.containers[].imagePullPolicy определяет, когда kubelet тянет образ из registry:
| Policy | Поведение | Use case |
|---|---|---|
Always | Pull каждый раз | Latest или mutable теги, dev окружения, security-critical |
IfNotPresent | Pull если нет локально | Production с immutable теги или digest |
Never | Не pull никогда | Air-gapped кластеры, preloaded images |
Default rule: что подразумевается, когда policy не указана
Это любимый CKAD trick-question. Если imagePullPolicy явно не задан:
- Если тег образа =
latest→ default становитсяAlways. - Если тега нет вообще (
image: nginx, что эквивалентноnginx:latest) →Always. - Если тег указан и не
latest(nginx:1.27) → default становитсяIfNotPresent. - Если образ указан через digest (
nginx@sha256:abc..., с тегом или без) → defaultIfNotPresent. Digest immutable, повторный pull бессмыслен.
# imagePullPolicy не указан — будет Always (тег latest)
- image: myapp
- image: myapp:latest
# imagePullPolicy не указан — будет IfNotPresent
- image: myapp:1.0.3
# imagePullPolicy не указан — будет IfNotPresent (digest immutable)
- image: myapp@sha256:abc123...
# imagePullPolicy: Always явно — переопределяет default
- image: myapp:1.0.3
imagePullPolicy: Always
Это значит, что использование :latest даёт двойной эффект: (1) tag mutable; (2) kubelet тянет на каждый restart. На больших кластерах с rolling updates это создаёт реальную нагрузку на registry и замедляет старты.
Когда IfNotPresent ломается
IfNotPresent быстр и эффективен, но имеет неочевидную ловушку. Допустим, вы делаете CI/CD с тегом myapp:v2-beta и переписываете этот тег при каждом push новой версии (mutable tag pattern):
- Pod на node-A pull-ил
myapp:v2-beta→ получил digest abc. - Вы пушите новый
myapp:v2-beta→ digest стал def. - Pod на node-B pull-ит → получает def (новую версию).
- На node-A
IfNotPresent— kubelet видит, что образ есть, и НЕ pull-ит. Pod на node-A работает по старому digest abc.
Получается split-brain: одна и та же image: myapp:v2-beta крутит две разные версии кода на разных nodes. Решение — либо imagePullPolicy: Always для mutable тегов, либо использовать immutable теги / digest pinning.
Private registries: imagePullSecrets
Публичные registries (Docker Hub anonymously, registry.k8s.io) не требуют auth. Приватные (внутрикорпоративные Harbor, ECR, GHCR с private repos) — требуют. kubelet должен предъявить credentials при pull. Это делается через imagePullSecrets.
imagePullSecret — это Secret типа kubernetes.io/dockerconfigjson. Внутри — base64-кодированный JSON в формате ~/.docker/config.json:
{
"auths": {
"registry.example.com": {
"username": "ci-bot",
"password": "ghp_xxx",
"email": "[email protected]",
"auth": "Y2ktYm90Omdocl94eHg="
}
}
}
Создание через kubectl — проще, чем вручную:
kubectl create secret docker-registry my-registry-creds \
--docker-server=registry.example.com \
--docker-username=ci-bot \
--docker-password='ghp_xxx' \
[email protected] \
-n myapp
Использование в Pod:
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
imagePullSecrets:
- name: my-registry-creds
containers:
- name: app
image: registry.example.com/team/app:1.0
kubelet при pull увидит imagePullSecrets, прочитает Secret, выберет нужный entry в auths по hostname registry и пошлёт credentials.
imagePullSecrets — список (можно указать несколько). kubelet попробует каждый, пока pull не удастся. Полезно, когда образ может тянуться из разных registries.
Killer момент: imagePullSecrets в ServiceAccount
Указывать imagePullSecrets в каждом Pod — утомительно. Альтернативный (и часто более правильный) подход — привязать секрет к ServiceAccount. Каждый Pod в кластере привязан к ServiceAccount (default, если не указан явно). Если у ServiceAccount задан imagePullSecrets, kubelet автоматически применит его ко всем Pod-ам с этим SA.
apiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: myapp
imagePullSecrets:
- name: my-registry-creds
Теперь любой Pod в namespace myapp с spec.serviceAccountName: default (или без указания SA — он и так default) получит этот pull-secret. Удобно для multi-team setups: команда раз настраивает SA, дальше все деплои работают.
# Pod может не указывать ServiceAccount — будет использован default
apiVersion: v1
kind: Pod
metadata:
name: myapp
namespace: myapp
spec:
containers:
- name: app
image: registry.example.com/team/app:1.0
# imagePullSecrets не указан, но возьмётся из ServiceAccount default
Это самый production-friendly способ организовать pull-creds: единое место, RBAC-контролируемое, не дублируется в Pod-spec.
ImagePullBackOff: troubleshooting
ImagePullBackOff — kubelet не смог скачать образ и ждёт по backoff перед следующей попыткой. Backoff растёт так же, как для CrashLoopBackOff (10s → 20s → … → 300s cap). Перед ним обычно мелькает ErrImagePull (свежая ошибка).
Алгоритм диагностики:
# 1. Что говорит kubelet — Events
kubectl describe pod <name>
# Events:
# Failed to pull image "registry.example.com/team/app:1.0": rpc error: code = Unknown
# desc = failed to pull and unpack image: failed to resolve reference: pulling from
# host registry.example.com failed with status code [manifests 1.0]: 401 Unauthorized
Причины и их симптомы:
# 2. Проверим, что Secret вообще существует
kubectl get secret my-registry-creds -n myapp
# 3. И что он типа dockerconfigjson
kubectl get secret my-registry-creds -n myapp -o jsonpath='{.type}'
# kubernetes.io/dockerconfigjson
# 4. Содержимое (base64-decoded для дебага)
kubectl get secret my-registry-creds -n myapp -o jsonpath='{.data.\.dockerconfigjson}' \
| base64 -d | jq
# 5. Привязан ли к ServiceAccount?
kubectl get sa default -n myapp -o yaml | yq '.imagePullSecrets'
Self-hosted registries (Harbor, in-cluster registry) часто используют self-signed TLS. kubelet по умолчанию отвергнет такой сертификат и выдаст x509: certificate signed by unknown authority. Решение: добавить CA в trust store узла (/etc/containerd/certs.d/) или, для dev, разрешить insecure_skip_verify в конфиге containerd. Это уровень администратора кластера, не Pod-spec.
Обзор: где конкретно живёт image cache
Для понимания: после pull образ хранится в content store containerd (по умолчанию /var/lib/containerd). На каждой node — свой кэш. Когда вы пересоздаёте Pod на той же node — IfNotPresent использует этот кэш. Когда Pod планируется на новый node — pull повторяется. Поэтому в больших кластерах с высоким churn-ом полезно node image preloading или registry mirror внутри кластера.