Skip to content

How hard can it be? – Creating your own Kubernetes Operator with Ansible – Part 1

One can extend the power of Kubernetes using a so called Operator. A Operator is a piece of software, that runs constantly in a loop and checks for resources to manage. A Operator normally is written in GoLang, but the Operator Framework of Kubernetes enables you also to create one using Ansible roles or playbooks.

Why to use Ansible to create an Operator you might ask? I admit, it comes with some downsides on the flexibility and power. In the end, it’s more flexible to write a pure Go program that can handle all sort of states. But I’m not Go battleproof (far from it), and so I took a look into the Ansible Operator.

In Part 1 of this series, we create an Operator, that does watch for Helloworld resources and spins up hello-world pods in a customizeable count. It’s just to get started. In part 2, we will dive down more in detail and create something with an actual use. You can find all code written in this post also in my Github repository.

So what it takes to get this started? Here are the prerequisites:

To create our own Operator, we use the Operator SDK. A framework that builds us the basis for an Operator. In case of an Ansible Operator, it creates the role structure and all configurations we need. Let’s start from scratch and initialize our hello-world-operator with the Operator SDK.

mkdir hello-world-operator
cd hello-world-operator

operator-sdk init hello-world-operator \
--domain thedatabase.me \
--plugins=ansible

Writing kustomize manifests for you to edit...
Next: define a resource with:
$ operator-sdk create api

As told us from the output, the next step is to create the API.

operator-sdk create api --version v1alpha1 \
--kind Helloworld \
--generate-role

Writing kustomize manifests for you to edit..

Let’s have a look what has been created by the SDK.

❯ tree .
.
├── config
│   ├── crd
│   │   ├── bases
│   │   │   └── thedatabase.me_helloworlds.yaml
│   │   └── kustomization.yaml
│   ├── default
│   │   ├── kustomization.yaml
│   │   ├── manager_auth_proxy_patch.yaml
│   │   └── manager_config_patch.yaml
│   ├── manager
│   │   ├── controller_manager_config.yaml
│   │   ├── kustomization.yaml
│   │   └── manager.yaml
│   ├── manifests
│   │   └── kustomization.yaml
│   ├── prometheus
│   │   ├── kustomization.yaml
│   │   └── monitor.yaml
│   ├── rbac
│   │   ├── auth_proxy_client_clusterrole.yaml
│   │   ├── auth_proxy_role_binding.yaml
│   │   ├── auth_proxy_role.yaml
│   │   ├── auth_proxy_service.yaml
│   │   ├── helloworld_editor_role.yaml
│   │   ├── helloworld_viewer_role.yaml
│   │   ├── kustomization.yaml
│   │   ├── leader_election_role_binding.yaml
│   │   ├── leader_election_role.yaml
│   │   ├── role_binding.yaml
│   │   ├── role.yaml
│   │   └── service_account.yaml
│   ├── samples
│   │   ├── kustomization.yaml
│   │   └── _v1alpha1_helloworld.yaml
│   ├── scorecard
│   │   ├── bases
│   │   │   └── config.yaml
│   │   ├── kustomization.yaml
│   │   └── patches
│   │       ├── basic.config.yaml
│   │       └── olm.config.yaml
│   └── testing
│       ├── debug_logs_patch.yaml
│       ├── kustomization.yaml
│       ├── manager_image.yaml
│       └── pull_policy
│           ├── Always.yaml
│           ├── IfNotPresent.yaml
│           └── Never.yaml
├── Dockerfile
├── Makefile
├── molecule
│   ├── default
│   │   ├── converge.yml
│   │   ├── create.yml
│   │   ├── destroy.yml
│   │   ├── kustomize.yml
│   │   ├── molecule.yml
│   │   ├── prepare.yml
│   │   ├── tasks
│   │   │   └── helloworld_test.yml
│   │   └── verify.yml
│   └── kind
│       ├── converge.yml
│       ├── create.yml
│       ├── destroy.yml
│       └── molecule.yml
├── playbooks
├── PROJECT
├── requirements.yml
├── roles
│   └── helloworld
│       ├── defaults
│       │   └── main.yml
│       ├── files
│       ├── handlers
│       │   └── main.yml
│       ├── meta
│       │   └── main.yml
│       ├── README.md
│       ├── tasks
│       │   └── main.yml
│       ├── templates
│       └── vars
│           └── main.yml
└── watches.yaml

28 directories, 58 files

We concentrate for now on two things. The watches.yaml and all what’s under the roles directory. The watches.yaml tells Kubernetes for which custom resource (we configured a kind: Helloworld as custom resource. This then calls our Ansible role.

---
# Use the 'create api' subcommand to add watches to this file.
- version: v1alpha1
  group: thedatabase.me
  kind: Helloworld
  role: helloworld
#+kubebuilder:scaffold:watch

Everything that’s under the roles directory, describes our Ansible role which runs then as the upper mentioned loop. So every time, a new custom resource of kind Helloworld is created or updated, our role will be called to do it’s magic. So let’s implement in our role what we want to get done on the Kubernetes cluster. We do so in the roles/helloworld/tasks/main.yml. Let’s add this deployment:

---

- name: Start hello-world
  kubernetes.core.k8s:
    definition:
      kind: Deployment
      apiVersion: apps/v1
      metadata:
        name: '{{ ansible_operator_meta.name }}-helloworld'
        namespace: '{{ ansible_operator_meta.namespace }}'
      spec:
        replicas: "{{ size }}"
        selector:
          matchLabels:
            app: helloworld
        template:
          metadata:
            labels:
              app: helloworld
          spec:
            containers:
            - name: helloworld
              image: "docker.io/hello-world:latest"

In order to get a default value for size (so the amount of replicas), we also add the following to roles/helloworld/defaults/main.yml.

size: 1

To get the operator rolled out, we need to create the container image. Here comes the container registry in place which I mentioned above. Before you push it, you need to update the Makefile you can find in the projects root folder. Change the line IMG ?= controller:latest fitting to your needs. In my case, I push my image to the Github Container Registry. Keep in mind, that you either need to provide the credentials to your private Docker registry or you make it public.

IMG ?= ghcr.io/thedatabaseme/hello-world-operator:$(VERSION)

Now build and push the Operator image.

make docker-build docker-push 

And finally, deploy the Operator:

make deploy
cd config/manager && /usr/local/bin/kustomize edit set image controller=ghcr.io/thedatabaseme/hello-world-operator:0.0.1
/usr/local/bin/kustomize build config/default | kubectl apply -f -
namespace/hello-world-operator-system created
customresourcedefinition.apiextensions.k8s.io/helloworlds.thedatabase.me created
serviceaccount/hello-world-operator-controller-manager created
role.rbac.authorization.k8s.io/hello-world-operator-leader-election-role created
clusterrole.rbac.authorization.k8s.io/hello-world-operator-manager-role created
clusterrole.rbac.authorization.k8s.io/hello-world-operator-metrics-reader created
clusterrole.rbac.authorization.k8s.io/hello-world-operator-proxy-role created
rolebinding.rbac.authorization.k8s.io/hello-world-operator-leader-election-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/hello-world-operator-manager-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/hello-world-operator-proxy-rolebinding created
configmap/hello-world-operator-manager-config created
service/hello-world-operator-controller-manager-metrics-service created
deployment.apps/hello-world-operator-controller-manager created

Let’s check the status of the Operator then:

kubectl get all -n hello-world-operator-system

NAME                                                           READY   STATUS    RESTARTS   AGE
pod/hello-world-operator-controller-manager-5d654f8689-8stj4   2/2     Running   0          6m26s

NAME                                                              TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
service/hello-world-operator-controller-manager-metrics-service   ClusterIP   10.107.246.135   <none>        8443/TCP   8m10s

NAME                                                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/hello-world-operator-controller-manager   1/1     1            1           8m10s

NAME                                                                 DESIRED   CURRENT   READY   AGE
replicaset.apps/hello-world-operator-controller-manager-5d654f8689   1         1         1       8m10s

So what’s left now, is to create a CRD of the kind Helloworld. Therefore, a sample manifest has been created by the SDK under config/samples/_v1alpha1_helloworld.yaml. Let’s adjust the size specification right away.

apiVersion: thedatabase.me/v1alpha1
kind: Helloworld
metadata:
  name: helloworld-sample
  namespace: helloworld
spec:
  size: 3

And apply the manifest (but create the namespace helloworld first).

kubectl create ns helloworld
namespace/helloworld created
kubectl apply -f config/samples/_v1alpha1_helloworld.yaml
helloworld.thedatabase.me/helloworld-sample created

kubectl get all -n helloworld
NAME                                                READY   STATUS             RESTARTS      AGE
pod/helloworld-sample-helloworld-698884b845-4jvll   0/1     Completed          3 (30s ago)   48s
pod/helloworld-sample-helloworld-698884b845-tx999   1/1     Running            3 (30s ago)   48s
pod/helloworld-sample-helloworld-698884b845-vgvfb   0/1     CrashLoopBackOff   2 (27s ago)   48s

NAME                                           READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/helloworld-sample-helloworld   1/3     3            1           48s

NAME                                                      DESIRED   CURRENT   READY   AGE
replicaset.apps/helloworld-sample-helloworld-698884b845   3         3         1       48s

As you can see, the Operator has created a deployment which created three hello-world pods. This example is a bit misfitting, cause the hello-world pods are running only for a second and then are completed. The deployment leads to, that the pods getting restarted over and over again. But the Operator does what we wanted him to do.

So there’s only one thing missing for now. To undeploy the hello-world-operator again. This can be done by the make command also.

make undeploy
/usr/local/bin/kustomize build config/default | kubectl delete -f -
namespace "hello-world-operator-system" deleted
customresourcedefinition.apiextensions.k8s.io "helloworlds.thedatabase.me" deleted
serviceaccount "hello-world-operator-controller-manager" deleted
role.rbac.authorization.k8s.io "hello-world-operator-leader-election-role" deleted
clusterrole.rbac.authorization.k8s.io "hello-world-operator-manager-role" deleted
clusterrole.rbac.authorization.k8s.io "hello-world-operator-metrics-reader" deleted
clusterrole.rbac.authorization.k8s.io "hello-world-operator-proxy-role" deleted
rolebinding.rbac.authorization.k8s.io "hello-world-operator-leader-election-rolebinding" deleted
clusterrolebinding.rbac.authorization.k8s.io "hello-world-operator-manager-rolebinding" deleted
clusterrolebinding.rbac.authorization.k8s.io "hello-world-operator-proxy-rolebinding" deleted
configmap "hello-world-operator-manager-config" deleted
service "hello-world-operator-controller-manager-metrics-service" deleted
deployment.apps "hello-world-operator-controller-manager" deleted

I hope you look forward for the second part of the series.

Philip

Leave a Reply

Your email address will not be published. Required fields are marked *