ProHoster > блог > адміністраванне > Go? Bash! Сустракайце shell-operator (агляд і відэа даклада з KubeCon EU'2020)
Go? Bash! Сустракайце shell-operator (агляд і відэа даклада з KubeCon EU'2020)
У гэтым годзе галоўная еўрапейская канферэнцыя па Kubernetes – KubeCon + CloudNativeCon Europe 2020 – была віртуальнай. Зрэшты, такая змена фармату не перашкодзіла нам выступіць з даўно запланаваным дакладам “Go? Bash! Meet the Shell-operator», прысвечаным нашаму Open Source-праекту shell-operator.
У гэтым артыкуле, напісаным па матывах выступу, прадстаўлены падыход да спрашчэння працэсу стварэння аператараў для Kubernetes і паказана, як з мінімальнымі намаганнямі пры дапамозе shell-operator'а можна зрабіць свой уласны.
Прадстаўляем відэа з дакладам (~23 хвіліны на ангельскай, прыкметна інфарматыўна артыкула) і асноўны выцісканне з яго ў тэкставым выглядзе. Паехалі!
Мы ў «Фланце» увесь час усё аптымізуем і аўтаматызуем. Сёння размова пойдзе аб яшчэ адной займальнай канцэпцыі. Сустракайце: cloud-native shell-скрыптынг!
Зрэшты, давайце пачнем з кантэксту, у якім усё гэта адбываецца, - з Kubernetes.
Kubernetes API і кантролеры
API у Kubernetes можна прадставіць у выглядзе нейкага файлавага сервера з дырэкторыямі пад кожны тып аб'ектаў. Аб'екты (рэсурсы) на гэтым серверы прадстаўлены YAML-файламі. Акрамя таго, у сервера маецца базавы API, які дазваляе рабіць тры рэчы:
атрымліваць рэсурс па яго kind'у і імені;
мяняць рэсурс (пры гэтым сервер захоўвае толькі "правільныя" аб'екты - усе некарэктна сфармаваныя або прызначаныя для іншых дырэкторый адкідаюцца);
сачыць за рэсурсам (у гэтым выпадку карыстач адразу атрымлівае яго бягучую/абноўленую версію).
Такім чынам, Kubernetes выступае гэтакім файлавым серверам (для YAML-маніфестаў) з трыма базавымі метадамі (так, наогул-то ёсць і іншыя, але мы іх пакуль апусцім).
Праблема ў тым, што сервер умее толькі захоўваць інфармацыю. Каб прымусіць яе працаваць, неабходны кантролер - другое па важнасці і фундаментальнасці паняцце ў свеце Kubernetes.
Адрозніваюць два асноўных тыпу кантролераў. Першы бярэ інфармацыю з Kubernetes, апрацоўвае яе ў адпаведнасці са ўкладзенай логікай і вяртае ў K8s. Другі – бярэ інфармацыю з Kubernetes, але, у адрозненне ад першага тыпу, мяняе стан нейкіх знешніх рэсурсаў.
Давайце разгледзім падрабязней працэс стварэння Deployment'а ў Kubernetes:
Deployment Controller (уваходны ў kube-controller-manager) атрымлівае інфармацыю аб Deployment'е і стварае ReplicaSet.
ReplicaSet на аснове гэтай інфармацыі стварае дзве рэплікі (два pod'а), але гэтыя pod'ы яшчэ не запланаваны.
Планавальнік плануе pod'ы і дадае ў іх YAML'ы інфармацыю аб вузлах.
Kubelet'ы ўносяць змены ў вонкавы рэсурс (скажам, Docker).
Затым уся гэтая паслядоўнасць паўтараецца ў зваротным парадку: kubelet правярае кантэйнеры, вылічае статут pod'а і адсылае яго зваротна. Кантролер ReplicaSet атрымлівае статут і абнаўляе стан набору рэплік. Тое ж самае адбываецца з Deployment Controller'ом, і карыстач, нарэшце, атрымлівае абноўлены (бягучы) статут.
Shell-operator
Атрымліваецца, што ў аснове Kubernetes ляжыць сумесная праца розных кантролераў (аператары Kubernetes таксама кантролеры). Узнікае пытанне, як стварыць свой аператар з мінімальнымі намаганнямі? І тут на дапамогу прыходзіць распрацаваны намі shell-operator. Ён дазваляе сістэмным адміністратарам ствараць уласныя аператары, выкарыстоўваючы звыклыя метады.
Просты прыклад: капіраванне сакрэтаў
Давайце разгледзім просты прыклад.
Выкажам здагадку, у нас ёсць кластар Kubernetes. У ім ёсць прастора імёнаў default з некаторым Secret'ом mysecret. Акрамя гэтага, у кластары ёсць і іншыя прасторы імёнаў. Да некаторых з іх прымацаваны пэўны лэйбл. Наша мэта - скапіяваць Secret ў прасторы імёнаў з лэйблам.
Задача ўскладняецца тым, што ў кластары могуць з'яўляцца новыя прасторы імёнаў, і ў некаторых з іх можа быць дадзены лэйбл. З іншага боку, пры выдаленні лэйбла Secret таксама павінен выдаляцца. У дадатак да ўсяго, сам Secret таксама можа мяняцца: у гэтым выпадку новы Secret павінен быць скапіяваны ва ўсе прасторы імёнаў з лэйбламі. Калі Secret выпадкова выдаляецца ў які-небудзь прасторы імёнаў, наш аператар павінен яго адразу аднавіць.
Зараз, калі задача сфармуляваная, сітавіна прыступіць да яе рэалізацыі з дапамогай shell-operator. Але спачатку варта сказаць некалькі слоў аб самім shell-operator's.
Прынцыпы працы shell-operator
Як і іншыя працоўныя нагрузкі ў Kubernetes, shell-operator функцыянуе ў сваім pod'е. У гэтым pod'е ў каталогу /hooks захоўваюцца выкананыя файлы. Гэта могуць быць скрыпты на Bash, Python, Ruby і т.д. Такія выкананыя файлы мы завем хуками (гаплікі).
Shell-operator падпісваецца на падзеі Kubernetes і запускае гэтыя хукі ў адказ на тыя з падзеяў, што нам патрэбныя.
Якім чынам shell-operator даведаецца, які хук і калі запускаць? Справа ў тым, што ў кожнага хука ёсць дзве стадыі. Падчас старту shell-operator запускае ўсе хукі з аргументам --config - Гэта стадыя канфігуравання. А ўжо пасля яе хукі запускаюцца нармальным чынам - у адказ на падзеі, да якіх яны прывязаныя. У апошнім выпадку хук атрымлівае кантэкст прывязкі.binding context) – дадзеныя ў фармаце JSON, падрабязней пра якія мы пагаворым ніжэй.
Які робіцца аператар на Bash
Цяпер мы гатовы да рэалізацыі. Для гэтага нам спатрэбіцца напісаць дзве функцыі (дарэчы, рэкамендуемы бібліятэку shell_lib, якая моцна спрашчае напісанне хукаў на Bash):
першая патрэбна для стадыі канфігуравання - яна выводзіць кантэкст прывязкі;
другая змяшчае асноўную логіку хука.
#!/bin/bash
source /shell_lib.sh
function __config__() {
cat << EOF
configVersion: v1
# BINDING CONFIGURATION
EOF
}
function __main__() {
# THE LOGIC
}
hook::run "$@"
Наступны крок - вызначыцца з тым, якія аб'екты нам патрэбныя. У нашым выпадку трэба адсочваць:
сакрэт-крыніца на прадмет наяўнасці змен;
усе namespace'ы ў кластары, каб ведаць, да якіх з іх прымацаваны лэйбл;
сакрэты-мэты, каб пераканацца, што ўсе яны сінхранізаваныя з сакрэтам-крыніцай.
Падпісваемся на сакрэт-крыніцу
Binding configuration для яго дастаткова простая. Мы паказваем, што нас цікавіць Secret з назовам mysecret у прасторы імёнаў default:
Як бачыце, у канфігурацыі з'явілася новае поле з імем jqFilter. Як намякае яго назву, jqFilter адфільтроўвае ўсю лішнюю інфармацыю і стварае новы аб'ект JSON з палямі, якія ўяўляюць для нас цікавасць. Хук з падобнай канфігурацыяй атрымае наступны binding context:
Ён змяшчае ў сабе масіў filterResults для кожнай прасторы імёнаў у кластары. Булева пераменная hasLabel паказвае, ці прымацаваны лэйбл да дадзенай прасторы імёнаў. Селектар keepFullObjectsInMemory: false кажа аб тым, што няма неабходнасці трымаць поўныя аб'екты ў памяці.
Адсочваем сакрэты-мэты
Мы падпісваемся на ўсе Secret'ы, у якіх зададзена анатацыя managed-secret: "yes" (гэта нашы мэтавыя dst_secrets):
У гэтым выпадку jqFilter адфільтроўвае ўсю інфармацыю за выключэннем прасторы імёнаў і параметру resourceVersion. Апошні параметр быў перададзены анатацыі пры стварэнні сакрэту: ён дазваляе параўноўваць версіі сакрэтаў і падтрымліваць іх у актуальным стане.
Хук, наладжаны падобнай выявай, пры выкананні атрымае тры кантэксту прывязкі, апісаныя вышэй. Іх можна ўявіць як свайго роду здымак (здымак) кластара.
На аснове ўсёй гэтай інфармацыі можна распрацаваць базавы алгарытм. Ён перабірае ўсе прасторы імёнаў і:
калі hasLabel мае значэнне true для бягучай прасторы імёнаў:
параўноўвае глабальны сакрэт з лакальным:
калі яны аднолькавыя - нічога не робіць;
калі яны адрозніваюцца - выконвае kubectl replace або create;
калі hasLabel мае значэнне false для бягучай прасторы імёнаў:
пераконваецца, што Secret адсутнічае ў дадзенай прасторы імёнаў:
калі лакальны Secret прысутнічае - выдаляе яго з дапамогай kubectl delete;
калі лакальны Secret не выяўлены - нічога не робіць.
Вось так мы змаглі стварыць просты кантролер Kubernetes, выкарыстаўшы 35 радкоў YAML-канфігаў і прыкладна такая ж колькасць кода на Bash! Задача shell-operator у тым, каб звязаць іх разам.
Зрэшты, капіраванне сакрэтаў - гэта не адзіная вобласць прымянення ўтыліты. Вось яшчэ некалькі прыкладаў, якія пакажуць, на што ён здольны.
Прыклад 1: занясенне змен у ConfigMap
Давайце разгледзім Deployment, які складаецца з трох pod'аў. Pod'ы выкарыстоўваюць ConfigMap для захоўвання некаторай канфігурацыі. Падчас запуску pod'ов ConfigMap знаходзіўся ў некаторым стане (назавём яго v.1). Адпаведна, усе pod'ы выкарыстоўваюць менавіта гэтую версію ConfigMap.
Цяпер выкажам здагадку, што ConfigMap змяніўся (v.2). Аднак pod'ы будуць выкарыстоўваць ранейшую версію ConfigMap (v.1):
Як зрабіць так, каб яны перайшлі на новы ConfigMap (v.2)? Адказ просты: скарыстацца template'ам. Давайце дадамо анатацыю з кантрольнай сумай у раздзел template канфігурацыі Deployment'а:
У выніку ва ўсіх pod'ах будзе прапісана гэтая кантрольная сума, і яна будзе такой жа, як у Deployment'a. Цяпер трэба проста абнаўляць анатацыю пры змене ConfigMap. І shell-operator дарэчы ў гэтым выпадку. Усё, што трэба - гэта запраграмаваць хук, які падпішацца на ConfigMap і абновіць кантрольную суму.
Калі карыстач унясе змены ў ConfigMap, shell-operator іх заўважыць і пералічыць кантрольную суму. Пасля чаго ў гульню ўступіць магія Kubernetes: аркестратар заб'е pod, створыць новы, дачакаецца, калі той стане. Ready, і пяройдзе да наступнага. У выніку Deployment сінхранізуецца і пяройдзе на новую версію ConfigMap.
Прыклад 2: праца з Custom Resource Definitions
Як вядома, Kubernetes дазваляе ствараць кастамныя тыпы (kinds) аб'ектаў. Напрыклад, можна стварыць kind MysqlDatabase. Дапушчальны, у гэтага тыпу маюцца два metadata-параметра: name и namespace.
apiVersion: example.com/v1alpha1
kind: MysqlDatabase
metadata:
name: foo
namespace: bar
У нас ёсць кластар Kubernetes з рознымі прасторамі імёнаў, у якіх мы можам ствараць базы дадзеных MySQL. У гэтым выпадку shell-operator можна выкарыстоўваць для адсочвання рэсурсаў MysqlDatabase, іх падлучэння да MySQL-серверу і сінхранізацыі жаданага і назіранага станаў кластара.
Прыклад 3: маніторынг кластарнай сеткі
Як вядома, выкарыстанне ping'а з'яўляецца найпростым спосабам маніторынгу сеткі. У гэтым прыкладзе мы пакажам, як рэалізаваць такі маніторынг з дапамогай shell-operator.
Перш за ўсё, трэба падпісацца на вузлы. Shell-operator'у патрэбныя імя і IP-адрас кожнага вузла. Зь іх дапамогай ён будзе пінгаваць гэтыя вузлы.
Параметр executeHookOnEvent: [] прадухіляе запуск хука ў адказ на любую падзею (гэта значыць у адказ на змену, даданне, выдаленне вузлоў). Аднак ён будзе запускацца (і абнаўляць спіс вузлоў) па раскладзе - кожную хвіліну, як загадвае поле schedule.
Цяпер узнікае пытанне, як менавіта мы даведаемся аб праблемах накшталт страты пакетаў? Давайце зірнем на код:
function __main__() {
for i in $(seq 0 "$(context::jq -r '(.snapshots.nodes | length) - 1')"); do
node_name="$(context::jq -r '.snapshots.nodes['"$i"'].filterResult.name')"
node_ip="$(context::jq -r '.snapshots.nodes['"$i"'].filterResult.ip')"
packets_lost=0
if ! ping -c 1 "$node_ip" -t 1 ; then
packets_lost=1
fi
cat >> "$METRICS_PATH" <<END
{
"name": "node_packets_lost",
"add": $packets_lost,
"labels": {
"node": "$node_name"
}
}
END
done
}
Мы перабіраем спіс вузлоў, атрымліваем іх імёны і IP-адрасы, пінгуем і адпраўляем вынікі ў Prometheus. Shell-operator умее экспартаваць метрыкі ў Prometheus, захоўваючы іх у файл, размешчаны паводле шляху, паказанаму ў зменнай асяроддзі $METRICS_PATH.
Вось так можна зрабіць аператара для простага маніторынгу сеткі ў кластары.
Механізм чэргаў
Гэты артыкул быў бы няпоўным без апісання яшчэ аднаго важнага механізму, убудаванага ў shell-operator. Уявіце, што ён выконвае нейкі хук у адказ на падзею ў кластары.
Што адбудзецца, калі ў гэты ж час у кластары здарыцца яшчэ адно падзея?
Ці запусціць shell-operator яшчэ адзін асобнік Хука?
А што, калі ў кластары адразу адбудуцца, скажам, пяць падзей?
Ці будзе shell-operator апрацоўваць іх раўналежна?
А як наконт спажываных рэсурсаў, такіх як памяць і CPU?
На шчасце, у shell-operator маецца ўбудаваны механізм чэргаў. Усе падзеі змяшчаюцца ў чаргу і апрацоўваюцца паслядоўна.
Праілюструем гэта на прыкладах. Дапусцім, што ў нас ёсць два хука. Першая падзея дастаецца першаму хуку. Пасля таго, як яго апрацоўка завершана, чарга рухаецца наперад. Наступныя тры падзеі перанакіроўваюцца ў другі хук - яны здабываюцца з чаргі і паступаюць у яго "пачкам". Гэта значыць хук атрымлівае масіў падзей - ці, дакладней, масіў кантэкстаў прывязкі.
Таксама гэтыя падзеі можна аб'яднаць у адну вялікую. За гэта адказвае параметр group у канфігурацыі прывязкі.
Можна ствараць любую колькасць чэргаў/хукаў і іх разнастайных камбінацый. Напрыклад, адна чарга можа працаваць з двума хукамі, ці наадварот.
Усё, што трэба зрабіць, - адпаведным чынам наладзіць поле queue у канфігурацыі прывязкі. Калі не пазначана імя чаргі, хук запускаецца ў чарзе па змаўчанні (default). Падобны механізм чэргаў дазваляе поўнасцю вырашыць усе праблемы кіравання рэсурсамі пры рабоце з хукамі.
Заключэнне
Мы расказалі, што такое shell-operator, паказалі, як з яго дапамогай можна хутка і без асаблівых намаганняў ствараць аператары Kubernetes, і прывялі некалькі прыкладаў яго выкарыстання.
Падрабязная інфармацыя аб shell-operator, а таксама кароткае кіраўніцтва па ім выкарыстанню даступныя ў адпаведным рэпазітары на GitHub. Не саромейцеся звяртацца да нас з пытаннямі: абмеркаваць іх можна ў спецыяльнай Telegram-групе (па-руску) або ў гэтым форуме (на англійскай).
А калі спадабалася - мы заўсёды рады новым issues/PR/зоркам на GitHub, дзе, дарэчы, можна знайсці і іншыя. цікавыя праекты. Сярод іх варта асабліва вылучыць addon-operator, які даводзіцца старэйшым братам для shell-operator. Гэта ўтыліта выкарыстоўвае чарты Helm для ўсталёўкі дадаткаў, умее дастаўляць абнаўленні і сачыць за рознымі параметрамі/значэннямі чартаў, кантралюе працэс усталёўкі чартаў, а таксама можа мадыфікаваць іх у адказ на падзеі ў кластары.