Píšeme operátora pro Kubernetes v Golangu

Pozn. přek.: Operátoři (operators) jsou pomocný software pro Kubernetes, který má automatizovat provádění rutinních úkolů nad objekty clustera při určitých událostech. O operátorech jsme již psali v tomto článku, kde jsme se zabývali základními myšlenkami a principy jejich fungování. Pokud byl ten materiál spíše pohledem na využití hotových komponentů pro Kubernetes, pak nabízený překlad nového článku je pohledem vývojáře/DevOps inženýra, který se zabývá realizací nového operátora.

Píšeme operátora pro Kubernetes v Golangu

Tento příspěvek s příkladem z reálného života jsem se rozhodl napsat po mých pokusech najít dokumentaci pro vytvoření operátora pro Kubernetes, které zahrnovaly prozkoumání kódu.

Příklad, který bude popsán, je následující: v našem clustere Kubernetes každý Namespace představuje pískoviště nějakého týmu a my jsme chtěli omezit přístup k nim tak, aby si týmy mohly hrát pouze ve svých pískovištích.

Toho lze dosáhnout přiřazením uživatelské skupině, která má RoleBinding k konkrétním Namespace a ClusterRole s právem na úpravy. YAML reprezentace bude vypadat takto:

---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: kubernetes-team-1
  namespace: team-1
subjects:
- kind: Group
  name: kubernetes-team-1
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: edit
apiGroup: rbac.authorization.k8s.io

(rolebinding.yaml, v raw)

Takový RoleBinding můžeme vytvořit i ručně, ale po překonání stovky prostorů jmen se to stává únavnou činností. Právě zde pomáhají operátoři Kubernetes — umožňují automatizovat vytváření zdrojů Kubernetes na základě změn v těchto zdrojích. V našem případě chceme vytvářet RoleBinding při vytvoření Namespace.

Nejprve definujeme funkci main, která provádí potřebnou konfiguraci pro spuštění operátora a poté vyvolává akci operátora:

(Pozn. přek.: zde a dále jsou komentáře v kódu přeloženy do češtiny. Kromě toho byly odsazení upraveny na mezery místo [doporučovaných v Go] tabulátorů výhradně pro lepší čitelnost v rámci formátování Hubru. Po každém výpisu jsou uvedeny odkazy na originál na GitHubu, kde jsou zachovány anglické komentáře a tabulátory.

func main() {
  // Nastavujeme výstup logů na konzolový STDOUT
  log.SetOutput(os.Stdout)

  sigs := make(chan os.Signal, 1) // Vytváříme kanál pro příjem signálů OS
  stop := make(chan struct{})     // Vytváříme kanál pro příjem stop signálu

  // Registrujeme příjem SIGTERM v kanálu sigs
  signal.Notify(sigs, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) 

  // Gorutiny si mohou samy přidávat sebe do WaitGroup,
  // aby se dokončení jejich vykonání čekalo
  wg := &sync.WaitGroup{} 

  runOutsideCluster := flag.Bool("run-outside-cluster", false, "Nastavte tuto volbu při spuštění mimo cluster.")
  flag.Parse()
  // Vytváříme clientset pro interakci s Kubernetes clusterem
  clientset, err := newClientSet(*runOutsideCluster)

  if err != nil {
    panic(err.Error())
  }

  controller.NewNamespaceController(clientset).Run(stop, wg)

  <-sigs // Čekáme na signály (do příjmu signálu se nic dalšího neděje)
  log.Printf("Vypínám...")

  close(stop) // Říkáme gorutinám, aby se zastavily
  wg.Wait()   // Očekáváme, až se vše zastaví
}

(main.go, v raw)

Provádíme následující:

  1. Nastavujeme zpracovatele specifických signálů operačního systému pro vyvolání správného (graceful) ukončení fungování operátora.
  2. Používáme WaitGroup, abychom správně zastavili všechny gorutiny před ukončením aplikace.
  3. Poskytujeme přístup k clusteru vytvořením clientset.
  4. Spouštíme NamespaceController, ve kterém bude umístěna veškerá naše logika.

Nyní potřebujeme základ pro logiku, a v našem případě to je zmíněný NamespaceController:

// NamespaceController следит через Kubernetes API за изменениями
// в пространствах имен и создает RoleBinding для конкретного namespace.
type NamespaceController struct {
  namespaceInformer cache.SharedIndexInformer
  kclient           *kubernetes.Clientset
}

// NewNamespaceController создает новый NewNamespaceController
func NewNamespaceController(kclient *kubernetes.Clientset) *NamespaceController {
  namespaceWatcher := &NamespaceController{}

  // Создаем информер для слежения за Namespaces
  namespaceInformer := cache.NewSharedIndexInformer(
    &cache.ListWatch{
      ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
        return kclient.Core().Namespaces().List(options)
      },
      WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
        return kclient.Core().Namespaces().Watch(options)
      },
    },
    &v1.Namespace{},
    3*time.Minute,
    cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
  )

  namespaceInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
    AddFunc: namespaceWatcher.createRoleBinding,
  })

  namespaceWatcher.kclient = kclient
  namespaceWatcher.namespaceInformer = namespaceInformer

  return namespaceWatcher
}

