Пришло время окунуться в написание операторов на Golang.
Контекст
Предположим, что у нас есть несколько k8s-кластеров, в которых хотят жить несколько продуктовых команд.
Разные команды ожидают от нод, выделенных под свои сервисы, каких-то своих специфичных настроек. Простановку специальных лейблов, установленного ПО для GPU-адаптеров и т.п.
Мы тут типа SRE, и хотим устранять toil не через личное геройство, а выдачу коллегам инструментов. Поэтому, мы не будем писать для разработчиков “гайды по заказу и приёмке нод” и просить создавать таски в Jira, а просто напишем оператора, который будет делать всё за нас, а от клиенту протранслируем необходимость заполнить спеку желаемого.
В первой части мы продумаем, как оно должно работать. А, собственно, напишем код - уже во второй.
Самые торопливые могут сразу пройти посмотреть на код 🏍
Планируем работу
Логика работы нам нужна примерно такая:
- Пользователи описывают свои ожидания в объектах
NodeAgreement
, а наш оператор на них смотрит - Если надо, оператор создаст задачу в нативной
Job
-е, чтобы привести ноду к ожидаемому состоянию
Грубо прикинем, что в объект NodeAgreement
должны входить:
- Условия для определения, соответствует ли
Node
ожиданиям - Спецификация
Job
-ы, которая ноду приведёт к нужному состоянию
Наверное, что-то подобное уже можно найти в CNCF или OperatorHub, но мы тут хотим погрузиться чуть дальше, чем простой helm install
, ага.
Начинаем писать код
Генерируем скелет
Базовый код сильно шаблонный, и его возможно инициализировать несколькими командами:
$ operator-sdk init \
--domain agrrh.com \
--owner agrrh \
--repo github.com/agrrh/k8s-node-agreements-operator
DEBU[0000] Debug logging is set
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
Get controller runtime:
$ go get sigs.k8s.io/controller-runtime@v0.15.0
Update dependencies:
$ go mod tidy
Next: define a resource with:
$ operator-sdk create api
SDK сразу предлагает сгенерить наши объекты, сделаем это.
Генерируем объекты
Тут у меня выпадала вот эта ошибка, решилось правкой в Makefile версии
CONTROLLER_TOOLS_VERSION
на актуальнуюv0.15.0
. Тут самый внимательный читатель может заметить, что статью я написал чуть раньше, чем опубликовал 😼
Создадим наш объект NodeAgreement
:
operator-sdk create api \
--controller \
--resource \
--group iaas \
--kind NodeAgreement \
--namespaced false \
--plural nodeagreements \
--version v1alpha1
По нему мы сможем смотреть, что же команда потребителей хочет от своих нод.
В целом, это вся подготовительная работа по части кодогенерации.
У вас получится полупустой репозиторий, где модели объектов будут лежать в api/v1alpha1/
и мы, очевидно, захотим добавить что-то в их спецификацию.
Место для внедрения reconcilation-логики будет в internal/controller
и мы захотим поправить методы Reconcile()
.
├── api/
├── bin/
├── cmd/
├── config/
├── hack/
├── internal/
├── Dockerfile
├── go.mod
├── go.sum
├── Makefile
├── PROJECT
└── README.md
Но делать это всё мы будем не сейчас.
Продумываем логику работы
Как мы будем выбирать ноды?
Так как у нас есть Kube API, которое отдаёт JSON, то проще всего будет прогонять его через jq-выражение и ожидать true/false, чтобы понять, подлежит нода обработке, или нет.
Пользователь сможет написать jq-выражение и тут же его проверить:
$ kubectl get node node1 -o json \
| jq -e '
.metadata.labels["kubernetes.io/arch"] == "amd64"
and
.metadata.annotations["iaas.agrrh.com/team1-node-prepared"] == "true"
'
false
Возможно, несколько многословно, с учетом вложенных словарей и ключей, но задачу решает.
Как запускать полезную нагрузку на конкретной ноде?
Мы можем использовать нативные Job
-ы, где дочерний Pod
нацелен на ноду через nodeSelector
.
Как мы поймём, что теперь с нодой всё хорошо?
Очень просто, нагрузка должна будет отредактировать ноду в случае успеха.
За это будут отвечать RBAC-сущности - Role
и ServiceAccount
, которые можно создать на уровне оператора. Пусть они позволяют работать с ресурсом node
и этого будет достаточно.
Далее по метке или аннотации мы поймём, что нода готова.
Надо ли очищать ноду от нагрузки?
Это может варьироваться. И этим можно управлять из ресурса.
Мы добавим параметр, drainRequired: bool
, и пусть он регулирует, надо ли освобождать ноду для выполнения нашей задачи.
Как ограничить blast radius?
Мы сможем задать некий параметр, например, propagationLimit: "N%"
, который позволит обрабатывать не более N% нод за один раз.
Ревьюим логику
Ещё раз посмотрим, что получилось:
Ветка с Drain
-ом и прочие “если” тут опущены для простоты. Например, мы не будем пытаться подчищать за собой Job-ы, и прогнать задачу второй раз будет невозможно, пока она не будет удалена. Ну, это всё-таки бложик.
А какие-то особенные ситуации и сообщения можно будет докинуть, скажем, в status
объекта NodeAgreement
.
Подытожим
На этом с планированием мы закончили, во второй части перейдём к написанию кода.
- Модель операторов хороша, она позволяет автоматизировать всё то, что ваши пользователи обычно пишут в Jira-задачи
- Кодогенерация - это прекрасно
- Kubernetes - это фреймворк, над которым можно надстроить всё, что угодно
Ссылки
- GitHub Repo / part 1: github.com/agrrh/k8s-node-agreements-operator