Scriviamo un operatore per Kubernetes in Golang

Nota del traduttore.: Gli operatori — sono software ausiliari per Kubernetes, progettati per automatizzare l'esecuzione di azioni routinarie sugli oggetti del cluster in determinate circostanze. Abbiamo già scritto sugli operatori in questo articolo, dove abbiamo trattato le idee e i principi fondamentali del loro funzionamento. Tuttavia, se quel materiale rappresentava più una prospettiva sull'uso di componenti già pronti per Kubernetes, la traduzione di questo nuovo articolo offre invece la visione di uno sviluppatore/ingegnere DevOps impegnato nella realizzazione di un nuovo operatore.

Scriviamo un operatore per Kubernetes in Golang

Ho deciso di scrivere questo post con un esempio della vita reale dopo i miei tentativi di trovare documentazione sulla creazione di un operatore per Kubernetes, che sono passati attraverso lo studio del codice.

L'esempio che sarà descritto è il seguente: nel nostro cluster Kubernetes ogni Namespace rappresenta un ambiente sandbox di un team, e volevamo limitare l'accesso in modo che i team potessero giocare solo nei propri sandbox.

Questo obiettivo può essere raggiunto assegnando all'utente un gruppo che ha RoleBinding a un Namespace e ClusterRole con diritto di modifica. La rappresentazione YAML apparirà così:

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

Creare questo RoleBinding è possibile anche manualmente, ma dopo aver superato la soglia di cento spazi dei nomi, diventa un compito noioso. Qui entra in gioco l'operatore Kubernetes: consente di automatizzare la creazione delle risorse Kubernetes in base alle modifiche delle risorse. Nel nostro caso, vogliamo creare RoleBinding durante la creazione Namespace.

iniziamo definendo la funzione main, che esegue la configurazione necessaria per avviare l'operatore e quindi richiama l'azione dell'operatore:

(Nota del traduttore.: da qui in poi i commenti nel codice sono tradotti in italiano. Inoltre, gli spazi sono stati corretti in spazi al posto delle [tabs raccomandate in Go] esclusivamente per una maggiore leggibilità nel layout di Habra. Dopo ogni elenco, sono forniti i collegamenti all'originale su GitHub, dove sono mantenuti i commenti in lingua inglese e i tab.

func main() {
  // Impostiamo l'output dei log su STDOUT della console
  log.SetOutput(os.Stdout)

  sigs := make(chan os.Signal, 1) // Creiamo un canale per ricevere segnali OS
  stop := make(chan struct{})     // Creiamo un canale per ricevere il segnale di arresto

  // Registriamo la ricezione di SIGTERM nel canale sigs
  signal.Notify(sigs, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) 

  // Le Goroutines possono aggiungersi da sole al WaitGroup,
  // in modo che la loro terminazione venga attesa
  wg := &sync.WaitGroup{} 

  runOutsideCluster := flag.Bool("run-outside-cluster", false, "Impostare questo flag quando si esegue al di fuori del cluster.")
  flag.Parse()
  // Creiamo un clientset per interagire con il cluster Kubernetes
  clientset, err := newClientSet(*runOutsideCluster)

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

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

  <-sigs // Aspettiamo segnali (fino a ricevere un segnale non succede nulla)
  log.Printf("Interruzione in corso...")

  close(stop) // Diamo il segnale alle goroutines di fermarsi
  wg.Wait()   // Attendiamo che tutto sia fermo
}

(main.go, nel raw)

Facciamo quanto segue:

  1. Configuriamo un gestore per segnali specifici del sistema operativo per avviare una chiusura corretta (graceful) dell'operatore.
  2. Utilizziamo WaitGroup, per fermare correttamente tutte le goroutines prima della chiusura dell'applicazione.
  3. Forniamo accesso al cluster creando un clientset.
  4. Avviamo NamespaceController, in cui sarà situata tutta la nostra logica.

Ora ci serve una base per la logica, e nel nostro caso questo è il menzionato 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, nel raw)

Qui configuriamo SharedIndexInformer, che attenderà efficacemente (utilizzando la cache) le modifiche nei namespace (per saperne di più sugli informers, leggi l'articolo «Come funziona realmente il pianificatore Kubernetes?» — nota del traduttore.). Dopodiché, colleghiamo EventHandler all'informer, grazie al quale, quando viene aggiunto un namespace (Namespace), viene chiamata la funzione createRoleBinding.

Il passo successivo è definire questa funzione 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("Failed to create Role Binding: %s", err.Error()))
  } else {
    log.Println(fmt.Sprintf("Created AD RoleBinding for Namespace: %s", roleBinding.Name))
  }
}

(controller.go, nel raw)

Otteniamo il namespace come obj e lo convertiamo in un oggetto Namespace. Poi definiamo RoleBinding, basandoci su quanto menzionato all'inizio nel file YAML, utilizzando l'oggetto fornito Namespace e creando RoleBinding. Infine, registriamo se la creazione è avvenuta con successo.

L'ultima funzione da definire è Esegui:

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

Qui diciamo WaitGroup, che avvieremo una goroutine e poi chiamiamo namespaceInformer, che è stato precedentemente definito. Quando arriva il segnale di arresto, concluderà la funzione, comunicherà WaitGroup, che non è più in esecuzione, e questa funzione terminerà il suo lavoro.

Informazioni sulla costruzione e l'esecuzione di questo operatore nel cluster Kubernetes possono essere trovate in repository su GitHub.

A questo punto, l'operatore che crea RoleBinding all'arrivo Namespace nel cluster Kubernetes è pronto.

Fonte: habr.com

Acquista un hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista un hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster