Operatorzy dla Kubernetes: jak uruchamiać aplikacje stateful

Przyp. tłum.: Operatorzy (operators) to pomocnicze oprogramowanie dla Kubernetes, mające na celu automatyzację wykonywania rutynowych działań na obiektach klastra w określonych zdarzeniach. Już pisaliśmy o operatorach w w tym artykule, gdzie opowiadaliśmy o podstawowych ideach i zasadach ich działania. Jeśli tamten materiał był raczej spojrzeniem z perspektywy eksploatacji gotowych komponentów dla Kubernetes, to obecnie proponowany przekład nowego artykułu to już wizja dewelopera/DevOps-inżyniera, zmartwionego realizacją nowego operatora.

Operatorzy dla Kubernetes: jak uruchamiać aplikacje stateful

Ten post z przykładem z życia wziętym postanowiłem napisać po własnych próbach znalezienia dokumentacji dotyczącej tworzenia operatora dla Kubernetes, które przeszły przez badanie kodu.

Przykład, który będzie opisany, jest następujący: w naszym klastrze Kubernetes każdy Namespace reprezentuje środowisko-piaskownicę jakiejś drużyny, i chcieliśmy ograniczyć dostęp do nich tak, aby drużyny mogły bawić się tylko w swoich piaskownicach.

Osiągnięcie tego celu można zrealizować, przypisując użytkownikowi grupę, która ma RoleBinding do konkretnych Namespace i ClusterRole z prawem do edytowania. Reprezentacja YAML będzie wyglądać tak:

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

Taką RoleBinding można stworzyć i ręcznie, ale po przekroczeniu liczby stu przestrzeni nazw staje się to żmudnym zajęciem. Właśnie tutaj pomagają operatorzy Kubernetes — pozwalają zautomatyzować tworzenie zasobów Kubernetes w oparciu o zmiany w zasobach. W naszym przypadku chcemy tworzyć RoleBinding przy tworzeniu Namespace.

Na początku zdefiniujmy funkcję main, która wykonuje wymaganą konfigurację do uruchomienia operatora, a następnie wywołuje działanie operatora:

(Przyp. tłum.: tutaj i dalej komentarze w kodzie zostały przetłumaczone na język polski. Ponadto wcięcia zostały poprawione na spacje zamiast [zalecanych w Go] tabulatorów, wyłącznie w celu lepszej czytelności w ramach układu Habra. Po każdym bloku kodu podane są linki do oryginału na GitHub, gdzie zachowane są anglojęzyczne komentarze i tabulatory.)

func main() {
  // Ustawiamy wyjście logów na konsolowy STDOUT
  log.SetOutput(os.Stdout)

  sigs := make(chan os.Signal, 1) // Tworzymy kanał do odbierania sygnałów OS
  stop := make(chan struct{})     // Tworzymy kanał do odbierania sygnału stopu

  // Rejestrujemy odbieranie SIGTERM w kanale sigs
  signal.Notify(sigs, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) 

  // Goroutines mogą same dodawać się do WaitGroup,
  // aby zakończenie ich wykonania było oczekiwana
  wg := &sync.WaitGroup{} 

  runOutsideCluster := flag.Bool("run-outside-cluster", false, "Ustaw tę flagę, gdy uruchamiasz poza klastrem.")
  flag.Parse()
  // Tworzymy clientset do interakcji z klastrem Kubernetes
  clientset, err := newClientSet(*runOutsideCluster)

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

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

  <-sigs // Czekamy na sygnały (do otrzymania sygnału nic więcej się nie dzieje)
  log.Printf("Zamykamy...")

  close(stop) // Mówimy goroutines, aby się zatrzymały
  wg.Wait()   // Czekamy, aż wszystko zostanie zatrzymane
}

(main.go, w raw)

Robimy to:

  1. Konfigurujemy obsługę konkretnych sygnałów systemu operacyjnego, aby prawidłowo zakończyć działanie operatora.
  2. Używamy WaitGroup, aby prawidłowo zatrzymać wszystkie goroutines przed zakończeniem działania aplikacji.
  3. Zapewniamy dostęp do klastra poprzez stworzenie clientset.
  4. Uruchamiamy NamespaceController, w którym mieści się cała nasza logika.

Teraz potrzebujemy podstawy dla logiki, a w naszym przypadku jest to wspomniany 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, w raw)

Tutaj konfigurujemy SharedIndexInformer, który efektywnie (wykorzystując cache) będzie oczekiwał na zmiany w przestrzeniach nazw (szczegóły na temat informersów można znaleźć w artykule „How does the Kubernetes scheduler actually work?» — przyp. tłum.). Następnie podłączamy EventHandler do informera, dzięki czemu podczas dodawania przestrzeni nazw (Namespace) wywoływana jest funkcja createRoleBinding.

Następnym krokiem jest zdefiniowanie tej funkcji 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("Nie udało się utworzyć Role Binding: %s", err.Error()))
  } else {
    log.Println(fmt.Sprintf("Utworzono AD RoleBinding dla Namespace: %s", roleBinding.Name))
  }
}

(controller.go, w raw)

Otrzymujemy przestrzeń nazw jako ‘ zdefiniowana jest metoda i przekształcamy go w obiekt Namespace. Następnie definiujemy RoleBinding, opierając się na wspomnianym na początku pliku YAML, używając dostarczonego obiektu Namespace i tworząc RoleBinding. Na koniec logujemy, czy utworzenie przebiegło pomyślnie.

Ostatnia funkcja, którą należy zdefiniować, to Run:

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

Tutaj mówimy WaitGroup, że uruchomimy goroutine, a następnie wywołujemy namespaceInformer, który został wcześniej zdefiniowany. Kiedy nadejdzie sygnał zatrzymania, zakończy funkcję, informując WaitGroup, że już nie jest wykonywana, i ta funkcja zakończy swoje działanie.

Informacje o budowaniu i uruchamianiu tego operatora w klastrze Kubernetes można znaleźć w repozytorium na GitHubie.

Na tym operator, który tworzy RoleBinding w momencie pojawienia się Namespace w klastrze Kubernetes, jest gotowy.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster