[Перевод] Правильное сравнение Kubernetes Apply, Replace и Patch
Для Kubernetes есть несколько вариантов обновления ресурсов: apply, edit, patch и replace. С тем, что каждый из них делает и когда их применять, есть путаница. Давайте разберемся.
Если поискать в Google фразу «kubernetes apply vs replace», находится ответ на StackOverflow, который не верен. При поиске «kubernetes apply vs patch» первая же ссылка — документация на kubectl patch
, которая не включает сравнение apply
и patch
. В этой статье будут рассмотрены различные варианты, а также правильное использование каждого из них.
В течение жизненного цикла ресурса Kubernetes (сервиса, deployment, ingress и т.д.) иногда нужно поменять, добавить или удалить некоторые свойства этого ресурса. Например, добавить примечание, увеличить или уменьшить число реплик.
Kubernetes CLI
Если вы уже работаете с кластерами Kubernetes через CLI, то уже знакомы с apply
и edit
. Команда apply
читает спецификацию ресурса из файла и делает «upsert» в кластер Kubernetes, т.е. создает ресурс, если его нет, и обновляет его, если он существует. Команда edit
читает ресурс через API, после чего пишет спецификацию ресурса в локальный файл, который затем открывается в текстовом редакторе. После того, как вы отредактируете и сохраните файл, kubectl
отправит сделанные изменения обратно через API, который заботливо применит эти изменения к ресурсу.
Не все знают команды patch
и replace
. Команда patch
позволяет изменить часть спецификации ресурса, предоставляя только измененную часть в командной строке. Команда replace
работает так же, как и edit
, но только все надо сделать вручную: необходимо скачать текущую версию спецификации ресурса, например, с использованием kubectl get -o yaml
, отредактировать ее, затем использовать replace
для обновления ресурса по измененной спецификации. Команда replace
не отработает, если между чтением и заменой ресурса произошли какие-либо изменения.
Kubernetes API
Вы, вероятно, знакомы с методами CoreV1().Pods().Update()
, replaceNamespacedService
или patch_namespaced_deployment
, если работаете с кластерами через некоторую клиентскую библиотеку для API Kubernetes с использованием некоторого языка программирования. Библиотека обрабатывает эти методы с помощью запросов по протоколу HTTP, используя методы PUT
и PATCH
. При этом update
и replace
используют PUT
, а patch
, как бы это не было банально, использует PATCH
.
Стоит отметить, что kubectl
также работает с кластерами через API. Иначе говоря, kubectl
— это обертка поверх клиентской библиотеки для языка Go, обеспечивающая в значительной мере возможность предоставить подкоманды в более компактном и читабельном виде в довесок к штатным возможностям API. К примеру, как вы уже могли заметить, метод apply
не был упомянут выше в предыдущем абзаце. В настоящее время (май 2020, прим. переводчика) вся логика kubectl apply
, т.е. создание несуществующих ресурсов и обновление существующих, работает полностью на стороне кода kubectl
. Предпринимаются усилия по переносу логики apply
на сторону API, но это все еще на стадии бета-тестирования. Подробнее распишу ниже.
Patch по умолчанию
Лучше всего применять patch
, если вы желаете обновить ресурс. Так работают как клиентские библиотеки поверх API Kubernetes, так и kubectl
(неудивительно, ведь он — обертка клиентской библиотеки, прим. переводчика).
Работать стратегически
Все команды kubectl
apply
, edit
и patch
используют метод PATCH
в запросах HTTP для обновления существующего ресурса. Если вникнуть детальнее в реализацию команд, то во всех используется подход strategic-merge patching для обновлении ресурсов, хотя команда patch
может использовать и другие подходы (подробнее об этом ниже). Подход strategic-merge patching пытается «сделать все правильно» при объединении предоставленной спецификации с существующей спецификацией. Более конкретно, он пытается объединить как объекты, так и массивы, что значит, что изменения, как правило, являются аддитивными. Например, запуск команды patch
с новой переменной среды в спецификации контейнера pod, приводит к тому, что эта переменная среды добавляется к существующим переменным среды, а не перезаписывает их. Для удаления с помощью этого подхода следует принудительно установить значение параметра в null в предоставленной спецификации. Какие же из команд kubectl
для обновления лучше всего использовать?
Если вы создаете и управляете своими ресурсами с помощью kubectl apply
, при обновлении лучше всегда использовать kubectl apply
, чтобы kubectl
мог управлять конфигурацией и правильно отслеживать запрошенные изменения от применения к применению. Преимущество всегда использовать apply
заключается в том, что он отслеживает ранее примененную спецификацию, позволяя ему знать, когда свойства спецификации и элементы массива явно удаляются. Это позволяет использовать apply
для удаления свойств и элементов массива, в то время как обычное стратегическое слияние работать не будет. Команды edit
и patch
не обновляют примечания, которые kubectl apply
применяет для отслеживания своих изменений, поэтому любые изменения, которые отслеживаются и делаются через API Kubernetes, но внесенные через команды edit
и patch
, невидимы для последующих команд apply
, то есть apply
не удаляет их, даже если они не появляются во входной спецификации для apply
(В документации сказано, что edit
и patch
делают обновления примечаний, используемых apply
, но на практике — нет).
Если вы не используете команду apply
, можно использовать как edit
, так и patch
, выбирая ту команду, которая больше подходит под вносимое изменение. При добавлении и изменении свойств спецификации оба подхода примерно одинаковы. При удалении свойств спецификации или элементов массива edit
ведет себя как одноразовый запуск apply
, в том числе отслеживает, какой была спецификация до и после ее редактирования, поэтому можно явно удалять свойства и элементы массива из ресурса. Нужно явно установить значение свойства в null в спецификации для patch
, чтобы удалить его из ресурса. Удаление элемента массива с использованием strategic-merge patching является более сложным, поскольку требуется использование директив слияния. Смотрите другие подходы к обновлению ниже для выбора более приемлемых альтернатив.
Чтобы реализовать в клиентской библиотеке методы обновления, ведущие себя аналогично вышеприведенным командам kubectl
, следует в запросах выставлять content-type
в application/strategic-merge-patch+json
. Если вы хотите удалить свойства в спецификации, вам нужно явно установить их значения в null аналогично kubectl patch
. Если нужно удалять элементы массива, вам следует включить директивы слияния в спецификацию обновления или использовать другой подход к обновлениям.
Другие подходы к обновлениям
В Kubernetes поддерживаются два других подхода к обновлениям: JSON merge patch и JSON patch. Подход JSON merge patch принимает частичную спецификацию Kubernetes в качестве входных данных и поддерживает слияние объектов подобно подходу strategic-merge patching. Различие между ними заключается в том, что он поддерживает только замену массивов, включая массив контейнеров в спецификации pod. Это означает, что при использовании JSON merge patch вам необходимо предоставить полные спецификации для всех контейнеров в случае изменения какого-либо свойства любого контейнера. Таким образом, этот подход полезен для удаления элементов из массива в спецификации. В командной строке вы можете выбрать JSON merge patch, используя kubectl patch --type=merge
. При работе с API Kubernetes следует использовать метод запроса PATCH
и установке content-type
в application/merge-patch+json
.
Подход JSON patch вместо того, чтобы предоставлять частичную спецификацию ресурса, использует предоставление изменений, которые вы хотите внести в ресурс, в виде массива, в котором каждый элемент массива представляет собой описание изменения, вносимого в ресурс. Этот подход является более гибким и мощным способом выражения вносимых изменений, но за счет того, что список вносимых изменений идет в отдельном, не Kubernetes, формате, вместо отправки частичной спецификации ресурса. В kubectl
вы можете выбрать JSON patch, используя kubectl patch --type=json
. При использовании API Kubernetes этот подход работает с использованием метода запроса PATCH
и установке content-type
в application/json-patch+json
.
Нужна уверенность — используем replace
В некоторых случаях нужна уверенность в том, что в ресурс не будут внесены изменения между временем чтения ресурса и его обновлением. Иначе говоря, стоит убедиться в том, что все изменения будут атомарными. В этом случае для обновления ресурсов стоит использовать replace
. К примеру если есть ConfigMap со счетчиком, обновляемым несколькими источниками, следует быть уверенным в том, что два источника не будут обновлять счетчик одновременно, что приведет к потере обновления. Для демонстрации представьте себе последовательность событий используя подход patch
:
- A и B получают текущее состояние ресурса из API
- Каждый из них локально обновляет спецификацию, увеличивая счетчик на единицу, а также добавляя «A» или «B» соответственно в примечание «updated-by»
- А чуть быстрее обновляет ресурс
- B обновляет ресурс
В результате обновление A потеряно. Последняя операция patch
выигрывает, счетчик увеличивается на единицу вместо двух, а значение примечания «updated-by» заканчивается на «B» и не содержит «A». Давайте сравним вышесказанное с тем, что происходит, когда обновления выполняются с использованием подхода replace
:
- A и B получают текущее состояние ресурса из API
- Каждый из них локально обновляет спецификацию, увеличивая счетчик на единицу, а также добавляя «A» или «B» соответственно в примечание «updated-by»
- А чуть быстрее обновляет ресурс
- B пытается обновить ресурс, но обновление отклоняется API, потому что версия ресурса в спецификации
replace
не совпадает с текущей версией ресурса в Kubernetes, поскольку версия ресурса была увеличена при выполнении операции replace со стороны A.
В приведенном выше случае B придется заново извлечь ресурс, внести изменения в новое состояние и попытаться снова сделать replace
. В результате счетчик будет увеличен на два, а примечание «updated-by» будет содержать «AB» в конце.
Вышеприведенный пример подразумевает, что при выполнении replace
выполняется полная замена всего ресурса. Спецификация, используемая для replace
, должна быть не частичной, или частями как в apply
, а полной, включая добавление resourceVersion
в метаданные спецификации. Если вы не включили resourceVersion
или предоставленная вами версия не является текущей, замена будет отклонена. Таким образом, лучший подход для использования replace
— прочитать ресурс, обновить его и немедленно заменить. Используя kubectl
, это может выглядеть так:
$ kubectl get deployment my-deployment -o json \
| jq '.spec.template.spec.containers[0].env[1].value = "new value"' \
| kubectl replace -f -
При этом стоит заметить, что следующие две команды, выполненные последовательно, выполнятся успешно, поскольку deployment.yaml
не содержит свойство .metadata.resourceVersion
$ kubectl create -f deployment.yaml
$ kubectl replace -f deployment.yaml
Казалось бы, это противоречит тому, о чем говорилось выше, т.е. «добавление resourceVersion
в метаданные спецификации». Утверждать так неверно? Нет, это не так, поскольку если kubectl
замечает, что вы не указали resourceVersion
, он прочитает ее из ресурса и добавит в указанную вами спецификацию и только потом выполнит replace
. Поскольку эта потенциально опасная, если полагаться на атомарность, магия работает полностью на стороне kubectl
, не стоит полагаться на нее при использовании клиентских библиотек, работающих с API. В этом случае вам придется прочитать текущую спецификацию ресурса, обновить ее и затем выполнить PUT
запрос.
Нельзя сделать patch — делаем replace
Иногда нужно внести некоторые изменения, которые не могут быть обработаны API. В этих случаях можно принудительно заменить ресурс, удаляя и заново создавая его. Это делается с помощью kubectl replace --force
. Запуск команды немедленно удаляет ресурсы, а затем воссоздает их с предоставленной спецификации. В API нету обработчика «принудительно заменить», а для того, чтобы сделать так через API, нужно выполнить две операции. Для начала надо удалить ресурс, устанавливая для него gracePeriodSeconds
в ноль (0) и propagationPolicy
в «Background», а затем заново создать этот ресурс с желаемой спецификацией.
Внимание: данный подход потенциально опасен, может привести к неопределенному состоянию
Apply на стороне сервера
Как упоминалось выше, разработчики Kubernetes работают над реализацией логики apply
из kubectl
в API Kubernetes. Логика apply
доступна в Kubernetes 1.18 через kubectl apply --server-side
или через API, используя метод PATCH
с content-type
application/apply-patch+YAML
.
Примечание: JSON также является корректным YAML, так что можно послать спецификацию в виде JSON, даже еслиcontent-type
будетapplication/apply-patch+yaml
.
Кроме того, что логика kubectl
становится доступной для всех через API, apply
на стороне сервера отслеживает ответственных за поля в спецификации, таким образом разрешая безопасный множественный доступ для ее бесконфликтного редактирования. Иначе говоря, если apply
на стороне сервера получит более широкое распространение, появится универсальный безопасный интерфейс управления ресурсами для разных клиентов, к примеру, kubectl, Pulumi или Terraform, GitOps, а также самописных скриптов, использующих клиентские библиотеки.
Итоги
Надеюсь, что этот короткий обзор разных способов обновления ресурсов в кластерах был полезен для вас. Полезно знать, что противники не просто apply против replace, ведь можно обновить ресурс с помощью apply, edit, patch или replace. Ведь в принципе каждых подход имеет свою область применения. Для атомарных изменений предпочтительнее replace, в противном случае стоит использовать strategic-merge patch через apply. В крайнем случае я рассчитываю на то, что вы поняли, что нельзя доверять Google или StackOerflow при поиске «kubernetes apply vs replace». По крайней мере до тех пор, пока эта статья не заменит текущий ответ.