Chapter 23. Operator

An Operator is a Controller that uses a CRD to encapsulate operational knowledge for a specific application in an algorithmic and automated form. The Operator pattern allows us to extend the Controller pattern from the preceding chapter for more flexibility and greater expressiveness.

Problem

You learned in Chapter 22, Controller how to extend the Kubernetes platform in a simple and decoupled way. However, for extended use cases, plain custom controllers are not powerful enough, as they are limited to watching and managing Kubernetes intrinsic resources only. Sometimes we want to add new concepts to the Kubernetes platform, which requires additional domain objects. Let’s say we chose Prometheus as our monitoring solution, and we want to add it as a monitoring facility to Kubernetes in a well-defined way. Wouldn’t it be wonderful if we had a Prometheus resource describing our monitoring setup and all the deployment details, similar to the way we define other Kubernetes resources? Moreover, could we then have resources describing which services we have to monitor (e.g., with a label selector)?

These situations are precisely the kind of use cases where CustomResourceDefinitions (CRDs) are very helpful. They allow extensions of the Kubernetes API, by adding custom resources to your Kubernetes cluster and using them as if they were native resources. Custom resources, together with a Controller acting on these resources, form the Operator pattern.

This quote by Jimmy Zelinskie probably describes the characteristics of Operators best:

An operator is a Kubernetes controller that understands two domains: Kubernetes and something else. By combining knowledge of both areas, it can automate tasks that usually require a human operator that understands both domains.

Solution

As you saw in Chapter 22, Controller, we can efficiently react to state changes of default Kubernetes resources. Now that you understand one half of the Operator pattern, let’s have a look now at the other half—representing custom resources on Kubernetes using CRD resources.

Custom Resource Definitions

With a CRD, we can extend Kubernetes to manage our domain concepts on the Kubernetes platform. Custom resources are managed like any other resource, through the Kubernetes API, and eventually stored in the backend store Etcd. Historically, the predecessors of CRDs were ThirdPartyResources.

The preceding scenario is actually implemented with these new custom resources by the CoreOS Prometheus operator to allow seamless integration of Prometheus to Kubernetes. The Prometheus CRD is defined as in Example 23-1, which also explains most of the available fields for a CRD.

Example 23-1. CustomResourceDefinition
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: prometheuses.monitoring.coreos.com     1
spec:
  group: monitoring.coreos.com                 2
  names:
    kind: Prometheus                           3
    plural: prometheuses                       4
  scope: Namespaced                            5
  version: v1                                  6
  validation:
    openAPIV3Schema: ....                      7
1

Name

2

API group it belongs to

3

Kind used to identify instances of this resource

4

Naming rule for creating the plural form, used for specifying a list of those objects

5

Scope—whether the resource can be created cluster-wide or is specific to a namespace

6

Version of the CRD

7

OpenAPI V3 schema for validation (not shown here)

An OpenAPI V3 schema can also be specified to allow Kubernetes to validate a custom resource. For simple use cases, this schema can be omitted, but for production-grade CRDs, the schema should be provided so that configuration errors can be detected early.

Additionally, Kubernetes allows us to specify two possible subresources for our CRD via the spec field subresources:

scale

With this property, a CRD can specify how it manages its replica count. This field can be used to declare the JSON path, where the number of desired replicas of this custom resource is specified: the path to the property that holds the actual number of running replicas and an optional path to a label selector that can be used to find copies of custom resource instances. This label selector is usually optional, but is required if you want to use this custom resource with the HorizontalPodAutoscaler explained in Chapter 24, Elastic Scale.

status

When we set this property, a new API call is available that only allows you to change the status. This API call can be secured individually and allows for status updates from outside the controller. On the other hand, when we update a custom resource as a whole, the status section is ignored as for standard Kubernetes resources.

Example 23-2 shows a potential subresource path as is also used for a regular Pod.

Example 23-2. Subresource definition for a CustomResourceDefinition
kind: CustomResourceDefinition
# ...
spec:
  subresources:
    status: {}
    scale:
      specReplicasPath: .spec.replicas 1
      statusReplicasPath: .status.replicas 2
      labelSelectorPath: .status.labelSelector 3
