Intro
When I recently decided to port a “non-containerized” application onto Kubernetes, I struggled to find a solid approach to manage its lifecycle (deploy, upgrade, rollback, resize, etc.). It seems that most solutions involve delegating logic to a human or some custom scripts that need to be developed for every application.
This, of course, is far from ideal. Thankfully, CoreOS came up with a solution called Operators.
What is an Operator?
An Operator is a concept that literally replaces boring system administration tasks. An Operator does the lifecycle management of an application running on k8s. A more abstract definition: an Operator is an application aware k8s object that can be implemented using Helm
, Go
or Ansible
(though a Helm Operator cannot manage the entire application lifecycle [read more]).
Routes or Services, for example, are k8s built-in resources. In order to enable k8s to understand your application, you have to create a custom resource and tell k8s how to interpret it using a custom resource definition.
An Operator of your application compares desired state (as described in config files) with the current state and reconciles if necessary.
If choosing an Ansible-based Operator, playbooks, or roles respectively, take the action to reconcile. Typically, a custom resource event triggers an Ansible task.
A considerable advantage of an Operator is, that it can be version controlled.
An Example
An example implementation of an Operator includes the following steps:
- RBAC: Define role-based access controls.
- CRD: Create a custom resource definition.
- DC: Write a deployment config.
- CR: Write a custom resource config.
Role-based Access Control
Create a service account:
apiVersion: v1
kind: ServiceAccount
metadata:
name: example-operator
Create role:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
creationTimestamp: null
name: example-operator
rules:
- apiGroups:
...
Create role binding:
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: example-operator
subjects:
- kind: ServiceAccount
name: example-operator
roleRef:
kind: Role
name: example-operator
apiGroup: rbac.authorization.k8s.io
Custom Resource Definition
Create a custom resource definition that tells k8s about your application:
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: exampleappservices.app.example.com
spec:
group: app.example.com
names:
kind: ExampleAppService
listKind: ExampleAppServiceList
plural: exampleappservices
singular: exampleappservice
scope: Namespaced
subresources:
status: {}
versions:
- name: v1alpha1
served: true
storage: true
Deployment Config
Create a deployment config:
apiVersion: apps/v1
kind: Deployment
metadata:
name: example-operator
spec:
replicas: 1
selector:
matchLabels:
name: example-operator
template:
metadata:
labels:
name: example-operator
spec:
serviceAccountName: example-operator
containers:
- name: ansible
command:
- /usr/local/bin/ao-logs
- /tmp/ansible-operator/runner
- stdout
# Replace this with the built image name
image: "{{ REPLACE_IMAGE }}"
imagePullPolicy: "Always"
volumeMounts:
- mountPath: /tmp/ansible-operator/runner
name: runner
readOnly: true
- name: operator
# Replace this with the built image name
image: "{{ REPLACE_IMAGE }}"
imagePullPolicy: "Always"
volumeMounts:
- mountPath: /tmp/ansible-operator/runner
name: runner
env:
- name: WATCH_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: OPERATOR_NAME
value: "example-operator"
- name: ANSIBLE_GATHERING
value: explicit
volumes:
- name: runner
emptyDir: {}
Custom Resource
Create a custom resource:
apiVersion: app.example.com/v1alpha1
kind: ExampleAppService
metadata:
name: example-exampleappservice
spec:
# Add fields here
size: 3
As you can see, creating an Operator involves an acceptable amount of work. And it gets even better thanks to the Operator SDK which creates all of the required configs for you.
What is the Operator SDK?
The Operator SDK is a framework that provides high level APIs and abstractions to write operational logic more intuitively. Further, it provides useful tools for bootstrapping the implementation of an Operator.
As already mentioned, an Operator can be implemented using Helm
, Go
or Ansible
. In this post, I am focusing on an Ansible-based Operator, because it allows to implement the entire lifecycle and does not require a single line of code.
Steps to build an Operator
After installation of the Operator SDL, follow these steps (tested with Operator SDK v0.12.0):
- Create Operator skeletonAs a result, the Operator SDK generates a couple of files:
operator-sdk new \ --type=ansible --kind ExampleAppService --generate-playbook \ --api-version app.example.com/v1alpha1 example-operator
$ tree example-operator/ example-operator/ ├── build │ ├── Dockerfile │ └── test-framework │ ├── ansible-test.sh │ └── Dockerfile ├── deploy │ ├── crds │ │ ├── app.example.com_exampleappservices_crd.yaml │ │ └── app.example.com_v1alpha1_exampleappservice_cr.yaml │ ├── operator.yaml │ ├── role_binding.yaml │ ├── role.yaml │ └── service_account.yaml ├── molecule # Ansible testing framework │ └── ... ├── playbook.yml ├── roles │ └── exampleappservice │ ├── defaults │ │ └── main.yml │ ├── files │ ├── handlers │ │ └── main.yml │ ├── meta │ │ └── main.yml │ ├── README.md │ ├── tasks │ │ └── main.yml │ ├── templates │ └── vars │ └── main.yml └── watches.yaml
- Add Ansible tasks and configure the Watches file (for test purposes, you can simply leave it as it is).
- Build Operator
- Create and tag container image:
cd example-operator/ && \ operator-sdk build \ registry.com/example/example-operator:v0.0.1 && cd ../
- Push container image:
docker push registry.com/example/example-operator:v0.0.1
- In
example-operator/deploy/operator.yaml
, set image placeholderREPLACE_IMAGE
to the previously-built image:sed -i 's|{{ REPLACE_IMAGE }}|registry.com/example/example-operator:v0.0.1|g' \ example-operator/deploy/operator.yaml
- Create and tag container image:
- Deploy Operator
- Create CRD:
kubectl create -f \ example-operator/deploy/crds/app.example.com_exampleappservices_crd.yaml
- Create service account:
kubectl create -f example-operator/deploy/service_account.yaml`
- Create role:
kubectl create -f example-operator/deploy/role.yaml
- Create role binding:
kubectl create -f example-operator/deploy/role_binding.yaml
- Create Operator deployment object:
kubectl create -f example-operator/deploy/operator.yaml
- Set
imagePullPolicy
toAlways
. For some reason, the variable w/ default value does not work, but that’s another story.
- Set
- Create an instance of your app:
kubectl create -f \ example-operator/deploy/crds/app.example.com_v1alpha1_exampleappservice_cr.yaml
- Create CRD:
- Check Status
- Get deployment overview:
kubectl get deployment
- Get deployment overview:
- Destroy Operator
- Delete the app:
kubectl delete -f \ example-operator/deploy/crds/app.example.com_v1alpha1_exampleappservice_cr.yaml
- Delete the operator:
kubectl delete -f example-operator/deploy/operator.yaml
- Delete the app:
It is straight forward and does the heavy lifting for you. The Watches file (watches.yaml
) defines for what k8s events the Operator has to watch and holds the mapping between events and the Ansible roles that they trigger.
It is crucial that the Ansible tasks are idempotent. These tasks will be executed frequently by the Operator, thus the result should always be the same.
Read more about the Operator Framework in combination with Ansible here: official docs.
You can find a list of pre-built operators here: https://operatorhub.io/.
Sources
Most of the information in this post is based on the following sources: