Putting the CRD in Christmas Decorations

It’s a few days in to the holiday shutdown at work, so I’ve been enjoying some downtime with my family at home. There’s been plenty of last-minute shopping, gift wrapping, baking, and, evidently, building operators with the Operator SDK.

For the unaquainted, the Operator Framework is a toolkit that makes it easy to manage complex applications on top of Kubernetes. While I’ve had the chance to use the Go SDK for a few projects, I’ve recently been reading more about Ansible operators. Ansible operators allow you use Ansible roles to configure an application and respond to any changes to its Kubernetes resources. An Ansible operator allows you to handle complex scenarios just like the Go SDK, but lets you use the familiar Ansible syntax (no Go code required) and take advantage of the large Ansible module ecosystem.

While operators are designed to manage resources inside Kubernetes, they also do a great job at managing resources outside of the cluster such as TLS certificates or even external monitoring checks. As I was thinking of a good first Ansible operator, I looked up from my couch and saw my Christmas tree. I already had many of my Christmas lights integrated with Home Assistant, so why not take advantage of the easy-to-use REST API and automate my Christmas lights?

This post will walk you through the process of how I created my first (admittedly not very useful) Ansible operator.

Creating the operator

If you haven’t already, go ahead and install the operator-sdk CLI tool. We’ll be using this tool to create the initial structure and boilerplate for our operator.

In the commands below, I named it christmas-decoration-operator. The API group is decorations.easte.rs with version v1alpha1. You can choose whatever group name you’d like, though it should be some domain name you own to ensure it does not overlap with another resource. We also set the name of our custom resource to Light via the --kind parameter.

operator-sdk new christmas-decoration-operator --api-version=decorations.easte.rs/v1alpha1 --kind=Light --type=ansible
cd christmas-decoration-operator

Defining the custom resource

Now that we have our Light custom resource, we need to determine its specification. In this case, we simply need 2 parameters to manage a light in Home Assistant: its entity ID and its state (on/off). We’ll add these to the example spec in deploy/crds/decorations.easte.rs_v1alpha1_light_cr.yaml:

apiVersion: decorations.easte.rs/v1alpha1
kind: Light
metadata:
  name: example-light
spec:
  # Add fields here
  entity: "light.example"
  state: "on"

Configuring the operator

While the code generated by the Operator SDK has very sane defaults, we’re going to make a few tweaks and add some custom config.

Watch Configuration

Operators work by watching Kubernetes resources and reacting to changes. When a resource changes, a reconciliation is triggered. In the case of an Ansible operator, reconciliation means a role or playbook is applied to the resource. We’ll be reducing the reconciliation period to 30 seconds in watches.yaml, meaning that the role will be applied every 30 seconds even without changes to the Light resource. This is especially useful for external resources like ours where the state can change in Home Assistant without the operator knowing.

- version: v1alpha1
  group: decorations.easte.rs
  kind: Light
  role: /opt/ansible/roles/light
  reconcilePeriod: 30s

Home Assistant Connection Secret

Our operator needs to connect to Home Assistant to interact with its REST API. Since this operator is namespaced (only manages resources in the same namespace), it will only need to talk to one instance of Home Assistant. We’ll use a secret to store the connection informating, which will be injected into the operator via environment variables. An example secret is available at deploy/secret-example.yaml.

apiVersion: v1
kind: Secret
metadata:
  name: christmas-decoration-operator-hass
stringData:
  baseURL: https://demo.home-assistant.io
  accessToken: hunter2

We’ll update the deployment of our Operator to use this secret as well in deploy/operator.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: christmas-decoration-operator
spec:
  template:
    spec:
      containers:
        - name: operator
          env:
            - name: HASS_BASE_URL
              valueFrom:
                secretKeyRef:
                  name: christmas-decoration-operator-hass
                  key: baseURL
            - name: HASS_ACCESS_TOKEN
              valueFrom:
                secretKeyRef:
                  name: christmas-decoration-operator-hass
                  key: accessToken

Update image and pull policy

There are a few other variables we need to update in the deployment generated by the Operator SDK: the location of the operator’s container image and the image pull policy.

sed -i 's|{{ REPLACE_IMAGE }}|quay.io/patrickeasters/christmas-decoration-operator:v0.0.1|g' deploy/operator.yaml
sed -i 's|{{ pull_policy\|default('\''Always'\'') }}|Always|g' deploy/operator.yaml

## MacOS users will need to use these alternate commands
sed -i "" 's|{{ REPLACE_IMAGE }}|quay.io/patrickeasters/christmas-decoration-operator:v0.0.1|g' deploy/operator.yaml
sed -i "" 's|{{ pull_policy\|default('\''Always'\'') }}|Always|g' deploy/operator.yaml

Creating the role

At last, we need to develop the actual fun part of our operator. The SDK generated a skeleton role named light, which will be run every time a Light resource is being reconciled. Normally, your role would heavily use the k8s Ansible module to manage resources in the cluster, but in our very simple case we’ll just be using the uri module to interact with Home Assistant. I added the following tasks in roles/light/tasks/main.yml:

- name: Validate state param
  fail:
    msg: "state must be 'on' or 'off'"
  when: state is not defined or (state != "on" and state != "off")

- name: Get current state
  uri:
    url: "{{ hass_base_url }}/api/states/{{ entity }}"
    method: GET
    headers:
      authorization: "Bearer {{ hass_access_token }}"
  register: current_state

- name: Set state
  uri:
    url: "{{ hass_base_url }}/api/services/homeassistant/turn_{{ state }}"
    method: POST
    headers:
      authorization: "Bearer {{ hass_access_token }}"
    body:
      entity_id: "{{ entity }}"
    body_format: json
  changed_when: true
  when: current_state.json.state != state

We also need to access those environment variables we created earlier with the Home Assistant connection info, so we’ll add these as role variables in roles/light/vars/main.yml:

hass_base_url: "{{ lookup('env','HASS_BASE_URL') }}"
hass_access_token: "{{ lookup('env','HASS_ACCESS_TOKEN') }}"

Deploying the operator

At last, we have a role and all the other necessary bits needed to run our operator. First we’ll build and push the operator image to a container registry.

operator-sdk build quay.io/patrickeasters/christmas-decoration-operator:v0.0.1
docker push quay.io/patrickeasters/christmas-decoration-operator:v0.0.1

Now we will create all the resources needed to run our operator. We’ll create the custom resource definition (CRD), RBAC, and finally the deployment for the operator.

kubectl create -f deploy/crds/decorations.easte.rs_lights_crd.yaml
kubectl create -f deploy/service_account.yaml
kubectl create -f deploy/role.yaml
kubectl create -f deploy/role_binding.yaml
kubectl create -f deploy/secret.yaml
kubectl create -f deploy/operator.yaml

Creating our first custom resource

Now that we have an operator deployed, it’s waiting for some new lights to manage. In this example, I’m creating a resource for my Christmas tree. Here I’m declaring that the light named light.wall_plug_level in Home Assistant should be turned off.

cat <<EOF | kubectl apply -f -
apiVersion: decorations.easte.rs/v1alpha1
kind: Light
metadata:
  name: tree
spec:
  entity: light.wall_plug_level
  state: "on"
EOF

You can now view a list of your lights inside Kubernetes:

> kubectl get lights
NAME   AGE
tree   14h

View operator logs

Once there is at least one custom resource, we can look at the operator’s logs to see how things are progressing:

# Watch operator logs
kubectl logs deploy/christmas-decoration-operator -c operator -f

# Watch nicely-formatted output of Ansible runs
kubectl logs deploy/christmas-decoration-operator -c ansible -f

See it in action

Now for the really cool part: watching your Christmas lights turn on with a kubectl command. Watch below as my Christmas tree lights turn on in response to the following command that sets the state of my tree “on”:

kubectl patch light tree --type merge -p '{"spec":{"state":"on"}}'

Make something new!

Managing Christmas decorations in Kubernetes isn’t likely to be applicable to most of our jobs, but it’s an easy example to show that you really can do just about anything with an operator. Hopefully this inspires you to try making one yourself.

What are you looking to automate? Let me know on Twitter or in the comments.

comments powered by Disqus