1

JSON path to the number of declared replicas

2

JSON path to the number of active replicas

3

JSON path to a label selector to query for the number of active replicas

Once we define a CRD, we can easily create such a resource, as shown in Example 23-3.

Example 23-3. A Prometheus custom resource
apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:
  name: prometheus
spec:
  serviceMonitorSelector:
    matchLabels:
      team: frontend
  resources:
    requests:
      memory: 400Mi

The metadata: section has the same format and validation rules as any other Kubernetes resource. The spec: contains the CRD-specific content, and Kubernetes validates against the given validation rule from the CRD.

Custom resources alone are not of much use without an active component to act on them. To give them some meaning, we need again our well-known Controller, which watches the lifecycle of these resources and acts according to the declarations found within the resources.

Controller and Operator Classification

Before we dive into writing our Operator, let’s look at a few kinds of classifications for Controllers, Operators, and especially CRDs. Based on the Operator’s action, broadly the classifications are as follows:

Installation CRDs

Meant for installing and operating applications on the Kubernetes platform. Typical examples are the Prometheus CRDs, which we can use for installing and managing Prometheus itself.

Application CRDs

In contrast, these are used to represent an application-specific domain concept. This kind of CRD allows applications deep integration with Kubernetes, which involves combining Kubernetes with an application-specific domain behavior. For example, the ServiceMonitor CRD is used by the Prometheus operator to register specific Kubernetes Services for being scraped by a Prometheus server. The Prometheus operator takes care of adapting the Prometheus’ server configuration accordingly.

Note

Note that an Operator can act on different kinds of CRDs as the Prometheus operator does in this case. The boundary between these two categories of CRDs is blurry.

In our categorization of Controller and Operator, an Operator is-a Controller that uses CRDs.1 However, even this distinction is a bit fuzzy as there are variations in between.

One example is a controller, which uses a ConfigMap as a kind of replacement for a CRD. This approach makes sense in scenarios where default Kubernetes resources are not enough, but creating CRDs is not feasible either. In this case, ConfigMap is an excellent middle ground, allowing encapsulation of domain logic within the content of a ConfigMap. An advantage of using a plain ConfigMap is that you don’t need to have the cluster-admin rights you need when registering a CRD. In certain cluster setups, it is just not possible for you to register such a CRD (e.g., like when running on public clusters like OpenShift Online).

However, you can still use the concept of Observe-Analyze-Act when you replace a CRD with a plain ConfigMap that you use as your domain-specific configuration. The drawback is that you don’t get essential tool support like kubectl get for CRDs; you have no validation on the API Server level, and no support for API versioning. Also, you don’t have much influence on how you model the status: field of a ConfigMap, whereas for a CRD you are free to define your status model as you wish.

Another advantage of CRDs is that you have a fine-grained permission model based on the kind of CRD, which you can tune individually. This kind of RBAC security is not possible when all your domain configuration is encapsulated in ConfigMaps, as all ConfigMaps in a namespace share the same permission setup.

From an implementation point of view, it matters whether we implement a controller by restricting its usage to vanilla Kubernetes objects or whether we have custom resources managed by the controller. In the former case, we already have all types available in the Kubernetes client library of our choice. For the CRD case, we don’t have the type information out of the box, and we can either use a schemaless approach for managing CRD resources, or define the custom types on our own, possibly based on an OpenAPI schema contained in the CRD definition. Support for typed CRDs varies by client library and framework used.

Figure 23-1 shows our Controller and Operator categorization starting from simpler resource definition options to more advanced with the boundary between Controller and Operator being the use of custom resources.

Spectrum of Controllers and Operators
Figure 23-1. Spectrum of Controllers and Operators

For Operators, there is even a more advanced Kubernetes extension hook option. When Kubernetes-managed CRDs are not sufficient to represent a problem domain, you can extend the Kubernetes API with an own aggregation layer. We can add a custom implemented APIService resource as a new URL path to the Kubernetes API.

To connect a given Service with name custom-api-server and backed by a Pod with your service, you can use a resource like that shown in Example 23-4.

