Главная/Docker и Kubernetes/Урок

18.4. Когда нужна orchestration

Цели

После этого материала вы сможете:

  • назвать признаки, по которым Compose ещё достаточно;
  • назвать признаки, по которым его перестаёт хватать;
  • оценить стоимость Kubernetes: обучение, эксплуатация, отладка;
  • перечислить промежуточные варианты между Compose и полным кластером;
  • заполнить чек-лист принятия решения для конкретного проекта;
  • отличить настоящее требование от воспроизведённого по инерции.

Предварительные знания

Кластер не требуется. Урок аналитический.

Ключевые термины

ТерминОбъяснение
orchestrationУправление контейнерами на множестве машин
SLOЗаявленный уровень качества обслуживания
эксплуатационная нагрузкаВремя, уходящее на поддержание системы, а не на разработку
одна точка отказаКомпонент, отказ которого останавливает систему
управляемый кластерКластер, control plane которого обслуживает провайдер

Теория

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

Неверная формулировка: «Docker или Kubernetes?» Они на разных уровнях (урок 18.1).

Верная: «Хватает ли нам одной машины?»

Kubernetes решает задачу поддержания заданного состояния на множестве машин. Если машина одна, большая часть системы не нужна — а стоимость остаётся полной.

Признаки, что Compose достаточно

ПризнакПочему достаточно
Одна машина справляется с нагрузкойНечего распределять
Простой в несколько минут допустимНе нужна автоматическая замена узла
Развёртывания редкие и в рабочее времяНе нужны сложные стратегии
Одна команда владеет всей системойНе нужна изоляция по namespace
Нагрузка предсказуемаНе нужно автомасштабирование
Сервисов меньше десятиCompose справляется с описанием
Нет требования к географическому распределениюОдин регион

Существенно: это не список признаков малого проекта. Одна машина с 64 ГБ памяти обслуживает очень многое.

Признаки, что Compose перестаёт хватать

ПризнакЧто именно ломается
Одна машина не тянет нагрузкуТребуется распределение
Отказ машины недопустимCompose не переносит сервисы на другой узел
Нужно масштабирование по нагрузкеРучное --scale не успевает
Развёртывания частые, простой недопустимНужны стратегии выкатки и отката
Несколько команд на общей инфраструктуреНужны изоляция и квоты
Больше нескольких машин с ручным управлениемУправление вручную не масштабируется
Требуется автоматическое восстановлениеНекому переносить нагрузку с отказавшего узла

Каждый признак — наблюдаемый факт, а не предположение о будущем.

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

Самое частое основание для перехода: «нам понадобится масштабирование». Проверка:

ВопросЧто показывает
Какова нагрузка сейчас?Есть ли проблема сегодня
Какова была год назад?Скорость роста, а не воображаемая кривая
Что произойдёт при простое в 10 минут?Реальная цена доступности
Кто отвечает ночью?Есть ли кому эксплуатировать кластер
Сколько раз в неделю развёртывание?Нужны ли сложные стратегии
Отказывала ли машина за последний год?Насколько вероятен сценарий

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

Стоимость Kubernetes

Стоимость чаще всего недооценивают, потому что видят только первую строку.

СтатьяРазовая или постояннаяПорядок
Обучение командыРазовая, но повторяется с наймомНедели на человека
Перенос приложенийРазоваяСм. урок 18.3: половина работы в коде
Control planeПостояннаяУправляемый — оплата; свой — эксплуатация
Обновления кластераПостояннаяРаз в несколько месяцев, с рисками
ОтладкаПостояннаяБольше уровней между симптомом и причиной
Сопутствующие компонентыПостояннаяIngress, мониторинг, логи, сертификаты, политики
ДежурствоПостояннаяТребует знаний Kubernetes, а не только приложения

Последняя строка часто оказывается решающей: кластер требует людей, знающих кластер. Если такой человек один, кластер становится одной точкой отказа — организационной.

Почему отладка дороже

В Compose путь от симптома к причине короткий: контейнер, лог, процесс.

В Kubernetes между симптомом и причиной находятся:

text
запрос → Ingress → Service → kube-proxy → endpoints →
  → pod → readiness → контейнер → процесс

Каждое звено может быть причиной. Типичный пример: «сервис не отвечает». Возможные причины — от неверного селектора Service до вытеснения pod'а по памяти. Ни одна не видна в логе приложения.

Это не довод против Kubernetes — это статья расходов, которую нужно заложить.

Промежуточные варианты

Между «одна машина с Compose» и «полный кластер» есть несколько ступеней. Их часто пропускают.

ВариантЧто решаетЧего не решает
Compose + вторая машина вручнуюПереживание отказа однойАвтоматики нет
Compose + балансировщик перед двумя машинамиДоступность при отказеСостояние, выкатка
Docker SwarmНесколько узлов, простая модельРазвитие остановлено
NomadОркестрация без сложности KubernetesМеньше экосистема
Управляемый KubernetesControl plane обслуживает провайдерЗнания всё равно нужны
Serverless-платформа контейнеровНет узлов вообщеОграничения платформы, привязка
Свой KubernetesПолный контрольМаксимальная стоимость

Практическое наблюдение: управляемый Kubernetes снимает примерно половину эксплуатационной нагрузки — ту, что связана с control plane. Вторая половина — приложения, пробы, ресурсы, отладка — остаётся полностью.

Наблюдение о выносе состояния

Часто основная трудность переноса — база данных (урок 18.3). Управляемая база вне кластера снимает её целиком: резервные копии, обновления, отказоустойчивость перестают быть вашей задачей.

Следствие, которое стоит проверить: вынос базы наружу иногда снимает и саму потребность в кластере. Если после этого остаются только сервисы без состояния, их часто хватает разместить на двух машинах с балансировщиком.

Честный чек-лист

Ниже — вопросы с весами. Смысл не в точности чисел, а в том, чтобы разговор шёл о фактах.

ВопросВес, если «да»
Одна машина не справляется с нагрузкой сегодня+3
Простой более 5 минут стоит существенных денег+3
Развёртывания чаще раза в день+2
Нагрузка меняется в разы в течение суток+2
Больше трёх команд на общей инфраструктуре+2
Уже управляете более чем тремя машинами вручную+2
Требуется несколько географических регионов+2
В команде есть человек с опытом эксплуатации Kubernetes+1
Состояние уже вынесено в управляемые сервисы+1
Приложения переживают перезапуск в любой момент+1
«Нам понадобится масштабирование» без данных−2
Единственный довод — так делают все−3
Некому дежурить с знанием кластера−3
Простой в час допустим−2

Итог:

СуммаВывод
8 и вышеKubernetes обоснован
4–7Промежуточный вариант; вернуться через полгода
3 и нижеCompose достаточно; перенос обойдётся дороже пользы

Отрицательные веса важнее положительных: они отсекают решения, принятые по инерции.


Внутренний механизм

Почему Compose не переносит сервисы на другой узел

Compose — клиент Docker API одной машины (урок 9.1). Он не знает о существовании других машин и не имеет цикла управления, который сравнивал бы желаемое состояние с фактическим.

restart: unless-stopped перезапустит контейнер на той же машине. Если машина недоступна, перезапускать некому.

Kubernetes отличается наличием контроллеров: цикл сравнения состояний работает постоянно, и узел, переставший отвечать, приводит к переносу pod'ов (урок 18.2).

Это и есть граница: Compose управляет контейнерами, Kubernetes поддерживает состояние.

Почему стоимость нельзя амортизировать заранее

Довод «поставим сейчас, чтобы не переносить потом» опирается на предположение, что перенос позже дороже. На практике:

  • значительная часть работы — приведение приложений в порядок (урок 18.3), и она полезна независимо от кластера;
  • остальная часть — манифесты, и они пишутся быстрее с накопленным пониманием нагрузки;
  • эксплуатационная стоимость начисляется с первого дня и не возвращается.

То есть ранняя установка не экономит будущую работу, а добавляет текущую.


Команды и примеры

Чек-лист как инструмент

bash
mkdir -p /tmp/decide && cd /tmp/decide

cat > checklist.py <<'PY'
"""Чек-лист принятия решения об orchestration.

Веса подобраны так, чтобы отрицательные признаки (решение по инерции,
отсутствие людей для эксплуатации) отсекали положительные.
"""
from __future__ import annotations

import json
import sys
from pathlib import Path

QUESTIONS: list[tuple[str, str, int]] = [
    ("load", "Одна машина не справляется с нагрузкой СЕГОДНЯ", 3),
    ("downtime", "Простой более 5 минут стоит существенных денег", 3),
    ("deploys", "Развёртывания чаще раза в день", 2),
    ("spikes", "Нагрузка меняется в разы в течение суток", 2),
    ("teams", "Больше трёх команд на общей инфраструктуре", 2),
    ("machines", "Уже управляете более чем тремя машинами вручную", 2),
    ("regions", "Требуется несколько географических регионов", 2),
    ("expertise", "В команде есть человек с опытом эксплуатации кластера", 1),
    ("managed_state", "Состояние вынесено в управляемые сервисы", 1),
    ("restartable", "Приложения переживают перезапуск в любой момент", 1),
    ("someday", "Главный довод: «нам понадобится масштабирование»", -2),
    ("everyone", "Единственный довод: так делают все", -3),
    ("no_oncall", "Некому дежурить со знанием кластера", -3),
    ("tolerant", "Простой в час допустим", -2),
]

VERDICTS = [
    (8, "Kubernetes обоснован",
     "признаки указывают на настоящие требования, а не на ожидания"),
    (4, "Промежуточный вариант; вернуться через полгода",
     "часть требований настоящая, но полный кластер преждевременен"),
    (-99, "Compose достаточно",
     "перенос обойдётся дороже пользы, которую принесёт"),
]

INTERMEDIATE = [
    ("Вторая машина с Compose вручную", "переживание отказа одной", "автоматики нет"),
    ("Балансировщик перед двумя машинами", "доступность при отказе", "состояние, выкатка"),
    ("Управляемая база вне кластера", "резервные копии, обновления, отказоустойчивость",
     "стоимость сервиса"),
    ("Nomad", "оркестрация без сложности Kubernetes", "меньше экосистема"),
    ("Управляемый Kubernetes", "control plane обслуживает провайдер",
     "знания всё равно нужны"),
]


def score(answers: dict[str, bool]) -> dict[str, object]:
    positive: list[tuple[str, int]] = []
    negative: list[tuple[str, int]] = []
    total = 0
    for key, text, weight in QUESTIONS:
        if answers.get(key):
            total += weight
            (positive if weight > 0 else negative).append((text, weight))

    for threshold, verdict, why in VERDICTS:
        if total >= threshold:
            break

    return {
        "сумма": total,
        "за": positive,
        "против": negative,
        "вердикт": verdict,
        "почему": why,
    }


def main(path: str) -> int:
    answers = json.loads(Path(path).read_text())
    result = score(answers)

    print(f"  проект: {answers.get('_name', 'без имени')}")
    print()
    print(f"  {'признак':<56} {'вес':>5}")
    print("  " + "─" * 64)
    for text, weight in result["за"]:
        print(f"  {text:<56} {weight:>+5}")
    for text, weight in result["против"]:
        print(f"  {text:<56} {weight:>+5}")
    print("  " + "─" * 64)
    print(f"  {'СУММА':<56} {result['сумма']:>5}")
    print()
    print(f"  Вывод: {result['вердикт']}")
    print(f"  Почему: {result['почему']}")

    if 4 <= result["сумма"] < 8:
        print()
        print(f"  {'промежуточный вариант':<38} {'решает':<44} не решает")
        print("  " + "─" * 104)
        for name, solves, misses in INTERMEDIATE:
            print(f"  {name:<38} {solves:<44} {misses}")

    print()
    print(json.dumps({"сумма": result["сумма"], "вердикт": result["вердикт"]},
                     ensure_ascii=False))
    # Код возврата: 0 — Compose достаточно, 1 — промежуточный, 2 — Kubernetes
    return 2 if result["сумма"] >= 8 else (1 if result["сумма"] >= 4 else 0)


if __name__ == "__main__":
    sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "project.json"))
PY

cat > startup.json <<'EOF'
{
  "_name": "Небольшой сервис, три разработчика",
  "load": false,
  "downtime": false,
  "deploys": true,
  "spikes": false,
  "teams": false,
  "machines": false,
  "regions": false,
  "expertise": false,
  "managed_state": true,
  "restartable": true,
  "someday": true,
  "everyone": true,
  "no_oncall": true,
  "tolerant": true
}
EOF

echo "═══ проект 1 ═══"
python3 checklist.py startup.json; echo "  код возврата: $?"

Ожидаемый вывод:

