Eine Einführung in das Operator Framework

Automatisierung mit Kubernetes Operators

Martin Helmich

© Shutterstock / Alexander Supertramp

In der Kubernetes-Sprache ist ein Operator ein Stück Software, das operatives Wissen (daher der Name) über Betrieb und Installation eines bestimmten Programms oder einer Systemkomponente seinerseits in Software abbildet und damit automatisieren kann. Lernen Sie, wie Sie einen solchen Operator mithilfe des Operator SDKs selbst in Go programmieren können.

Kubernetes erfreut sich in der DevOps-Szene aktuell größter Beliebtheit; das liegt nicht zuletzt daran, dass Kubernetes – im Zusammenspiel mit der jeweils gewählten Containertechnologie – von Haus aus zahlreiche Möglichkeiten liefert, wiederkehrende Vorgänge einfach zu automatisieren. Dazu zählen beispielsweise das Ausführen von Rolling Updates für Applikationen, die in der Regel ohne Downtime ausgeführt werden können. Bei aller Automatisierung stoßen jedoch auch irgendwann die Kubernetes-Bordmittel an ihre Grenzen. Insbesondere bei Einrichtung und Betrieb besonders komplexer Softwarekomponenten ist immer noch Spezialwissen des Administrators gefragt. Klassische Beispiele für solche Komponenten sind komplexe, zustandsbehaftete und schlimmstenfalls noch geclusterte Anwendungen, wie beispielsweise Datenbanksysteme.

Operators sind ein Ansatz, das Wissen über Set-up und Betrieb solcher Anwendungen in Software zu gießen, um einen noch höheren Automatisierungsgrad zu erreichen. Anfang März startete Red Hat OperatorHub.io – ein Projekt mit dem Ziel, ein zentrales Verzeichnis für solche Operators zu schaffen. Zum aktuellen Zeitpunkt finden sich hier bereits zahlreiche Operators für den Betrieb verschiedenster zustandsbehafteter Anwendungen, wie beispielsweise Apache Kafka, Prometheus, Percona MySQL und andere. Eine Suche auf GitHub offenbart zahlreiche weitere Operators.

Operators und Custom Resources

Eines der Kernprinzipien von Kubernetes besteht darin, dass verschiedene Objekttypen genutzt werden können, um den gewünschten Zustand des Systems zu beschreiben. Kubernetes (genauer gesagt, der Controller Manager) sorgt dann dafür, dass der Ist-Zustand des Systems mit dem gewünschten Zustand übereinstimmt. Erstellt ein Nutzer also ein Deployment-Objekt, sorgt der Controller Manager dafür, dass für dieses Deplyoment ein ReplicaSet erstellt wird (und für das ReplicaSet wiederum die gewünscht Anzahl Pods).

Die meisten Operators funktionieren nach demselben Prinzip – der Nutzer agiert mit ihnen über Objekte des Kubernetes API – entweder direkt über das API oder beispielsweise kubectl. Statt der Standardtypen bieten zahlreiche Operators sogenannte Custom Resources an. Mit ihnen kann das Kubernetes API um eigene benutzerdefinierte Datentypen erweitert werden. Die Definition eines solchen Datentyps erfolgt ebenfalls wieder über das Kubernetes API in Form einer CustomResourceDefinition (CRD). Listing 1 zeigt, wie solch eine Definition aussehen könnte.

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: example.stable.helmich.me
spec:
  group: stable.helmich.me
  versions:
  - name: v1alpha1
    served: true
    storage: true
  scope: Namespaced
  names:
    plural: examples
    singular: example
    kind: Example
    shortNames:
    - ex

Eine genaue Beschreibung, mit welchen Eigenschaften eine CRD spezifiziert werden kann, findet sich unter auf Kubernetes.io. Nachdem eine solche Definition erstellt wurde (beispielsweise mit dem Befehl kubectl create), können nun Instanzen dieser Custom Resource erstellt werden. Listing 2 zeigt ein Beispiel für die oben definierte Example-Ressource.

apiVersion: stable.helmich.me/v1alpha1
kind: Example
metadata:
  name: first-example
  namespace: default
spec:
  foo: bar

Auch solch ein Objekt kann ganz normal etwa über kubectl create erstellt werden. Existierende Objekte können im Anschluss mit dem Befehl kubectl get examples (hier kommt die spec.names-Eigenschaft aus Listing 1 ins Spiel) wieder aufgelistet werden. Mit solchen Custom Resources eröffnen sich vielfältige Möglichkeiten, die ein Kubernetes Operator nutzen kann. Der übliche Arbeitsablauf eines Operators sieht dabei aus wie folgt:

  1. Der Operator erstellt (üblicherweise bei der Installation) eine CustomResourceDefinition
  2. Der Nutzer kann nun über die üblichen Kubernetes-Bordmittel (wie etwa kubectl) Instanzen dieser CRD erstellen
  3. Der Operator beobachtet die Instanzen der von ihm verwalteten CRDs (in aller Regel über die WATCH-Funktion des Kubernetes API), und erstellt anhand der in der Custom Resource gespeicherten Informationen weitere Kubernetes-Objekte, wie etwa Deployments, StatefulSets und andere. Wird die Custom Resource bearbeitet, propagiert der Operator diese Änderungen an die erstellten Unterobjekte weiter.
  4. Wenn der Nutzer die Instanz der Custom Resource entfernt (z. B. per kubectl delete), löscht ein Operator üblicherweise auch die erstellten Unterobjekte.

Das Beispielprojekt

Die Funktionsweise eines Operators sowie die Verwendung des Operator SDKs (mehr dazu in den nächsten Abschnitten) lassen sich am besten anhand eines Beispiels erläutern. Für diesen Artikel möchten wir daher einen Beispiel-Operator schreiben, der eine einfache Webapplikation verwaltet – genauer gesagt, Instanzen des Image martinhelmich/helloworld, eine Anwendung, die (wer hätte es gedacht?) HTTP-Anfragen mit „Hello World“ beantwortet. Die Applikation soll über CRDs des Namens „HelloWorld“ verwaltet werden. Für jede Instanz soll der Operator ein Deployment-, sowie ein Service– und ein Ingress-Objekt erstellen. Über die Custom Resource sollen Nutzer konfigurieren können, wie viele Instanzen der Applikation laufen sollen, wer genau gegrüßt werden (statt immer nur „World“) und über welchen Hostnamen die Applikation erreichbar sein soll.

Das Operator Framework

Ein Kubernetes-Operator ist selbst ein relativ komplexes Stück Software – und dabei machen viele Operators, die es bisher für verschiedene Zwecke gibt, eigentlich das Gleiche. Zu diesem Zweck veröffentlichte CoreOS letztes Jahr das Operator Framework, mit dem die Entwicklung und der Betrieb eigener Operators vereinfacht werden sollen. Das Framework besteht aus dem Operator SDK, einer Go-Bibliothek, und (optionalerweise) aus dem Operator Lifecycle Manager (oder kurz OLM), einer im Cluster laufenden Komponente, die Deployment und Betrieb der Operators selbst verwaltet.

Um einen neuen Operator zu erstellen, muss auf der Entwicklungsmaschine zunächst das Operator SDK installiert werden. Hierzu helfen folgende Befehle auf der Kommandozeile:

$ git clone https://github.com/operator-framework/operator-sdk $GOPATH/src/github.com/operator-framework/operator-sdk
$ make dep
$ make install

Im Anschluss steht auf dem System der Befehl operator-sdk zur Verfügung. Der kann nun benutzt werden, um einen neuen Operator zu erstellen. Dabei handelt es sich zunächst einmal um ein ganz normales Go-Programm, das innerhalb des $GOPATH liegen muss:

$ mkdir -p $GOPATH/src/github.com/martin-helmich
$ cd $GOPATH/src/github/martin-helmich
$ operator-sdk new helloworld-operator

Der letzte Befehl erstellt ein neues Go-Projekt und lädt dabei eine respektable Menge Abhängigkeiten für das vendor/-Verzeichnis nach. Dieser Schritt kann also ein wenig dauern.

Definieren der CRD

Nachdem das Projekt erstellt wurde, kann im Projektverzeichnis nun der Befehl operator-sdk add api genutzt werden, um eine neue CRD zu erstellen:

$ operator-sdk add api --api-version=example.helmich.me/v1alpha1 --kind HelloWorld