Example 23-4. API aggregation with a custom APIService
apiVersion: apiregistration.k8s.io/v1beta1
kind: APIService
metadata:
  name: v1alpha1.sample-api.k8spatterns.io
spec:
  group: sample-api.k8spattterns.io
  service:
    name: custom-api-server
  version: v1alpha1

Besides the Service and Pod implementation, we need some additional security configuration for setting up the ServiceAccount under which the Pod is running.

Once it is set up, every request to the API Server https://<api server ip>/apis/sample-api.k8spatterns.io/v1alpha1/namespaces/<ns>/... is directed to our custom Service implementation. It’s up to this custom Service’s implementation to handle these requests, including persisting the resources managed via this API. This approach is different from the preceding CRD case, where Kubernetes itself completely manages the custom resources.

With a custom API Server, you have many more degrees of freedom, which allows going beyond watching resource lifecycle events. On the other hand, you also have to implement much more logic, so for typical use cases, an operator dealing with plain CRDs is often good enough.

A detailed exploration of the API Server capabilities is beyond the scope of this chapter. The official documentation as well as a complete sample-apiserver have more detailed information. Also, you can use the apiserver-builder library, which helps with implementing API Server aggregation.

Now, let’s see how we can develop and deploy our operators with CRDs.

Operator Development and Deployment

At the time of this writing (2019), operator development is an area of Kubernetes that is actively evolving, with several toolkits and frameworks available for writing operators. The three main projects aiding in the creation of operators are as follows:

  • CoreOS Operator Framework

  • Kubebuilder developed under the SIG API Machinery of Kubernetes itself

  • Metacontroller from Google Cloud Platform

We touch on them very briefly next, but be aware that all of these projects are quite young and might change over time or even get merged.

Operator framework

The Operator Framework provides extensive support for developing Golang-based operators. It provides several subcomponents:

  • The Operator SDK provides a high-level API for accessing a Kubernetes cluster and a scaffolding to start up an operator project

  • The Operator Lifecycle Manager manages the release and updates of operators and their CRDs. You can think about it as a kind of “operator operator.”

  • Operator Metering enables using reporting for operators.

We won’t go into much detail here about the Operator SDK, which is still evolving, but the Operator Lifecycle Manager (OLM) provides particularly valuable help when using operators. One issue with CRDs is that these resources can be registered only cluster-wide and hence require cluster-admin permissions.2 While regular Kubernetes users can typically manage all aspects of the namespaces they have granted access to, they can’t just use operators without interaction with a cluster administrator.

To streamline this interaction, the OLM is a cluster service running in the background under a service account with permissions to install CRDs. A dedicated CRD called ClusterServiceVersion (CSV) is registered along with the OLM and allows us to specify the Deployment of an operator together with references to the CRD definitions associated with this operator. As soon as we have created such a CSV, one part of the OLM waits for that CRD and all of its dependent CRDs to be registered. If this is the case, the OLM deploys the operator specified in the CSV. Another part of the OLM can be used to register these CRDs on behalf of a nonprivileged user. This approach is an elegant way to allow regular cluster users to install their operators.

Kubebuilder

Kubebuilder is a project by the SIG API Machinery with comprehensive documentation.3 Like the Operator SDK, it supports scaffolding of Golang projects and the management of multiple CRDs within one project.

There is a slight difference from the Operator Framework in that Kubebuilder works directly with the Kubernetes API, whereas the Operator SDK adds some extra abstraction on top of the standard API, which makes it easier to use (but lacks some bells and whistles).

The support for installing and managing the lifecycle of an operator is not as sophisticated as the OLM from the Operator Framework. However, both projects have a significant overlap, they might eventually converge in one way or another.

Metacontroller

Metacontroller is very different from the other two operator building frameworks as it extends Kubernetes with APIs that encapsulate the common parts of writing custom controllers. It acts similarly to Kubernetes Controller Manager by running multiple controllers that are not hardcoded but defined dynamically through Metacontroller-specific CRDs. In other words, it’s a delegating controller that calls out to the service providing the actual Controller logic.

Another way to describe Metacontroller would be as declarative behavior. While CRDs allow us to store new types in Kubernetes APIs, Metacontroller makes it easy to define the behavior for standard or custom resources declaratively.

When we define a controller through Metacontroller, we have to provide a function that contains only the business logic specific to our controller. Metacontroller handles all interactions with the Kubernetes APIs, runs a reconciliation loop on our behalf, and calls our function through a webhook. The webhook gets called with a well-defined payload describing the CRD event. As the function returns the value, we return a definition of the Kubernetes resources that should be created (or deleted) on behalf of our controller function.

This delegation allows us to write functions in any language that can understand HTTP and JSON, and that do not have any dependency on the Kubernetes API or its client libraries. The functions can be hosted on Kubernetes, or externally on a Functions-as-a-Service provider, or somewhere else.

We cannot go into many details here, but if your use case involves extending and customizing Kubernetes with simple automation or orchestration, and you don’t need any extra functionality, you should have a look at Metacontroller, especially when you want to implement your business logic in a language other than Go. Some controller examples will demonstrate how to implement StatefulSet, Blue-Green Deployment, Indexed Job, and Service per Pod by using Metacontroller only.

Example

Let’s have a look at a concrete Operator example. We extend our example in Chapter 22, Controller and introduce a CRD of type ConfigWatcher. An instance of this CRD specifies then a reference to the ConfigMap to watch and which Pods to restart if this ConfigMap changes. With this approach, we remove the dependency of the ConfigMap on the Pods, as we don’t have to modify the ConfigMap itself to add triggering annotations. Also, with our simple annotation-based approach in the Controller example, we can connect only a ConfigMap to a single application, too. With a CRD, arbitrary combinations of ConfigMaps and Pods are possible.

This ConfigWatcher custom resource looks like that in Example 23-5.

Example 23-5. Simple ConfigWatcher resource
kind: ConfigWatcher
apiVersion: k8spatterns.io/v1
metadata:
  name: webapp-config-watcher
spec:
  configMap: webapp-config    1
  podSelector:                2
    app: webapp
1

Reference to ConfigMap to watch

2

Label selector to determine Pods to restart

In this definition, the attribute configMap references the name of the ConfigMap to watch. The field podSelector is a collection of labels and their values, which identify the Pods to restart.

We can define the type of this custom resource with a CRD, as shown in Example 23-6.

Example 23-6. ConfigWatcher CRD
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: configwatchers.k8spatterns.io
spec:
  scope: Namespaced                   1
  group: k8spatterns.io               2
  version: v1                         3
  names:
    kind: ConfigWatcher               4
    singular: configwatcher           5
    plural: configwatchers
  validation:
    openAPIV3Schema:                  6
      properties:
        spec:
          properties:
            configMap:
              type: string
              description: "Name of the ConfigMap"
            podSelector:
              type: object
              description: "Label selector for Pods"
              additionalProperties:
                type: string
1

Connected to a namespace

2

Dedicated API group

3

Initial version

4

Unique kind of this CRD

5

Labels of the resource as used in tools like kubectl

6

OpenAPI V3 schema specification for this CRD

For our operator to be able to manage custom resources of this type, we need to attach a ServiceAccount with the proper permissions to our operator’s Deployment. For this task, we introduce a dedicated Role that is used later in a RoleBinding to attach it to the ServiceAccount in Example 23-7.

Example 23-7. Role definition allowing access to custom resource
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: config-watcher-crd
rules:
- apiGroups:
  - k8spatterns.io
  resources:
  - configwatchers
  - configwatchers/finalizers
  verbs: [ get, list, create, update, delete, deletecollection, watch ]

With these CRDs in place, we can now define custom resources as in Example 23-5.

To make sense of these resources, we have to implement a controller that evaluates these resources and triggers a Pod restart when the ConfigMap changes.

We expand here on our Controller script in Example 22-2 and adapt the event loop in the controller script.

In the case of a ConfigMap update, instead of checking for a specific annotation, we do a query on all resources of kind ConfigWatcher and check whether the modified ConfigMap is included as a configMap: value. Example 23-8 shows the reconciliation loop. Refer to our Git repository for the full example, which also includes detailed instructions for installing this operator.