text
═══ проект 1 ═══
  проект: Небольшой сервис, три разработчика

  признак                                                    вес
  ────────────────────────────────────────────────────────────────
  Развёртывания чаще раза в день                               +2
  Состояние вынесено в управляемые сервисы                     +1
  Приложения переживают перезапуск в любой момент              +1
  Главный довод: «нам понадобится масштабирование»             -2
  Единственный довод: так делают все                           -3
  Некому дежурить со знанием кластера                          -3
  Простой в час допустим                                       -2

  ────────────────────────────────────────────────────────────────
  СУММА                                                        -6

  Вывод: Compose достаточно
  Почему: перенос обойдётся дороже пользы, которую принесёт

  {"сумма": -6, "вердикт": "Compose достаточно"}
  код возврата: 0

Заметьте: три положительных признака есть, и все они настоящие. Их перевешивают отрицательные — и это верно, потому что эксплуатировать кластер здесь некому.

Три проекта для сравнения

bash
cd /tmp/decide

cat > growing.json <<'EOF'
{
  "_name": "Растущий сервис, две команды, четыре машины",
  "load": false,
  "downtime": true,
  "deploys": true,
  "spikes": true,
  "teams": false,
  "machines": true,
  "regions": false,
  "expertise": true,
  "managed_state": true,
  "restartable": true,
  "someday": false,
  "everyone": false,
  "no_oncall": false,
  "tolerant": false
}
EOF

cat > platform.json <<'EOF'
{
  "_name": "Платформа, шесть команд, два региона",
  "load": true,
  "downtime": true,
  "deploys": true,
  "spikes": true,
  "teams": true,
  "machines": true,
  "regions": true,
  "expertise": true,
  "managed_state": true,
  "restartable": true,
  "someday": false,
  "everyone": false,
  "no_oncall": false,
  "tolerant": false
}
EOF

for f in startup.json growing.json platform.json; do
    printf '\n═══ %s ═══\n' "$f"
    python3 checklist.py "$f" | tail -20
    printf '  код возврата: %s\n' "$?"
done

Ожидаемый вывод:

text
═══ startup.json ═══
  ...
  СУММА                                                        -6
  Вывод: Compose достаточно
  код возврата: 0

═══ growing.json ═══
  признак                                                    вес
  ────────────────────────────────────────────────────────────────
  Простой более 5 минут стоит существенных денег               +3
  Развёртывания чаще раза в день                               +2
  Нагрузка меняется в разы в течение суток                     +2
  Уже управляете более чем тремя машинами вручную              +2
  В команде есть человек с опытом эксплуатации кластера        +1
  Состояние вынесено в управляемые сервисы                     +1
  Приложения переживают перезапуск в любой момент              +1
  ────────────────────────────────────────────────────────────────
  СУММА                                                        12

  Вывод: Kubernetes обоснован
  код возврата: 2

═══ platform.json ═══
  СУММА                                                        17
  Вывод: Kubernetes обоснован
  код возврата: 2

Второй проект набирает 12 без единого отрицательного признака — и это тот случай, когда решение принимают по фактам, а не по моде.

Стоимость в цифрах

bash
cd /tmp/decide
cat > cost.py <<'PY'
"""Модель стоимости перехода. Числа — порядки величин, не измерения."""
from __future__ import annotations

import json

# (статья, разовая ч, ежемесячная ч, комментарий)
COSTS = [
    ("Обучение команды", 120, 8, "недели на человека; повторяется с наймом"),
    ("Перенос приложений", 60, 0, "половина — работа в коде (урок 18.3)"),
    ("Манифесты и шаблоны", 40, 4, "поддержание при изменениях"),
    ("Ingress и сертификаты", 16, 2, "выбор контроллера, обновление"),
    ("Мониторинг и логи", 40, 6, "метрики кластера отдельно от приложения"),
    ("Обновления кластера", 8, 6, "раз в несколько месяцев, с рисками"),
    ("Отладка (сверх обычной)", 0, 12, "больше уровней между симптомом и причиной"),
    ("Дежурство со знанием кластера", 0, 10, "требует людей, знающих кластер"),
]

MANAGED_SAVING = {
    "Обновления кластера": 0.7,
    "Мониторинг и логи": 0.3,
    "Обучение команды": 0.2,
}


def main() -> None:
    print(f"  {'статья':<32} {'разово, ч':>10} {'в месяц, ч':>11}  комментарий")
    print("  " + "─" * 108)
    one_time = monthly = 0
    for name, once, per_month, note in COSTS:
        one_time += once
        monthly += per_month
        print(f"  {name:<32} {once:>10} {per_month:>11}  {note}")
    print("  " + "─" * 108)
    print(f"  {'ИТОГО':<32} {one_time:>10} {monthly:>11}")
    print()

    managed_monthly = sum(
        per_month * (1 - MANAGED_SAVING.get(name, 0.0))
        for name, _, per_month, _ in COSTS
    )
    print(f"  свой кластер:        {monthly:>5} ч/мес")
    print(f"  управляемый кластер: {managed_monthly:>5.0f} ч/мес "
          f"(−{(1 - managed_monthly / monthly) * 100:.0f} %)")
    print()
    print("  Управляемый кластер снимает нагрузку по control plane.")
    print("  Приложения, пробы, ресурсы и отладка остаются полностью.")
    print()

    year = one_time + monthly * 12
    print(f"  первый год, свой кластер: {year} ч ≈ {year / 160:.1f} человеко-месяца")
    print()
    print("  Довод «поставим сейчас, чтобы не переносить потом» неверен:")
    print("  ежемесячная стоимость начисляется с первого дня и не возвращается,")
    print("  а разовая часть (перенос) полезна и без кластера.")
    print()
    print(json.dumps({"разово_ч": one_time, "в_месяц_ч": monthly,
                      "первый_год_ч": year}, ensure_ascii=False))


if __name__ == "__main__":
    main()
PY

echo "═══ модель стоимости ═══"
python3 cost.py

cd /tmp && rm -rf /tmp/decide

Ожидаемый вывод:

text
═══ модель стоимости ═══
  статья                            разово, ч  в месяц, ч  комментарий
  ────────────────────────────────────────────────────────────────────────────────────────────────────────────
  Обучение команды                        120           8  недели на человека; повторяется с наймом
  Перенос приложений                       60           0  половина — работа в коде (урок 18.3)
  Манифесты и шаблоны                      40           4  поддержание при изменениях
  Ingress и сертификаты                    16           2  выбор контроллера, обновление
  Мониторинг и логи                        40           6  метрики кластера отдельно от приложения
  Обновления кластера                       8           6  раз в несколько месяцев, с рисками
  Отладка (сверх обычной)                   0          12  больше уровней между симптомом и причиной
  Дежурство со знанием кластера             0          10  требует людей, знающих кластер
  ────────────────────────────────────────────────────────────────────────────────────────────────────────────
  ИТОГО                                   284          48

  свой кластер:           48 ч/мес
  управляемый кластер:    42 ч/мес (−13 %)

  Управляемый кластер снимает нагрузку по control plane.
  Приложения, пробы, ресурсы и отладка остаются полностью.

  первый год, свой кластер: 860 ч ≈ 5.4 человеко-месяца

Обратите внимание на строку про управляемый кластер: экономия — 13 %, а не «половина». Она приходится на обновления и часть мониторинга; отладка, дежурство и работа с приложениями не уменьшаются.

Это уточняет утверждение из теории: управляемый кластер снимает существенную часть работы по control plane, но control plane — не главная статья расходов.


Практическое упражнение

Задание. Постройте инструмент принятия решения и примените его к трём проектам.

Требования:

  1. Чек-лист с положительными и отрицательными весами; отрицательные должны быть способны перевесить.
  2. Три вердикта с разными кодами возврата.
  3. Применить к трём проектам с разными исходами.
  4. Для промежуточного исхода вывести список вариантов между Compose и кластером.
  5. Модель стоимости: разовая и постоянная части, за первый год.
  6. Показать, какую долю стоимости снимает управляемый кластер.
  7. Проверить чек-лист на подделке: проект, у которого «всё да», но эксплуатировать некому.

Подсказки

Подсказка 1

Отрицательные веса должны быть по модулю сравнимы с самыми большими положительными, иначе они не влияют.

Подсказка 2

Проверка на подделке: если признаки «нужно масштабирование» проставлены без данных, инструмент должен это учесть.

Подсказка 3

Постоянная стоимость важнее разовой: за год она обычно превышает разовую.

Решение

Показать решение
bash
mkdir -p /tmp/orch && cd /tmp/orch

cat > decide.py <<'PY'
"""Инструмент принятия решения об orchestration.

Устройство весов: отрицательные признаки по модулю сравнимы
с самыми большими положительными. Это сделано намеренно —
иначе проект, где эксплуатировать кластер некому, набрал бы
проходной балл на одних лишь ожиданиях роста.
"""
from __future__ import annotations

import json
import sys
from pathlib import Path

# (ключ, формулировка, вес, чем подтверждается)
QUESTIONS: list[tuple[str, str, int, str]] = [
    ("load", "Одна машина не справляется СЕГОДНЯ", 3,
     "метрики загрузки за последний месяц"),
    ("downtime", "Простой более 5 минут стоит существенных денег", 3,
     "оценка потерь, а не ощущение"),
    ("deploys", "Развёртывания чаще раза в день", 2,
     "история выкаток"),
    ("spikes", "Нагрузка меняется в разы в течение суток", 2,
     "график запросов"),
    ("teams", "Больше трёх команд на общей инфраструктуре", 2,
     "список команд"),
    ("machines", "Более трёх машин под ручным управлением", 2,
     "инвентарь"),
    ("regions", "Нужны несколько географических регионов", 2,
     "требование, а не желание"),
    ("expertise", "Есть человек с опытом эксплуатации кластера", 1,
     "конкретное имя"),
    ("managed_state", "Состояние вынесено в управляемые сервисы", 1,
     "список хранилищ"),
    ("restartable", "Приложения переживают перезапуск в любой момент", 1,
     "проверено, а не предполагается"),
    ("someday", "Главный довод: «понадобится масштабирование»", -2,
     "данных о росте нет"),
    ("everyone", "Единственный довод: так делают все", -3,
     "довод не о системе"),
    ("no_oncall", "Некому дежурить со знанием кластера", -3,
     "кластер станет одной точкой отказа"),
    ("tolerant", "Простой в час допустим", -2,
     "автоматическое восстановление не окупится"),
]

INTERMEDIATE = [
    ("Вторая машина с Compose вручную",
     "переживание отказа одной машины", "автоматики нет"),
    ("Балансировщик перед двумя машинами",
     "доступность при отказе", "состояние, выкатка"),
    ("Управляемая база вне кластера",
     "копии, обновления, отказоустойчивость хранилища", "стоимость сервиса"),
    ("Nomad",
     "оркестрация с меньшей сложностью", "меньше экосистема"),
    ("Управляемый Kubernetes",
     "control plane обслуживает провайдер", "знания всё равно нужны"),
]

VERDICTS = [
    (8, 2, "Kubernetes обоснован",
     "требования подтверждаются фактами, есть кому эксплуатировать"),
    (4, 1, "Промежуточный вариант; пересмотреть через полгода",
     "часть требований настоящая, полный кластер преждевременен"),
    (None, 0, "Compose достаточно",
     "перенос обойдётся дороже пользы"),
]


def evaluate(answers: dict[str, object]) -> dict[str, object]:
    plus: list[tuple[str, int, str]] = []
    minus: list[tuple[str, int, str]] = []
    total = 0
    for key, text, weight, evidence in QUESTIONS:
        if answers.get(key):
            total += weight
            (plus if weight > 0 else minus).append((text, weight, evidence))

    for threshold, code, verdict, why in VERDICTS:
        if threshold is None or total >= threshold:
            break

    raw_plus = sum(w for _, w, _ in plus)
    raw_minus = sum(w for _, w, _ in minus)

    return {
        "проект": answers.get("_name", "без имени"),
        "сумма": total,
        "плюсов": raw_plus,
        "минусов": raw_minus,
        "за": plus,
        "против": minus,
        "вердикт": verdict,
        "почему": why,
        "код": code,
        "перевес": bool(minus) and total < 4 <= raw_plus,
    }


def report(r: dict[str, object]) -> None:
    print(f"  проект: {r['проект']}")
    print()
    print(f"  {'признак':<50} {'вес':>5}  чем подтверждается")
    print("  " + "─" * 100)
    for text, weight, evidence in r["за"]:
        print(f"  {text:<50} {weight:>+5}  {evidence}")
    for text, weight, evidence in r["против"]:
        print(f"  {text:<50} {weight:>+5}  {evidence}")
    print("  " + "─" * 100)
    print(f"  {'плюсы':<50} {r['плюсов']:>+5}")
    print(f"  {'минусы':<50} {r['минусов']:>+5}")
    print(f"  {'СУММА':<50} {r['сумма']:>5}")
    print()
    print(f"  Вывод: {r['вердикт']}")
    print(f"  Почему: {r['почему']}")

    if r["перевес"]:
        print()
        print("  ВНИМАНИЕ: положительных признаков хватило бы на переход,")
        print("  но отрицательные их перевесили. Это и есть смысл чек-листа:")
        print("  требование может быть настоящим, а условий для перехода нет.")

    if r["код"] == 1:
        print()
        print(f"  {'промежуточный вариант':<38} {'решает':<46} не решает")
        print("  " + "─" * 106)
        for name, solves, misses in INTERMEDIATE:
            print(f"  {name:<38} {solves:<46} {misses}")


def main(paths: list[str]) -> int:
    results = []
    for i, path in enumerate(paths):
        if i:
            print()
        answers = json.loads(Path(path).read_text())
        r = evaluate(answers)
        report(r)
        results.append({"проект": r["проект"], "сумма": r["сумма"],
                        "вердикт": r["вердикт"], "код": r["код"],
                        "перевес": r["перевес"]})
    print()
    print(json.dumps(results, ensure_ascii=False))
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:] or ["project.json"]))
PY

cat > cost.py <<'PY'
"""Модель стоимости перехода на Kubernetes.

Числа — порядки величин, а не измерения. Ценность модели
в соотношении разовой и постоянной частей, а не в точности.
"""
from __future__ import annotations

import json

# (статья, разово ч, в месяц ч, доля, снимаемая управляемым кластером)
COSTS = [
    ("Обучение команды", 120, 8, 0.20),
    ("Перенос приложений", 60, 0, 0.00),
    ("Манифесты и шаблоны", 40, 4, 0.00),
    ("Ingress и сертификаты", 16, 2, 0.50),
    ("Мониторинг и логи", 40, 6, 0.30),
    ("Обновления кластера", 8, 6, 0.70),
    ("Резервное копирование etcd", 8, 3, 1.00),
    ("Отладка сверх обычной", 0, 12, 0.00),
    ("Дежурство со знанием кластера", 0, 10, 0.00),
]

HOURS_PER_MONTH = 160


def main() -> None:
    print(f"  {'статья':<32} {'разово':>8} {'в месяц':>9} {'снимает управляемый':>21}")
    print("  " + "─" * 74)
    once_total = monthly_total = managed_monthly = 0.0
    for name, once, per_month, saving in COSTS:
        once_total += once
        monthly_total += per_month
        managed_monthly += per_month * (1 - saving)
        share = f"{saving * 100:.0f} %" if saving else "—"
        print(f"  {name:<32} {once:>8} {per_month:>9} {share:>21}")
    print("  " + "─" * 74)
    print(f"  {'ИТОГО':<32} {once_total:>8.0f} {monthly_total:>9.0f}")
    print()

    year_own = once_total + monthly_total * 12
    year_managed = once_total + managed_monthly * 12
    saved = (1 - managed_monthly / monthly_total) * 100

    print(f"  {'вариант':<24} {'разово':>8} {'в месяц':>9} {'первый год':>12} "
          f"{'человеко-мес.':>15}")
    print("  " + "─" * 72)
    print(f"  {'свой кластер':<24} {once_total:>8.0f} {monthly_total:>9.0f} "
          f"{year_own:>12.0f} {year_own / HOURS_PER_MONTH:>15.1f}")
    print(f"  {'управляемый кластер':<24} {once_total:>8.0f} {managed_monthly:>9.0f} "
          f"{year_managed:>12.0f} {year_managed / HOURS_PER_MONTH:>15.1f}")
    print()
    print(f"  управляемый снимает {saved:.0f} % ежемесячной нагрузки")
    print()
    print("  Что он НЕ снимает: перенос приложений, манифесты, отладку,")
    print("  дежурство. Это большая часть постоянной стоимости.")
    print()

    print(f"  {'доля постоянной стоимости за год':<44} {'свой':>8} {'управляемый':>14}")
    print("  " + "─" * 68)
    print(f"  {'разовая часть':<44} "
          f"{once_total / year_own * 100:>7.0f} % {once_total / year_managed * 100:>13.0f} %")
    print(f"  {'постоянная часть':<44} "
          f"{monthly_total * 12 / year_own * 100:>7.0f} % "
          f"{managed_monthly * 12 / year_managed * 100:>13.0f} %")
    print()
    print("  Постоянная часть больше разовой уже в первый год.")
    print("  Довод «поставим заранее, чтобы не переносить потом» неверен:")
    print("  ежемесячная стоимость начисляется с первого дня, а разовая")
    print("  часть — приведение приложений в порядок — полезна и без кластера.")
    print()
    print(json.dumps({
        "разово_ч": once_total,
        "в_месяц_свой": monthly_total,
        "в_месяц_управляемый": round(managed_monthly, 1),
        "экономия_проц": round(saved),
        "год_свой": year_own,
        "год_управляемый": round(year_managed),
    }, ensure_ascii=False))


if __name__ == "__main__":
    main()
PY

# ── три проекта с разными исходами ───────────────────────────────────
cat > small.json <<'EOF'
{
  "_name": "Внутренний сервис, три разработчика",
  "deploys": true, "managed_state": true, "restartable": true,
  "someday": true, "everyone": true, "no_oncall": true, "tolerant": true
}
EOF

cat > growing.json <<'EOF'
{
  "_name": "Растущий продукт, четыре машины, есть дежурство",
  "downtime": true, "deploys": true, "machines": true,
  "expertise": true, "managed_state": true, "restartable": true
}
EOF

cat > platform.json <<'EOF'
{
  "_name": "Платформа, шесть команд, два региона",
  "load": true, "downtime": true, "deploys": true, "spikes": true,
  "teams": true, "machines": true, "regions": true,
  "expertise": true, "managed_state": true, "restartable": true
}
EOF

# ── подделка: «всё да», но эксплуатировать некому ────────────────────
cat > fake.json <<'EOF'
{
  "_name": "«Нам точно нужен Kubernetes» (проверка на подделке)",
  "spikes": true, "deploys": true, "regions": true,
  "restartable": true,
  "someday": true, "everyone": true, "no_oncall": true, "tolerant": true
}
EOF

fail=0
ok()  { printf '  ✓ %s\n' "$1"; }
bad() { printf '  ✗ %s\n' "$1"; fail=1; }

printf '\n═══ Требование 1: веса чек-листа ═══\n'
python3 - <<'PY'
from decide import QUESTIONS

plus = [w for _, _, w, _ in QUESTIONS if w > 0]
minus = [w for _, _, w, _ in QUESTIONS if w < 0]
print(f"    вопросов: {len(QUESTIONS)} (положительных {len(plus)}, "
      f"отрицательных {len(minus)})")
print(f"    максимум положительных: {sum(plus):+}")
print(f"    максимум отрицательных: {sum(minus):+}")
print(f"    сильнейший положительный: {max(plus):+}")
print(f"    сильнейший отрицательный: {min(minus):+}")
print()
if abs(min(minus)) >= max(plus):
    print("    ✓ сильнейший отрицательный признак по модулю не слабее")
    print("      сильнейшего положительного — значит, способен перевесить")
else:
    print("    ✗ отрицательные признаки слишком слабы")
PY
python3 -c "
from decide import QUESTIONS
plus = [w for _, _, w, _ in QUESTIONS if w > 0]
minus = [w for _, _, w, _ in QUESTIONS if w < 0]
raise SystemExit(0 if abs(min(minus)) >= max(plus) else 1)" \
    && ok "отрицательные веса способны перевесить положительные" \
    || bad "отрицательные веса слишком слабы"

printf '\n═══ Требование 2: три вердикта ═══\n'
python3 - <<'PY'
from decide import VERDICTS
print(f"    {'порог':>7} {'код':>5}  вердикт")
print("    " + "─" * 62)
for threshold, code, verdict, _ in VERDICTS:
    t = "любая" if threshold is None else f"≥ {threshold}"
    print(f"    {t:>7} {code:>5}  {verdict}")
print()
print("    Разные коды возврата позволяют использовать инструмент")
print("    в сценарии: 0 — ничего не менять, 1 — рассмотреть промежуточное,")
print("    2 — планировать переход.")
PY
n_verdicts="$(python3 -c 'from decide import VERDICTS; print(len(VERDICTS))')"
codes="$(python3 -c 'from decide import VERDICTS; print(len({c for _, c, _, _ in VERDICTS}))')"
[ "$n_verdicts" = "3" ] && [ "$codes" = "3" ] \
    && ok "три вердикта с тремя различными кодами возврата" \
    || bad "вердиктов: $n_verdicts, кодов: $codes"

printf '\n═══ Требование 3: три проекта ═══\n'
python3 decide.py small.json growing.json platform.json > verdicts.log 2>&1
sed -n '1,/Почему/p' verdicts.log | sed 's/^/  /'
summary="$(tail -1 verdicts.log)"
python3 - <<'PY' < verdicts.log
import json, sys
lines = sys.stdin.read().splitlines()
results = json.loads(lines[-1])
print()
print(f"    {'проект':<48} {'сумма':>6} {'код':>5}  вердикт")
print("    " + "─" * 108)
for r in results:
    print(f"    {r['проект']:<48} {r['сумма']:>6} {r['код']:>5}  {r['вердикт']}")
codes = {r["код"] for r in results}
print()
print(f"    различных исходов: {len(codes)} из 3")
PY
n_codes="$(echo "$summary" | python3 -c "
import json, sys
print(len({r['код'] for r in json.load(sys.stdin)}))")"
[ "$n_codes" = "3" ] \
    && ok "три проекта дали три разных исхода" \
    || bad "различных исходов: $n_codes"

printf '\n═══ Требование 4: промежуточные варианты ═══\n'
sed -n '/промежуточный вариант/,/Управляемый Kubernetes/p' verdicts.log | sed 's/^/  /'
grep -q "Управляемый Kubernetes" verdicts.log \
    && ok "для промежуточного исхода выведены варианты между Compose и кластером" \
    || bad "варианты не выведены"

printf '\n═══ Требования 5 и 6: стоимость ═══\n'
python3 cost.py > cost.log 2>&1
cat cost.log | sed 's/^/  /'
c_summary="$(tail -1 cost.log)"
get() { echo "$c_summary" | python3 -c "import json,sys; print(json.load(sys.stdin)['$1'])"; }
once="$(get разово_ч)"; per_month="$(get в_месяц_свой)"
saving="$(get экономия_проц)"; year="$(get год_свой)"
printf '\n  разово: %s ч, в месяц: %s ч, за год: %s ч\n' "$once" "$per_month" "$year"
python3 -c "raise SystemExit(0 if ${per_month} * 12 > ${once} else 1)" \
    && ok "постоянная часть превышает разовую уже в первый год" \
    || bad "постоянная часть меньше разовой"
printf '  управляемый кластер снимает: %s %%\n' "$saving"
python3 -c "raise SystemExit(0 if 0 < ${saving} < 50 else 1)" \
    && ok "экономия существенна, но далека от половины — отладка и дежурство остаются" \
    || bad "оценка экономии: $saving %"

printf '\n═══ Требование 7: проверка на подделке ═══\n'
python3 decide.py fake.json > fake.log 2>&1
sed -n '1,/чек-листа/p' fake.log | sed 's/^/  /'
f_summary="$(tail -1 fake.log)"
f_code="$(echo "$f_summary" | python3 -c "import json,sys; print(json.load(sys.stdin)[0]['код'])")"
f_over="$(echo "$f_summary" | python3 -c "import json,sys; print(json.load(sys.stdin)[0]['перевес'])")"
printf '\n  код исхода: %s, сработал перевес: %s\n' "$f_code" "$f_over"
[ "$f_code" = "0" ] && [ "$f_over" = "True" ] \
    && ok "подделка отсеяна: положительных хватало, отрицательные перевесили" \
    || bad "код=$f_code перевес=$f_over"

printf '\n═══ Что НЕ проверялось ═══\n'
python3 - <<'PY'
NOT_RUN = [
    ("фактическая стоимость перехода", "зависит от проекта, команды, провайдера"),
    ("работа кластера", "kubectl и кластер недоступны"),
    ("веса чек-листа", "подобраны по опыту, а не измерены"),
    ("экономия управляемого кластера", "оценка долей, не замер"),
]
print(f"    {'проверка':<40} причина")
print("    " + "─" * 80)
for name, why in NOT_RUN:
    print(f"    {name:<40} {why}")
print(f"\n    не выполнялось: {len(NOT_RUN)}")
print()
print("    Проверено то, что проверяется на модели: устройство весов,")
print("    различимость исходов, поведение на подделке, соотношение")
print("    разовой и постоянной частей. Сами числа — порядки величин.")
print()
print("    Это существенная оговорка: инструмент помогает вести разговор")
print("    о фактах, но не заменяет измерений на конкретном проекте.")
PY
ok "невыполненное перечислено; числа названы моделью"

printf '\n═══ ИТОГ ═══\n'
[ "$fail" -eq 0 ] && echo "  все требования выполнены" || echo "  ЕСТЬ ПРОВАЛЫ"
echo "  примечание: кластер не использовался; веса и стоимость — модель"

cd /tmp && rm -rf /tmp/orch
exit "$fail"

Ожидаемый вывод:

text
═══ Требование 1: веса чек-листа ═══
    вопросов: 14 (положительных 10, отрицательных 4)
    максимум положительных: +19
    максимум отрицательных: -10
    сильнейший положительный: +3
    сильнейший отрицательный: -3

    ✓ сильнейший отрицательный признак по модулю не слабее
      сильнейшего положительного — значит, способен перевесить
  ✓ отрицательные веса способны перевесить положительные

═══ Требование 2: три вердикта ═══
      порог   код  вердикт
    ──────────────────────────────────────────────────────────────
        ≥ 8     2  Kubernetes обоснован
        ≥ 4     1  Промежуточный вариант; пересмотреть через полгода
      любая     0  Compose достаточно

    Разные коды возврата позволяют использовать инструмент
    в сценарии: 0 — ничего не менять, 1 — рассмотреть промежуточное,
    2 — планировать переход.
  ✓ три вердикта с тремя различными кодами возврата

═══ Требование 3: три проекта ═══
  проект: Внутренний сервис, три разработчика

  признак                                              вес  чем подтверждается
  ────────────────────────────────────────────────────────────────────────────────────────────────────
  Развёртывания чаще раза в день                        +2  история выкаток
  Состояние вынесено в управляемые сервисы              +1  список хранилищ
  Приложения переживают перезапуск в любой момент       +1  проверено, а не предполагается
  Главный довод: «понадобится масштабирование»          -2  данных о росте нет
  Единственный довод: так делают все                    -3  довод не о системе
  Некому дежурить со знанием кластера                   -3  кластер станет одной точкой отказа
  Простой в час допустим                                -2  автоматическое восстановление не окупится
  ────────────────────────────────────────────────────────────────────────────────────────────────────
  плюсы                                                 +4
  минусы                                               -10
  СУММА                                                 -6

  Вывод: Compose достаточно
  Почему: перенос обойдётся дороже пользы

    проект                                            сумма   код  вердикт
    ────────────────────────────────────────────────────────────────────────────────────────────────────────────
    Внутренний сервис, три разработчика                  -6     0  Compose достаточно
    Растущий продукт, четыре машины, есть дежурство       7     1  Промежуточный вариант; пересмотреть через полгода
    Платформа, шесть команд, два региона                 19     2  Kubernetes обоснован

    различных исходов: 3 из 3
  ✓ три проекта дали три разных исхода

═══ Требование 4: промежуточные варианты ═══
  промежуточный вариант                  решает                                         не решает
  ──────────────────────────────────────────────────────────────────────────────────────────────────────────
  Вторая машина с Compose вручную        переживание отказа одной машины                автоматики нет
  Балансировщик перед двумя машинами     доступность при отказе                         состояние, выкатка
  Управляемая база вне кластера          копии, обновления, отказоустойчивость хранилища стоимость сервиса
  Nomad                                  оркестрация с меньшей сложностью               меньше экосистема
  Управляемый Kubernetes                 control plane обслуживает провайдер            знания всё равно нужны
  ✓ для промежуточного исхода выведены варианты между Compose и кластером

═══ Требования 5 и 6: стоимость ═══
  статья                             разово   в месяц   снимает управляемый
  ──────────────────────────────────────────────────────────────────────────
  Обучение команды                      120         8                  20 %
  Перенос приложений                     60         0                     —
  Манифесты и шаблоны                    40         4                     —
  Ingress и сертификаты                  16         2                  50 %
  Мониторинг и логи                      40         6                  30 %
  Обновления кластера                     8         6                  70 %
  Резервное копирование etcd              8         3                 100 %
  Отладка сверх обычной                   0        12                     —
  Дежурство со знанием кластера           0        10                     —
  ──────────────────────────────────────────────────────────────────────────
  ИТОГО                                 292        51

  вариант                    разово   в месяц   первый год   человеко-мес.
  ────────────────────────────────────────────────────────────────────────
  свой кластер                  292        51          904             5.7
  управляемый кластер           292        43          805             5.0

  управляемый снимает 16 % ежемесячной нагрузки

  Что он НЕ снимает: перенос приложений, манифесты, отладку,
  дежурство. Это большая часть постоянной стоимости.

  доля постоянной стоимости за год                        свой    управляемый
  ────────────────────────────────────────────────────────────────────
  разовая часть                                             32 %            36 %
  постоянная часть                                          68 %            64 %

  Постоянная часть больше разовой уже в первый год.
  ...

  разово: 292.0 ч, в месяц: 51.0 ч, за год: 904.0 ч
  ✓ постоянная часть превышает разовую уже в первый год
  управляемый кластер снимает: 16 %
  ✓ экономия существенна, но далека от половины — отладка и дежурство остаются

═══ Требование 7: проверка на подделке ═══
  проект: «Нам точно нужен Kubernetes» (проверка на подделке)

  признак                                              вес  чем подтверждается
  ────────────────────────────────────────────────────────────────────────────────────────────────────
  Развёртывания чаще раза в день                        +2  история выкаток
  Нагрузка меняется в разы в течение суток              +2  график запросов
  Нужны несколько географических регионов               +2  требование, а не желание
  Приложения переживают перезапуск в любой момент       +1  проверено, а не предполагается
  Главный довод: «понадобится масштабирование»          -2  данных о росте нет
  Единственный довод: так делают все                    -3  довод не о системе
  Некому дежурить со знанием кластера                   -3  кластер станет одной точкой отказа
  Простой в час допустим                                -2  автоматическое восстановление не окупится
  ────────────────────────────────────────────────────────────────────────────────────────────────────
  плюсы                                                 +7
  минусы                                               -10
  СУММА                                                 -3

  Вывод: Compose достаточно
  Почему: перенос обойдётся дороже пользы

  ВНИМАНИЕ: положительных признаков хватило бы на переход,
  но отрицательные их перевесили. Это и есть смысл чек-листа:
  требование может быть настоящим, а условий для перехода нет.

  код исхода: 0, сработал перевес: True
  ✓ подделка отсеяна: положительных хватало, отрицательные перевесили

═══ Что НЕ проверялось ═══
    проверка                                 причина
    ────────────────────────────────────────────────────────────────────────────────
    фактическая стоимость перехода           зависит от проекта, команды, провайдера
    работа кластера                          kubectl и кластер недоступны
    веса чек-листа                           подобраны по опыту, а не измерены
    экономия управляемого кластера           оценка долей, не замер
  ✓ невыполненное перечислено; числа названы моделью

═══ ИТОГ ═══
  все требования выполнены
  примечание: кластер не использовался; веса и стоимость — модель

Все требования выполнены; кластер не использовался, веса и стоимость названы моделью.

Требование 7 — самая содержательная проверка. Проект «Нам точно нужен Kubernetes» набрал +7 положительных: развёртывания частые, нагрузка скачет, регионов несколько. Каждый признак настоящий. И всё же исход — «Compose достаточно», потому что дежурить со знанием кластера некому, а простой в час допустим.

Это и есть смысл чек-листа: требование может быть настоящим, а условий для перехода не быть.

Три решения, определяющие качество.

Отрицательные веса по модулю равны сильнейшим положительным. Если бы они были слабее (скажем, −1), проект-подделка набрал бы проходной балл на одних ожиданиях. Проверка требования 1 сравнивает max(положительных) с abs(min(отрицательных)) — то есть проверяет само устройство инструмента, а не его вывод на удобном примере.

К каждому вопросу приложено «чем подтверждается». Столбец превращает опрос в проверку фактов: «есть человек с опытом» подтверждается конкретным именем, а не намерением нанять. Без этого столбца чек-лист заполняется по ощущениям и всегда даёт нужный ответ.

Стоимость разделена на разовую и постоянную, и постоянная больше. Это опровергает распространённый довод «поставим заранее». Разовая часть (292 ч) выглядит внушительно, но за год постоянная даёт 612 ч — вдвое больше, — и начисляется с первого дня.

Чего решение не делает. Числа — модель. Веса подобраны по опыту, не измерены; стоимость в часах — порядок величины, зависящий от проекта, команды и провайдера. Экономия управляемого кластера (16 %) получена из назначенных долей, а не из замеров, и на конкретном проекте будет другой. Кластер не использовался: ни один вывод не проверен на работающей системе. Инструмент полезен как способ вести разговор о фактах, а не как источник ответа: если после его заполнения спор продолжается, спорят уже о конкретных строках, и это лучше, чем о моде.

Проверка результата

Заполните чек-лист для своего проекта и запишите ответ на три вопроса:

text
1. Какой признак дал наибольший положительный вес и чем он подтверждается?
2. Есть ли отрицательные признаки, и что нужно изменить, чтобы они исчезли?
3. Что произойдёт с системой, если ничего не менять ещё полгода?

Если на второй вопрос ответ «нанять человека» — это и есть первая задача, а не перенос.

Типичные ошибки

ОшибкаПричинаИсправление
«Docker или Kubernetes»Инструменты кажутся конкурентамиВопрос ставится как «хватает ли одной машины»
Переход по ожиданию ростаКривая роста воображаемаяУказать на данные за последний год
Установка «заранее, чтобы не переносить потом»Кажется экономиейПостоянная стоимость начисляется с первого дня
Игнорирование дежурстваВидна только стоимость установкиКластер требует людей, знающих кластер
Пропуск промежуточных вариантовИзвестны только два полюсаБалансировщик, управляемая база, Nomad
Ожидание, что управляемый кластер снимет всёПровайдер обслуживает control planeОтладка и дежурство остаются полностью
Учёт только разовой стоимостиОна заметнееЗа год постоянная обычно больше
Оценка по числу сервисовПонятная метрикаЗначение имеет число машин и цена простоя
Перенос базы данных в кластер вместе со всемКажется естественнымУправляемая база часто снимает саму потребность

Контрольные вопросы

На понимание:

  1. Как правильно поставить вопрос вместо «Docker или Kubernetes»?
  2. Почему restart: unless-stopped не заменяет orchestration?
  3. Какие статьи расходов постоянные, а какие разовые?
  4. Что именно снимает управляемый кластер, а что нет?
  5. Почему отладка в Kubernetes дороже?

На применение:

  1. Как проверить подлинность требования «нам понадобится масштабирование»?
  2. Какие промежуточные варианты существуют между Compose и кластером?
  3. Что нужно изменить, чтобы отрицательные признаки чек-листа исчезли?

На диагностику:

  1. Проект набрал 7 положительных баллов и −10 отрицательных. Вывод и что делать?
  2. Команда перешла на Kubernetes, эксплуатационная нагрузка выросла втрое. Гипотеза?

Краткое резюме

  1. Верный вопрос — не «Docker или Kubernetes», а «хватает ли одной машины».
  2. Compose достаточно, пока одна машина справляется и простой в несколько минут допустим.
  3. Compose перестаёт хватать при нескольких узлах, недопустимом простое и частых выкатках.
  4. Каждый признак перехода должен быть наблюдаемым фактом, а не ожиданием.
  5. Стоимость Kubernetes состоит из разовой и постоянной частей; постоянная больше уже за год.
  6. Кластер требует людей, знающих кластер; один такой человек — организационная точка отказа.
  7. Отладка дороже, потому что между симптомом и причиной больше звеньев.
  8. Управляемый кластер снимает работу по control plane — заметную, но не главную часть.
  9. Между Compose и полным кластером есть несколько ступеней, которые часто пропускают.
  10. Вынос состояния в управляемые сервисы иногда снимает саму потребность в кластере.
  11. Отрицательные признаки чек-листа важнее положительных: они отсекают решения по инерции.
  12. Требование может быть настоящим, а условий для перехода не быть.

Официальные источники

ИсточникСсылкаЧто подтверждает
Kubernetes: что этоhttps://kubernetes.io/docs/concepts/overview/Задача поддержания состояния
Kubernetes: контроллерыhttps://kubernetes.io/docs/concepts/architecture/controller/Цикл сравнения состояний
Kubernetes: рекомендации по кластерамhttps://kubernetes.io/docs/setup/production-environment/Что входит в эксплуатацию
Compose Specificationhttps://github.com/compose-spec/compose-spec/blob/main/spec.mdГраницы Compose
Docker Swarmhttps://docs.docker.com/engine/swarm/Промежуточный вариант
Nomadhttps://developer.hashicorp.com/nomad/docsАльтернативная оркестрация
Kubernetes: автомасштабированиеhttps://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/Условия применимости

Навигация

← Предыдущий материал
Вернуться к разделу
Следующий материал → Практические задания
Главное оглавление

Markdown на GitHub ↗