Écrivons un opérateur pour Kubernetes en Golang

Note de traduction.: Les opérateurs (operators) sont des logiciels auxiliaires pour Kubernetes, conçus pour automatiser l'exécution d'actions répétitives sur les objets du cluster lors de certains événements. Nous avons déjà écrit sur les opérateurs dans cet article, où nous avons présenté les idées fondamentales et les principes de leur fonctionnement. Mais si ce document était plutôt un aperçu de l'exploitation de composants prêts à l'emploi pour Kubernetes, la traduction de cet article nouvellement proposé est désormais la perspective d'un développeur ou d'un ingénieur DevOps, préoccupé par la mise en œuvre d'un nouvel opérateur.

Écrivons un opérateur pour Kubernetes en Golang

J'ai décidé d'écrire ce post avec un exemple de la vie réelle après mes tentatives de trouver de la documentation sur la création d'un opérateur pour Kubernetes, passant par l'examen du code.

L'exemple que je vais décrire est le suivant : dans notre cluster Kubernetes, chaque Namespace représente un environnement de bac à sable pour une certaine équipe, et nous voulions restreindre l'accès afin que les équipes ne puissent jouer que dans leurs propres bacs à sable.

L'objectif souhaité peut être atteint en attribuant à l'utilisateur un groupe qui a RoleBinding pour ce qui est de Namespace et ClusterRole avec le droit d'édition. La représentation YAML ressemblera à ceci :

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

On peut créer cela RoleBinding manuellement, mais après avoir dépassé la centaine d'espaces de noms, cela devient une tâche fastidieuse. C'est ici que les opérateurs Kubernetes aident — ils permettent d'automatiser la création de ressources Kubernetes en fonction des modifications apportées aux ressources. Dans notre cas, nous voulons créer RoleBinding lors de la création Namespace.

Tout d'abord, définissons une fonction main, qui exécute la configuration requise pour démarrer l'opérateur et appelle ensuite l'action de l'opérateur :

(Note de traduction.: ici et plus loin, les commentaires dans le code sont traduits en russe. De plus, les indentations ont été corrigées en espaces au lieu des [tabulations recommandées en Go] uniquement pour une meilleure lisibilité dans le cadre de la mise en page de Habr. Après chaque extrait, des liens vers l'original sur GitHub sont fournis, où les commentaires en anglais et les tabulations sont préservés.

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

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

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

  // Goroutines can add themselves to the 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 a clientset for interacting 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) // Instruct goroutines to stop
  wg.Wait()   // Wait for everything to be stopped
}

(main.go, sur raw)

Nous faisons ce qui suit :

  1. Configurer les gestionnaires pour des signaux spécifiques du système d'exploitation afin de déclencher une fermeture correcte (gracieuse) de l'opérateur.
  2. Utiliser WaitGroup, afin d'arrêter correctement tous les goroutines avant de terminer l'application.
  3. Fournir l'accès au cluster par la création de clientset.
  4. Lançons NamespaceController, où toute notre logique sera située.

Maintenant, nous avons besoin d'une base pour la logique, et dans notre cas, c'est le mentionné. 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, sur raw)

Ici, nous configurons SharedIndexInformer, qui attendra efficacement (en utilisant un cache) les modifications dans les espaces de noms. (pour en savoir plus sur les informers, lisez l'article «Comment fonctionne réellement le planificateur Kubernetes ?» — Note de traduction.). Ensuite, nous connectons EventHandler à l'informer, ce qui déclenche la fonction lors de l'ajout d'un espace de noms (Namespace). createRoleBinding.

L'étape suivante consiste à définir cette fonction 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("Échec de la création de Role Binding : %s", err.Error()))
  } else {
    log.Println(fmt.Sprintf("RoleBinding AD créé pour l'espace de noms : %s", roleBinding.Name))
  }
}

(controller.go, sur raw)

Nous obtenons l'espace de noms comme obj et le transformons en objet Namespace. Ensuite, nous définissons RoleBinding, basé sur ce qui a été mentionné dans le fichier YAML au début, en utilisant l'objet fourni Namespace et en créant RoleBinding. Enfin, nous enregistrons si la création a réussi.

La dernière fonction à définir est Exécution:

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

Ici, nous disons WaitGroup, que nous démarrerons une goroutine et ensuite nous appelons namespaceInformer, qui a été préalablement défini. Lorsqu'un signal d'arrêt est reçu, il terminera la fonction, indiquera WaitGroup, qu'il n'est plus en cours d'exécution, et que cette fonction se terminera.

Des informations sur la construction et le déploiement de cet opérateur dans le cluster Kubernetes peuvent être trouvées dans répertoire sur GitHub.

À ce stade, l'opérateur qui crée RoleBinding lorsqu'il apparaît Namespace dans le cluster Kubernetes, est prêt.

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster