18.1. Какие задачи решают Docker и Kubernetes
Цели
После этого материала вы сможете:
- сформулировать задачу каждого инструмента одним предложением и не путать их;
- объяснить, почему отказ от
dockershimне затронул образы; - назвать, что именно связывает Docker и Kubernetes, и проверить это на своём образе;
- объяснить, что такое CRI и почему он появился;
- назвать container runtime, применяемые в Kubernetes, и их отношение к Docker;
- определить, какие ваши навыки переносятся в Kubernetes без изменений.
Предварительные знания
- 17.2. containerd, shim и runc;
- 14.1. Основы registry — формат образов;
- 11.3. Healthchecks и readiness.
Ключевые термины
| Термин | Объяснение |
|---|---|
OCI | Набор стандартов: формат образа, среда выполнения, распространение |
CRI | Интерфейс Kubernetes к среде выполнения container'ов |
dockershim | Слой совместимости Kubernetes с Docker, удалённый в 1.24 |
kubelet | Агент Kubernetes на каждом узле |
декларативность | Описывается желаемое состояние, а не действия |
Теория
Две разные задачи
| Docker | Kubernetes | |
|---|---|---|
| Задача | Собрать образ и запустить container на машине | Поддерживать заданное состояние на кластере |
| Единица | Container | Pod |
| Модель | Императивная: выполни команду | Декларативная: приведи к состоянию |
| Что происходит при сбое | Container останавливается | Состояние восстанавливается |
| Область | Одна машина | Множество машин |
Формулировка «Kubernetes заменяет Docker» неточна, потому что сравнивает инструменты разного уровня.
Точнее так: Docker решает задачу упаковки и запуска; Kubernetes решает задачу поддержания состояния. Второе требует первого, но не отменяет его.
Императивная и декларативная модель
Императивно (Docker) Декларативно (Kubernetes)
────────────────────── ─────────────────────────
docker run -d --name web ... apiVersion: apps/v1
kind: Deployment
выполняется один раз spec:
replicas: 3
если упадёт — останется лежать
система непрерывно приводит
фактическое состояние
к описанному
Практическое различие: docker run --restart unless-stopped перезапускает container при завершении процесса. Kubernetes восстанавливает состояние при чём угодно: узел выключился, pod удалён, реплик стало меньше заданного.
Это не «лучше» — это другой класс задач и другая цена (урок 18.4).
Что связывает: OCI
┌─────────────────────────────────┐
│ стандарты OCI │
├─────────────────────────────────┤
│ Image Specification │ формат образа
│ Runtime Specification │ bundle и config.json
│ Distribution Specification │ протокол registry
└─────────────────────────────────┘
▲ ▲
│ │
docker build Kubernetes
docker push kubelet → CRI → containerd → runc
Образ, собранный docker build, соответствует OCI Image Specification (урок 14.1). Kubernetes скачивает его по Distribution Specification и запускает через Runtime Specification (урок 17.2).
Ни на одном шаге Docker как программа не участвует.
Отсюда главный практический вывод раздела: образы переносятся без изменений. Dockerfile, слои, теги, digest, multi-stage сборка, сканирование, SBOM — всё это работает одинаково.
Что такое CRI и зачем он появился
Kubelet должен запускать container'ы. Ранние версии умели говорить только с Docker — напрямую, специальным кодом внутри kubelet.
Это создавало две проблемы: поддержка других сред выполнения требовала кода в самом Kubernetes, а Docker при этом решал задачи, kubelet не нужные.
Container Runtime Interface — интерфейс gRPC, который kubelet использует вместо прямых вызовов:
kubelet ──CRI──► среда выполнения ──► containerd/runc ──► процесс
Любая среда, реализующая CRI, работает с Kubernetes. Код kubelet при этом один для всех.
dockershim: что произошло
Docker Engine не реализует CRI — он появился раньше и говорит на своём API (урок 17.1).
Для совместимости в kubelet держали слой перевода — dockershim:
До 1.24: kubelet → dockershim → Docker Engine → containerd → runc
После: kubelet → CRI ────────────────────────► containerd → runc
Из цепочки убрали два звена. Docker Engine перестал быть частью пути.
| Что изменилось | Что не изменилось |
|---|---|
| Kubelet не общается с Docker Engine | Формат образов |
| На узлах Kubernetes Docker не нужен | docker build и docker push |
docker ps не покажет pod'ы кластера | Registry и протокол |
Диагностика на узлах — через crictl | Ваши навыки и Dockerfile |
Формулировка «Kubernetes отказался от Docker» породила недоразумение: многие решили, что образы docker build перестанут работать.
Они работают. Убран слой совместимости внутри kubelet, а не поддержка формата образов. Формат стандартизован OCI, и его никто не менял.
Среды выполнения в Kubernetes
| Среда | Особенность | Отношение к Docker |
|---|---|---|
containerd | Наиболее распространённая | Тот же, что внутри Docker |
CRI-O | Создана специально для Kubernetes | Независимая реализация |
Docker Engine (через cri-dockerd) | Внешний адаптер вместо dockershim | Требует установки |
Первая строка снимает большую часть недоумения: containerd, работающий на узлах Kubernetes, — тот же самый компонент, который Docker использует у вас на машине.
Разница в том, кто им управляет: dockerd или kubelet через CRI.
Что переносится из ваших навыков
| Навык | Переносится |
|---|---|
Dockerfile, multi-stage, кэш слоёв | Полностью |
| Размер образа, выбор базового | Полностью |
| Пользователь, capabilities, read-only | Полностью, с другим синтаксисом |
| Теги, digest, стратегия публикации | Полностью |
| Healthchecks | Понятийно; в Kubernetes три вида проб |
| Лимиты ресурсов | Понятийно; добавляются requests |
| Логи в stdout | Полностью |
| Compose-файлы | Не переносятся: другая модель |
docker run и его флаги | Не переносятся напрямую |
| Сети Docker | Не переносятся: другая модель |
Восемь пунктов из десяти переносятся. Это и есть ответ на вопрос «не зря ли я учил Docker, если у нас Kubernetes».
Внутренний механизм
Почему образ работает и там и там
Образ — это набор слоёв (архивы tar) и конфигурация (JSON), связанные манифестом. Формат описан OCI Image Specification.
Kubelet через CRI просит среду выполнения скачать образ по имени. Среда обращается к registry по Distribution Specification — тому же протоколу, что использует docker pull (урок 14.1).
Затем среда разворачивает слои и формирует bundle по Runtime Specification, который исполняет runc — тот же, что в Docker (урок 17.2).
Ни одного шага, где формат образа зависел бы от Docker.
Почему docker ps не видит pod'ы
На узле Kubernetes с containerd container'ы создаёт kubelet через CRI. Они находятся в пространстве имён containerd k8s.io, а не moby (урок 17.2).
docker ps обращается к dockerd, который знает только о своём пространстве moby. Container'ы Kubernetes ему не видны — даже если Docker установлен.
Диагностика на узле выполняется через crictl — клиент CRI:
crictl ps
crictl images
crictl logs <id>
Команды намеренно похожи на docker, но обращаются к другому интерфейсу.
Команды и примеры
Образ соответствует стандарту
mkdir -p /tmp/k8s && cd /tmp/k8s
mkdir -p src
cat > src/app.py <<'PY'
"""Приложение, пригодное и для Docker, и для Kubernetes."""
from __future__ import annotations
import http.server
import json
import os
import signal
import sys
import threading
PORT = int(os.environ.get("PORT", "8000"))
_ready = threading.Event()
_stop = threading.Event()
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self) -> None:
if self.path == "/healthz":
code, body = 200, {"состояние": "жив"}
elif self.path == "/readyz":
ok = _ready.is_set()
code = 200 if ok else 503
body = {"состояние": "готов" if ok else "не готов"}
else:
code, body = 200, {"сервис": "работает", "uid": os.getuid()}
payload = json.dumps(body, ensure_ascii=False).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def log_message(self, *args: object) -> None:
pass
def _on_term(signum: int, _frame: object) -> None:
print(f"получен сигнал {signum}: закрываем готовность, затем завершаемся",
flush=True)
_ready.clear()
_stop.set()
if __name__ == "__main__":
signal.signal(signal.SIGTERM, _on_term)
signal.signal(signal.SIGINT, _on_term)
server = http.server.ThreadingHTTPServer(("0.0.0.0", PORT), Handler)
threading.Thread(target=server.serve_forever, daemon=True).start()
_ready.set()
print(f"готов на порту {PORT}", flush=True)
_stop.wait()
server.shutdown()
sys.exit(0)
PY
cat > Dockerfile <<'EOF'
# syntax=docker/dockerfile:1
FROM python:3.13-slim AS production
ENV PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 PORT=8000
WORKDIR /app
COPY src/ ./src/
RUN useradd --create-home --uid 10001 app && chown -R app:app /app
USER app
EXPOSE 8000
HEALTHCHECK --interval=5s --timeout=3s --retries=3 --start-period=3s \
CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/healthz')"]
ENTRYPOINT ["python", "-m", "src.app"]
EOF
echo "═══ собираем образ ═══"
docker build -q -t k8s-ready:1 . > /dev/null
echo " собран"
echo "═══ формат образа ═══"
docker inspect k8s-ready:1 --format '{{.Os}}/{{.Architecture}}' | sed 's/^/ платформа: /'
docker save k8s-ready:1 -o img.tar 2>/dev/null
mkdir -p unpacked && tar -xf img.tar -C unpacked 2>/dev/null
python3 - <<'PY'
import json
from pathlib import Path
root = Path("unpacked")
index = root / "index.json"
if index.exists():
data = json.loads(index.read_text())
print(f" index.json: schemaVersion={data.get('schemaVersion')}")
for m in data.get("manifests", [])[:2]:
print(f" mediaType: {m.get('mediaType')}")
print(" формат OCI Image Layout")
else:
manifest = root / "manifest.json"
if manifest.exists():
print(" формат Docker Image (совместим с OCI)")
print(" index.json отсутствует — классический формат save")
blobs = list((root / "blobs").rglob("*")) if (root / "blobs").exists() else []
print(f" blob-файлов: {len([b for b in blobs if b.is_file()])}")
PY
rm -rf unpacked img.tar
echo "═══ что это значит ═══"
cat <<'TXT'
Образ описан стандартом OCI Image Specification.
Kubernetes скачивает его по Distribution Specification —
тому же протоколу, что docker pull.
Затем разворачивает слои и формирует bundle
по Runtime Specification, который исполняет runc.
Ни на одном шаге Docker как программа не участвует.
Отсюда: образы переносятся БЕЗ ИЗМЕНЕНИЙ.
TXT
Ожидаемый вывод:
═══ собираем образ ═══
собран
═══ формат образа ═══
платформа: linux/amd64
index.json: schemaVersion=2
mediaType: application/vnd.oci.image.manifest.v1+json
формат OCI Image Layout
blob-файлов: 5
═══ что это значит ═══
Образ описан стандартом OCI Image Specification.
Kubernetes скачивает его по Distribution Specification —
тому же протоколу, что docker pull.
...
mediaType: application/vnd.oci.image.manifest.v1+json — это и есть стандарт OCI. Не «формат Docker», а общий формат.
Что убрал dockershim
cd /tmp/k8s
cat > dockershim.py <<'PY'
"""Что изменилось при удалении dockershim в Kubernetes 1.24."""
from __future__ import annotations
CHAINS = {
"До 1.24 (с dockershim)": [
"kubelet", "dockershim", "Docker Engine", "containerd", "shim", "runc",
],
"После 1.24 (containerd напрямую)": [
"kubelet", "CRI", "containerd", "shim", "runc",
],
"После 1.24 (CRI-O)": [
"kubelet", "CRI", "CRI-O", "runc",
],
"У вас на машине (Docker)": [
"docker CLI", "dockerd", "containerd", "shim", "runc",
],
}
CHANGED = [
("kubelet общается с Docker Engine", "больше нет"),
("Docker нужен на узлах кластера", "не нужен"),
("docker ps показывает pod'ы", "не показывает"),
("Диагностика на узле", "через crictl вместо docker"),
]
UNCHANGED = [
("Формат образов", "OCI Image Specification — не менялся"),
("docker build", "работает; образ пригоден для Kubernetes"),
("docker push и registry", "Distribution Specification — тот же"),
("Dockerfile", "не изменился ни на строку"),
("Слои, кэш, multi-stage", "механизм тот же"),
("Теги и digest", "те же"),
("runc", "тот же исполнитель"),
]
def main() -> None:
print(" Цепочки выполнения:\n")
for name, chain in CHAINS.items():
print(f" {name}")
print(f" {' → '.join(chain)}")
print(f" звеньев: {len(chain)}")
print()
before = len(CHAINS["До 1.24 (с dockershim)"])
after = len(CHAINS["После 1.24 (containerd напрямую)"])
print(f" Убрано звеньев: {before - after}")
print()
print(f" {'что ИЗМЕНИЛОСЬ':<42} стало")
print(" " + "─" * 78)
for what, now in CHANGED:
print(f" {what:<42} {now}")
print()
print(f" {'что НЕ изменилось':<42} почему")
print(" " + "─" * 78)
for what, why in UNCHANGED:
print(f" {what:<42} {why}")
print()
print(" Формулировка «Kubernetes отказался от Docker» породила")
print(" недоразумение: многие решили, что образы docker build")
print(" перестанут работать. Они работают — убран слой")
print(" совместимости внутри kubelet, а не поддержка формата.")
if __name__ == "__main__":
main()
PY
echo "═══ что убрал dockershim ═══"
python3 dockershim.py
Ожидаемый вывод:
═══ что убрал dockershim ═══
Цепочки выполнения:
До 1.24 (с dockershim)
kubelet → dockershim → Docker Engine → containerd → shim → runc
звеньев: 6
После 1.24 (containerd напрямую)
kubelet → CRI → containerd → shim → runc
звеньев: 5
После 1.24 (CRI-O)
kubelet → CRI → CRI-O → runc
звеньев: 4
У вас на машине (Docker)
docker CLI → dockerd → containerd → shim → runc
звеньев: 5
Убрано звеньев: 1
что ИЗМЕНИЛОСЬ стало
──────────────────────────────────────────────────────────────────────────────
kubelet общается с Docker Engine больше нет
Docker нужен на узлах кластера не нужен
docker ps показывает pod'ы не показывает
Диагностика на узле через crictl вместо docker
что НЕ изменилось почему
──────────────────────────────────────────────────────────────────────────────
Формат образов OCI Image Specification — не менялся
docker build работает; образ пригоден для Kubernetes
docker push и registry Distribution Specification — тот же
Dockerfile не изменился ни на строку
Слои, кэш, multi-stage механизм тот же
Теги и digest те же
runc тот же исполнитель
Формулировка «Kubernetes отказался от Docker» породила
недоразумение: многие решили, что образы docker build
перестанут работать. Они работают — убран слой
совместимости внутри kubelet, а не поддержка формата.
Последняя цепочка показательна: у вас на машине containerd → shim → runc — те же три звена, что на узле Kubernetes.
Различается только то, кто ими управляет.
Императивная и декларативная модель
cd /tmp/k8s
cat > models.py <<'PY'
"""Сравнение императивной и декларативной модели на сценариях отказа."""
from __future__ import annotations
SCENARIOS = [
{
"событие": "Процесс завершился с ошибкой",
"docker": "restart policy перезапустит",
"k8s": "перезапустит container в pod'е",
"различие": "нет",
},
{
"событие": "Container удалён командой",
"docker": "останется удалённым",
"k8s": "pod будет создан заново",
"различие": "ЕСТЬ",
},
{
"событие": "Узел выключился",
"docker": "container не работает",
"k8s": "pod'ы переедут на другой узел",
"различие": "ЕСТЬ",
},
{
"событие": "Нужно 3 экземпляра, работает 2",
"docker": "не отслеживается",
"k8s": "создаст третий",
"различие": "ЕСТЬ",
},
{
"событие": "Readiness-проба не проходит",
"docker": "статус unhealthy, и всё",
"k8s": "трафик перестаёт идти на этот pod",
"различие": "ЕСТЬ",
},
{
"событие": "Обновление версии образа",
"docker": "остановить и запустить заново",
"k8s": "постепенная замена с контролем готовности",
"различие": "ЕСТЬ",
},
{
"событие": "Приложение вышло за лимит памяти",
"docker": "OOM kill, перезапуск по политике",
"k8s": "OOM kill, перезапуск, учёт в событиях",
"различие": "нет",
},
]
def main() -> None:
print(f" {'событие':<38} {'Docker':<34} {'Kubernetes':<38}")
print(" " + "─" * 112)
differ = 0
for s in SCENARIOS:
if s["различие"] == "ЕСТЬ":
differ += 1
print(f" {s['событие']:<38} {s['docker']:<34} {s['k8s']:<38}")
print()
print(f" различаются: {differ} из {len(SCENARIOS)}")
print()
print(" Общее у совпадающих строк: они про ОДНУ машину и ОДИН container.")
print(" Различаются те, где нужно ПОДДЕРЖИВАТЬ состояние:")
print(" число экземпляров, размещение по узлам, направление трафика.")
print()
print(" Это и есть граница: Docker запускает, Kubernetes поддерживает.")
print(" Второе требует первого и не отменяет его.")
if __name__ == "__main__":
main()
PY
echo "═══ где модели различаются ═══"
python3 models.py
Ожидаемый вывод:
═══ где модели различаются ═══
событие Docker Kubernetes
────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Процесс завершился с ошибкой restart policy перезапустит перезапустит container в pod'е
Container удалён командой останется удалённым pod будет создан заново
Узел выключился container не работает pod'ы переедут на другой узел
Нужно 3 экземпляра, работает 2 не отслеживается создаст третий
Readiness-проба не проходит статус unhealthy, и всё трафик перестаёт идти на этот pod
Обновление версии образа остановить и запустить заново постепенная замена с контролем готовности
Приложение вышло за лимит памяти OOM kill, перезапуск по политике OOM kill, перезапуск, учёт в событиях
различаются: 5 из 7
Общее у совпадающих строк: они про ОДНУ машину и ОДИН container.
Различаются те, где нужно ПОДДЕРЖИВАТЬ состояние:
число экземпляров, размещение по узлам, направление трафика.
Это и есть граница: Docker запускает, Kubernetes поддерживает.
Второе требует первого и не отменяет его.
Пятая строка — самая практичная. В Docker unhealthy меняет статус и не более того (урок 11.3); в Kubernetes readiness-проба управляет трафиком.
Это и объясняет, почему healthcheck в Compose полезен только там, где на него кто-то опирается.
Что переносится из навыков
cd /tmp/k8s
cat > skills.py <<'PY'
"""Какие навыки работы с Docker переносятся в Kubernetes."""
from __future__ import annotations
import json
SKILLS = [
("Dockerfile: инструкции, порядок, кэш", "полностью", "формат тот же"),
("Multi-stage сборка", "полностью", "механизм сборщика, не среды выполнения"),
("Размер образа, выбор базового", "полностью", "те же соображения"),
("USER, capabilities, read-only", "полностью", "другой синтаксис: securityContext"),
("Теги и digest, стратегия публикации", "полностью", "тот же registry"),
("Логи в stdout", "полностью", "kubectl logs читает так же"),
("Обработка SIGTERM", "полностью", "Kubernetes шлёт тот же сигнал"),
("Сканирование, SBOM, provenance", "полностью", "работа с образом"),
("Healthcheck", "понятийно", "в Kubernetes три пробы вместо одной"),
("Лимиты ресурсов", "понятийно", "добавляются requests для планирования"),
("Compose-файлы", "НЕ переносятся", "другая модель объектов"),
("docker run и его флаги", "НЕ переносятся", "заменяются описанием pod'а"),
("Сети Docker", "НЕ переносятся", "плоская сеть кластера, другая модель"),
("Volumes Docker", "частично", "понятие есть, устройство иное"),
]
def main() -> None:
print(f" {'навык':<40} {'переносится':<16} почему")
print(" " + "─" * 96)
counts = {"полностью": 0, "понятийно": 0, "частично": 0, "НЕ переносятся": 0}
for name, degree, why in SKILLS:
counts[degree] = counts.get(degree, 0) + 1
print(f" {name:<40} {degree:<16} {why}")
total = len(SKILLS)
carried = counts["полностью"] + counts["понятийно"]
print()
for degree, n in counts.items():
print(f" {degree:<16} {n:>2} из {total}")
print()
print(f" переносится полностью или понятийно: {carried} из {total} "
f"({carried / total * 100:.0f} %)")
print()
print(" Не переносится то, что описывает ЗАПУСК: compose-файлы,")
print(" флаги docker run, модель сетей. Переносится то,")
print(" что описывает АРТЕФАКТ и его поведение.")
print()
print(json.dumps({"всего": total, "переносится": carried},
ensure_ascii=False))
if __name__ == "__main__":
main()
PY
echo "═══ переносимость навыков ═══"
python3 skills.py
Ожидаемый вывод:
═══ переносимость навыков ═══
навык переносится почему
────────────────────────────────────────────────────────────────────────────────────────────────
Dockerfile: инструкции, порядок, кэш полностью формат тот же
Multi-stage сборка полностью механизм сборщика, не среды выполнения
Размер образа, выбор базового полностью те же соображения
USER, capabilities, read-only полностью другой синтаксис: securityContext
Теги и digest, стратегия публикации полностью тот же registry
Логи в stdout полностью kubectl logs читает так же
Обработка SIGTERM полностью Kubernetes шлёт тот же сигнал
Сканирование, SBOM, provenance полностью работа с образом
Healthcheck понятийно в Kubernetes три пробы вместо одной
Лимиты ресурсов понятийно добавляются requests для планирования
Compose-файлы НЕ переносятся другая модель объектов
docker run и его флаги НЕ переносятся заменяются описанием pod'а
Сети Docker НЕ переносятся плоская сеть кластера, другая модель
Volumes Docker частично понятие есть, устройство иное
полностью 8 из 14
понятийно 2 из 14
частично 1 из 14
НЕ переносятся 3 из 14
переносится полностью или понятийно: 10 из 14 (71 %)
Не переносится то, что описывает ЗАПУСК: compose-файлы,
флаги docker run, модель сетей. Переносится то,
что описывает АРТЕФАКТ и его поведение.
Проверка образа на пригодность
cd /tmp/k8s
cat > k8s-readiness.sh <<'SH'
#!/usr/bin/env bash
# Проверка образа на пригодность для Kubernetes.
#
# Все проверки выполняются средствами Docker: кластер не нужен.
set -uo pipefail
IMAGE="${1:?укажите образ}"
NAME="k8sready-$$"
issues=0
pass() { printf ' ✓ %s\n' "$1"; }
warn() { printf ' ⚠ %s\n' "$1"; issues=$((issues + 1)); }
cleanup() { docker rm -f "$NAME" > /dev/null 2>&1; }
trap cleanup EXIT INT TERM
printf '\n Пригодность для Kubernetes: %s\n\n' "$IMAGE"
# 1. Формат образа
os_arch="$(docker inspect "$IMAGE" --format '{{.Os}}/{{.Architecture}}' 2>/dev/null)"
pass "платформа: $os_arch (OCI Image Specification)"
# 2. Пользователь: в Kubernetes задаётся securityContext, но образ должен позволять
user="$(docker inspect "$IMAGE" --format '{{.Config.User}}')"
case "$user" in
""|0|root) warn "USER не задан — потребуется runAsUser в securityContext" ;;
*) pass "USER: $user — совместимо с runAsNonRoot" ;;
esac
# 3. ENTRYPOINT в exec form: Kubernetes шлёт SIGTERM напрямую процессу
form="$(docker inspect "$IMAGE" --format '{{json .Config.Entrypoint}}' | python3 -c "
import json, sys
v = json.load(sys.stdin) or []
shells = {'/bin/sh', 'sh', '/bin/bash', 'bash'}
print('shell' if len(v) >= 2 and v[0] in shells and v[1] == '-c'
else ('exec' if v else 'none'))
")"
[ "$form" = "shell" ] \
&& warn "ENTRYPOINT в shell form — SIGTERM не дойдёт при завершении pod'а" \
|| pass "ENTRYPOINT в $form form"
# 4. Логи в stdout
pass "логи: проверяются запуском ниже"
# 5. Эндпоинты проб
docker run -d --name "$NAME" -p 0:8000 "$IMAGE" > /dev/null 2>&1 || {
warn "container не запустился"; printf '\n замечаний: %s\n\n' "$issues"; exit 1; }
sleep 4
port="$(docker port "$NAME" 8000/tcp 2>/dev/null | head -1 | sed 's/.*://')"
if [ -n "$port" ]; then
for path in /healthz /readyz; do
code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 \
"http://127.0.0.1:$port$path" 2>/dev/null)"
case "$code" in
200) pass "эндпоинт $path отвечает 200 — годится для пробы" ;;
"") warn "эндпоинт $path недоступен" ;;
*) warn "эндпоинт $path вернул $code" ;;
esac
done
else
warn "порт не опубликован"
fi
# 6. Логи попали в stdout
n_lines="$(docker logs "$NAME" 2>&1 | grep -c . || echo 0)"
[ "${n_lines:-0}" -gt 0 ] \
&& pass "логи идут в stdout: $n_lines строк" \
|| warn "логов в stdout нет — kubectl logs будет пуст"
# 7. Обработка SIGTERM
t0="$(python3 -c 'import time; print(time.monotonic())')"
docker stop -t 10 "$NAME" > /dev/null 2>&1
t1="$(python3 -c 'import time; print(time.monotonic())')"
secs="$(python3 -c "print(f'{$t1 - $t0:.1f}')")"
code="$(docker inspect "$NAME" --format '{{.State.ExitCode}}' 2>/dev/null)"
python3 -c "import sys; sys.exit(0 if $secs < 3 else 1)" \
&& pass "SIGTERM обработан за $secs с (код $code)" \
|| warn "SIGTERM не обработан: $secs с — pod будет убит по таймауту"
printf '\n замечаний: %s\n' "$issues"
printf ' проверки выполнены средствами Docker; кластер не требовался\n\n'
[ "$issues" -gt 0 ] && exit 1
exit 0
SH
chmod +x k8s-readiness.sh
echo "═══ проверка пригодного образа ═══"
./k8s-readiness.sh k8s-ready:1 || true
echo "═══ проверка непригодного ═══"
cat > Dockerfile.bad <<'EOF'
# syntax=docker/dockerfile:1
FROM python:3.13-slim
ENV PORT=8000
WORKDIR /app
COPY src/ ./src/
ENTRYPOINT python -m src.app
EOF
docker build -q -f Dockerfile.bad -t k8s-bad:1 . > /dev/null 2>&1
./k8s-readiness.sh k8s-bad:1 2>&1 | tail -8 || true
docker rmi -f k8s-ready:1 k8s-bad:1 > /dev/null 2>&1
cd /tmp && rm -rf /tmp/k8s
Ожидаемый вывод:
═══ проверка пригодного образа ═══
Пригодность для Kubernetes: k8s-ready:1
✓ платформа: linux/amd64 (OCI Image Specification)
✓ USER: app — совместимо с runAsNonRoot
✓ ENTRYPOINT в exec form
✓ логи: проверяются запуском ниже
✓ эндпоинт /healthz отвечает 200 — годится для пробы
✓ эндпоинт /readyz отвечает 200 — годится для пробы
✓ логи идут в stdout: 1 строк
✓ SIGTERM обработан за 0.3 с (код 0)
замечаний: 0
проверки выполнены средствами Docker; кластер не требовался
═══ проверка непригодного ═══
⚠ USER не задан — потребуется runAsUser в securityContext
⚠ ENTRYPOINT в shell form — SIGTERM не дойдёт при завершении pod'а
✓ эндпоинт /healthz отвечает 200 — годится для пробы
✓ эндпоинт /readyz отвечает 200 — годится для пробы
✓ логи идут в stdout: 1 строк
⚠ SIGTERM не обработан: 10.2 с — pod будет убит по таймауту
замечаний: 3
Все проверки выполнены средствами Docker — кластер не понадобился.
Строка про SIGTERM важна для Kubernetes даже сильнее, чем для Docker: при завершении pod'а сигнал шлётся процессу, и необработанный SIGTERM означает жёсткое убийство по истечении terminationGracePeriodSeconds.
Практическое упражнение
Задание. Проверьте образ на пригодность для Kubernetes, не имея кластера.
Требования:
- Показать, что образ соответствует OCI Image Specification.
- Перечислить, что изменилось при удалении
dockershim, и что не изменилось. - Показать цепочки выполнения до и после, посчитав звенья.
- Составить перечень навыков с указанием степени переносимости.
- Написать проверку пригодности образа и испытать её на непригодном.
- Объяснить, почему
docker psне покажет pod'ы на узле Kubernetes.
Подсказки
Подсказка 1
Формат образа виден в index.json после распаковки docker save: mediaType содержит vnd.oci.
Подсказка 2
Пригодность проверяется тем же, чем валидация образа в уроке 15.5, плюс эндпоинты проб.
Подсказка 3
Kubernetes создаёт container'ы в пространстве имён containerd k8s.io, а dockerd знает только moby.
Решение
Показать решение
mkdir -p /tmp/k8slab && cd /tmp/k8slab
mkdir -p src
cat > src/app.py <<'PY'
"""Сервис с эндпоинтами для проб Kubernetes."""
from __future__ import annotations
import http.server
import json
import os
import signal
import sys
import threading
import time
PORT = int(os.environ.get("PORT", "8000"))
START_DELAY = float(os.environ.get("START_DELAY", "0"))
_ready = threading.Event()
_stop = threading.Event()
_started_at = time.monotonic()
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self) -> None:
if self.path == "/healthz":
# liveness: процесс жив и способен отвечать
code, body = 200, {"состояние": "жив",
"uptime": round(time.monotonic() - _started_at, 1)}
elif self.path == "/readyz":
# readiness: готов принимать трафик
ok = _ready.is_set()
code = 200 if ok else 503
body = {"состояние": "готов" if ok else "не готов"}
elif self.path == "/startupz":
# startup: инициализация завершена
ok = time.monotonic() - _started_at >= START_DELAY
code = 200 if ok else 503
body = {"инициализация": "завершена" if ok else "идёт"}
else:
code, body = 200, {"сервис": "работает", "uid": os.getuid(),
"pid": os.getpid()}
payload = json.dumps(body, ensure_ascii=False).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def log_message(self, *args: object) -> None:
pass
def _on_term(signum: int, _frame: object) -> None:
# Порядок важен: сначала закрыть готовность, потом завершаться.
# В Kubernetes это даёт время убрать pod из балансировки.
print(f"сигнал {signum}: закрываю готовность", flush=True)
_ready.clear()
time.sleep(0.2)
print("завершаюсь", flush=True)
_stop.set()
def main() -> int:
signal.signal(signal.SIGTERM, _on_term)
signal.signal(signal.SIGINT, _on_term)
server = http.server.ThreadingHTTPServer(("0.0.0.0", PORT), Handler)
threading.Thread(target=server.serve_forever, daemon=True).start()
if START_DELAY:
time.sleep(START_DELAY)
_ready.set()
print(f"готов на порту {PORT}, uid={os.getuid()}", flush=True)
_stop.wait()
server.shutdown()
return 0
if __name__ == "__main__":
sys.exit(main())
PY
cat > Dockerfile <<'EOF'
# syntax=docker/dockerfile:1
FROM python:3.13-slim AS production
ENV PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 PORT=8000
WORKDIR /app
COPY src/ ./src/
RUN useradd --create-home --uid 10001 app && chown -R app:app /app
USER app
EXPOSE 8000
HEALTHCHECK --interval=5s --timeout=3s --retries=3 --start-period=3s \
CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/healthz')"]
ENTRYPOINT ["python", "-m", "src.app"]
EOF
cat > Dockerfile.bad <<'EOF'
# syntax=docker/dockerfile:1
FROM python:3.13-slim
ENV PORT=8000
WORKDIR /app
COPY src/ ./src/
# Ошибка 1: USER не задан
# Ошибка 2: shell form — SIGTERM не дойдёт
# Ошибка 3: нет EXPOSE
ENTRYPOINT python -m src.app
EOF
cat > check-oci.py <<'PY'
"""Проверка соответствия образа стандарту OCI."""
from __future__ import annotations
import json
import subprocess
import sys
import tarfile
import tempfile
from pathlib import Path
OCI_MEDIA_TYPES = {
"application/vnd.oci.image.manifest.v1+json",
"application/vnd.oci.image.index.v1+json",
"application/vnd.oci.image.config.v1+json",
"application/vnd.oci.image.layer.v1.tar+gzip",
}
DOCKER_MEDIA_TYPES = {
"application/vnd.docker.distribution.manifest.v2+json",
"application/vnd.docker.container.image.v1+json",
}
def analyze(image: str) -> dict[str, object]:
with tempfile.TemporaryDirectory() as tmp:
tar_path = Path(tmp) / "image.tar"
subprocess.run(["docker", "save", image, "-o", str(tar_path)],
capture_output=True, check=False)
if not tar_path.exists():
return {"ошибка": "docker save не удался"}
unpacked = Path(tmp) / "unpacked"
unpacked.mkdir()
try:
with tarfile.open(tar_path) as tf:
tf.extractall(unpacked, filter="data")
except (tarfile.TarError, TypeError):
with tarfile.open(tar_path) as tf:
tf.extractall(unpacked)
result: dict[str, object] = {"формат": "неизвестен", "медиатипы": []}
index = unpacked / "index.json"
if index.exists():
data = json.loads(index.read_text())
result["формат"] = "OCI Image Layout"
result["schemaVersion"] = data.get("schemaVersion")
types = [m.get("mediaType") for m in data.get("manifests", [])]
result["медиатипы"] = types
result["oci_медиатипов"] = sum(1 for t in types if t in OCI_MEDIA_TYPES)
elif (unpacked / "manifest.json").exists():
result["формат"] = "Docker Image (совместим с OCI)"
blobs = unpacked / "blobs"
if blobs.exists():
result["blob_файлов"] = sum(1 for p in blobs.rglob("*") if p.is_file())
layout = unpacked / "oci-layout"
result["oci_layout_есть"] = layout.exists()
return result
def main(image: str) -> int:
info = analyze(image)
print(json.dumps(info, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "k8slab:good"))
PY
cat > k8s-check.sh <<'SH'
#!/usr/bin/env bash
# Проверка пригодности образа для Kubernetes.
# Кластер не требуется: всё проверяется средствами Docker.
set -uo pipefail
IMAGE="${1:?образ}"
NAME="k8slab-check-$$"
issues=0
pass() { printf ' ✓ %s\n' "$1"; }
warn() { printf ' ⚠ %s\n' "$1"; issues=$((issues + 1)); }
cleanup() { docker rm -f "$NAME" > /dev/null 2>&1; }
trap cleanup EXIT INT TERM
printf '\n Проверка: %s\n\n' "$IMAGE"
# Статические
user="$(docker inspect "$IMAGE" --format '{{.Config.User}}' 2>/dev/null)"
case "$user" in
""|0|root) warn "USER не задан — нужен runAsUser в securityContext" ;;
*) pass "USER: $user — совместимо с runAsNonRoot" ;;
esac
form="$(docker inspect "$IMAGE" --format '{{json .Config.Entrypoint}}' 2>/dev/null \
| python3 -c "
import json, sys
v = json.load(sys.stdin) or []
shells = {'/bin/sh','sh','/bin/bash','bash'}
print('shell' if len(v) >= 2 and v[0] in shells and v[1] == '-c'
else ('exec' if v else 'none'))
")"
[ "$form" = "shell" ] \
&& warn "ENTRYPOINT в shell form — SIGTERM не дойдёт до процесса" \
|| pass "ENTRYPOINT в $form form"
ports="$(docker inspect "$IMAGE" \
--format '{{range $p, $_ := .Config.ExposedPorts}}{{$p}} {{end}}' 2>/dev/null)"
[ -n "$ports" ] && pass "EXPOSE: $ports" || warn "EXPOSE не задан — не критично, но полезно"
# Динамические
docker run -d --name "$NAME" -p 0:8000 "$IMAGE" > /dev/null 2>&1 || {
warn "container не запустился"
printf '\n замечаний: %s\n\n' "$issues"
exit 1
}
sleep 4
port="$(docker port "$NAME" 8000/tcp 2>/dev/null | head -1 | sed 's/.*://')"
if [ -n "$port" ]; then
for path in /healthz /readyz /startupz; do
code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 \
"http://127.0.0.1:$port$path" 2>/dev/null)"
case "$code" in
200) pass "$path → 200: годится для пробы" ;;
"") warn "$path недоступен" ;;
*) warn "$path → $code" ;;
esac
done
else
warn "порт 8000 не опубликован"
fi
logs="$(docker logs "$NAME" 2>&1 | grep -c . || echo 0)"
[ "${logs:-0}" -gt 0 ] \
&& pass "логи в stdout: $logs строк — kubectl logs их увидит" \
|| warn "логов в stdout нет"
t0="$(python3 -c 'import time; print(time.monotonic())')"
docker stop -t 10 "$NAME" > /dev/null 2>&1
t1="$(python3 -c 'import time; print(time.monotonic())')"
secs="$(python3 -c "print(f'{$t1 - $t0:.1f}')")"
code="$(docker inspect "$NAME" --format '{{.State.ExitCode}}' 2>/dev/null)"
if python3 -c "import sys; sys.exit(0 if $secs < 3 else 1)"; then
pass "SIGTERM обработан за $secs с (код $code)"
else
warn "SIGTERM не обработан: $secs с — pod убьют по terminationGracePeriodSeconds"
fi
printf '\n замечаний: %s\n\n' "$issues"
[ "$issues" -gt 0 ] && exit 1
exit 0
SH
chmod +x k8s-check.sh
fail=0
ok() { printf ' ✓ %s\n' "$1"; }
bad() { printf ' ✗ %s\n' "$1"; fail=1; }
printf '\n═══ Подготовка ═══\n'
docker build -q -t k8slab:good . > /dev/null 2>&1 && printf ' k8slab:good собран\n'
docker build -q -f Dockerfile.bad -t k8slab:bad . > /dev/null 2>&1 \
&& printf ' k8slab:bad собран (три внесённые ошибки)\n'
printf '\n═══ Требование 1: соответствие OCI ═══\n'
python3 check-oci.py k8slab:good > oci.json 2>&1
python3 - <<'PY'
import json
from pathlib import Path
d = json.loads(Path("oci.json").read_text())
print(f" формат: {d.get('формат')}")
print(f" oci-layout присутствует: {d.get('oci_layout_есть')}")
print(f" blob-файлов: {d.get('blob_файлов', '—')}")
types = d.get("медиатипы") or []
if types:
print(" медиатипы манифестов:")
for t in types:
mark = "OCI" if "vnd.oci" in t else "Docker"
print(f" [{mark}] {t}")
PY
fmt="$(python3 -c "
import json
from pathlib import Path
print(json.loads(Path('oci.json').read_text()).get('формат', ''))")"
printf '\n Формат описан стандартом, а не Docker ом.\n'
printf ' Kubernetes скачивает образ по Distribution Specification\n'
printf ' и запускает по Runtime Specification — через runc.\n'
case "$fmt" in
*OCI*) ok "образ соответствует OCI: переносится без изменений" ;;
*) bad "формат: $fmt" ;;
esac
printf '\n═══ Требования 2-3: dockershim ═══\n'
python3 - <<'PY'
CHAINS = {
"До 1.24 (dockershim)": ["kubelet", "dockershim", "Docker Engine",
"containerd", "shim", "runc"],
"После 1.24 (containerd)": ["kubelet", "CRI", "containerd", "shim", "runc"],
"После 1.24 (CRI-O)": ["kubelet", "CRI", "CRI-O", "runc"],
"На вашей машине": ["docker CLI", "dockerd", "containerd", "shim", "runc"],
}
for name, chain in CHAINS.items():
print(f" {name} — звеньев {len(chain)}")
print(f" {' → '.join(chain)}")
before = len(CHAINS["До 1.24 (dockershim)"])
after = len(CHAINS["После 1.24 (containerd)"])
print(f"\n убрано звеньев: {before - after}")
CHANGED = [
"kubelet больше не общается с Docker Engine",
"Docker не нужен на узлах кластера",
"docker ps не покажет pod'ы",
"диагностика на узле — через crictl",
]
UNCHANGED = [
"формат образов (OCI Image Specification)",
"docker build и docker push",
"протокол registry (Distribution Specification)",
"Dockerfile — ни строки не изменилось",
"слои, кэш, multi-stage",
"теги и digest",
"runc — тот же исполнитель",
]
print("\n ИЗМЕНИЛОСЬ:")
for item in CHANGED:
print(f" · {item}")
print("\n НЕ ИЗМЕНИЛОСЬ:")
for item in UNCHANGED:
print(f" · {item}")
print(f"\n изменений: {len(CHANGED)}, неизменного: {len(UNCHANGED)}")
print()
print(" Обратите внимание на последнюю цепочку: у вас на машине")
print(" containerd → shim → runc — ТЕ ЖЕ три звена, что на узле")
print(" Kubernetes. Различается только то, кто ими управляет.")
PY
ok "цепочки посчитаны; разделено изменившееся и неизменное"
printf '\n═══ Требование 4: переносимость навыков ═══\n'
python3 - <<'PY'
import json
SKILLS = [
("Dockerfile, порядок, кэш", "полностью"),
("Multi-stage сборка", "полностью"),
("Размер образа, базовый образ", "полностью"),
("USER, capabilities, read-only", "полностью"),
("Теги, digest, публикация", "полностью"),
("Логи в stdout", "полностью"),
("Обработка SIGTERM", "полностью"),
("Сканирование, SBOM", "полностью"),
("Healthcheck", "понятийно"),
("Лимиты ресурсов", "понятийно"),
("Volumes", "частично"),
("Compose-файлы", "НЕ переносятся"),
("Флаги docker run", "НЕ переносятся"),
("Сети Docker", "НЕ переносятся"),
]
counts: dict[str, int] = {}
print(f" {'навык':<34} переносится")
print(" " + "─" * 54)
for name, degree in SKILLS:
counts[degree] = counts.get(degree, 0) + 1
print(f" {name:<34} {degree}")
total = len(SKILLS)
carried = counts.get("полностью", 0) + counts.get("понятийно", 0)
print()
for degree, n in counts.items():
print(f" {degree:<16} {n:>2}")
print(f"\n переносится полностью или понятийно: {carried} из {total} "
f"({carried / total * 100:.0f} %)")
print()
print(" Не переносится то, что описывает ЗАПУСК.")
print(" Переносится то, что описывает АРТЕФАКТ и его поведение.")
print(json.dumps({"всего": total, "переносится": carried}, ensure_ascii=False))
PY
carried="$(python3 -c "
print(11)")"
ok "перечень составлен: большинство навыков переносится"
printf '\n═══ Требование 5: проверка пригодности ═══\n'
./k8s-check.sh k8slab:good; good_rc=$?
./k8s-check.sh k8slab:bad; bad_rc=$?
printf ' коды возврата: пригодный=%s непригодный=%s\n' "$good_rc" "$bad_rc"
[ "$good_rc" -eq 0 ] && [ "$bad_rc" -ne 0 ] \
&& ok "проверка различает образы; кластер не потребовался" \
|| bad "коды: $good_rc и $bad_rc"
printf '\n═══ Требование 6: почему docker ps не видит pod'"'"'ы ═══\n'
python3 - <<'PY'
print(" На узле Kubernetes container'ы создаёт kubelet через CRI.")
print(" Они попадают в пространство имён containerd k8s.io.")
print()
print(f" {'кто создал':<24} {'пространство имён':<20} кто видит")
print(" " + "─" * 68)
print(f" {'dockerd (docker run)':<24} {'moby':<20} docker ps")
print(f" {'kubelet через CRI':<24} {'k8s.io':<20} crictl ps")
print()
print(" docker ps обращается к dockerd, который знает только moby.")
print(" Container'ы Kubernetes ему не видны — даже если Docker установлен.")
print()
print(" Диагностика на узле:")
print(" crictl ps вместо docker ps")
print(" crictl images вместо docker images")
print(" crictl logs <id> вместо docker logs")
print(" ctr --namespace k8s.io containers list — низкий уровень")
print()
print(" Команды намеренно похожи, но обращаются к другому интерфейсу.")
PY
ok "объяснено через пространства имён containerd"
printf '\n═══ Что НЕ проверялось ═══\n'
python3 - <<'PY'
NOT_RUN = [
("запуск в настоящем кластере", "кластер недоступен в этом окружении"),
("поведение проб liveness/readiness", "требует kubelet"),
("crictl на узле", "нужен узел Kubernetes"),
("перепланирование pod'а при отказе узла", "требует кластера из нескольких узлов"),
]
print(f" {'проверка':<44} причина")
print(" " + "─" * 84)
for name, why in NOT_RUN:
print(f" {name:<44} {why}")
print(f"\n не выполнялось: {len(NOT_RUN)}")
print()
print(" Проверено то, что можно проверить средствами Docker:")
print(" формат образа, пользователь, форма ENTRYPOINT, эндпоинты проб,")
print(" логи в stdout, обработка SIGTERM. Этого достаточно, чтобы")
print(" утверждать: образ пригоден. Поведение в кластере — отдельный вопрос.")
PY
ok "невыполненные проверки перечислены; названо, что проверено взамен"
printf '\n═══ ИТОГ ═══\n'
[ "$fail" -eq 0 ] && echo " все требования выполнены" || echo " ЕСТЬ ПРОВАЛЫ"
echo " примечание: кластер Kubernetes не использовался"
docker rmi -f k8slab:good k8slab:bad > /dev/null 2>&1
cd /tmp && rm -rf /tmp/k8slab
exit "$fail"
Ожидаемый вывод:
═══ Подготовка ═══
k8slab:good собран
k8slab:bad собран (три внесённые ошибки)
═══ Требование 1: соответствие OCI ═══
формат: OCI Image Layout
oci-layout присутствует: True
blob-файлов: 5
медиатипы манифестов:
[OCI] application/vnd.oci.image.manifest.v1+json
Формат описан стандартом, а не Docker ом.
Kubernetes скачивает образ по Distribution Specification
и запускает по Runtime Specification — через runc.
✓ образ соответствует OCI: переносится без изменений
═══ Требования 2-3: dockershim ═══
До 1.24 (dockershim) — звеньев 6
kubelet → dockershim → Docker Engine → containerd → shim → runc
После 1.24 (containerd) — звеньев 5
kubelet → CRI → containerd → shim → runc
После 1.24 (CRI-O) — звеньев 4
kubelet → CRI → CRI-O → runc
На вашей машине — звеньев 5
docker CLI → dockerd → containerd → shim → runc
убрано звеньев: 1
ИЗМЕНИЛОСЬ:
· kubelet больше не общается с Docker Engine
· Docker не нужен на узлах кластера
· docker ps не покажет pod'ы
· диагностика на узле — через crictl
НЕ ИЗМЕНИЛОСЬ:
· формат образов (OCI Image Specification)
· docker build и docker push
· протокол registry (Distribution Specification)
· Dockerfile — ни строки не изменилось
· слои, кэш, multi-stage
· теги и digest
· runc — тот же исполнитель
изменений: 4, неизменного: 7
Обратите внимание на последнюю цепочку: у вас на машине
containerd → shim → runc — ТЕ ЖЕ три звена, что на узле
Kubernetes. Различается только то, кто ими управляет.
✓ цепочки посчитаны; разделено изменившееся и неизменное
═══ Требование 4: переносимость навыков ═══
навык переносится
──────────────────────────────────────────────────────
Dockerfile, порядок, кэш полностью
Multi-stage сборка полностью
...
переносится полностью или понятийно: 10 из 14 (71 %)
Не переносится то, что описывает ЗАПУСК.
Переносится то, что описывает АРТЕФАКТ и его поведение.
✓ перечень составлен: большинство навыков переносится
═══ Требование 5: проверка пригодности ═══
Проверка: k8slab:good
✓ USER: app — совместимо с runAsNonRoot
✓ ENTRYPOINT в exec form
✓ EXPOSE: 8000/tcp
✓ /healthz → 200: годится для пробы
✓ /readyz → 200: годится для пробы
✓ /startupz → 200: годится для пробы
✓ логи в stdout: 1 строк — kubectl logs их увидит
✓ SIGTERM обработан за 0.5 с (код 0)
замечаний: 0
Проверка: k8slab:bad
⚠ USER не задан — нужен runAsUser в securityContext
⚠ ENTRYPOINT в shell form — SIGTERM не дойдёт до процесса
⚠ EXPOSE не задан — не критично, но полезно
✓ /healthz → 200: годится для пробы
✓ /readyz → 200: годится для пробы
✓ /startupz → 200: годится для пробы
✓ логи в stdout: 1 строк — kubectl logs их увидит
⚠ SIGTERM не обработан: 10.2 с — pod убьют по terminationGracePeriodSeconds
замечаний: 4
коды возврата: пригодный=0 непригодный=1
✓ проверка различает образы; кластер не потребовался
═══ Требование 6: почему docker ps не видит pod'ы ═══
На узле Kubernetes container'ы создаёт kubelet через CRI.
Они попадают в пространство имён containerd k8s.io.
кто создал пространство имён кто видит
────────────────────────────────────────────────────────────────────
dockerd (docker run) moby docker ps
kubelet через CRI k8s.io crictl ps
docker ps обращается к dockerd, который знает только moby.
Container'ы Kubernetes ему не видны — даже если Docker установлен.
...
✓ объяснено через пространства имён containerd
═══ Что НЕ проверялось ═══
проверка причина
────────────────────────────────────────────────────────────────────────────────────
запуск в настоящем кластере кластер недоступен в этом окружении
поведение проб liveness/readiness требует kubelet
crictl на узле нужен узел Kubernetes
перепланирование pod'а при отказе узла требует кластера из нескольких узлов
не выполнялось: 4
Проверено то, что можно проверить средствами Docker:
формат образа, пользователь, форма ENTRYPOINT, эндпоинты проб,
логи в stdout, обработка SIGTERM. Этого достаточно, чтобы
утверждать: образ пригоден. Поведение в кластере — отдельный вопрос.
✓ невыполненные проверки перечислены; названо, что проверено взамен
═══ ИТОГ ═══
все требования выполнены
примечание: кластер Kubernetes не использовался
Все требования выполнены; кластер не использовался, и это отмечено.
Требование 3 даёт результат, снимающий главное недоразумение: containerd → shim → runc на вашей машине и на узле Kubernetes — те же три звена. Убрано было одно: dockershim, слой перевода внутри kubelet.
Три решения, определяющие качество.
Соответствие OCI проверяется медиатипами, а не утверждается. Строка application/vnd.oci.image.manifest.v1+json в index.json — это признак формата, записанный в самом артефакте. Утверждение «образы совместимы» превращается в наблюдаемый факт.
Цепочки выполнения сравниваются числом звеньев. Формулировка «убрали Docker» звучит как большое изменение. Подсчёт показывает: убрано одно звено из шести, и оставшиеся три нижних — те же, что работают у вас локально. Масштаб изменения становится измеримым.
Проверка пригодности испытана на непригодном образе. Ноль замечаний на правильном образе доказывал бы только, что скрипт запускается. Четыре замечания на образе с внесёнными ошибками показывают, что он способен их находить, — и одновременно перечисляют, что именно делает образ непригодным.
Чего решение не делает. Кластер Kubernetes не использовался: kubectl, crictl и настоящие пробы недоступны в этом окружении, и это перечислено отдельным блоком. Проверка эндпоинтов подтверждает, что они отвечают, но не что пробы настроены верно, — это вопрос манифеста, а не образа (урок 18.2). Перечень переносимости навыков — оценка, основанная на устройстве обеих систем, а не измерение. Наконец, cri-dockerd как способ сохранить Docker на узлах упомянут, но не проверялся.
Проверка результата
docker save ОБРАЗ | tar -xO index.json 2>/dev/null | python3 -m json.tool | head
docker inspect ОБРАЗ --format '{{.Os}}/{{.Architecture}} {{.Config.User}}'
docker inspect ОБРАЗ --format '{{json .Config.Entrypoint}}'
docker run -d --name probe -p 0:8000 ОБРАЗ && sleep 4 && time docker stop probe
Наличие vnd.oci в медиатипах подтверждает соответствие стандарту.
Типичные ошибки
| Ошибка | Причина | Исправление |
|---|---|---|
Считать, что образы docker build не работают в Kubernetes | Формулировка про dockershim | Формат стандартизован OCI; работают |
| Ставить Docker на узлы кластера | «Он же нужен для образов» | Не нужен; образы собирают отдельно |
Искать pod'ы через docker ps | Привычка | Другое пространство имён; crictl ps |
| Сравнивать Docker и Kubernetes напрямую | Оба про container'ы | Разные уровни: запуск против поддержания |
| Ожидать, что Compose-файл заработает | Похожие понятия | Другая модель объектов |
Считать restart: always эквивалентом Deployment | Оба «перезапускают» | Разные события вызывают реакцию |
Оставлять shell form ENTRYPOINT | Работает в Docker | В Kubernetes SIGTERM не дойдёт |
Не задавать USER в образе | Задаётся в манифесте | runAsNonRoot потребует runAsUser |
Контрольные вопросы
На понимание:
- Сформулируйте задачу Docker и задачу Kubernetes одним предложением каждую.
- Что связывает две системы и почему образы переносятся?
- Что убрал
dockershimи что при этом не изменилось? - Почему
docker psне показывает pod'ы на узле Kubernetes? - Чем императивная модель отличается от декларативной на примере отказа узла?
На применение:
- Как проверить образ на пригодность для Kubernetes без кластера?
- Какие ваши навыки переносятся полностью, а какие нет?
- Что означает
containerdна узле Kubernetes для того, кто знает Docker?
На диагностику:
- Pod убивается через 30 секунд после начала завершения. Гипотеза?
kubectl logsпуст, хотя приложение работает. Причина?
Краткое резюме
- Docker собирает образ и запускает container на машине; Kubernetes поддерживает состояние на кластере.
- Связующее звено — стандарты OCI: образ, среда выполнения, распространение.
- Образ, собранный
docker build, соответствует OCI и работает в Kubernetes без изменений. - CRI — интерфейс kubelet к среде выполнения; Docker Engine его не реализует.
dockershimбыл слоем перевода внутри kubelet и удалён в версии 1.24.- Убрано одно звено; формат образов, registry и
Dockerfileне изменились. containerdна узле Kubernetes — тот же компонент, что внутри Docker.- Container'ы Kubernetes живут в пространстве имён
k8s.io, а неmoby. - Диагностика на узле выполняется через
crictl, а неdocker. - Императивная модель выполняет команду; декларативная поддерживает состояние.
- Различие проявляется при отказе узла, изменении числа реплик и управлении трафиком.
- Большинство навыков работы с образами переносится полностью; не переносится то, что описывает запуск.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
| OCI Image Specification | https://github.com/opencontainers/image-spec/blob/main/spec.md | Формат образа |
| OCI Runtime Specification | https://github.com/opencontainers/runtime-spec/blob/main/spec.md | Bundle и запуск |
| OCI Distribution Specification | https://github.com/opencontainers/distribution-spec/blob/main/spec.md | Протокол registry |
| Kubernetes: CRI | https://kubernetes.io/docs/concepts/architecture/cri/ | Интерфейс среды выполнения |
| Kubernetes: удаление dockershim | https://kubernetes.io/blog/2022/02/17/dockershim-faq/ | Что изменилось и что нет |
| Kubernetes: container runtimes | https://kubernetes.io/docs/setup/production-environment/container-runtimes/ | containerd, CRI-O |
crictl | https://kubernetes.io/docs/tasks/debug/debug-cluster/crictl/ | Диагностика на узле |
| Kubernetes: обзор | https://kubernetes.io/docs/concepts/overview/ | Декларативная модель |
Навигация
Вернуться к разделу
Следующий материал → Объекты Kubernetes
Главное оглавление