Dieser Befehl macht mehrere Sachen auf einmal. Zunächst einmal wird im Verzeichnis deploy/crds/ die CRD selbst (zusammen mit einer Beispielinstanz) in Form zweier .yaml-Dateien erstellt. Im Verzeichnis pkg/apis/example/v1alpha1/ (der Name leitet sich aus der beim Erstellen verwendeten API-Version ab) werden zudem die benötigten Go Structs erstellt, auf die die Custom Resources später abgebildet werden sollen.

Als Nächstes können wir nun die erstellten CRDs an unsere eigenen Bedürfnisse anpassen. Hierfür am interessantesten ist das Feld spec.validation.openAPIV3Schema: Hier kann ein JSON-Schema hinterlegt werden. Der Kubernetes-API-Server nutzt dieses Schema später, um vom Nutzer erstellte Instanzen dieser Ressource zu validieren. Listing 3 zeigt, wie dieses Schema im Fall der HelloWorld-CRD (im Verzeichnis deploy/crds/) aussehen könnte.

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: helloworlds.example.helmich.me
spec:
  # […]
  validation:
    openAPIV3Schema:
      properties:
        spec:
          type: object
          properties:
            replicas:
              type: int
              min: 0
            host:
              type: string
            recipient:
              type: string

Innerhalb des .spec-Attributs können Nutzer des HelloWorld-Operators also später über .spec.replicas, .spec.host und .spec.recipient konfigurieren, wie viele Instanzen einer Applikation gestartet werden sollen, über welchen Namen sie erreichbar sein soll und wer (statt „World“) gegrüßt werden soll. Listing 4 zeigt eine Beispielressource.

apiVersion: example.helmich.me/v1alpha1
kind: HelloWorld
metadata:
  name: example
spec:
  replicas: 3
  host: helloworld.helmich.me
  recipient: World

Die Go-Clientbibliothek für Kubernetes bildet jeden Objekttyp des API auf einen eigens definierten Struct-Datentyp ab. CRDs bilden da keine Ausnahme. Für den HelloWorld-Operator wurden bereits entsprechende Struct-Typen erstellt, die nun natürlich noch angepasst werden müssen, sodass sie die replicas-, host– und recipient-Attribute kennen. Hierzu bearbeiten wir das Struct HelloWorldSpec in der Datei pkg/apis/example/v1alpha1/helloworld_types.go und fügen die in Listing 5 gezeigten Attribute hinzu.

type HelloWorldSpec struct {
  Replicas  *int32 `json:"replicas"`
  Host      string `json:"host"`
  Recipient string `json:"recipient"`
}

Damit die Clientbibliothek einen API-Datentyp verwenden kann, muss dieser das Interface k8s.io/apimachinery/pkg/runtime.Object implementieren. Außerdem müssen alle Datentypen in einer Registry angemeldet werden, die die Clientbibliothek zur Laufzeit vorhält. Insbesondere die vom runtime.Object-Interface vorgeschriebene DeepCopy-Methode hat es in sich; sie muss implementiert werden, damit die Clientbibliothek vollständige Kopien eines Datensatzes erstellen kann.

Nutzer des Operator SDKs haben es jedoch angenehm: Über den Befehl operator-sdk generate k8s können alle benötigten Funktionen (insbesondere die DeepCopy-Methoden) sowie der Code zur Registrierung der eigenen API-Typen automatisch generiert werden:

$ operator-sdk generate k8s

Die hier beschriebenen Schritte können während der Entwicklung eines Operators beliebig oft wiederholt werden. Falls Ihr Operator bereits im Produktiveinsatz ist, sollten Sie jedoch Vorsicht walten lassen, wenn Sie CRDs nachträglich verändern – wenn Sie beispielsweise neue Validierungsregeln oder völlig neue Attribute hinzufügen, sind diese bei im Cluster bereits existierenden Objekten nicht auf magische Art und Weise vorhanden. Nutzen Sie in diesem Fall lieber die Versionierungsfeatures des Kubernetes-API und erstellen Sie eine neue CRD mit einer höheren Versionsnummer.

Erstellen des Controllers

Nachdem die CustomResourceDefinition und die dazugehörigen Go-Datentypen erstellt wurden, kann es nun an das Herzstück des Operators gehen: den Controller. Der ist üblicherweise dafür verantwortlich, das Kubernetes-API auf Änderungen zu überwachen und gegebenenfalls darauf zu reagieren. Und, wie sollte es anders sein, kann auch ein Controller über die Funktionen des Operator SDKs automatisch erstellt werden. Hierzu kann der Befehl operator-sdk add controller benutzt werden:

$ operator-sdk add controller --api-version=example.helmich.me/v1alpha1 --kind=HelloWorld

Dieser Befehl erstellt eine neue Datei pkg/controller/helloworld/helloworld_controller.go (der genaue Name ist wieder vom Namen der CRD abhängig), in der nun die Operator-spezifische Controller-Logik implementiert werden kann.

Der Controller besteht aus zwei wichtigen Komponenten: Dem Controller selbst und dem Reconciler. Der Controller-Datentyp ist Teil von Kubernetes und wird vom Operator lediglich instanziiert. Das passiert in der generierten Datei in der add(…)-Methode. In dieser Methode wird dem Controller zudem mitgeteilt, welche Ressourcen des Kubernetes-API er überwachen soll. In der generierten Beispieldatei wird der Controller so konfiguriert, dass er sowohl auf Änderungen an Instanzen seiner Custom Resource reagiert als auch auf Änderungen an von ihm verwalteten Pods.

Generell ist es sinnvoll, wenn der Operator die Objekte überwacht, die er auch selbst erstellt. Wenn unser HelloWorld-Operator also beispielsweise Deployment-, Service- und Ingress-Objekte erstellt, sollte er auch auf Änderungen an diesen Objekten reagieren. Listing 6 zeigt, wie die entsprechenden „Watches“ auf kompakte Weise konfiguriert werden können.

func add(mgr manager.Manager, r reconcile.Reconciler) error {
  c, err := controller.New("helloworld-controller", mgr, controller.Options{Reconciler: r})
  if err != nil {
    return err
  }

  err = c.Watch(&source.Kind{Type: &examplev1alpha1.HelloWorld{}}, &handler.EnqueueRequestForObject{})
  if err != nil {
    return err
  }

  watchTypes := []runtime.Object{
    &appsv1.Deployment{},
    &corev1.Service{},
    &extv1beta1.Ingress{},
  }

  for i := range watchTypes {
    err := c.Watch(&source.Kind{Type: watchTypes[i]}, &handler.EnqueueRequestForOwner{
      IsController: true,
      OwnerType:    &examplev1alpha1.HelloWorld{},
    })
    if err != nil {
      return err
    }
  }

  return err
}

Auf diese Weise wurde nun konfiguriert, wann der Operator tätig werden soll; was jedoch genau in dem Fall passieren soll, haben wir bisher noch nicht betrachtet.

Die Desired State Reconciliation implementieren

Den Hauptteil der Arbeit eines Controllers übernimmt der Reconciler. Er muss sicherstellen, dass der tatsächliche Zustand des Kubernetes-Clusters auch dem gewünschten Sollzustand (dem Desired State) entspricht. Dieser wird üblicherweise in Form abstrakter Ressourcen wie etwa Deployment-Objekten oder in unserem Fall HelloWorld-Objekten beschrieben. Diese Objekte setzt ein Controller üblicherweise in untergeordnete Kubernetes-Objekte um – so wie der Deployment Controller beispielsweise für ein Deployment mehrere ReplicaSets verwaltet, und jedes ReplicaSet wiederum mehrere Pods verwalten kann. Dieser stetige Abgleich von Ist- und Soll-Zustand wird im Englischen als Reconciliation bezeichnet.

In unserem Beispiel-Operator übernimmt diese Aufgabe das Struct ReconcileHelloWorld, das ebenfalls bereits automatisch generiert wurde. Dieser Datentyp muss dabei das Interface sigs.k8s.io/controller-runtime/pkg/reconcile.Reconciler implementieren. Dieses Interface schreibt vor, dass die Methode Reconcile(request reconcile.Request) (reconcile.Result, error) implementiert werden muss. Diese Methode wird vom Controller automatisch aufgerufen, sobald sich eins der beobachteten Objekte verändert. Außerdem wird sie beim Start des Controllers einmal für alle Objekte aufgerufen.

Innerhalb des Reconcilers steht zudem das Attribut r.client zur Verfügung, das den Zugriff auf das Kubernetes API ermöglicht. Der erste Schritt eines Reconciler-Durchlaufs sollte darin bestehen, eine aktuelle Version der betroffenen Custom Resource vom Kubernetes API zu laden. Wie das geht, zeigt Listing 7.

func (r *ReconcileHelloWorld) Reconcile(request reconcile.Request) (reconcile.Result, error) {
  cr := examplev1alpha1.HelloWorld{}
  ctx := context.TODO()

  if err := r.client.Get(ctx, request.NamespacedName, &cr); err != nil {
    if errors.IsNotFound(err) {
      // falls die CR nicht mehr existiert, räumt Kubernetes eventuelle
      // Unterobjekte automatisch auf.
      return reconcile.Result{}, nil
    }
    return reconcile.Result{}, err
  }

  return reconcile.Result{}, nil
}

Nachdem die Ressource vom API geladen wurde, können in derselben Methode die benötigten Unterobjekte erstellt (bzw. aktualisiert werden). Listing 8 zeigt das Vorgehen anhand des Deployment-Objekts. Die vollständige Implementierung, die Deployment, Service und Ingress erstellt, finden Sie im verlinkten vollständigen Codebeispiel.

func (r *ReconcileHelloWorld) Reconcile(request reconcile.Request) (reconcile.Result, error) {
  // [siehe Listing 7]

  labels := map[string]string{"app": cr.Name}
  deployment, err := r.buildDeployment(cr, labels)
  if err != nil {
    return reconcile.Result{}, err
  }

  existingDeployment := appsv1.Deployment{}
  if err != nil && errors.IsNotFound(err) {
    // in diesem Fall existiert das Deployment noch nicht, und muss mit
    // r.client.Create zunächst erstellt werden.
    if err := r.client.Create(ctx, deployment); err != nil {
      return reconcile.Result{}, err
    }
  } else if err == nil {
    // in diesem Fall existiert das Deployment bereits und muss ggf. mit
    // r.client.Update aktualisiert werden.
    foundDepl.Spec.Replicas = instance.Spec.Replicas
    if err := r.client.Update(ctx, &foundDepl); err != nil {
      return reconcile.Result{}, err
    }
  } else if err != nil {
    return reconcile.Result{}, err
  }

  return reconcile.Result{}, nil
}

Die Methode buildDeployment(cr, labels), die in Listing 7 verwendet wird, finden Sie im verlinkten Codebeispiel. Sie baut aus der geladenen HelloWorld-Ressource eine Instanz des Structs k8s.io/api/apps/v1.Deployment zusammen, mit der im Anschluss weitergearbeitet wird.

Den Operator lokal bauen und testen

Bevor Sie nun den Operator gleich in Ihrem Cluster ausrollen, können Sie ihn zunächst von Ihrer lokalen Entwicklungsmaschine aus starten und testen. Hierzu hilft der Befehl operator-sdk up local. Er sorgt dafür, dass das Operator SDK den Operator kompiliert und lokal startet. Der gestartete Operator verbindet sich dabei mit dem aktuell aktiven Kubernetes-Cluster aus der $HOME/.kube/config-Datei – Vorsicht also, wenn Sie gerade mit kubectl in einem Produktivcluster gearbeitet haben. Standardmäßig lauscht der Operator dabei auf Änderungen im default-Namespace. Über das –namespace-Argument beim Start können Sie auch einen anderen Namespace angeben.

Bevor Sie den Operator starten, sollte zunächst – falls noch nicht geschehen – die CRD erstellt werden. Das geht wie gewohnt über kubectl create:

$ kubectl create -f deploy/crds/example_v1alpha1_helloworld_crd.yaml

Nachdem Sie die CRD erstellt haben, können Sie den Operator starten und dann auch gleich ein HelloWorld-Objekt erstellen. Dieses sollte vom Operator nahezu sofort erkannt werden, und er sollte die davon abhängigen Ressourcen erstellen:

$ kubectl create -f deploy/crds/example_v1alpha1_helloworld_cr.yaml

Deployment im Cluster

Auch für das Deployment des Operators im Cluster hat das Operator SDK handliche Automatisierungen parat. So können Sie beispielsweise den Befehl operator-sdk build nutzen, um den Operator zu kompilieren und auch gleich ein passendes Container-Image zu bauen. Dazu muss dem Befehl als Parameter der Name des zu bauenden Image übergeben werden. Das so erstellte Image kann anschließend beispielsweise per docker push in eine Registry gepusht werden.

$ operator-sdk build martinhelmich/helloworld-operator:latest
$ docker push martinhelmich/helloworld-operator:latest

Um den Operator im Cluster zu installieren, können Sie die Dateien im Verzeichnis deploy/ nutzen. Diese erstellen sowohl den Operator selbst als auch die benötigten Rollen und Service-Accounts für den Operator. In der Datei operator.yaml müssen Sie noch den Platzhalter REPLACE_IMAGE mit dem Namen des zuvor gebauten Container-Image ersetzen.

$ kubectl apply -f deploy

Beachten Sie, dass die Rolle, die in der Datei role.yaml für den Operator spezifiziert wird, standardmäßig sehr weitreichende Rechte eingeräumt bekommt. Es kann sinnvoll sein, diese Rechte auf ein für Ihren speziellen Operator notwendiges Maß einzuschränken.

Wie gehts weiter?

Das Operator SDK ist ein hilfreiches Tool, um die vielen Fleißaufgaben bei der Entwicklung eines Kubernetes Operators bequem zu automatisieren. Neben dem Operator SDK bietet das Operator Framework auch noch den Operator Lifecycle Manager (OLM) an. Dabei handelt es sich im Grunde genommen selbst um einen Operator, der den Betrieb von Operatoren automatisiert. Um den OLM zu nutzen, müsste unser Operator nun noch ein Operator-Manifest mitliefern, in dem beispielsweise die benötigten Rechte, die Deployment-Strategie des Operators und einige andere Optionen angegeben werden. Die Installation des Operators über das manuelle Anwenden von Dateien mit kubectl (wie im vorigen Abschnitt) könnte damit ebenfalls entfallen.

Eine vollständige Einführung in den Operator Lifecycle Manager ginge an dieser Stelle zu weit. Es sei jedoch auf die offizielle Dokumentation des Operator Frameworks verwiesen.

Und zu guter Letzt: Das in diesem Artikel vorgestellte Beispiel zeigt nur einen Bruchteil des Potenzials, den Kubernetes Operators bieten. Böse Zungen könnten behaupten, das Beispiel dieses Artikels hätte mit einem Helm Chart viel schneller und einfacher umgesetzt werden können – und wahrscheinlich hätten sie Recht. Im Unterschied zu einem Helm Chart hat ein Operator jedoch die Möglichkeit, während seiner gesamten Laufzeit Veränderungen des Systemzustands zu bemerken und darauf zu reagieren. Darüber hinaus kann ein Operator direkt mit der von ihm verwalteten Applikation interagieren. Das ist insbesondere bei Software sinnvoll, die zur Laufzeit konfiguriert werden kann (oder muss); so könnte ein Operator für ein Datenbanksystem beispielsweise auch Benutzer, einzelne Datenbanken oder Back-ups davon in Form von Custom Resources verwalten und ihm Rahmen seiner State Reconciliation direkt mit dem System interagieren.

Zum Abschluss sei jedem Leser ein Blick in die Liste der bereits bestehenden Operatoren empfohlen – beispielsweise auf OperatorHub oder GitHub. Beides sind perfekte Anlaufpunkte, um Inspiration für eigene Operatoren zu finden, oder auch bestehende Operatoren zu finden, mit denen sich Abläufe in der eigenen Kubernetes-Umgebung noch weiter automatisieren lassen.

Verwandte Themen:

Geschrieben von
Martin Helmich
Martin Helmich
Martin Helmich ist Open-Source-Enthusiast, Science-Fiction-Nerd und Systemarchitekt beim ostwestfälischen Webhostingdienstleister Mittwald. Dort beschäftigt er sich unter anderem mit dem Aufbau und Betrieb von Cloud-Infrastrukturen auf Basis von Docker und Kubernetes. An der Fachhochschule in Vechta lehrt er zu den Themen Software Engineering und Datenbanktechnik, und daneben findet man ihn häufig beim Tüfteln an einem seiner Open-Source-Projekte.
Kommentare

Hinterlasse einen Kommentar

avatar
4000
  Subscribe  
Benachrichtige mich zu: