Wir schreiben einen Operator für Kubernetes in Golang

Hinweis.: Operatoren sind Hilfsprogramme für Kubernetes, die dazu dienen, die Ausführung routinemäßiger Aufgaben an Objekten im Cluster bei bestimmten Ereignissen zu automatisieren. Wir haben bereits über Operatoren in in diesem Artikelgeschrieben, wo wir die grundlegenden Ideen und Prinzipien ihrer Funktionsweise erklärt haben. Während das vorherige Material eher einen Blick auf die Nutzung fertiger Komponenten für Kubernetes bot, bietet die nun übersetzte neue Artikelversion die Perspektive eines Entwicklers / DevOps-Ingenieurs, der mit der Umsetzung eines neuen Operators beschäftigt ist.

Wir schreiben einen Operator für Kubernetes in Golang

Ich habe beschlossen, diesen Beitrag mit einem Beispiel aus der Praxis zu schreiben, nachdem ich versucht hatte, Dokumentationen zur Erstellung eines Operators für Kubernetes zu finden, die mir durch das Studium des Codes begegnet sind.

Das Beispiel, das beschrieben wird, ist folgendes: In unserem Kubernetes-Cluster stellt jedes Namespace eine Sandbox-Umgebung für ein Team dar, und wir wollten den Zugang zu diesen Umgebungen so einschränken, dass die Teams nur in ihren eigenen Sandkästen arbeiten können.

Um dies zu erreichen, kann einem Benutzer eine Gruppe zugewiesen werden, die über ein RoleBinding zu einem bestimmten Namespace und ClusterRole mit Bearbeitungsrechten verfügt. Die YAML-Darstellung würde folgendermaßen aussehen:

---
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, in raw)

Einen solchen zu erstellen RoleBinding ist ebenfalls manuell möglich, wird jedoch schwierig, wenn die Anzahl der Namespaces die Hundert überschreitet. Hier kommen die Kubernetes-Operatoren ins Spiel – sie ermöglichen es, die Erstellung von Kubernetes-Ressourcen basierend auf Änderungen in den Ressourcen zu automatisieren. In unserem Fall möchten wir erstellen RoleBinding bei der Erstellung Namespace.

Zuerst definieren wir die Funktion main, die die erforderliche Konfiguration für den Operator bereitstellt und dann die Aktion des Operators anruft:

(Hinweis.: Hier und im Folgenden wurden die Kommentare im Code ins Deutsche übersetzt. Zudem wurden die Einrückungen von Tabs [wie in Go empfohlen] auf Leerzeichen umgestellt, um die Lesbarkeit in der Darstellung von Habra zu verbessern. Nach jedem Listing sind Links zur Originalversion auf GitHub eingefügt, wo die englischen Kommentare und Tabs erhalten geblieben sind.

func main() {
  // Set the log output to console STDOUT
  log.SetOutput(os.Stdout)

  sigs := make(chan os.Signal, 1) // Create a channel to receive OS signals
  stop := make(chan struct{})     // Create a channel for stop signals

  // Register to receive SIGTERM in the sigs channel
  signal.Notify(sigs, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) 

  // Goroutines can add themselves to WaitGroup,
  // to wait for their completion
  wg := &sync.WaitGroup{} 

  runOutsideCluster := flag.Bool("run-outside-cluster", false, "Set this flag when running outside of the cluster.")
  flag.Parse()
  // Create clientset to interact with the Kubernetes cluster
  clientset, err := newClientSet(*runOutsideCluster)

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

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

  <-sigs // Wait for signals (nothing happens until a signal is received)
  log.Printf("Shutting down...")

  close(stop) // Tell goroutines to stop
  wg.Wait()   // Wait for everything to stop
}

(main.go, in raw)

Wir machen Folgendes:

  1. Wir konfigurieren einen Handler für spezifische Betriebssystemsignale, um eine ordnungsgemäße (graceful) Beendigung des Operators zu erreichen.
  2. Wir verwenden WaitGroup, um sicherzustellen, dass alle Goroutines korrekt gestoppt werden, bevor die Anwendung beendet wird.
  3. Wir gewähren Zugriff auf den Cluster, indem wir clientset.
  4. Starten wir NamespaceController, in dem unsere gesamte Logik untergebracht wird.

Jetzt benötigen wir eine Grundlage für die Logik, und in unserem Fall ist dies das erwähnte 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, in raw)

Hier konfigurieren wir SharedIndexInformer, das effizient (unter Verwendung eines Caches) auf Änderungen in den Namespaces wartet (mehr über Informers erfahren Sie im Artikel „Wie funktioniert der Kubernetes-Scheduler wirklich?» — Anmerk. d. Ü.). Danach verbinden wir EventHandler mit dem Informer, wodurch bei der Hinzufügung eines Namespaces (Namespace) die Funktion createRoleBinding.

bestimmt werden muss. 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("Fehler beim Erstellen der Role Binding: %s", err.Error()))
  } else {
    log.Println(fmt.Sprintf("AD RoleBinding für Namespace erstellt: %s", roleBinding.Name))
  }
}

(controller.go, in raw)

Wir erhalten einen Namespace wie obj und wandeln ihn in ein Objekt um Namespace. Dann definieren wir RoleBinding, basierend auf dem oben erwähnten YAML-Dokument, unter Verwendung des bereitgestellten Objekts Namespace und erstellen RoleBinding. Schließlich loggen wir, ob die Erstellung erfolgreich war.

Die letzte Funktion, die definiert werden muss, ist Ausführen:

// 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, in raw)

Hier geben wir an WaitGroup, dass wir eine Goroutine starten und dann namespaceInformer, die zuvor definiert wurde, aufrufen. Wenn ein Stoppsignal empfangen wird, wird die Funktion beendet, und es wird gemeldet, WaitGroup, dass sie nicht mehr ausgeführt wird, und diese Funktion wird beendet.

Informationen zum Erstellen und Ausführen dieses Operators im Kubernetes-Cluster finden Sie im Repository auf GitHub.

Nun ist der Operator, der RoleBinding bei Auftreten von Namespace im Kubernetes-Cluster bereit.

Quelle: habr.com

Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server 🔥 Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster