Wir schreiben einen Operator für Kubernetes in Golang

Anmerkung des Übersetzers.: Operatoren (Operators) sind Hilfssoftware für Kubernetes, die darauf abzielt, die Ausführung routinemäßiger Aufgaben im Zusammenhang mit Clusterobjekten bei bestimmten Ereignissen zu automatisieren. Wir haben bereits über Operatoren in diesem Artikel, wo wir die grundlegenden Ideen und Prinzipien ihrer Funktionsweise erläutert haben, geschrieben. Während das Material damals eher einen Blick aus der Perspektive der Nutzung fertiger Kubernetes-Komponenten bot, ist die jetzt vorgeschlagene Übersetzung eines neuen Artikels eine Sichtweise eines Entwicklers/DevOps-Ingenieurs, der mit der Implementierung eines neuen Operators beschäftigt ist.

Wir schreiben einen Operator für Kubernetes in Golang

Ich habe diesen Beitrag mit einem Beispiel aus dem echten Leben nach meinen Versuchen verfasst, Dokumentationen zur Erstellung eines Operators für Kubernetes zu finden, die durch das Studium des Codes gingen.

Das Beispiel, das beschrieben wird, ist: In unserem Kubernetes-Cluster stellt jeder Namespace eine Sandbox-Umgebung für ein Team dar, und wir wollten den Zugriff so beschränken, dass die Teams nur in ihren eigenen Sandkästen spielen konnten.

Das gewünschte Ziel kann erreicht werden, indem einem Benutzer eine Gruppe zugewiesen wird, die RoleBinding zu den Namespace und Clusterrolle mit Bearbeitungsrechten hat. 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 Operator kann man auch manuell erstellen, aber nach der Erreichung von über hundert Namespaces wird dies mühsam. Hier kommen Kubernetes-Operatoren ins Spiel – sie ermöglichen die Automatisierung der Erstellung von Kubernetes-Ressourcen basierend auf Änderungen an Ressourcen. In unserem Fall wollen wir erstellen RoleBinding bei der Erstellung RoleBinding Zuerst definieren wir die Funktion Namespace.

, die die erforderliche Konfiguration zum Starten des Operators vornimmt und dann die Aktion des Operators aufruft: main: Hier und im Folgenden sind die Kommentare im Code ins Deutsche übersetzt. Außerdem wurden die Einrückungen aus Tabulatoren, die in Go empfohlen werden, aus Gründen der besseren Lesbarkeit in Leerzeichen umgewandelt. Nach jedem Listing werden Links zum Original auf GitHub bereitgestellt, wo die englischen Kommentare und Tabs erhalten bleiben.

(Anmerkung des Übersetzers.: Hier und im Folgenden sind die Kommentare im Code ins Russische übersetzt. Außerdem wurden die Einrückungen aus Gründen der besseren Lesbarkeit in der Habr-Layout auf Leerzeichen anstelle von [in Go empfohlenen] Tabs geändert. Nach jeder Auflistung finden Sie Links zum Original auf GitHub, wo die englischen Kommentare und Tabs beibehalten wurden.)

func main() {
  // Wir richten die Protokollausgabe auf STDOUT in der Konsole ein
  log.SetOutput(os.Stdout)

  sigs := make(chan os.Signal, 1) // Wir erstellen einen Kanal für das Empfangen von Betriebssystemsignalen
  stop := make(chan struct{})     // Wir erstellen einen Kanal für das Empfangen des Stoppsignals

  // Wir registrieren das Empfangen von SIGTERM im Kanal sigs
  signal.Notify(sigs, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) 

  // Goroutinen können sich selbst zur WaitGroup hinzufügen,
  // damit auf deren Abschluss gewartet werden kann
  wg := &sync.WaitGroup{} 

  runOutsideCluster := flag.Bool("run-outside-cluster", false, "Setzen Sie dieses Flag, wenn Sie außerhalb des Clusters arbeiten.")
  flag.Parse()
  // Wir erstellen ein Clientset zur Interaktion mit dem Kubernetes-Cluster
  clientset, err := newClientSet(*runOutsideCluster)

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

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

  <-sigs // Wir warten auf Signale (bis ein Signal empfangen wird, passiert nichts weiter)
  log.Printf("Herunterfahren...")

  close(stop) // Wir sagen den Goroutinen, dass sie anhalten sollen
  wg.Wait()   // Wir warten, bis alles gestoppt ist
}

(main.go, in raw)

Wir machen Folgendes:

  1. Wir konfigurieren Handler für spezifische Betriebssystemsignale, um eine korrekte (graceful) Beendigung des Operators auszulösen.
  2. Verwenden Sie WaitGroup, um alle Goroutinen korrekt vor der Beendigung der Anwendung anzuhalten.
  3. Wir bieten Zugang zum Cluster durch die Erstellung von Clientset.
  4. Starten NamespaceController, in dem unsere gesamte Logik untergebracht ist.

Jetzt brauchen wir die Grundlage für die Logik, und in unserem Fall ist das der 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 richten wir SharedIndexInformer, der effizient (unter Verwendung des Caches) auf Änderungen in den Namensräumen wartet (mehr über Informer erfahren Sie im Artikel „Wie funktioniert der Kubernetes-Scheduler tatsächlich?» — Anm. d. Ü.). Danach verbinden wir EventHandler mit dem Informer, sodass beim Hinzufügen eines Namensraums (Namespace) die Funktion createRoleBinding.

Der nächste Schritt ist, diese Funktion zu definieren 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 Rollenbindung: %s", err.Error()))
  } else {
    log.Println(fmt.Sprintf("Erstellte AD Rolle-Bindung für Namespace: %s", roleBinding.Name))
  }
}

(controller.go, in raw)

Wir erhalten den Namensraum als obj und wandeln ihn in ein Objekt um Namespace. Dann definieren wir RoleBinding, basierend auf der im YAML-Datei zu Beginn erwähnten, 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 sprechen wir WaitGroup, dass wir eine Goroutine starten und dann namespaceInformer, die im Voraus 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 ihre Arbeit einstellen.

Informationen über den Aufbau und die Ausführung dieses Operators im Kubernetes-Cluster finden Sie in dem Repository auf GitHub.

Dieser Operator, der erstellt wird, RoleBinding wird aktiviert, wenn Namespace im Kubernetes-Cluster erscheint, bereit.

Quelle: habr.com

60GB SSD 8Gb DDR4