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'а можна зрабіць свой уласны.

Go? Bash! Сустракайце shell-operator (агляд і відэа даклада з KubeCon EU'2020)

Прадстаўляем відэа з дакладам (~23 хвіліны на ангельскай, прыкметна інфарматыўна артыкула) і асноўны выцісканне з яго ў тэкставым выглядзе. Паехалі!

Мы ў «Фланце» увесь час усё аптымізуем і аўтаматызуем. Сёння размова пойдзе аб яшчэ адной займальнай канцэпцыі. Сустракайце: cloud-native shell-скрыптынг!

Зрэшты, давайце пачнем з кантэксту, у якім усё гэта адбываецца, - з Kubernetes.

Kubernetes API і кантролеры

API у Kubernetes можна прадставіць у выглядзе нейкага файлавага сервера з дырэкторыямі пад кожны тып аб'ектаў. Аб'екты (рэсурсы) на гэтым серверы прадстаўлены YAML-файламі. Акрамя таго, у сервера маецца базавы API, які дазваляе рабіць тры рэчы:

  • атрымліваць рэсурс па яго kind'у і імені;
  • мяняць рэсурс (пры гэтым сервер захоўвае толькі "правільныя" аб'екты - усе некарэктна сфармаваныя або прызначаныя для іншых дырэкторый адкідаюцца);
  • сачыць за рэсурсам (у гэтым выпадку карыстач адразу атрымлівае яго бягучую/абноўленую версію).

Такім чынам, Kubernetes выступае гэтакім файлавым серверам (для YAML-маніфестаў) з трыма базавымі метадамі (так, наогул-то ёсць і іншыя, але мы іх пакуль апусцім).

Go? Bash! Сустракайце shell-operator (агляд і відэа даклада з KubeCon EU'2020)

Праблема ў тым, што сервер умее толькі захоўваць інфармацыю. Каб прымусіць яе працаваць, неабходны кантролер - другое па важнасці і фундаментальнасці паняцце ў свеце 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'ом, і карыстач, нарэшце, атрымлівае абноўлены (бягучы) статут.

Go? Bash! Сустракайце shell-operator (агляд і відэа даклада з KubeCon EU'2020)

Shell-operator

Атрымліваецца, што ў аснове Kubernetes ляжыць сумесная праца розных кантролераў (аператары Kubernetes таксама кантролеры). Узнікае пытанне, як стварыць свой аператар з мінімальнымі намаганнямі? І тут на дапамогу прыходзіць распрацаваны намі shell-operator. Ён дазваляе сістэмным адміністратарам ствараць уласныя аператары, выкарыстоўваючы звыклыя метады.

Go? Bash! Сустракайце shell-operator (агляд і відэа даклада з KubeCon EU'2020)

Просты прыклад: капіраванне сакрэтаў

Давайце разгледзім просты прыклад.

Выкажам здагадку, у нас ёсць кластар 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 і т.д. Такія выкананыя файлы мы завем хуками (гаплікі).

Go? Bash! Сустракайце shell-operator (агляд і відэа даклада з KubeCon EU'2020)

Shell-operator падпісваецца на падзеі Kubernetes і запускае гэтыя хукі ў адказ на тыя з падзеяў, што нам патрэбныя.

Go? Bash! Сустракайце shell-operator (агляд і відэа даклада з KubeCon EU'2020)

Якім чынам 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:

Go? Bash! Сустракайце shell-operator (агляд і відэа даклада з KubeCon EU'2020)

function __config__() {
  cat << EOF
    configVersion: v1
    kubernetes:
    - name: src_secret
      apiVersion: v1
      kind: Secret
      nameSelector:
        matchNames:
        - mysecret
      namespace:
        nameSelector:
          matchNames: ["default"]
      group: main
EOF

У выніку хук будзе запускацца пры змене сакрэту-крыніцы (src_secret) і атрымліваць наступны binding context:

Go? Bash! Сустракайце shell-operator (агляд і відэа даклада з KubeCon EU'2020)

Як бачыце, у ім змяшчаецца імя і аб'ект цалкам.

Сочым за прасторамі імёнаў

Цяпер трэба падпісацца на namespaces. Для гэтага пакажам наступную binding configuration:

- name: namespaces
  group: main
  apiVersion: v1
  kind: Namespace
  jqFilter: |
    {
      namespace: .metadata.name,
      hasLabel: (
       .metadata.labels // {} |  
         contains({"secret": "yes"})
      )
    }
  group: main
  keepFullObjectsInMemory: false

Як бачыце, у канфігурацыі з'явілася новае поле з імем jqFilter. Як намякае яго назву, jqFilter адфільтроўвае ўсю лішнюю інфармацыю і стварае новы аб'ект JSON з палямі, якія ўяўляюць для нас цікавасць. Хук з падобнай канфігурацыяй атрымае наступны binding context:

Go? Bash! Сустракайце shell-operator (агляд і відэа даклада з KubeCon EU'2020)

Ён змяшчае ў сабе масіў filterResults для кожнай прасторы імёнаў у кластары. Булева пераменная hasLabel паказвае, ці прымацаваны лэйбл да дадзенай прасторы імёнаў. Селектар keepFullObjectsInMemory: false кажа аб тым, што няма неабходнасці трымаць поўныя аб'екты ў памяці.

Адсочваем сакрэты-мэты

Мы падпісваемся на ўсе Secret'ы, у якіх зададзена анатацыя managed-secret: "yes" (гэта нашы мэтавыя dst_secrets):

- name: dst_secrets
  apiVersion: v1
  kind: Secret
  labelSelector:
    matchLabels:
      managed-secret: "yes"
  jqFilter: |
    {
      "namespace":
        .metadata.namespace,
      "resourceVersion":
        .metadata.annotations.resourceVersion
    }
  group: main
  keepFullObjectsInMemory: false

У гэтым выпадку jqFilter адфільтроўвае ўсю інфармацыю за выключэннем прасторы імёнаў і параметру resourceVersion. Апошні параметр быў перададзены анатацыі пры стварэнні сакрэту: ён дазваляе параўноўваць версіі сакрэтаў і падтрымліваць іх у актуальным стане.

Хук, наладжаны падобнай выявай, пры выкананні атрымае тры кантэксту прывязкі, апісаныя вышэй. Іх можна ўявіць як свайго роду здымак (здымак) кластара.

Go? Bash! Сустракайце shell-operator (агляд і відэа даклада з KubeCon EU'2020)

На аснове ўсёй гэтай інфармацыі можна распрацаваць базавы алгарытм. Ён перабірае ўсе прасторы імёнаў і:

  • калі hasLabel мае значэнне true для бягучай прасторы імёнаў:
    • параўноўвае глабальны сакрэт з лакальным:
      • калі яны аднолькавыя - нічога не робіць;
      • калі яны адрозніваюцца - выконвае kubectl replace або create;
  • калі hasLabel мае значэнне false для бягучай прасторы імёнаў:
    • пераконваецца, што Secret адсутнічае ў дадзенай прасторы імёнаў:
      • калі лакальны Secret прысутнічае - выдаляе яго з дапамогай kubectl delete;
      • калі лакальны Secret не выяўлены - нічога не робіць.

Go? Bash! Сустракайце shell-operator (агляд і відэа даклада з KubeCon EU'2020)

Рэалізацыю алгарытму на Bash вы можаце спампаваць у нашым рэпазітары з прыкладамі.

Вось так мы змаглі стварыць просты кантролер 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):

Go? Bash! Сустракайце shell-operator (агляд і відэа даклада з KubeCon EU'2020)

Як зрабіць так, каб яны перайшлі на новы ConfigMap (v.2)? Адказ просты: скарыстацца template'ам. Давайце дадамо анатацыю з кантрольнай сумай у раздзел template канфігурацыі Deployment'а:

Go? Bash! Сустракайце shell-operator (агляд і відэа даклада з KubeCon EU'2020)

У выніку ва ўсіх pod'ах будзе прапісана гэтая кантрольная сума, і яна будзе такой жа, як у Deployment'a. Цяпер трэба проста абнаўляць анатацыю пры змене ConfigMap. І shell-operator дарэчы ў гэтым выпадку. Усё, што трэба - гэта запраграмаваць хук, які падпішацца на ConfigMap і абновіць кантрольную суму.

Калі карыстач унясе змены ў ConfigMap, shell-operator іх заўважыць і пералічыць кантрольную суму. Пасля чаго ў гульню ўступіць магія Kubernetes: аркестратар заб'е pod, створыць новы, дачакаецца, калі той стане. Ready, і пяройдзе да наступнага. У выніку Deployment сінхранізуецца і пяройдзе на новую версію ConfigMap.

Go? Bash! Сустракайце shell-operator (агляд і відэа даклада з KubeCon EU'2020)

Прыклад 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-серверу і сінхранізацыі жаданага і назіранага станаў кластара.

Go? Bash! Сустракайце shell-operator (агляд і відэа даклада з KubeCon EU'2020)

Прыклад 3: маніторынг кластарнай сеткі

Як вядома, выкарыстанне ping'а з'яўляецца найпростым спосабам маніторынгу сеткі. У гэтым прыкладзе мы пакажам, як рэалізаваць такі маніторынг з дапамогай shell-operator.

Перш за ўсё, трэба падпісацца на вузлы. Shell-operator'у патрэбныя імя і IP-адрас кожнага вузла. Зь іх дапамогай ён будзе пінгаваць гэтыя вузлы.

configVersion: v1
kubernetes:
- name: nodes
  apiVersion: v1
  kind: Node
  jqFilter: |
    {
      name: .metadata.name,
      ip: (
       .status.addresses[] |  
        select(.type == "InternalIP") |
        .address
      )
    }
  group: main
  keepFullObjectsInMemory: false
  executeHookOnEvent: []
schedule:
- name: every_minute
  group: main
  crontab: "* * * * *"

Параметр 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 у канфігурацыі прывязкі.

Go? Bash! Сустракайце shell-operator (агляд і відэа даклада з KubeCon EU'2020)

Можна ствараць любую колькасць чэргаў/хукаў і іх разнастайных камбінацый. Напрыклад, адна чарга можа працаваць з двума хукамі, ці наадварот.

Go? Bash! Сустракайце shell-operator (агляд і відэа даклада з KubeCon EU'2020)

Усё, што трэба зрабіць, - адпаведным чынам наладзіць поле queue у канфігурацыі прывязкі. Калі не пазначана імя чаргі, хук запускаецца ў чарзе па змаўчанні (default). Падобны механізм чэргаў дазваляе поўнасцю вырашыць усе праблемы кіравання рэсурсамі пры рабоце з хукамі.

Заключэнне

Мы расказалі, што такое shell-operator, паказалі, як з яго дапамогай можна хутка і без асаблівых намаганняў ствараць аператары Kubernetes, і прывялі некалькі прыкладаў яго выкарыстання.

Падрабязная інфармацыя аб shell-operator, а таксама кароткае кіраўніцтва па ім выкарыстанню даступныя ў адпаведным рэпазітары на GitHub. Не саромейцеся звяртацца да нас з пытаннямі: абмеркаваць іх можна ў спецыяльнай Telegram-групе (па-руску) або ў гэтым форуме (на англійскай).

А калі спадабалася - мы заўсёды рады новым issues/PR/зоркам на GitHub, дзе, дарэчы, можна знайсці і іншыя. цікавыя праекты. Сярод іх варта асабліва вылучыць addon-operator, які даводзіцца старэйшым братам для shell-operator. Гэта ўтыліта выкарыстоўвае чарты Helm для ўсталёўкі дадаткаў, умее дастаўляць абнаўленні і сачыць за рознымі параметрамі/значэннямі чартаў, кантралюе працэс усталёўкі чартаў, а таксама можа мадыфікаваць іх у адказ на падзеі ў кластары.

Go? Bash! Сустракайце shell-operator (агляд і відэа даклада з KubeCon EU'2020)

Відэа і слайды

Відэа з выступу (~23 хвіліны):


Прэзентацыя даклада:

PS

Чытайце таксама ў нашым блогу:

Крыніца: habr.com

Дадаць каментар