(controller.go, v raw)

Zde nastavujeme SharedIndexInformer, který bude efektivně (používající cache) čekat na změny v názvech prostorů (o informerech se dozvíte více v článku „Jak skutečně funguje plánovač Kubernetes?» — pozn. přek.). Poté připojujeme EventHandler k informeru, díky čemuž při přidání prostoru (Namespace) se vyvolá funkce createRoleBinding.

Dalším krokem je definovat tuto funkci createRoleBinding:

func (c *NamespaceController) createRoleBinding(obj interface{}) {
  namespaceObj := obj.(*v1.Namespace)
  namespaceName := namespaceObj.Name

  roleBinding := &v1beta1.RoleBinding{
    TypeMeta: metav1.TypeMeta{
      Kind:       "RoleBinding",
      APIVersion: "rbac.authorization.k8s.io/v1beta1",
    },
    ObjectMeta: metav1.ObjectMeta{
      Name:      fmt.Sprintf("ad-kubernetes-%s", namespaceName),
      Namespace: namespaceName,
    },
    Subjects: []v1beta1.Subject{
      v1beta1.Subject{
        Kind: "Group",
        Name: fmt.Sprintf("ad-kubernetes-%s", namespaceName),
      },
    },
    RoleRef: v1beta1.RoleRef{
      APIGroup: "rbac.authorization.k8s.io",
        Kind:     "ClusterRole",
        Name:     "edit",
    },
  }

  _, err := c.kclient.Rbac().RoleBindings(namespaceName).Create(roleBinding)

  if err != nil {
    log.Println(fmt.Sprintf("Nezdařilo se vytvořit Role Binding: %s", err.Error()))
  } else {
    log.Println(fmt.Sprintf("Vytvořen AD RoleBinding pro Namespace: %s", roleBinding.Name))
  }
}

(controller.go, v raw)

Získáváme prostor jako obj a převedeme jej na objekt Namespace. Poté určíme RoleBinding, na základě zmíněného v úvodním YAML souboru, pomocí poskytnutého objektu Namespace a vytvoříme RoleBinding. Nakonec logujeme, zda se vytvoření podařilo.

Poslední funkce, kterou je třeba definovat, je Spustit:

// Run запускает процесс ожидания изменений в пространствах имён
// и действия в соответствии с этими изменениями.
func (c *NamespaceController) Run(stopCh <-chan struct{}, wg *sync.WaitGroup) {
  // Когда эта функция завершена, пометим как выполненную
  defer wg.Done()

  // Инкрементируем wait group, т.к. собираемся вызвать goroutine
  wg.Add(1)

  // Вызываем goroutine
  go c.namespaceInformer.Run(stopCh)

  // Ожидаем получения стоп-сигнала
  <-stopCh
}

(controller.go, v raw)

Zde říkáme WaitGroup, že spustíme goroutine a poté zavoláme namespaceInformer, který byl předem definován. Jakmile přijde signál ke zastavení, ukončí funkci, oznámí WaitGroup, že již není v provozu, a tato funkce ukončí svou činnost.

Informace o sestavení a spuštění tohoto operátoru v klastru Kubernetes lze nalézt v repozitáři na GitHubu.

Na tomto operátoru, který vytváří RoleBinding při vzniku Namespace v klastru Kubernetes, je připraven.

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster