diff --git a/editorial/agent-rewrites/125.json b/editorial/agent-rewrites/125.json index 61f823a..d43fb10 100644 --- a/editorial/agent-rewrites/125.json +++ b/editorial/agent-rewrites/125.json @@ -1,7 +1,7 @@ { "index": 125, "slug": "editorial-2024-07-mechanism-release-engineering", - "title": "Инженерия релиза: как связать артефакт, миграцию и откат", - "excerpt": "Один номер версии не доказывает, что команда выпускает нужный код. Разбираем цепочку commit → artifact → migration → rollout и ставим проверяемый стоп перед ошибочным deploy.", - "contentHtml": "
После выкладки сервис отвечает старым поведением, хотя в CI и карточке релиза стоит одна версия — 2024.07.0. Откат возвращает прежний контейнер, но ошибка в данных остаётся. Команда повторяет deploy, меняет таймаут и смотрит на зелёный статус job. Это не исправляет расхождение. Цена ошибки — потерянное время, спор о том, что именно работает, и риск усугубить миграцию данных.
Тезис простой: релиз нужно проверять как цепочку связей, а не как строку с версией. Commit должен быть источником артефакта. Rollout должен ссылаться на точный digest артефакта. Миграция должна называть целевую версию и границу совместимости. Для возврата нужно заранее назвать версию и digest. Если хотя бы одна связь не сходится, процесс останавливается до deploy.
\nТег отвечает на вопрос «как назвали выпуск». Он не отвечает на вопросы «из какого commit собрали образ», «какой образ запросил rollout» и «для какой схемы написана миграция». Для этих вопросов нужны неизменяемые значения и явные предикаты.
\nartifact.sourceCommitId === commit.id — артефакт собран из заявленного commit.rollout.requestedArtifactDigest === artifact.digest — намерение выкладки указывает тот же контент.migration.targetReleaseVersion === release.version — миграция относится к этому выпуску.returnPoint.version и returnPoint.digest заполнены — у возврата есть конкретная точка.Эти условия проверяют согласованность записей. Они не доказывают, что deploy завершился, что registry доступен или что миграция обратима. Execution result и release evidence — разные вещи. Успешный rollout может работать с неправильным артефактом. Согласованный record может ещё не быть разрешением на выкладку.
\nНиже — синтетические записи. Они не получены из production и не описывают реальную доставку.
\nconst commit = {\n id: 'synthetic-commit-91',\n releaseVersion: '2024.07.0'\n};\n\nconst artifact = {\n digest: 'sha256:synthetic-artifact-42',\n sourceCommitId: 'synthetic-commit-other-91'\n};\n\nconst migration = {\n targetReleaseVersion: '2024.07.0',\n compatibleWith: '2024.06.x'\n};\n\nconst rollout = {\n requestedArtifactDigest: 'sha256:synthetic-artifact-42'\n};\n\nconst checks = {\n sourceMatches: artifact.sourceCommitId === commit.id,\n artifactMatches: rollout.requestedArtifactDigest === artifact.digest,\n migrationMatches: migration.targetReleaseVersion === commit.releaseVersion\n};\n\nconst canDeploy = Object.values(checks).every(Boolean);\n// false: остановить процесс и сверить записи\n\nДве проверки проходят. Артефакт и rollout называют один digest, миграция нацелена на правильную версию. Но source commit не совпадает. Поэтому canDeploy равен false. Нельзя делать вывод, что контейнер содержит код из synthetic-commit-91. Нельзя лечить это повторным запуском того же deploy. Сначала нужно найти источник расхождения и заново зафиксировать запись.
Обратный путь важен не меньше. Возврат контейнера к предыдущему digest не отменяет изменение схемы или данных. Если миграция уже прошла, прежний код может не поддерживать новую схему. В карточке возврата нужно разделить два действия: вернуть code artifact и решить, что делать с data effect. Если второго решения нет, честный статус — «возврат артефакта подготовлен, откат данных не определён».
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Везде одна версия, но поведение разное | Тег используют как единственный идентификатор | Сравнить exact commit id и artifact sourceCommitId | Остановить выпуск и пересобрать evidence chain |
| Rollout зелёный, но загружен не тот образ | Карточка хранит tag вместо digest | Сравнить requestedArtifactDigest с digest артефакта | Исправить intent record, не повторять deploy |
| После возврата код падает на данных | Rollback контейнера приняли за rollback данных | Проверить target schema, compatibility и migration status | Передать data effect отдельному владельцу и остановить автоматический возврат |
| Миграция прошла для другой версии | План миграции следует ветке или последнему main | Сравнить targetReleaseVersion с release.version | Закрыть gate и выпустить новый migration review |
| Невозможно объяснить, что вернётся | Return point описана словом «предыдущий» | Проверить конкретные version и digest | Не давать approval, пока точка возврата не названа |
Таблица полезна только тогда, когда каждая проверка имеет владельца и stop action. Строка «все jobs зелёные» недостаточна: она не связывает job с содержимым артефакта и контрактом данных. Строка «digest совпал» тоже недостаточна: она не подтверждает доступность сервиса после выкладки. Не смешивайте semantic consistency с результатом исполнения.
\nУ релиза есть как минимум два состояния: code state и data state. Deployment обычно управляет шаблоном Pod или другим runtime artifact. Миграция меняет схему, записи или внешний контракт. Эти операции могут иметь разные владельцы, журналы и способы возврата.
\nБезопасный порядок требует compatibility window. Новый код сначала должен работать со старой и новой формой данных, если это возможно. Затем миграция меняет данные. После проверки трафика команда может удалить старую ветку совместимости. В такой схеме возврат на старый код возможен только до закрытия окна. После него нужен отдельный план: обратная миграция, восстановление из backup или сохранение нового кода с исправлением.
\nЭто не универсальная стратегия миграций. Некоторые изменения нельзя отменить. Некоторые системы разрешают только forward migration. Статья не утверждает, что любой Kubernetes Deployment или любой image digest можно безопасно вернуть. Она требует назвать границу действия и не приписывать rollback то, чего он не делает.
\nfalse верните статус stop-and-reconcile-records. Не запускайте новую попытку ради зелёного job.Эта модель не проверяет настоящий Git history, подпись, identity builder, provenance, registry, environment configuration, secrets, права доступа, database state, трафик и telemetry. Она не выдаёт уровень SLSA и не доказывает, что конкретный attestation заслуживает доверия. Для этого нужны отдельные политики, хранилища и проверяющие компоненты.
\nУчебный код также не является CI-конфигурацией. Синтетические id и digest нужны, чтобы показать рассуждение на закрытом наборе данных. Реальные значения нельзя подменить в этом примере и затем считать результат производственным evidence. Практический перенос начинается с одного разрешённого release record и read-only проверки, а не с подключения fixture к deploy.
\nРелиз готов к авторизованному review, если второй проверяющий без устных пояснений находит в одной записи:
\nПроверяющий должен назвать результат каждой связи: true или false, stop action при false и владельца следующего вопроса. Если он может только сказать «job зелёный», критерий не выполнен. Это проверяемый предел статьи: согласовать записи до действия, не объявить production success.
Симптом после выкладки: сервис отвечает старым поведением, хотя в CI и карточке релиза написано 2024.07.0. Команда повторяет deploy, увеличивает таймаут и смотрит на зелёный job. Если перед этим миграция изменила данные, откат контейнера может вернуть старый код, но не вернуть прежнее состояние базы. В итоге спорят не о причине сбоя, а о том, какой именно артефакт вообще работает.
Номер версии удобен человеку, но слаб как единственное доказательство. Надёжнее проверять цепочку: заявленный commit связан с provenance сборки, rollout указывает на неизменяемый digest образа, миграция называет целевую границу совместимости, а для возврата записана конкретная ревизия. Это не разрешение на выкладку и не обещание успешного production deploy. Это stop-проверка, которая не даёт продолжить при расхождении записей.
\nСначала разделим четыре разных вопроса. Источник. Из какого точного commit получены входы сборки? Артефакт. Какой digest соответствует результату сборки? Изменение данных. Какую схему или форму записи ожидает новая версия? Исполнение. Какой digest запросил rollout и чем подтверждено его завершение?
\nЭти вопросы связаны, но не взаимозаменяемы. Provenance — это подписанная или иным образом проверяемая информация о том, как получен артефакт; она не заменяет политику потребителя. Digest идентифицирует содержимое образа, но не подтверждает, что образ запущен в нужном окружении. Успешный статус rollout говорит о состоянии Deployment, а не об обратимости миграции. Поэтому release record должен хранить отдельные поля, а проверка — отдельные результаты.
\n| Связь | Проверяемое утверждение | Что не следует из успеха проверки |
|---|---|---|
| commit → provenance | В provenance зафиксирован ожидаемый источник или dependency с точным идентификатором. | Сборка безопасна от всех атак и полностью воспроизводима. |
| provenance → artifact | Subject attestation относится к конкретному артефакту, а signer и builder входят в доверенную политику. | Артефакт без уязвимостей и подходит каждому окружению. |
| artifact → rollout | В намерении выкладки указан тот же immutable digest. | Контейнер уже запущен и прошёл проверки доступности. |
| migration → release | Миграция называет совместимую целевую версию и известный data effect. | Миграцию можно безопасно отменить одной командой. |
| return point → rollback | Названы точные version, digest и действие для данных. | Старый код поддерживает новую схему после закрытия окна совместимости. |
Ниже — самодостаточная проверка для командной строки. Записи вымышлены: значение примера в том, что одна связь намеренно сломана. Файл не вызывает registry, cluster или deploy runner; он только вычисляет логический результат из локального release record.
\ncat > release-record.json <<'JSON'\n{\n \"releaseVersion\": \"2024.07.0\",\n \"commit\": { \"id\": \"commit-91\" },\n \"provenance\": {\n \"sourceCommitId\": \"commit-91\",\n \"artifactDigest\": \"sha256:artifact-42\",\n \"builderId\": \"ci.example/build\"\n },\n \"rollout\": { \"requestedDigest\": \"sha256:artifact-other\" },\n \"migration\": {\n \"targetVersion\": \"2024.07.0\",\n \"compatibleWith\": [\"2024.06.x\"],\n \"dataEffect\": \"adds nullable profile.locale\"\n },\n \"returnPoint\": {\n \"version\": \"2024.06.4\",\n \"digest\": \"sha256:previous-17\",\n \"dataAction\": \"keep forward-compatible column\"\n }\n}\nJSON\n\njq -e '\n .provenance.sourceCommitId == .commit.id\n and .provenance.artifactDigest == .rollout.requestedDigest\n and .migration.targetVersion == .releaseVersion\n and (.returnPoint.version != \"\" and .returnPoint.digest != \"\")\n' release-record.json\n# jq: false, exit code 1 — rollout digest не совпал\n\nКоманда завершится с кодом 1, потому что sha256:artifact-other не равен digest, записанному в provenance. Три прочие части примера согласованы, но этого недостаточно: gate должен остановить действие при одном false. После исправления записи полезно повторить ту же команду и отдельно проверить, что исполнение rollout действительно завершилось. Логический true не подменяет runtime evidence.
Если в карточке и в CI одна версия, а поведение разное, первым делом не запускайте новый deploy. Получите фактический digest запущенного образа и сравните его с digest в намерении выкладки. Затем сопоставьте provenance с ожидаемым commit и доверенным builder. Здесь важно не восстановить «примерно ту же сборку», а найти конкретную точку, где цепочка перестала быть доказуемой.
\nЕсли rollout указывает на правильный digest, но сервис всё ещё ведёт себя иначе, это уже другой класс проверки: конфигурация окружения, feature flag, кеш, трафик, версия зависимого сервиса и само состояние приложения. Цепочка артефакта не доказывает идентичность всех этих входов. Она лишь не позволяет списать любой эффект на слово «релиз».
\nПри расхождении миграции с release version нужно остановить и повторно согласовать план данных. Подмена значения в карточке задним числом стирает след ошибки. Зафиксируйте, какая миграция уже запущена, какие записи она изменила и с какой версией остаётся совместимой. Если этих сведений нет, статус должен быть «данные не классифицированы», а не «rollback готов».
\nУ приложения есть минимум два состояния: кодовый артефакт и состояние данных. Kubernetes Deployment хранит историю ревизий и позволяет вернуть Pod template к предыдущей ревизии, если она ещё доступна. Это полезно при проблеме с образом или параметрами запуска. Но команда kubectl rollout undo не отменяет SQL-миграцию, изменение документа или уже отправленное внешнему сервису событие.
Поэтому миграцию стоит проектировать с окном совместимости, когда новый код умеет читать старую и новую форму данных. Сначала выкладывается код, способный работать в этом окне, затем выполняется изменение данных, после наблюдения удаляется старая ветка. Конкретный порядок зависит от хранилища и миграционного инструмента; это не универсальная лицензия на обратную миграцию.
\nДо закрытия окна точка возврата может быть обычным предыдущим артефактом. После закрытия нужно отдельное решение: forward fix, обратная миграция, восстановление резервной копии или сохранение нового кода с исправлением. В release record это должен быть явный dataAction, а не слово «откат» без объекта действия.
Этот маршрут подходит как минимальный контроль согласованности для релиза, где команда может получить commit, provenance, digest, план миграции и запись rollout. Он не заменяет сканирование уязвимостей, review кода, проверку секретов, контроль прав, тесты совместимости, резервное копирование, мониторинг или процедуру incident response.
\nSLSA описывает модель provenance и требования к её проверке, но не объявляет конкретный артефакт безопасным. GitHub отдельно предупреждает, что artifact attestation связывает артефакт с источником и инструкциями сборки, а решение о доверии требует собственной политики. В частном registry, другой CI-системе или без доверенного корня проверки команды и поля будут другими.
\nСинтетический release-record.json нельзя подключать к настоящему deploy без адаптации схемы, прав и источников фактов. Kubernetes-команды требуют доступа к конкретному кластеру и работают с историей, которую можно ограничить настройкой revisionHistoryLimit. Если миграция необратима или внешний эффект уже ушёл, честный результат может быть «код возвращён, data effect остаётся».
Перед авторизованной выкладкой второй инженер должен без устных пояснений найти в записи пять вещей: точный источник сборки, digest артефакта, digest в rollout, границу совместимости миграции и раздельный план возврата кода и данных. Для каждой связи должен быть результат true или false, а для false — владелец сверки и стоп-действие.
После выкладки добавьте к этим записям фактический результат rollout и наблюдаемый сигнал приложения. Только тогда можно обсуждать поведение окружения. Такая последовательность возвращает разговор к исходному симптому: мы проверяем не красивую строку версии, а то, что именно собрано, что именно запрошено, что изменилось в данных и что реально можно вернуть.
\nПосле выкладки сервис отвечает кодом старой версии, хотя в заявке указан новый релиз. В карточке сборки, образе и rollout стоит один tag. Команда повторяет запуск, но не может быстро ответить на три вопроса: из какого commit собран artifact, какой digest отправили и совместима ли migration с данными. Цена ошибки растёт с каждой попыткой: увеличивается окно сбоя, меняется состояние базы, а точку возврата приходится восстанавливать по разным журналам.
\nОдинаковая версия не связывает объекты сама по себе. Релиз готов к следующему действию только тогда, когда можно сравнить exact commit id, immutable digest, migration target и return point. Если одна связь неизвестна или ложна, проверка должна остановить выпуск. Новый retry не исправляет расхождение записей.
\nУ релиза есть несколько разных объектов. commit фиксирует исходный revision. artifact содержит собранное содержимое и digest. migration меняет схему или данные и должна назвать целевую версию и совместимость. rollout описывает намерение отправить конкретный digest. return point указывает версию и digest, к которым можно вернуться.
Эти записи не заменяют друг друга. Artifact должен ссылаться на exact commit, а не только на имя ветки. Rollout должен содержать digest artifact, а не mutable tag. Migration должна отвечать на вопрос о совместимости. Return point должен быть известен до разрешения операции. Такая цепочка не доказывает, что deploy уже состоялся. Она делает расхождение видимым до действия.
\nНиже приведён учебный пример с вымышленными значениями. Код не обращается к Git, registry, CI, Kubernetes API или базе данных. Он только сравнивает записи. Положительный результат означает согласованность этих записей, а не готовность реальной среды.
\nconst release = { version: '2024.07.0' };\nconst commit = { id: 'commit-7f4a0c1' };\nconst artifact = {\n digest: 'sha256:release-070-a1',\n sourceCommitId: 'commit-7f4a0c1',\n};\nconst migration = {\n targetReleaseVersion: '2024.07.0',\n compatibleWith: '2024.06.3',\n};\nconst rollout = {\n requestedArtifactDigest: 'sha256:release-070-a1',\n migrationVersion: '2024.07.0',\n};\n\nconst checks = {\n source: artifact.sourceCommitId === commit.id,\n migration: migration.targetReleaseVersion === release.version,\n artifact: rollout.requestedArtifactDigest === artifact.digest,\n rollout: rollout.migrationVersion === migration.targetReleaseVersion,\n};\n\nconst readyForReview = Object.values(checks).every(Boolean);\nif (!readyForReview) throw new Error('stop: reconcile release records');\nКаждая проверка отвечает только на один вопрос. Если заменить sourceCommitId на другой id, результат станет отрицательным. Если изменить digest в rollout, tag всё ещё будет выглядеть правильно, но содержимое уже не совпадёт. Отрицательный путь важнее зелёной строки: система не выбирает за инженера «примерно подходящую» запись.
Название readyForReview намеренно не означает readyForDeploy. Код не проверяет подпись, права, конфигурацию среды, состояние базы, доступность сервиса или факт доставки. Он лишь открывает следующий этап проверки, если четыре связи согласованы.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Tag совпадает, но artifact ссылается на другой commit. | Tag используют вместо точной связи с исходным revision. | Сравнить artifact.sourceCommitId и commit id. | Остановить выпуск. Исправить запись или пересобрать artifact после решения владельца. |
| Старый код не читает новую схему. | Rollback образа ошибочно считают rollback данных. | Проверить target migration, совместимость и обратную процедуру. | Вернуть только разрешённый artifact. Изменение данных рассмотреть отдельно. |
| Rollout прошёл с тем же tag, но другим digest. | Намерение ссылается на изменяемую метку. | Сравнить requested digest с digest artifact. | Не запускать rollout. Пересоздать запись после сверки. |
| После остановки предлагают повторить deploy. | Retry используют вместо объяснения mismatch. | Найти первую ложную связь и её источник. | Сначала исправить записи, затем повторить только проверки. |
| Точку возврата называют «предыдущим релизом». | У return point нет конкретного содержимого. | Проверить version и immutable digest. | Не обещать возврат, пока обе величины не записаны. |
Таблица разделяет разные классы риска. Ошибка commit относится к происхождению artifact. Ошибка digest относится к содержимому, выбранному для rollout. Ошибка migration относится к совместимости данных и кода. Неопределённый return point относится к возможности безопасно назвать действие после сбоя. Одно поле release=green не заменяет эти проверки.
Rollback Deployment возвращает прежнюю ревизию Pod template. Это помогает вернуть код и настройки, которые входят в этот template. Но такой rollback не отменяет произвольный SQL, удалённую запись, заполненное поле или изменение внешнего contract. Если migration уже изменила данные, старый artifact может не уметь с ними работать.
\nПоэтому return point должен содержать две границы. Первая говорит, какой artifact можно запустить. Вторая говорит, что разрешено делать с данными. Возможны обратная migration, совместимый промежуточный код, восстановление из резервной копии или запрет автоматического возврата. Пока выбранный путь не проверен, честная формулировка звучит так: «возврат artifact определён; откат данных не доказан».
\nТа же граница действует для provenance и attestation. Provenance описывает происхождение сборки. Attestation может подтверждать утверждение об этом происхождении. Ни одно из них само по себе не доказывает совместимость migration, approval rollout или здоровье сервиса. Эти вопросы требуют собственных источников и проверок.
\nСхема ловит расхождения между названными записями. Она не доказывает правдивость каждого значения. Она не проверяет историю Git, содержимое образа, подпись, policy CI, права на кластер, runtime configuration, состояние базы, трафик, метрики или факт доставки.
\nЕсли commit неизвестен, digest отсутствует, migration не содержит compatibility statement или return point не назван, результат должен быть отрицательным. Не подставляйте «последний main». Не ищите образ по tag. Не объявляйте rollback данных по факту возврата Pod template. Остановитесь на первой неизвестной границе и назначьте источник, который может её подтвердить.
\nЗапись готова к передаче на авторизованную проверку, если второй инженер без устных пояснений может показать exact commit id, artifact digest, связь artifact с commit, migration target и compatibility, rollout digest, migration version и return point. Для каждой строки есть источник, владелец и действие при mismatch. Проверка даёт либо все утверждения true, либо конкретный stop с названием ложной связи.
В учебном примере все значения вымышлены и не описывают production-результат. В реальном выпуске критерий нужно применять к доступным и разрешённым записям. Если одна строка не имеет источника или действия, выпуск не готов: сначала уточните contract, затем повторите сверку.
\nПосле выкладки сервис отвечает кодом старой версии, хотя в заявке указан новый релиз. В карточке сборки, образе и rollout стоит один tag. Команда повторяет запуск, но не может быстро ответить на три вопроса: из какого commit собран artifact, какой digest отправили и совместима ли migration с данными. Цена ошибки растёт с каждой попыткой: увеличивается окно сбоя, меняется состояние базы, а точку возврата приходится восстанавливать по разным журналам.
\nОдинаковая версия не связывает объекты сама по себе. Релиз готов к следующему действию только тогда, когда можно сравнить exact commit id, immutable digest, migration target и return point. Если одна связь неизвестна или ложна, проверка должна остановить выпуск. Новый retry не исправляет расхождение записей.
\nУ релиза есть несколько разных объектов. commit фиксирует исходный revision. artifact содержит собранное содержимое и digest. migration меняет схему или данные и должна назвать целевую версию и совместимость. rollout описывает намерение отправить конкретный digest. return point указывает версию и digest, к которым можно вернуться.
В Git tag является ссылкой в пространстве имён refs/tags/. Он удобен для имени релиза, но запись с одним tag не заменяет зафиксированный commit: ссылку нужно разрешить и сохранить полный идентификатор. Для контейнерного образа digest — content identifier: OCI описывает его как хеш содержимого, который можно независимо проверить.
Поэтому rollout должен ссылаться на digest, а не только на имя, которое может разрешиться иначе. Эти связи проверяют согласованность записей, но не являются разрешением на выкладку: они не проверяют права, политики CI, состояние registry или здоровье сервиса.
\nНиже приведён учебный пример с вымышленными значениями. Код не обращается к Git, registry, CI, Kubernetes API или базе данных. Он только сравнивает записи. Положительный результат означает согласованность этих записей, а не готовность реальной среды.
\nconst release = { version: '2024.07.0' };\nconst commit = { id: 'commit-7f4a0c1' };\nconst artifact = {\n digest: 'sha256:release-070-a1',\n sourceCommitId: 'commit-7f4a0c1',\n};\nconst migration = {\n targetReleaseVersion: '2024.07.0',\n compatibleWith: '2024.06.3',\n};\nconst rollout = {\n requestedArtifactDigest: 'sha256:release-070-a1',\n migrationVersion: '2024.07.0',\n};\nconst returnPoint = {\n version: '2024.06.3',\n digest: 'sha256:release-063-b7',\n};\n\nfunction inspect(candidateRollout) {\n const checks = {\n source: artifact.sourceCommitId === commit.id,\n migration: migration.targetReleaseVersion === release.version,\n artifact: candidateRollout.requestedArtifactDigest === artifact.digest,\n rollout: candidateRollout.migrationVersion === migration.targetReleaseVersion,\n returnPoint: Boolean(returnPoint.version && returnPoint.digest),\n };\n return { checks, ok: Object.values(checks).every(Boolean) };\n}\n\nconst aligned = inspect(rollout);\nconst broken = inspect({\n ...rollout,\n requestedArtifactDigest: 'sha256:release-070-b2',\n});\n\nconsole.log(JSON.stringify({ aligned, broken }, null, 2));\nif (aligned.ok !== true || broken.ok !== false) {\n throw new Error('unexpected validator result');\n}\nКаждая проверка отвечает только на один вопрос. Если заменить sourceCommitId на другой id, результат станет отрицательным. Если изменить digest в rollout, tag всё ещё будет выглядеть правильно, но содержимое уже не совпадёт. Отрицательный путь важнее зелёной строки: система не выбирает за инженера «примерно подходящую» запись.
Название readyForReview намеренно не означает readyForDeploy. Код не проверяет подпись, права, конфигурацию среды, состояние базы, доступность сервиса или факт доставки. Он лишь открывает следующий этап проверки, если четыре связи согласованы.
Сначала соберите evidence в режиме чтения. Команды ниже используют примерные имена и не изменяют удалённый Git или Kubernetes. Выполняйте их только в репозитории и namespace, к которым у вас есть разрешение. Git разрешает tag до полного commit; Kubernetes показывает image reference в Pod template, историю ревизий и состояние rollout.
\ntag=v2024.07.0\ngit rev-parse \"$tag^{commit}\"\ngit show -s --format='%H %s' \"$tag^{commit}\"\n\nkubectl -n production get deployment/app -o jsonpath='{.spec.template.spec.containers[?(@.name==\"app\")].image}{\"\\n\"}'\nkubectl -n production rollout history deployment/app\nkubectl -n production rollout status deployment/app --timeout=60s\nСохраните вывод рядом с release record и сравните его с заявленными значениями. Для контейнера ожидайте ссылку вида registry.example/app@sha256:...; если в Pod template остался только registry.example/app:v2024.07.0, имя ещё не доказывает выбранный digest. Последняя команда подтверждает состояние контроллера, но не происхождение образа, совместимость данных или пользовательский результат.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Tag совпадает, но artifact ссылается на другой commit. | Tag используют вместо точной связи с исходным revision. | Сравнить artifact.sourceCommitId и commit id. | Остановить выпуск. Исправить запись или пересобрать artifact после решения владельца. |
| Старый код не читает новую схему. | Rollback образа ошибочно считают rollback данных. | Проверить target migration, совместимость и обратную процедуру. | Вернуть только разрешённый artifact. Изменение данных рассмотреть отдельно. |
| Rollout прошёл с тем же tag, но другим digest. | Намерение ссылается на изменяемую метку. | Сравнить requested digest с digest artifact. | Не запускать rollout. Пересоздать запись после сверки. |
| После остановки предлагают повторить deploy. | Retry используют вместо объяснения mismatch. | Найти первую ложную связь и её источник. | Сначала исправить записи, затем повторить только проверки. |
| Точку возврата называют «предыдущим релизом». | У return point нет конкретного содержимого. | Проверить version и immutable digest. | Не обещать возврат, пока обе величины не записаны. |
Таблица разделяет разные классы риска. Ошибка commit относится к происхождению artifact. Ошибка digest относится к содержимому, выбранному для rollout. Ошибка migration относится к совместимости данных и кода. Неопределённый return point относится к возможности безопасно назвать действие после сбоя. Одно поле release=green не заменяет эти проверки.
В Kubernetes новая ревизия Deployment создаётся при изменении Pod template, например image или label. Rollback возвращает часть template к предыдущей ревизии. Он не отменяет произвольный SQL, удалённую запись, заполненное поле, отправленное событие или изменение во внешней системе. Поэтому успех kubectl rollout undo нельзя называть откатом данных.
Поэтому return point должен содержать две границы. Первая говорит, какой artifact можно запустить. Вторая говорит, что разрешено делать с данными. Возможны обратная migration, совместимый промежуточный код, восстановление из резервной копии или запрет автоматического возврата. Пока выбранный путь не проверен, честная формулировка звучит так: «возврат artifact определён; откат данных не доказан».
\nТа же граница действует для provenance и attestation. Provenance описывает происхождение сборки. Attestation может подтверждать утверждение об этом происхождении. Ни одно из них само по себе не доказывает совместимость migration, approval rollout или здоровье сервиса. Эти вопросы требуют собственных источников и проверок.
\nСхема ловит расхождения между названными записями. Она не доказывает правдивость каждого значения. Она не проверяет историю Git, содержимое образа, подпись, policy CI, права на кластер, runtime configuration, состояние базы, трафик, метрики или факт доставки.
\nЕсли commit неизвестен, digest отсутствует, migration не содержит compatibility statement или return point не назван, результат должен быть отрицательным. Не подставляйте «последний main». Не ищите образ по tag. Не объявляйте rollback данных по факту возврата Pod template. Остановитесь на первой неизвестной границе и назначьте источник, который может её подтвердить.
\nЗапись готова к передаче на авторизованную проверку, если второй инженер без устных пояснений может показать exact commit id, artifact digest, связь artifact с commit, migration target и compatibility, rollout digest, migration version и return point. Для каждой строки есть источник, владелец и действие при mismatch. Проверка даёт либо все утверждения true, либо конкретный stop с названием ложной связи.
В учебном примере все значения вымышлены и не описывают production-результат. В реальном выпуске критерий нужно применять к доступным и разрешённым записям. Если одна строка не имеет источника или действия, выпуск не готов: сначала уточните contract, затем повторите сверку.
\nПосле обновления образа часть Pod-ов долго остаётся Unready. В графике растёт CPU. Команда предлагает увеличить maxReplicas и CPU limit. Это может не изменить ни одного симптома. Readiness управляет допуском Pod к трафику Service. HPA рассчитывает реплики по метрике. Scheduler размещает Pod по requests. Runtime применяет limits. Один Pod, четыре контура.
Цена ошибки — не только лишние ресурсы. Новый Pod может не пройти readiness из-за зависимости. HPA может не считать CPU, если у контейнера нет request. Увеличенный limit может скрыть рост памяти до следующего отказа. Если изменить все поля сразу, команда потеряет причинную связь. Она не узнает, что именно сработало и какой риск остался.
\nТезис. Сначала нужно назвать контракт сигнала, затем проверить его источником того же типа. Не называйте Ready доказательством capacity. Не называйте CPU percentage самостоятельным числом. Не называйте значение из учебной модели показанием кластера.
Request задаёт reservation contract. Scheduler учитывает requests контейнеров при выборе Node. Для одного ресурса request Pod складывается из requests его контейнеров. Это не прогноз постоянного потребления. Это условие размещения.
\nLimit задаёт границу ресурса для контейнера. CPU и memory ведут себя по-разному. CPU limit может ограничивать выполнение. Memory limit не превращается в прогноз пикового потребления и не объясняет lifetime cache или batch buffer. Нельзя вывести безопасные значения из одного универсального коэффициента.
\nReadiness отвечает на другой вопрос: можно ли отправлять трафик этому Pod сейчас. Когда Pod не готов, Service не должен использовать его как backend. Readiness probe не обязана объяснять причину. Readiness gate добавляет named condition, но не задаёт ей смысл. Владелец приложения должен определить producer, переход в True и путь восстановления.
HPA формирует предложение по числу реплик из метрики и target. CPU utilization — процент относительно CPU request контейнеров, которые попали в выборку. Если relevant request отсутствует, controller не может получить такой utilization для контейнера. Значит, строка target: 65 без request не является рабочим scaling contract.
apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: api\nspec:\n replicas: 2\n template:\n spec:\n containers:\n - name: api\n image: registry.example/api@sha256:...\n resources:\n requests:\n cpu: 500m\n memory: 256Mi\n limits:\n cpu: \"1\"\n memory: 512Mi\n startupProbe:\n httpGet:\n path: /startup\n port: 8080\n readinessProbe:\n httpGet:\n path: /ready\n port: 8080\n readinessGates:\n - conditionType: example.com/SchemaReady\n---\napiVersion: autoscaling/v2\nkind: HorizontalPodAutoscaler\nmetadata:\n name: api\nspec:\n scaleTargetRef:\n apiVersion: apps/v1\n kind: Deployment\n name: api\n minReplicas: 2\n maxReplicas: 10\n metrics:\n - type: Resource\n resource:\n name: cpu\n target:\n type: Utilization\n averageUtilization: 70\nФрагмент учебный. Образ, пути probes, custom condition и значения ресурсов нельзя переносить в production без profile приложения и проверки среды. В манифесте видно главное: HPA percentage имеет denominator requests.cpu: 500m. Readiness gate не становится HPA metric. Limit 1 не меняет denominator HPA.
| Симптом | Возможная причина | Проверка | Действие |
|---|---|---|---|
| Pod Unready после релиза | Probe или gate не выполняет контракт; зависимость ещё не готова | Сопоставить condition, probe event и смысл Ready | Исправить owner и recovery path; не увеличивать replicas автоматически |
| CPU высокий, HPA не меняет реплики | Нет CPU request, нет metrics API или выбран другой target type | Проверить effective request, HPA status и источник метрики | Сначала восстановить denominator или метрику; не рисовать scale policy по одному графику |
| Memory близка к limit | Рост cache, batch, retained object или неверный limit | Определить lifetime памяти и проверить runtime evidence | Ограничить владельца роста; отделить memory action от CPU HPA |
| Новые Pod запускаются, но трафик не растёт | Readiness false, gate не становится True или Service не видит endpoint | Проверить Pod condition и endpoints разрешённым способом | Проверить traffic contract; не считать создание Pod доказательством capacity |
| Процент выглядит убедительно только в fixture | Синтетическое число приняли за telemetry | Проверить источник и marker данных | Ограничить вывод учебной моделью и запросить реальное evidence отдельно |
Пусть CPU request равен 500m, а HPA target — 70%. В модели controller это означает usage около 350m на Pod для целевой точки. Это 70% request, а не 70% Node и не 70% CPU limit. Если request изменить на 1000m, тот же target будет означать другую рабочую точку. Одновременно Scheduler начнёт резервировать больше CPU. Одно изменение затронет placement и interpretation метрики.
Теперь уберём request. Значение synthetic CPU 600m всё ещё выглядит конкретно, но процент больше не имеет объявленного denominator. Корректный verdict — «нельзя интерпретировать utilization», а не «нужно больше Pod». В этом отрицательном пути отсутствие действия HPA — ожидаемый результат проверки контракта. Сначала нужно определить serving unit, для которой request имеет смысл, и подтвердить состояние metrics API в разрешённой среде.
Другой пример — memory. Если synthetic observation показывает 470Mi при limit 512Mi, это не доказывает OOM, eviction, restart или throttling. Без runtime event это только учебное значение рядом с границей. Следующий вопрос относится к владельцу памяти: cache, batch buffer, response aggregation или connection pool. Readiness false при этом остаётся отдельным сигналом traffic, а не именем причины memory pressure.
\nПроверка должна отвечать на один вопрос и использовать один тип evidence. Условие Pod, HPA status, metrics API, controlled request и runtime event не взаимозаменяемы. Если источник не разрешён, его отсутствие фиксируют как blocker. Не подменяйте его значением из fixture, screenshot или случайным графиком.
\nУчебный код может держать три фиксированные карточки в памяти: steady serving с request, memory growth с false readiness и warmup без request. Он может проверять, что synthetic value не получила ярлык telemetry и что внешний field отклоняется. Он не должен читать kubeconfig, namespace, файл манифеста, CI, HTTP, trace или production. Комментарий syntheticObservedPodBehavior должен прямо говорить, что это не kubectl и не metrics API.
const card = {\n id: 'fixed-warmup-request-missing-v1',\n declared: { cpuRequest: null, cpuTarget: 65 },\n observed: {\n kind: 'embedded-fixed-js-object-not-telemetry',\n ready: false,\n cpu: '600m'\n }\n};\n\nconst verdict = card.declared.cpuRequest === null\n ? 'block-utilization-conclusion'\n : 'compare-with-request';\n\nconsole.log(verdict);\n// Учебная модель. Нет cluster, file, CI, HTTP или production data.\nДля этой карточки корректен verdict block-utilization-conclusion. Код не говорит, сколько реплик нужно реальному сервису. Он проверяет только отрицательную ветку: процент без request нельзя честно объяснить. Production-инструмент требует отдельной авторизации, источника данных и правил изменения. Учебный пример эти полномочия не получает.
Материал не выбирает размер Node, ratio CPU и memory, значения probes, тип custom metric или безопасный maxReplicas. Он не видит admission webhooks, quotas, EndpointSlice, runtime cgroups, Metrics Server, custom adapter, logs, traces и downstream dependencies. Официальная семантика Kubernetes не заменяет проверку конкретного кластера.
\nRollback должен возвращать snapshot декларации и проверять side effects. Откат HPA не исправляет неверный readiness contract. Возврат limit не объясняет рост памяти. Изменение probe не восстанавливает потерянные evidence. Для каждого изменения укажите owner, условие остановки, временную границу и наблюдаемый критерий отката.
\nКритерий готовности. Изменение готово, если profile назван, у каждого сигнала есть источник и owner, CPU utilization имеет declared request, Ready имеет отдельный traffic contract, synthetic данные не выданы за production, а отрицательная ветка приводит к остановке или явному blocker. Кроме того, команда может повторить исходную проверку тем же типом evidence и получить объяснимый результат. Если хотя бы одна строка отвечает «кажется», манифест не готов.
\nПосле обновления образа часть Pod остаётся на старой версии, новые Pod долго имеют статус NotReady, а CPU на графике растёт. Первая реакция обычно сводится к увеличению maxReplicas или CPU limit. Но эти поля принадлежат разным контроллерам. Можно добавить реплики и не получить ни одного нового backend в Service, а можно поднять limit и изменить поведение throttling, не исправив причину задержки.
Цена смешения сигналов — потеря причинной связи. Deployment может продолжать rollout, пока новая ReplicaSet не набирает доступные Pod. Readiness probe может исключить Pod из Service, хотя процесс жив. HPA может не рассчитать CPU utilization из-за отсутствующего request или недоступного metrics API. Сначала назовём владельца каждого сигнала, затем проверим ровно тот контур, который породил симптом.
\nГлавный принцип. Ready означает допуск к трафику, а не доказанную производительность. CPU utilization в HPA — отношение потребления к объявленному request, а не процент CPU Node и не процент limit. Rolling update отвечает за замену ReplicaSet, но не исправляет плохую readiness-проверку.
Deployment и ReplicaSet. Deployment хранит желаемый шаблон Pod и управляет ReplicaSet. При стратегии RollingUpdate новая ReplicaSet создаёт Pod, а старая постепенно уменьшается. Поля maxSurge и maxUnavailable ограничивают число дополнительных и недоступных Pod. Поэтому во время rollout нормально временно видеть старую и новую версии одновременно. Ненормально — считать сам факт создания нового Pod доказательством его готовности.
Readiness. Kubelet выполняет readiness probe на протяжении жизни контейнера. Если она не проходит, Pod получает состояние unready и Kubernetes Services не должны отправлять ему трафик. Такая probe отвечает только на вопрос «можно ли обслуживать запросы сейчас». Она не обязана доказывать отсутствие утечки памяти, правильность миграции или запас CPU. Для долгого старта применяют startupProbe, чтобы не превращать штатную инициализацию в перезапуски.
Service и EndpointSlice. Service выбирает Pod по label selector, а control plane формирует связанные EndpointSlice. В EndpointSlice условие ready отражает готовность endpoint; это полезная проверка фактического набора backend, а не только списка Pod. Сетевой mesh, балансировщик и настройка publishNotReadyAddresses могут добавить собственное поведение, поэтому команда должна проверить реальный путь трафика. Для обычного Service нельзя переносить вывод «Pod Running» на «Pod получает запросы».
Ресурсы и HPA. Scheduler использует requests контейнеров для выбора Node. Limits задают границу выполнения: CPU ограничивается throttling, а превышение memory limit может привести к OOM kill. HPA с ресурсной метрикой averageUtilization сравнивает usage с request. Если у релевантного контейнера нет request, utilization для этой метрики не определён, и HPA не обязан масштабировать по ней.
synthetic.example/contract на рисунке — условное имя custom condition; рисунок не представляет состояние конкретного кластера.Рассмотрим последовательность без привязки к конкретному облаку. У Deployment две реплики, стратегия — RollingUpdate. После смены образа контроллер создаёт новую ReplicaSet. Новый контейнер запускается, но readiness endpoint отвечает ошибкой, потому что приложение ещё загружает конфигурацию. Pod остаётся живым, однако Service не получает его в качестве готового endpoint. Старый Pod продолжает обслуживать запросы, пока контроллер не может безопасно уменьшить старую ReplicaSet.
В такой ситуации фраза «релиз завис» слишком общая. Нужно разделить пять наблюдений: какой image digest реально запущен; какую condition получил новый Pod; что написано в событиях probe; какие endpoints видит Service; какое решение показывает Deployment controller. Одного вывода kubectl get pods недостаточно.
| Симптом | Что могло произойти | Чем проверить | Что делать первым |
|---|---|---|---|
Новые Pod Running, но READY 0/1 | Readiness probe не проходит или custom gate остаётся false | kubectl describe pod, conditions и Events | Проверить контракт endpoint/gate и зависимость; не увеличивать replicas вслепую |
| Старая ReplicaSet не уменьшается | Новая версия не набрала доступные Pod или достигнут лимит rollout | kubectl rollout status, Deployment conditions, ReplicaSets | Сопоставить maxUnavailable с доступными Pod и найти причину Unready |
| CPU высок, HPA не меняет replicas | Нет request, нет resource metrics или выбран другой target type | kubectl describe hpa, HPA conditions, metrics API | Восстановить источник и знаменатель; не считать отсутствующий scale доказательством низкой нагрузки |
Pod Ready, но запросов нет | Selector/port не совпадает, EndpointSlice устарел или трафик идёт через другой слой | Service selector, EndpointSlice и путь data plane | Сверить label, порт и фактический backend; не менять probe без этого сравнения |
| Memory близка к limit | Растёт cache, buffer, retained object или слишком тесен лимит | Runtime metrics, restart reason, events и профиль приложения | Отделить memory investigation от CPU HPA и назвать владельца роста |
Ниже — учебный фрагмент. Он намеренно содержит requests, limits, startup/readiness probes, custom readiness gate и HPA. Образ, путь endpoint, имя condition и числа ресурсов нужно заменить на значения конкретного приложения. Custom readiness gate не станет True сам по себе: внешний контроллер или другой владелец состояния должен установить condition, иначе Pod останется неготовым.
apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: api\nspec:\n # Если Deployment управляется HPA, не дублируйте replicas\n # в постоянно применяемом manifest без отдельной политики.\n replicas: 2\n strategy:\n type: RollingUpdate\n rollingUpdate:\n maxSurge: 1\n maxUnavailable: 0\n selector:\n matchLabels:\n app: api\n template:\n metadata:\n labels:\n app: api\n spec:\n readinessGates:\n - conditionType: synthetic.example/contract\n containers:\n - name: api\n image: registry.example/api:v3.4.1\n ports:\n - name: http\n containerPort: 8080\n resources:\n requests:\n cpu: 500m\n memory: 256Mi\n limits:\n cpu: '1'\n memory: 512Mi\n startupProbe:\n httpGet:\n path: /startup\n port: http\n periodSeconds: 5\n failureThreshold: 24\n readinessProbe:\n httpGet:\n path: /ready\n port: http\n periodSeconds: 5\n failureThreshold: 2\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: api\nspec:\n selector:\n app: api\n ports:\n - name: http\n port: 80\n targetPort: http\n---\napiVersion: autoscaling/v2\nkind: HorizontalPodAutoscaler\nmetadata:\n name: api\nspec:\n scaleTargetRef:\n apiVersion: apps/v1\n kind: Deployment\n name: api\n minReplicas: 2\n maxReplicas: 10\n metrics:\n - type: Resource\n resource:\n name: cpu\n target:\n type: Utilization\n averageUtilization: 70\nВ этом примере CPU target равен 70% от request, то есть 350m на Pod при request 500m. Это рабочая точка алгоритма, а не обещание, что сервис выдержит нужную нагрузку. maxUnavailable: 0 увеличивает потребность в свободной ёмкости: новая версия должна стать доступной до удаления старой. На маленьком кластере rollout может остановиться из-за нехватки ресурсов, даже если probe исправна.
Команды ниже только читают состояние и подходят для namespace, к которому у вас есть доступ. Сначала запишите имя Deployment, namespace и новый image digest. Затем повторяйте команды с тем же объектом; так временная картина не смешивается с другой нагрузкой.
\nNS=demo\nDEPLOY=api\n\nkubectl -n \\\"$NS\\\" get deployment \\\"$DEPLOY\\\" -o wide\nkubectl -n \\\"$NS\\\" rollout status deployment/\\\"$DEPLOY\\\" --timeout=120s\nkubectl -n \\\"$NS\\\" get rs,pods -l app=api \\\n -o custom-columns='KIND:kind,NAME:metadata.name,IMAGE:spec.template.spec.containers[0].image,READY:status.containerStatuses[0].ready'\nkubectl -n \\\"$NS\\\" describe deployment \\\"$DEPLOY\\\"\nkubectl -n \\\"$NS\\\" describe pod -l app=api\nkubectl -n \\\"$NS\\\" get endpointslice \\\n -l kubernetes.io/service-name=api -o yaml\nkubectl -n \\\"$NS\\\" describe hpa \\\"$DEPLOY\\\"\nkubectl -n \\\"$NS\\\" top pod -l app=api\nКоманда rollout status сообщает, завершился ли rollout, но не объясняет каждую причину. describe pod даёт condition и Events, однако не заменяет логи приложения. get endpointslice показывает объект control plane; если между Service и клиентом есть mesh или внешний балансировщик, его состояние нужно проверять отдельно. top требует работающего resource metrics API и показывает текущий срез, а не историю.
Для безопасного чтения образа сравните digest, а не только короткий tag. Для проверки именно нового ReplicaSet выберите его label из вывода Deployment и повторите describe pod по этому selector. Если в cluster policy запрещён доступ к EndpointSlice или metrics API, это не повод подставлять число из fixture: результат проверки должен быть «источник недоступен».
При requests.cpu: 500m и среднем потреблении 350m utilization равен 70%. При том же потреблении, но request 1000m, utilization равен 35%. CPU workload не изменился, а решение HPA стало другим. Одновременно Scheduler увидит другой reservation contract. Поэтому изменение request нельзя считать только настройкой autoscaling: оно меняет и размещение, и интерпретацию процента.
Limit — другой знаменатель и другая граница. Если limit равен 1 CPU, контейнер может упереться в throttling на этой границе; HPA с averageUtilization всё равно сравнивает usage с request. Если memory приближается к 512Mi, это не объясняет само по себе высокий CPU и не доказывает OOM. Для memory нужно проверить restart reason, события и профиль приложения.
Есть ещё одна конфликтующая настройка. Kubernetes предупреждает: когда HPA активен, применение Deployment manifest с фиксированным spec.replicas может снова записать число реплик и вызвать колебания. Перед тем как удалить replicas из manifest, проверьте способ применения и разовый эффект: API по умолчанию может трактовать отсутствие поля как одну реплику. Это изменение нужно выполнять отдельным контролируемым шагом, а не попутно с исправлением probe.
Этот разбор не выбирает универсальные значения CPU, memory, probe timeout, maxReplicas или maxUnavailable. Их определяют профиль приложения, SLA, стоимость Node, размер ответа, время старта и downstream-зависимости. Статус Ready не проверяет бизнес-корректность ответа. HPA не заменяет очередь, rate limit, вертикальное масштабирование или capacity planning. Resource metrics не дают трассировку причины задержки.
Манифест не учитывает admission webhook, LimitRange, ResourceQuota, PodDisruptionBudget, topology spread, NetworkPolicy и правила конкретного service mesh. Эти механизмы могут изменить effective configuration или доступность. publishNotReadyAddresses: true меняет обычную семантику готовых endpoints, поэтому вывод о трафике нужно сверять с фактическим Service contract.
Откат делайте после сохранения ревизии и проверки зависимости. Для Deployment можно посмотреть историю и вернуть предыдущую ревизию командами ниже. Откат не исправляет неверный readiness endpoint и не освобождает уже занятый Node; после него снова проверьте conditions, endpoints и rollout status.
\nkubectl -n demo rollout history deployment/api\nkubectl -n demo rollout undo deployment/api --to-revision=3\nkubectl -n demo rollout status deployment/api --timeout=120s\nКритерий готовности. Изменение готово, если известны image digest и владелец состояния, новый Pod проходит startup/readiness по смыслу приложения, Service видит ожидаемые endpoints, rollout достигает доступного состояния, а HPA получает метрику с объявленным request или явно использует другой документированный target type. Для каждого отрицательного пути есть наблюдаемое действие: остановка, откат или передача конкретному владельцу. Если команда видит только Running, но не может показать condition, endpoint и источник метрики, причина ещё не доказана.
maxSurge, maxUnavailable, stalled rollout и rollback.ready, serving, terminating и роль EndpointSlice в маршрутизации.spec.replicas с HPA.После обновления образа часть Pod долго остаётся на старой версии. Другие Pod переходят в Ready, но сразу теряют готовность. В ответ команда увеличивает maxReplicas, поднимает CPU limit и запускает rollout ещё раз. Симптомы меняются, а причина остаётся. Цена ошибки — лишние реплики, неуправляемая нагрузка на Node и более длинный путь отката. В худшем случае Service получает Pod, который ещё не готов обслуживать запросы.
Тезис простой: Kubernetes не управляет контейнерной нагрузкой одной ручкой. Scheduler учитывает requests. Runtime ограничивает container по limits. Readiness решает, можно ли отправлять Pod трафик. HPA предлагает число реплик по своей метрике. Эти контуры связаны, но не заменяют друг друга. Пока у каждого значения нет явного смысла, процент CPU и статус Ready легко принять за доказательство capacity.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| HPA показывает процент, но реплики не растут | У container нет CPU request или метрика не имеет нужного знаменателя | Проверить request каждого container, тип metric и источник metrics API | Сначала исправить контракт метрики; не увеличивать replicas вслепую |
| Pod Unready после старта | Readiness проверяет недоступную зависимость, либо probe срабатывает раньше прогрева | Сопоставить probe, startup path, condition и Service endpoints | Изменить семантику readiness или порядок старта, затем повторить проверку |
| Pod Pending после изменения ресурсов | Сумма requests не помещается на доступные Node | Сравнить requests с allocatable, quota и правилами admission | Пересмотреть профиль workload или размещение |
| CPU limit увеличен, но задержка не исчезла | Причина находится в памяти, очереди или внешней зависимости | Разделить CPU, memory, queue, latency и dependency signals | Проверять один доминирующий ресурс, а не менять все поля сразу |
Requests отвечают за размещение. Scheduler использует CPU и memory requests при выборе Node. Для Pod учитывается сумма requests контейнеров. Request не обещает постоянное потребление и не задаёт верхнюю границу. Это объявленная потребность, по которой система решает, может ли Pod быть размещён.
\nLimits задают границу ресурса. CPU и memory limit принадлежат container. Они не являются целью HPA. Memory limit не описывает безопасный размер cache, а CPU limit не обещает throughput. При изменении limit нужно знать, какое поведение ожидается после достижения границы: ограничение CPU, ошибка выделения памяти или другой runtime effect. Без этого число в YAML не объясняет проблему.
\nReadiness управляет допуском к трафику. Когда readiness probe возвращает failure, Kubernetes не считает Pod готовым backend для Service. Это полезный сигнал маршрутизации. Он не говорит, почему приложение не готово, насколько высока latency и хватит ли ему CPU при пике. Probe должна проверять короткий факт, которым владеет приложение или его платформа. Проверка десятка внешних зависимостей превращает краткий сбой одной зависимости в удаление Pod из трафика.
\nHPA предлагает desired replicas. Для CPU utilization процент рассчитывается относительно CPU request целевых Pod. Поэтому target в 70 процентов — не 70 процентов Node и не 70 процентов limit. При request 500m такой target имеет другой смысл, чем при request 1000m. Изменение request одновременно влияет на placement и на интерпретацию HPA. Это одна причина, чтобы менять оба решения в одной проверяемой гипотезе.
\nПусть приложение обслуживает HTTP-запросы. Для одного container объявлены cpu request: 500m и cpu limit: 1000m. HPA использует targetAverageUtilization: 70. В учебном примере значение 70 процентов относится к 500m request. Условная точка сравнения равна 350m usage на Pod. Это арифметика для объяснения знаменателя, а не наблюдение из кластера и не рекомендация для реального сервиса.
apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: orders\nspec:\n replicas: 2\n template:\n spec:\n containers:\n - name: app\n image: registry.example/orders:sha256-example\n resources:\n requests:\n cpu: 500m\n memory: 384Mi\n limits:\n cpu: 1000m\n memory: 768Mi\n readinessProbe:\n httpGet:\n path: /ready\n port: 8080\n periodSeconds: 5\n---\napiVersion: autoscaling/v2\nkind: HorizontalPodAutoscaler\nmetadata:\n name: orders\nspec:\n scaleTargetRef:\n apiVersion: apps/v1\n kind: Deployment\n name: orders\n minReplicas: 2\n maxReplicas: 8\n metrics:\n - type: Resource\n resource:\n name: cpu\n target:\n type: Utilization\n averageUtilization: 70\nЭтот фрагмент показывает форму контракта. Он не доказывает, что 500m достаточно, что endpoint отвечает быстро или что HPA получит метрики. В реальной системе нужно отдельно проверить effective manifest после admission, состояние metrics API, readiness transitions и фактический workload. Если CPU request убрать, процентный target теряет ожидаемый знаменатель. Если readiness отвечает 200 до завершения прогрева, Service направит трафик слишком рано. Если readiness зависит от внешней базы, краткий сбой базы может убрать все backend.
Рассмотрим запуск новой версии. Приложение мигрирует локальный cache 20 секунд, но readiness endpoint начинает отвечать успехом через две секунды. HPA видит CPU прогрева и предлагает больше реплик. Новые Pod повторяют тот же тяжёлый startup. Число реплик растёт, а полезная ёмкость не появляется. Это не доказательство, что HPA сломан. Сначала нужно отделить startup work от serving work.
\nОбратная ошибка тоже опасна. Readiness проверяет внешний сервис, который не нужен каждому запросу. При коротком отказе зависимости все Pod становятся Unready, хотя основная функция могла бы продолжать работу. Увеличение replicas не помогает: новые Pod проходят ту же проверку и исключаются из Service. Действие находится в контракте readiness и failure policy, а не в capacity curve.
\nЭта модель не выбирает универсальные значения CPU и memory. Она не учитывает автоматически admission webhooks, ResourceQuota, PodDisruptionBudget, Node allocatable, runtime, Metrics Server, custom adapter, queueing, cache retention и downstream saturation. Официальная документация описывает общий механизм Kubernetes, но не сообщает конфигурацию конкретного кластера. Нельзя переносить учебную арифметику в production profile без измерения.
\nReadiness не заменяет liveness и startup probes. HPA не устраняет утечку памяти и не гарантирует доступность внешней зависимости. Limit не превращается в SLO. Если причина не разделяется одним evidence, правильное действие — остановить изменение и уточнить контракт. Это отрицательный результат, но он дешевле массового rollout без объяснимого эффекта.
\nИзменение готово, когда для одного workload выполнены все условия: владелец назван; effective requests и limits зафиксированы по container; смысл readiness записан одной фразой; HPA metric имеет объявленный знаменатель и источник; выбранное evidence получено в указанном периоде; ожидаемый эффект измерим; stop condition и rollback проверяемы. Если после изменения команда всё ещё говорит только «Pod стал лучше» или «процент выглядит нормально», контракт не закрыт.
\nПосле обновления образа часть Pod может долго оставаться на старой версии, а новая версия — перейти в Unready. Команда увеличивает maxReplicas, поднимает CPU limit и запускает rollout повторно. Симптомы меняются, но причина не становится яснее. Цена такой подмены — лишние реплики, перегруженные Node и откат, который трудно объяснить.
У Kubernetes здесь не одна «ручка нагрузки», а несколько независимых контуров. Scheduler размещает Pod по requests. Runtime применяет limits. Readiness определяет, можно ли отправлять Pod трафик Service. HPA рассчитывает желаемое число реплик по выбранной метрике. Эти контуры встречаются в одном манифесте, но не доказывают друг друга.
\n| Симптом | Вероятный контур | Проверка | Следующее действие |
|---|---|---|---|
HPA показывает <unknown> или не меняет реплики | Метрика недоступна либо для Pod нет нужного resource request | Проверить HPA conditions, metrics.k8s.io и request каждого container | Восстановить контракт метрики; не поднимать maxReplicas вслепую |
Pod запущен, но Unready | Readiness не проходит или приложение ещё прогревается | Сопоставить probe, Pod conditions, события и endpoint | Исправить условие готовности или добавить startup-защиту |
Pod остаётся Pending | Сумма requests не помещается на доступные Node | Сравнить requests с allocatable, quota и admission-правилами | Пересмотреть профиль ресурсов или размещение |
| CPU limit увеличили, но задержка не исчезла | Доминирует память, очередь или внешняя зависимость | Разделить CPU, memory, latency, queue и dependency signals | Изменить один подтверждённый фактор и повторить замер |
Request отвечает за планирование. Scheduler учитывает requests контейнеров при выборе Node. Для одного ресурса Pod получает сумму requests своих контейнеров. Поэтому изменение request может перевести Pod из «размещается» в «не помещается», даже если текущее потребление на Node пока невелико. Request — заявленная потребность для планирования, а не обещание постоянной скорости.
\nLimit задаёт потолок контейнера. CPU limit может ограничивать долю CPU-времени, а превышение memory limit может привести к срабатыванию механизма out-of-memory. Limit не сообщает, сколько запросов выдержит приложение, и не является автоматически целью HPA. Между «контейнер не превысил лимит» и «у сервиса есть запас по latency» нет логического равенства.
\nReadiness управляет маршрутизацией. При неуспешной readiness probe Kubernetes помечает контейнер неготовым, а адрес Pod перестаёт быть готовым endpoint для соответствующих Service. Это сигнал «можно ли принимать этот трафик сейчас», а не проверка всех зависимостей системы. Если endpoint опрашивает необязательную базу или внешний API, краткий сбой этой зависимости может убрать из трафика исправное приложение.
\nHPA меняет желаемое число реплик. Для resource metric с averageUtilization процент считается относительно соответствующего request. Target 70 процентов — это 70 процентов request, не Node и не limit. Для CPU request 500m учебная точка 70 процентов равна 350m на Pod. Это объяснение знаменателя, а не рекомендация профиля.
Deployment отвечает ещё за один отдельный вопрос: как заменить Pod старой версии на Pod новой. При RollingUpdate параметры maxUnavailable и maxSurge ограничивают число временно недоступных и дополнительных Pod. Readiness влияет на то, когда новая реплика считается пригодной для трафика, но сама по себе не гарантирует, что rollout завершится: новый образ может не стартовать, не пройти probe или не поместиться по requests.
Поэтому «новые Pod появились» и «новая версия обслуживает нагрузку» — разные проверки. Для отката нужно заранее знать имя Deployment, границу времени и состояние, которое считается безопасным. Команда kubectl rollout status подтверждает наблюдаемое состояние rollout, но не заменяет проверку ошибок приложения и latency.
Ниже один минимальный пример для HTTP-сервиса. В нём добавлены обязательные для Deployment selector и labels, startup probe для долгого старта и readiness probe для допуска к трафику. Значения ресурсов, пути и образ проектные: их нельзя переносить в production без измерения.
\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: orders\nspec:\n replicas: 2\n strategy:\n type: RollingUpdate\n rollingUpdate:\n maxUnavailable: 0\n maxSurge: 1\n selector:\n matchLabels:\n app: orders\n template:\n metadata:\n labels:\n app: orders\n spec:\n containers:\n - name: app\n image: registry.example/orders:2024-06-01\n ports:\n - name: http\n containerPort: 8080\n resources:\n requests:\n cpu: 500m\n memory: 384Mi\n limits:\n cpu: 1000m\n memory: 768Mi\n startupProbe:\n httpGet:\n path: /healthz\n port: http\n periodSeconds: 10\n failureThreshold: 30\n readinessProbe:\n httpGet:\n path: /ready\n port: http\n periodSeconds: 5\n failureThreshold: 3\n---\napiVersion: autoscaling/v2\nkind: HorizontalPodAutoscaler\nmetadata:\n name: orders\nspec:\n scaleTargetRef:\n apiVersion: apps/v1\n kind: Deployment\n name: orders\n minReplicas: 2\n maxReplicas: 8\n metrics:\n - type: Resource\n resource:\n name: cpu\n target:\n type: Utilization\n averageUtilization: 70\nПри таком request target 70 процентов означает условные 350m среднего CPU на Pod. Но манифест не доказывает, что 500m достаточно, endpoint действительно отделяет startup от serving, а metrics API доступен. Если у контейнера, который участвует в CPU utilization, нет CPU request, HPA не сможет определить эту utilization для метрики. Это повод проверить конфигурацию и condition HPA, а не подставить процент вручную.
\nЭти команды предполагают доступ к namespace и установленный kubectl. Первая команда проверяет манифест сервером без изменения объекта; остальные читают состояние или запускают обычный rollout после явного применения. В тестовом кластере замените namespace и имя образа на свои.
kubectl apply --dry-run=server -f deployment.yaml\nkubectl apply -f deployment.yaml\nkubectl rollout status deployment/orders --timeout=10m\nkubectl get deployment/orders\nkubectl get pods -l app=orders -o wide\nkubectl describe deployment/orders\nkubectl describe pod -l app=orders\nkubectl get hpa orders\nkubectl top pods -l app=orders\nОжидаемый результат нужно формулировать заранее: например, rollout завершается за 10 минут, две минимальные реплики находятся в состоянии Ready, а HPA получает числовую CPU-метрику. Если kubectl top или HPA показывают отсутствие метрик, это ограничение наблюдаемости. Оно не подтверждает ни низкую нагрузку, ни исправность autoscaling. Для остановившегося rollout дополнительно смотрите Events и condition Deployment; для Pending — причины планировщика и effective requests.
Представим новую версию, которая прогревает локальный cache 20 секунд. Readiness endpoint начинает отвечать через две секунды, а CPU во время старта высок. Если metrics API отдаёт данные и контроллер учитывает их, HPA может принять решение о масштабировании, но новые Pod повторят тот же startup. Реплик станет больше, а serving capacity не обязательно вырастет. Это не доказательство неисправности HPA: сначала нужно отделить startup work от serving work.
\nОбратная ошибка симметрична. Readiness проверяет внешний сервис, который нужен только части запросов. При кратком отказе все Pod становятся Unready, хотя основной маршрут ещё может работать. Дополнительные реплики проходят ту же проверку и тоже исключаются из Service. Действие находится в контракте readiness и политике деградации, а не в увеличении maxReplicas.
Эта модель не выбирает универсальные CPU и memory values. На результат влияют admission webhooks, ResourceQuota, PodDisruptionBudget, Node allocatable, планировщик, runtime, Metrics Server, custom adapter, очередь, cache и downstream saturation. Официальная документация описывает механизм Kubernetes, но не конфигурацию вашего кластера.
\nReadiness не заменяет liveness и startup probes. HPA не устраняет утечку памяти и не гарантирует доступность внешней зависимости. CPU utilization не равен throughput, а отсутствие роста реплик не всегда означает ошибку autoscaling: контроллер может упереться в min/max, не получить метрику или увидеть, что целевой workload уже соответствует target. Учебную арифметику нельзя объявлять production-результатом без профиля на реальной нагрузке.
\nИзменение можно считать объяснимым, когда для одного workload названы владелец и окно наблюдения; effective requests и limits зафиксированы по container; смысл readiness записан одной фразой; HPA metric имеет источник и знаменатель; rollout имеет timeout и rollback; а выбранный эффект измерен тем же evidence до и после. Формулировки «Pod стал лучше» и «процент выглядит нормально» этого критерия не закрывают.
\nПосле выкладки Pod получает статус Running, но запросы к сервису ждут дольше обычного. Иногда HPA увеличивает число реплик, а доступных backend не становится больше: новые Pod остаются неготовыми. В другой версии проблемы readiness отвечает успешно ещё до прогрева, и Service отправляет трафик в приложение, которое не держит рабочую нагрузку. Цена ошибки — задержки для клиентов, лишние реплики и трудный откат. Команда видит зелёный rollout и ищет причину уже под нагрузкой.
\nТезис простой: requests, limits, readiness и HPA описывают разные границы. Их нельзя настраивать как четыре независимые строки в манифесте. Сначала нужно назвать профиль приложения и единицу работы. Затем связать каждую границу с проверяемым сигналом. Тогда Kubernetes размещает Pod по одному правилу, допускает его к трафику по другому, а autoscaler меняет replicas по третьему. Это не даёт готовых чисел для любого сервиса. Зато не позволяет принять один сигнал за другой.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Pod долго остаётся Pending | Request не помещается на доступный Node или не учтены requests всех containers. | Сверить request каждого container с событиями scheduler и allocatable Node. | Исправить размер или размещение только после проверки профиля. |
| Pod Running, но сервис медленный | Running означает процесс, а не готовность и не capacity. | Сравнить Ready, readiness probe, latency и очередь за один период. | Разделить контракт готовности и гипотезу о ресурсе. |
| HPA меняет replicas без эффекта | Новые Pod не готовы, либо CPU target считается от неверного request. | Проверить effective request, metric source, Ready и время прогрева. | Не повышать maxReplicas, пока не исправлен сигнал. |
| Container получает OOMKilled | Memory limit ограничивает container, но не описывает жизненный цикл cache или объектов. | Сопоставить предел, рост working set и действие приложения при нехватке памяти. | Изменять limit вместе с политикой роста и восстановления. |
Request — заявка на ресурс для размещения. Scheduler использует её, когда выбирает Node. Для Pod ресурсная заявка складывается из заявок его containers. Request не равен фактическому потреблению. Container может использовать больше request, если на Node есть свободный ресурс и limit это позволяет. Поэтому фраза «у Pod есть 500m CPU» неполна: нужно сказать, это request, limit или наблюдаемое usage.
\nLimit — верхняя граница исполнения для container. Он не обещает пропускную способность и не является знаменателем CPU utilization HPA. Для CPU превышение limit ограничивает доступ к CPU. Для memory превышение может закончиться убийством container. Применение зависит от ресурса и среды, поэтому нельзя переносить правило для CPU на memory. Если limit задан без request, конкретная admission-политика может использовать limit как request. Effective значения нужно увидеть в разрешённой проверке, а не угадывать по шаблону.
\nReadiness отвечает на узкий вопрос: можно ли сейчас отправлять трафик в этот container. При failed readiness Kubernetes убирает Pod из EndpointSlice соответствующего Service. Probe не измеряет запас capacity, throughput и качество каждого ответа. Не стоит включать в неё все внешние зависимости без явной политики отказа: краткий сбой одной зависимости способен вывести из трафика все реплики. Обратная ошибка не менее опасна: слишком ранний success пускает запросы до окончания прогрева.
\nHPA периодически меняет desired replicas по наблюдаемой метрике. Для CPU utilization в процентах важен request, к которому относится usage. Target 70 процентов — это не 70 процентов Node и не 70 процентов limit. Если request отсутствует там, где он нужен для расчёта, вывод по такой метрике нельзя считать осмысленным. При этом новый Pod ещё должен пройти startup и readiness. Автомасштабирование не может исправить неверный healthcheck и не сокращает время загрузки большого cache.
\nНиже — ограниченный пример для HTTP-сервиса с устойчивой CPU-нагрузкой после прогрева. Значения не описывают реальный production workload. Они нужны, чтобы увидеть отношения между полями. Здесь request равен 500m, limit — 1000m, а target HPA относится к request. Startup probe отделяет запуск от liveness и readiness. Readiness проверяет локальный признак готовности приложения, а не весь внешний мир.
\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: app\nspec:\n replicas: 2\n selector:\n matchLabels:\n app: app\n template:\n metadata:\n labels:\n app: app\n spec:\n containers:\n - name: app\n image: registry.example/app:1.4.0\n resources:\n requests:\n cpu: \"500m\"\n memory: \"384Mi\"\n limits:\n cpu: \"1000m\"\n memory: \"768Mi\"\n startupProbe:\n httpGet: { path: /startup, port: 8080 }\n failureThreshold: 30\n periodSeconds: 2\n readinessProbe:\n httpGet: { path: /ready, port: 8080 }\n periodSeconds: 5\n livenessProbe:\n httpGet: { path: /live, port: 8080 }\n periodSeconds: 10\n---\napiVersion: autoscaling/v2\nkind: HorizontalPodAutoscaler\nmetadata:\n name: app\nspec:\n scaleTargetRef:\n apiVersion: apps/v1\n kind: Deployment\n name: app\n minReplicas: 2\n maxReplicas: 6\n metrics:\n - type: Resource\n resource:\n name: cpu\n target:\n type: Utilization\n averageUtilization: 70\nЭтот манифест не доказывает, что 500m достаточно. Он задаёт гипотезу: после прогрева одна реплика выдерживает согласованную steady-нагрузку, а CPU — её главный ограничитель. Если в реальности первым растёт memory или очередь, HPA по CPU не решает проблему. Если /ready отвечает до открытия рабочих пулов, реплики формально Ready, но фактически бесполезны. Отрицательный путь важен: когда гипотеза не подтверждается, нужно остановить изменение чисел и пересмотреть профиль, а не автоматически добавлять replicas.
\nСхему полезно читать слева направо. Профиль задаёт вопрос к manifest: что является единицей работы и какой ресурс ограничивает её первым. Request влияет на размещение. Limit ограничивает исполнение. Только затем readiness отвечает на вопрос о допуске к Service traffic. HPA использует свою метрику и свой знаменатель. Перепрыгнуть через профиль нельзя: иначе одинаковый target будет означать разные вещи для CPU-bound HTTP, memory-retaining cache и batch-задачи.
\nЭта схема не назначает ресурсы настоящему сервису и не подтверждает состояние кластера. Она не читает Metrics API, события Node, EndpointSlice, controller flags, feature gates, логи или trace. Официальные документы описывают механизм Kubernetes, но не знают версию, admission-политику и настройки конкретной среды. Поэтому учебные 500m, 384Mi, 70 процентов и два Pod нельзя выдавать за результат измерения.
\nЕсть и граница применимости. HPA по CPU подходит не каждому workload. Для memory-heavy приложения нужно отдельно описать рост рабочего набора и способ его ограничить. Для batch часто важнее размер очереди и время обработки. Для сервиса с дорогим warmup нужно учитывать startup и скорость появления Ready backend. Если исходный сигнал не объясняет цену ошибки, корректное действие — не менять manifest. Сначала нужно получить недостающее разрешённое наблюдение или признать задачу неготовой к настройке.
\nКонфигурация готова к обсуждению, когда для одного workload существует короткая карточка: request и limit каждого container, смысл readiness, условие startup, назначение liveness, metric HPA и её знаменатель, источник evidence, stop condition и rollback. Проверка должна связать каждое поле с одним наблюдаемым вопросом. После учебного прогона готовность не означает «Pod зелёный». Она означает, что команда может объяснить, какой сигнал изменился, почему это подтверждает или опровергает гипотезу и что произойдёт при отрицательном результате.
\nПосле выкладки Pod получает статус Running, но запросы к сервису ждут дольше обычного. Иногда HorizontalPodAutoscaler (HPA) увеличивает число реплик, а доступных backend не становится больше: новые Pod не проходят readiness. В другой версии проблемы endpoint отвечает успешно ещё до прогрева, и Service отправляет трафик в приложение, которое не готово к рабочей нагрузке. Команда видит зелёный rollout и ищет причину уже под трафиком.
У этих симптомов общий источник: в манифесте смешивают четыре разные границы. requests нужны планировщику для размещения, limits ограничивают выполнение container, readiness управляет допуском к трафику, а HPA меняет число реплик по метрике. Ни одно поле не обещает пропускную способность сервиса само по себе. Поэтому разберём не «правильные числа», а последовательность, в которой каждое число связывается с наблюдаемым результатом.
| Наблюдение | Что оно подтверждает | Что проверить дальше |
|---|---|---|
Pod остаётся Pending | Pod не назначен на Node; request может не помещаться в доступный ресурс. | События Pod, requests всех containers, allocatable Node, taints и affinity. |
Pod Running, но не Ready | Процесс запущен, но probe не разрешает отправлять ему трафик. | Смысл endpoint, результаты probe, время прогрева и состояние Service. |
Pod Ready, latency растёт | Текущая probe пропускает трафик; она не доказывает запас capacity. | CPU, memory, очередь, внешние вызовы и лимиты приложения. |
| HPA поднял replicas без эффекта | Решение о масштабировании было принято, но новые реплики могут не стать Ready или метрика не описывает bottleneck. | Метрику HPA, request-знаменатель, события, readiness и время появления backend. |
Container перезапускается с OOMKilled | Память пересекла ограничение или приложение получило другую фатальную ошибку. | Причину завершения, working set, limit, рост cache и политику восстановления. |
Начинайте с колонки «наблюдение», а не с изменения манифеста. Running описывает состояние процесса, не готовность к запросам. Значение CPU в HPA не означает процент от Node или от limit: для ресурсной метрики utilization это отношение usage к request. Эти различия и есть рабочая карта диагностики.
Request — заявка на ресурс. Scheduler учитывает requests контейнеров, когда выбирает Node; для обычного Pod суммарная заявка контейнеров определяет, сколько ресурса нужно зарезервировать при размещении. Request не равен фактическому usage: container может потреблять больше заявки, если это разрешают limit и свободный ресурс. Без request нельзя осмысленно интерпретировать CPU utilization HPA для такого контейнера.
\nLimit — ограничение выполнения контейнера. Для CPU превышение limit может привести к throttling, а для memory превышение может завершить процесс с OOMKilled. Limit не является обещанием throughput и не заменяет нагрузочное измерение. Если request или limit добавляет admission-политика namespace, смотрите итоговый объект в кластере, а не только исходный файл.
Readiness probe отвечает на узкий вопрос: можно ли сейчас отправить запрос этому контейнеру. При неуспешной readiness Kubernetes перестаёт считать Pod готовым endpoint для Service. Probe не измеряет запас capacity и не должна бездумно превращаться в проверку всех внешних зависимостей. Иначе краткий сбой партнёра способен убрать из трафика все реплики. Слишком ранний успешный ответ создаёт обратную проблему: приложение принимает запросы до открытия пулов, миграций или cache.
\nHPA периодически вычисляет желаемое число реплик по метрике. Для CPU с averageUtilization: 70 контроллер сравнивает среднее потребление с CPU request, а не с limit. Pod без нужного request не даёт корректного CPU utilization; Pod, который ещё не готов или не имеет метрики, может учитываться консервативно в расчёте. Поэтому HPA нельзя читать без describe, состояния реплик и понимания того, откуда пришла метрика.
Ниже — минимальный serving-workload для HTTP-приложения. Предположим, что после прогрева CPU — главный ограничитель, endpoint /startup появляется один раз, /ready проверяет локальную готовность пулов, а /live отвечает только при неисправимом состоянии процесса. Значения 500m, 384Mi, две реплики и target 70% — не рекомендация и не результат измерения. Это числа для воспроизводимого учебного прогона.
apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: app\nspec:\n replicas: 2\n selector:\n matchLabels:\n app: app\n template:\n metadata:\n labels:\n app: app\n spec:\n containers:\n - name: app\n image: registry.example/app:1.4.0\n ports:\n - name: http\n containerPort: 8080\n resources:\n requests:\n cpu: \"500m\"\n memory: \"384Mi\"\n limits:\n cpu: \"1000m\"\n memory: \"768Mi\"\n startupProbe:\n httpGet:\n path: /startup\n port: http\n periodSeconds: 2\n failureThreshold: 30\n readinessProbe:\n httpGet:\n path: /ready\n port: http\n periodSeconds: 5\n timeoutSeconds: 2\n livenessProbe:\n httpGet:\n path: /live\n port: http\n periodSeconds: 10\n timeoutSeconds: 2\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: app\nspec:\n selector:\n app: app\n ports:\n - name: http\n port: 80\n targetPort: http\n---\napiVersion: autoscaling/v2\nkind: HorizontalPodAutoscaler\nmetadata:\n name: app\nspec:\n scaleTargetRef:\n apiVersion: apps/v1\n kind: Deployment\n name: app\n minReplicas: 2\n maxReplicas: 6\n metrics:\n - type: Resource\n resource:\n name: cpu\n target:\n type: Utilization\n averageUtilization: 70\nstartupProbe отделяет медленный запуск от последующих проверок: пока она не завершилась успешно, liveness и readiness не начинают обычную работу. Это защищает процесс от преждевременного перезапуска, но не ускоряет прогрев. После прогрева readiness должна означать «этот Pod может принять обычный запрос», а не «процесс слушает порт». У HPA есть верхняя граница шесть реплик, но она не гарантирует, что шесть Pod поместятся в кластер или выдержат запросы.
Читать схему нужно слева направо. Сначала scheduler решает, где Pod может быть размещён по requests. Затем kubelet запускает контейнер и выполняет startup, readiness и liveness в своих ролях. Service направляет запросы только к готовым backend. Параллельно HPA получает свою метрику и меняет desired replicas. При таком порядке увеличение replicas не исправляет неверный readiness, а поднятие memory limit не исправляет очередь запросов.
\nСохраните манифест в файл app.yaml в тестовом namespace и сначала проверьте его сервером. Эта команда обращается к API и может требовать прав; локальный кластер, версия Kubernetes, policy admission и наличие Metrics API должны быть известны заранее.
kubectl config current-context\nkubectl auth can-i create deployment -n demo\nkubectl describe pod -n demo -l app=app --show-events\nkubectl apply --dry-run=server -n demo -f app.yaml\nkubectl diff -n demo -f app.yaml\nkubectl apply -n demo -f app.yaml\nkubectl rollout status deployment/app -n demo --timeout=120s\nkubectl get pods -n demo -l app=app -o wide\nkubectl get endpointslice -n demo -l kubernetes.io/service-name=app\nkubectl describe hpa app -n demo\nkubectl top pods -n demo -l app=app\nПоследняя команда требует работающего Metrics API, часто его предоставляет Metrics Server. Если она возвращает ошибку, это не доказательство нулевой нагрузки и не повод подставить число вручную. Зафиксируйте ошибку как ограничение прогона. kubectl diff и --dry-run=server также не заменяют rollout: первая сравнивает объект, вторая проверяет запрос к API, а третья показывает, что Deployment действительно продвигается. Команда describe pod -l удобна как быстрый запрос, но при нескольких Pod для детального анализа выберите конкретное имя из get pods.
Pending прочитайте kubectl describe pod и события. Сопоставьте requests с allocatable, а не с общей памятью Node. Если Pod размещён, переходите к probes.failureThreshold × periodSeconds. Для этого примера окно startup — до 60 секунд при последовательных неуспехах, но реальное время зависит от результата probe и приложения./ready. Если Pod Ready, а latency растёт, probe выполняет свой узкий контракт; ищите bottleneck в CPU, memory, очереди или внешнем вызове.describe hpa найдите текущую и целевую метрики, desired replicas, события и ошибки. Сопоставьте их с request. Отсутствие данных Metrics API нельзя трактовать как отсутствие нагрузки.maxReplicas. Сначала исправьте контракт готовности или startup, затем повторите тот же прогон. Если HPA масштабируется, но latency не улучшается, проверьте, является ли CPU главным ограничителем.В учебном файле доказан только синтаксический и объектный контракт, если его принял API. Значение 500m становится обоснованным request лишь после повторяемого измерения representative-нагрузки с согласованным latency budget и запасом на пики. Target 70% становится рабочей гипотезой только вместе с наблюдаемым временем масштабирования, размером очереди и готовностью новых реплик. Значение 768Mi нельзя оправдать одной строкой OOMKilled: нужно понять, растёт ли cache, есть ли утечка и сколько памяти нужно процессу при штатном пике.
После каждого изменения сохраняйте минимум: версию образа, итоговый Deployment, временное окно, входную нагрузку, latency/error rate, состояние Pod и HPA, а также решение об откате. Это превращает настройку из перебора коэффициентов в проверяемый эксперимент. Если не хватает разрешённого наблюдения, корректное действие — остановить эксперимент, а не додумывать результат.
\nСхема рассчитана на stateless HTTP-сервис, который можно безопасно масштабировать горизонтально. Она не решает координацию StatefulSet, порядок миграций базы, работу с локальным диском, очередями, GPU или внешним rate limit. HPA по CPU может быть плохим сигналом для memory-heavy приложения, batch-задачи или сервиса, где bottleneck — очередь и время ответа партнёра. Для таких случаев нужна другая метрика и отдельная проверка её источника.
\nТочный результат зависит от версии Kubernetes, API и controller flags, Metrics API, admission defaults, политики namespace, сетевого маршрута и реализации приложения. Схема также не проверяет PDB, topology spread, node autoscaling и безопасность образа. Наличие статуса Available или зелёного rollout не является доказательством производительности. Граница вывода простая: эта статья даёт порядок проверки, а не готовый production-манифест.
Новая команда просит создать сервис, а платформа предлагает одну форму: имя, владелец, runtime, репозиторий и несколько флагов. Сначала это выглядит удобно. Через месяц появляются локальные правки, особые healthcheck, другой retention и ручные исключения в CI. Два сервиса уже не похожи на исходный шаблон, но команда всё ещё считает их его вариантами. Цена ошибки — не только лишняя работа. Теряется владелец контракта, обновления перестают доходить до копий, а рискованный выбор прячется за кнопкой Create.
\nТезис простой: шаблон должен принимать только повторяемый класс задач. Совпадение с базовым контрактом ведёт в golden path. Одно заранее описанное отличие ведёт на review расширения. Несовместимый или одноразовый запрос нужно остановить. Отказ дешевле fork-а, который создаёт ложное ощущение поддержки.
\nПервый симптом — просьба добавить универсальное поле: «если понадобится, разрешим внешний data class», «пусть runtime выбирается позже», «добавим произвольный adapter». Поле кажется небольшим, но меняет инвариант. После него шаблон уже не описывает один тип сервиса. Он принимает несколько архитектурных решений без владельца.
\nВторой симптом — локальный patch сразу после создания репозитория. Команда удаляет обязательный шаг, переписывает pipeline или меняет доступы, а потом обещает вернуть полезное изменение в общий шаблон. Если различие не имеет имени, владельца, границы и условия удаления, это не extension. Это отдельный проект, который маскируется под стандартный путь.
\nТретий симптом — платформа выдаёт skeleton для задачи, у которой ещё нет data policy, access model или ответственного. Файлы создаются быстро, но структура начинает диктовать решение. Команда подгоняет требования под уже созданный репозиторий. Технический артефакт появляется раньше архитектурного договора.
\nРазделите решение на три слоя. Первый — входные факты: тип компонента, владелец, runtime, класс данных, требования к доставке и срок жизни. Второй — контракт: допустимые значения и обязательные шаги. Третий — результат: применить базовый путь, отправить ограниченное расширение на review или отказать до уточнения требований.
\nBackstage описывает шаблон как набор параметров и последовательных шагов. Это полезный механизм, но он не делает любой параметр безопасным. Параметр собирает значение. Решение о том, разрешено ли значение, должно жить в контракте и проверке. GitHub template repository копирует структуру и файлы в новый репозиторий, но создаёт несвязанную историю. Автоматического канала изменений исходного шаблона это не даёт.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Просят «универсальный» флаг | Поле меняет runtime, ownership, data class, access или retention | Сравнить поле с базовым инвариантом и назвать владельца | Убрать поле из golden path; оформить отдельный путь |
| После создания нужен patch | Различие не описано как versioned extension | Проверить identifier, owner, boundary и rollback | Остановить копирование; вынести различие на review |
| Шаблон приняли для миграции | Нет повторяемого типа и жизненного цикла | Проверить повторяемость того же контракта | Отказать шаблону и провести отдельное решение |
| Repository считают fork-ом | Смешаны копирование и наследование изменений | Проверить историю и канал обновлений | Зафиксировать самостоятельное владение или другой механизм |
Ниже — учебный фрагмент. Он не запускает реальную задачу и не доказывает пригодность набора полей для вашей организации. Базовый путь принимает внутренний HTTP-сервис с известным владельцем и утверждённым runtime. Observability adapter разрешён как именованное расширение. Внешние регулируемые данные форму не проходят.
\napiVersion: scaffolder.backstage.io/v1beta3\\nkind: Template\\nmetadata:\\n name: internal-http-service\\nspec:\\n owner: group:platform\\n type: service\\n parameters:\\n - title: Service contract\\n required: [name, owner, runtime, dataClass]\\n properties:\\n runtime:\\n type: string\\n enum: [node20, go122]\\n dataClass:\\n type: string\\n enum: [internal]\\n extension:\\n type: string\\n enum: [none, observability-adapter]\\n steps:\\n - id: write-skeleton\\n action: fetch:template\\n - id: publish\\n action: publish:github\nВ примере enum ограничивает форму, но не заменяет проверку прав и политики. Owner должен ссылаться на существующую группу, а публикация требует разрешений и проверки credentials. В рабочем шаблоне эти условия подтверждаются средствами вашей платформы. YAML не доказывает успешный запуск, безопасность или пригодность runtime.
\nПервая заявка содержит известный service type, владельца, approved runtime и internal data. Она совпадает с контрактом: golden path. Вторая содержит те же факты и один adapter из закрытого списка. Если у расширения есть owner, граница и способ удаления, это review extension. Третья описывает одноразовую регулируемую миграцию, но не содержит владельца, retention и access model. Ей нужен отказ от шаблона и отдельное решение.
\nFork отвечает на вопрос «как начать самостоятельный проект на основе текущих файлов». Он не отвечает на вопрос «как поддерживать общий контракт между проектами». Repository, созданный из template, получает несвязанную историю. Pull request между копией и шаблоном не становится штатным каналом синхронизации.
\nFork допустим, когда команда принимает независимый жизненный цикл. Тогда нужно записать владельца, область ответственности и способ получать будущие изменения. Если ожидается обновление всех созданных проектов из базового шаблона, нужен другой механизм доставки или честная граница поддержки.
\nНе расширяйте базовый шаблон ради редкого запроса. Новое поле увеличивает число состояний для всех пользователей. Особенно опасны поля, которые откладывают решение: «позже выберем runtime», «потом определим доступ», «retention настроит команда». Отсутствующее решение нельзя превратить в безопасный default названием параметра.
\nШаблон не выбирает владельца, не определяет классификацию данных и не делает action безопасным. Список runtime устаревает. Документация объясняет форму и порядок шагов, но не знает ваших сетевых прав, требований регулятора и правил отката. Эти условия проходят отдельную проверку.
\nУчебный YAML не является production-конфигурацией. В нём нет конкретной схемы прав, политики секретов, branch protection, SLO и обязательных проверок поставки. Не переносите его в рабочую систему без адаптации и review. Цифры adoption, скорости и снижения дефектов здесь не заявлены.
\nРешение готово, когда другая команда берёт ту же заявку и получает тот же вердикт по тем же фактам. Для golden path форма принимает только значения контракта. Для extension документ содержит owner, boundary, version и rollback. Для отказа есть причина и следующий вопрос вне шаблона. Неизвестное или рискованное значение останавливается до создания репозитория и не превращается в локальный patch.
\nПроверка состоит из четырёх записей: базовая заявка, узкое расширение, несовместимая заявка и повторный запуск первой. Готовность есть, если базовые записи дают одинаковый результат, расширение не меняет инварианты, отказ не создаёт артефакт, а повторный запуск не дублирует обязательные действия.
\nНовая команда просит создать сервис, а платформа предлагает одну форму: имя, владелец, runtime, репозиторий и несколько флагов. В первый день это похоже на хороший стандарт. Через месяц у сервиса появляется особый healthcheck, другой срок хранения данных, ручное исключение в CI и локальный скрипт для деплоя. Ещё через месяц команда просит добавить в форму «универсальный adapter», чтобы не делать отдельное решение.
\nТак шаблон превращается в каталог скрытых архитектурных решений. Кнопка Create всё ещё выглядит простой, но за ней уже нет единого контракта: разные команды получают разные права, жизненные циклы и обязанности поддержки. Тезис статьи простой: автоматизировать стоит повторяемую задачу, а несовпадение нужно увидеть до генерации репозитория. Полное совпадение ведёт в golden path, одно явно ограниченное отличие — на review расширения, изменение инварианта — к отказу от шаблона.
\nПервый симптом — просьба добавить поле без заранее определённого множества значений. «Пусть команда сама укажет runtime», «выберем хранилище позже», «разрешим любой внешний adapter» звучит как небольшая доработка формы. На деле каждое такое поле переносит решение из архитектурного обсуждения в момент генерации. Пользователь заполняет значение, но не получает ответа, кто отвечает за его безопасность, обновление и удаление.
\nВторой симптом — patch сразу после создания проекта. Команда выключает обязательную проверку, переписывает pipeline или меняет видимость репозитория, а затем обещает вернуть полезную часть в общий шаблон. Если различие нельзя назвать, назначить ему владельца, очертить границу и описать обратный ход, это не расширение базового пути. Это самостоятельный проект, который временно маскируется под стандартный.
\nТретий симптом — шаблон создаёт код раньше, чем зафиксированы класс данных и модель доступа. Skeleton уже содержит Dockerfile, workflow и manifest, поэтому команда начинает подгонять требования под готовую структуру. Платформа помогла быстро получить файлы, но незаметно стала источником политики. Удобная форма не должна принимать решение за владельца данных или службы безопасности.
\nВ Backstage Software Templates пользователь вводит параметры, после чего Scaffolder выполняет последовательность шагов. В документации среди таких шагов показаны загрузка skeleton, подстановка значений и публикация результата в GitHub или GitLab. Это механизм автоматизации, а не доказательство того, что любое сочетание параметров разрешено. Допустимые значения, обязательные поля и проверки должны быть частью контракта вашей платформы.
\nGitHub template repository решает другую задачу: создаёт новый репозиторий с той же структурой, ветками и файлами. GitHub отдельно предупреждает, что ветки из шаблона имеют несвязанные истории; новый fork, напротив, сохраняет историю родительского репозитория. Поэтому template — удобный старт нового проекта, но не канал доставки обновлений во все уже созданные проекты. Ошибка начинается, когда копию называют fork-ом и обещают ей автоматическое наследование.
\n| Наблюдение | Что проверяем | Вердикт | Следующий шаг |
|---|---|---|---|
| Все входы входят в закрытый контракт | Тип сервиса, владелец, runtime, класс данных и видимость имеют допустимые значения | Golden path | Запустить стандартные шаги и записать версию контракта |
| Есть одно отличие от базы | У отличия есть идентификатор, owner, граница, срок действия и rollback | Review extension | Рассмотреть отдельную ветку; не добавлять свободное поле в форму |
| Меняется инвариант | Появляется новый класс данных, модель доступа или жизненный цикл | Decline template | Остановить генерацию и провести отдельное архитектурное решение |
| Просят синхронизировать копии с базой | Есть ли реальный канал поставки обновлений и владелец миграций | Не обещать наследование | Выбрать upstream-механику или зафиксировать независимое владение |
Начните не с YAML, а с короткой заявки. Запишите тип компонента, владельца, runtime, класс данных, видимость репозитория, требования к доступу и срок хранения. Затем отделите инварианты от вариантов. Например, для внутреннего HTTP-сервиса инвариантами могут быть подтверждённый владелец, закрытый класс данных и один из поддерживаемых runtime. Название проекта и регион могут быть вариантами, если они не меняют модель риска.
\nУ контракта должны быть три явных результата. Golden path применяет стандарт без ручного решения. Расширение добавляет ровно одну заранее названную возможность и проходит review до создания артефакта. Отказ не означает «никогда»: он говорит, что текущая форма не является правильным местом для нового инварианта. Следующий вопрос должен вести к отдельному design path — с владельцем и проверками.
\nПоле допустимо только тогда, когда его множество значений закрыто или его проверка определена отдельно. Удобное правило: если для значения нельзя сразу назвать owner, policy и способ отката, значение не должно попадать в golden path. Неизвестный runtime нельзя сделать безопасным значением с помощью default, а внешний data class нельзя превратить во внутренний одним текстом подсказки.
\nНиже приведён минимальный учебный фрагмент для Backstage Scaffolder. Он показывает форму и порядок шагов, но не является готовой конфигурацией вашей инсталляции. В реальном проекте замените URL skeleton, организацию, группу владельца и action публикации на разрешённые вашей платформой значения.
\napiVersion: scaffolder.backstage.io/v1beta3\nkind: Template\nmetadata:\n name: internal-http-service\nspec:\n owner: group:platform\n type: service\n parameters:\n - title: Service contract\n required:\n - name\n - owner\n - runtime\n - dataClass\n - repoVisibility\n properties:\n name:\n title: Service name\n type: string\n pattern: '^[a-z0-9-]+$'\n owner:\n title: Owning group\n type: string\n runtime:\n title: Runtime\n type: string\n enum:\n - node20\n - go122\n dataClass:\n title: Data class\n type: string\n enum:\n - internal\n repoVisibility:\n title: Repository visibility\n type: string\n enum:\n - private\n - internal\n steps:\n - id: fetchBase\n name: Fetch approved skeleton\n action: fetch:template\n input:\n url: ./template\n values:\n name: ${{ parameters.name }}\n runtime: ${{ parameters.runtime }}\n - id: publish\n name: Publish repository\n action: publish:github\n input:\n repoUrl: 'github.com?owner=acme&repo=${{ parameters.name }}'\n repoVisibility: ${{ parameters.repoVisibility }}\nВ этом примере enum ограничивает runtime, класс данных и видимость, а pattern отсекает часть случайных имён. Это полезная граница формы, но не полноценная авторизация. Строка owner всё ещё должна разрешаться по каталогу групп, action публикации требует настроенной интеграции и credentials, а закрытый класс данных не отменяет проверку содержимого skeleton. Backstage описывает owner шаблона как ответственную сущность, но прямо отделяет это поле от runtime-авторизации. Проверки прав и секретов остаются обязанностью конфигурации вашей платформы.
\nПроверить обещание «проект наследует шаблон» можно без спора о терминах. Создайте тестовый репозиторий из шаблона, клонируйте оба репозитория и сравните корневые коммиты. В команде ниже замените acme/service-template и acme/orders-api на доступные вам репозитории. Команда создания изменяет удалённый GitHub и поэтому предназначена только для тестового владельца, у которого есть право создавать репозитории.
gh repo create acme/orders-api --private --template acme/service-template\ngh repo clone acme/service-template /tmp/service-template\ngh repo clone acme/orders-api /tmp/orders-api\nprintf 'template roots: '\ngit -C /tmp/service-template rev-list --max-parents=0 HEAD\nprintf 'created roots: '\ngit -C /tmp/orders-api rev-list --max-parents=0 HEAD\ngit -C /tmp/orders-api log --oneline --decorate -5\nОжидаемый результат для GitHub template — новый репозиторий с собственной начальной историей. Это не измерение качества шаблона и не проверка того, что файлы совпадают навсегда. Для проверки содержимого зафиксируйте commit шаблона, сравните нужные файлы и отдельно решите, как доставлять будущие изменения: pull request из upstream, пакет, генератор миграций или ручное владение. Если такого канала нет, в документации проекта нужно прямо написать: после создания репозиторий самостоятельный.
\nРасширение — это не поле «custom». Сначала дайте ему имя, например observability-adapter-v1, и опишите, что оно добавляет и чего не меняет. Затем назначьте owner, перечислите затронутые файлы и разрешения, задайте условие включения, срок пересмотра и rollback. Владелец должен быть способен принять инцидент и удалить расширение, а не только согласовать его в каталоге.
Проверка расширения должна включать положительный и отрицательный случаи. Положительный случай доказывает, что базовый сервис создаётся и получает adapter. Отрицательный — что произвольное значение или другой класс данных останавливают задачу до публикации. Если action уже создал репозиторий, а затем обнаружил несовместимость на deploy, граница стоит слишком поздно: потребуется cleanup, а часть риска уже прошла.
\nНе включайте редкое расширение в общий шаблон только потому, что так короче обсуждение. Каждая новая ветка увеличивает число состояний, которые нужно тестировать и поддерживать. Если один и тот же запрос повторяется и сохраняет общий инвариант, соберите доказательства и включите его в следующую версию контракта. Если запрос одноразовый или меняет модель ответственности, оставьте его отдельным проектом.
\nОписанный подход не заменяет threat model, классификацию данных, согласование доступа и правила вашей организации. Backstage может проверять схему параметров и выполнять actions, но конкретный набор integrations, permissions, secrets и custom actions зависит от инсталляции. Учебный YAML не доказывает безопасность публикации и не должен переноситься в production без адаптации и review.
\nКоманда gh repo create требует установленного GitHub CLI, входа в нужный аккаунт и права создания репозитория; выполнение команды изменяет внешний GitHub. Сравнение корневых коммитов доказывает различие историй только для конкретной пары репозиториев. Оно не проверяет актуальность файлов, policy или pipeline. Для GitLab, другой forge или внутреннего шаблонизатора действуют аналогичные вопросы, но точные команды и семантика могут отличаться.
Шаблон готов к использованию, когда другая команда может взять ту же заявку и получить тот же вердикт по тем же фактам. Для golden path разрешены только значения закрытого контракта. Для extension есть owner, boundary, version, проверка отрицательного пути и rollback. Для отказа указана причина и следующий владелец отдельного решения. Ни один неизвестный параметр не создаёт репозиторий «на авось».
\nМинимальный набор доказательств — четыре записи: стандартная заявка, разрешённое расширение, несовместимая заявка и повтор стандартной заявки. Первая и четвёртая дают один результат, вторая не меняет базовых инвариантов, третья останавливается до publish. После этого отдельно проверяются права, secrets, CI и ручные шаги окружения. Такой результат честнее, чем обещание, что одна форма поддерживает все будущие варианты.
\nspec.parameters, spec.steps, actions, условия шагов и обработка ошибок.kind: Template и owner сущности.--template и требования команды создания репозитория.