Example 23-8. WatchConfig controller reconciliation loop
curl -Ns $base/api/v1/${ns}/configmaps?watch=true |      1
while read -r event
do
  type=$(echo "$event" | jq -r '.type')
  if [ $type = "MODIFIED" ]; then                         2

    watch_url="$base/apis/k8spatterns.io/v1/${ns}/configwatchers"
    config_map=$(echo "$event" | jq -r '.object.metadata.name')

    watcher_list=$(curl -s $watch_url | jq -r '.items[]') 3

    watchers=$(echo $watcher_list |                      4
               jq -r "select(.spec.configMap == "$config_map") | .metadata.name")

    for watcher in watchers; do                           5
      label_selector=$(extract_label_selector $watcher)
      delete_pods_with_selector "$label_selector"
    done
  fi
done
1

Start a watch stream to watch for ConfigMap changes for a given namespace.

2

Check for a MODIFIED event only.

3

Get a list of all installed ConfigWatcher custom resources.

4

Extract from this list all ConfigWatcher elements that refer to this ConfigMap.

5

For every ConfigWatcher found, delete the configured Pod via a selector. The logic for calculating a label selector as well as the deletion of the Pods are omitted here for clarity. Refer to the example code in our Git repository for the full implementation.

As for the Controller example, this controller can be tested with a sample web application that is provided in our example Git repository. The only difference with this Deployment is that we use an unannotated ConfigMap for the application configuration.

Although our operator is quite functional, it is also clear that our shell script-based operator is still quite simple and doesn’t cover edge or error cases. You can find many more interesting, production-grade examples in the wild.

Awesome Operators has a nice list of real-world operators that are all based on the concepts covered in this chapter. We have already seen how a Prometheus operator can manage Prometheus installations. Another Golang-based operator is the Etcd Operator for managing an Etcd key-value store and automating operational tasks like backing up and restoring of the database.

If you are looking for an operator written in the Java programming languages, the Strimzi Operator is an excellent example of an operator that manages a complex messaging system like Apache Kafka on Kubernetes. Another good starting point for Java-based operators is the JVM Operator Toolkit, which provides a foundation for creating operators in Java and JVM-based languages like Groovy or Kotlin and also comes with a set of examples.

Discussion

While we have seen how to extend the Kubernetes platform, Operators are still not a silver bullet. Before using an operator, you should carefully look at your use case to determine whether it fits the Kubernetes paradigm.

In many cases, a plain Controller working with standard resources is good enough. This approach has the advantage that it doesn’t need any cluster-admin permission to register a CRD but has its limitations when it comes to security or validation.

An Operator is a good fit for modeling a custom domain logic that fits nicely with the declarative Kubernetes way of handling resources with reactive controllers.

More specifically, consider using an Operator with CRDs for your application domain for any of the following situations:

  • You want tight integration into the already existing Kubernetes tooling like kubectl.

  • You are at a greenfield project where you can design the application from the ground up.

  • You benefit from Kubernetes concepts like resource paths, API groups, API versioning, and especially namespaces.

  • You want to have already good client support for accessing the API with watches, authentication, role-based authorization, and selectors for metadata.

If your custom use case fits these criteria, but you need more flexibility in how custom resources can be implemented and persisted, consider using a custom API Server. However, you should also not consider Kubernetes extension points as the golden hammer for everything.

If your use case is not declarative, if the data to manage does not fit into the Kubernetes resource model, or you don’t need a tight integration into the platform, you are probably better off writing your standalone API and exposing it with a classical Service or Ingress object.

The Kubernetes documentation itself also has a chapter for suggestions on when to use a Controller, Operator, API aggregation, or custom API implementation.

1 is-a emphasizes the inheritance relationship between Operator and Controller, that an Operator has all characteristics of a Controller plus a bit more

2 This restriction might be lifted in the future, as there are plans for namespace-only registration of CRDs.

3 SIGs (Special Interest Groups) is the way the Kubernetes community organizes feature areas. You can find a list of current SIGs on the Kubernetes GitHub repository.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset