Zero To Operator
In 2.2 seconds 90 minutes (give-or-take)
Who am I?
Solly Ross (@directxman12 / metamagical.dev)
Software Engineer on GKE and KubeBuilder Maintainer
My mission is to make writing Kubernetes extensions less arcane
First of all, what’s an Operator?
Okay, please pre-emptively retrive your pitchforks bikeshedding
keyboards.
First of all, what’s an Operator?
A controller is a loop that reads desired state (spec
),
observed cluster state (others’ status
), and external state, and the
reconciles cluster state and external state with the desired state,
writing any observations down (to our own status
).
All of Kubernetes functions on this model.
An operator is a controller that encodes human operational knowledge: how do I run and manage a specific piece of complex software.
All operators are controllers, but not all controllers are operators.
How’s this going to work?
We’ll learn about the concepts…
…then code the implementation…
…and try it out against an actual cluster
How’s this going to work?
Scaffold & Design
Types
Behavior
Launching
Without further ado…
Without further ado…
🙋🏼♀️
Wait a minute, I feel like I’ve seen this somewhere before!
Yeah, but we’re more declarative now!
Alright, all set with the ado
Let’s talk about KubeBuilder, scaffolding, and CRD design!
What’s KubeBuilder?
and how do I captialize it?
Building blocks + opinions
KubeBuilder is a set of tooling and opinions how about how to structure custom controllers and operators, built on top of…
controller-runtime, which contains libraries for building the controller part of your operator, and…
controller-tools, which contains tools for generating CustomResourceDefinitions, etc for your operator
Enough talk, let’s build something!
We’ll be building an operator for a simple bespoke application: the Kubernetes Guestbook example.
The guestbook has two components: a frontend PHP app and a Redis instance (the backend).
We’ll need to manage and deploy both for the app to work, and we’ll want to expose the frontend via a service.
Enough talk, let’s build something!
What do we need?
KubeBuilder, plus an unlimitted supply of Xena tapes and hot pockets
Go 1.12+ (and probably git):
~ $ ~ $
* See also https://kubernetes.io/docs/tutorials/stateless-application/guestbook/
Go-go-gadget KubeBuilder!
Initialize a new Go module to hold the project
Initialize a new KubeBuilder project
Generate a Deployment for running the controller manager in Kubernetes
Configure the API Group suffix (
webapp --> webapp.metamagical.dev
1)
metamagical.dev is my own domain; please use yours here 🙂 ↩
Groups and Versions and Kinds, oh my!
An API group is a collection of related API types.
We call each API type a Kind.
Each API group has one or more API versions, which let us change the API over time
Each Kind is used in at least one Resource, which is a use
the
Kind in the API (generally, these are one-to-one with Kinds). They’re referred
to in lower-case.
Each Go type corresponds to a particular Group-Version-Kind.
What is an API, but a complicated pile of YAML?
Spec + Status + Metadata + List
- Spec
- holds desired state
- Status
- holds observed states
- Metadata
- holds name/namespace/etc
- List
- holds many objects
Practically speaking…
$
Practically speaking…
The root object holds the spec, status and metadata.
The list holds multiple root objects.
We use marker comments 2 like // +marker
to indicate
additional metadata about the types
On the root object, we can use markers to specify data about how the CRD behaves in general. Here, we specify that:
we’re using the status subresource (
// +kubebuilder:subresource:status
)we want custom print columns to show up in
kubectl get
output (// +kubebuider:printcolumn
)
Practically speaking…
The spec holds some desired state.
Each field has a json tag specifying the field name in the JSON/YAML3.
On spec (and status), markers specify metadata about types and fields, such as:
validation (
// +kubebulder:validation:xyz
)default values for the server to apply, without needing a webhook (
// +kubebuilder:default
)whether a field is optional or required (
// +optional
)
generally, it should be the same as the field name, but in
camelCase
instead ofPascalCase
. ↩
Practically speaking…
The status holds observed state. Status should always be recreatable from the state of the world. Don’t store information here that you don’t care about losing.
We use the same types, structures, and markers from the spec here.
Practically speaking…
We also need similar types for Redis. Printcolumns left as an excercise to the reader :-)
Notice that:
we can use godoc to set API documentation for our types
we can separate markers from fields by whitespace to help organize
A bit more detail on those points…
When we implement Kubernetes APIs, there’s a couple things to keep in mind:
We allow generally allow most Go types, with a couple exceptions:
floats aren’t allowed – use
resource.Quantity
instead 4We use tagged unions instead of interfaces
When we create optional fields, it’s important to think about whether or not we want the zero value to be usuable. When in doubt use a pointer for optional values
floats don’t round trip through different systems without changing, whereas
resource.Quantity
is consistent. You’ve probably seen Quantities in the resource requirements section of the Pod spec, like500m
. ↩
Let’s try it out
First, we’ll make sure our sample is all set…
Let’s try it out
…then, we’ll actually test it against the cluster! 5
$
$
$
$
Jump to
git checkout 86d516
to follow along ↩
Yeah, but how do I make it go?
Read, reconcile, repeat
Read our root object
Fetch other objects we care about
Ensure those objects are in the right state
Write our root object’s status
Soooo…. how do I do that?
We’ll get a Request to reconcile, and fetch the corresponding objects with a Client.
We’ll set up our desired state6, marking our child objects as owned by our root object.
We’ll ask the server to apply that state correctly with everyone else’s changes.
We’ll return a Result saying we’re all done processing for now, or an error saying to try again in a bit7.
but what was in those helper functions?
Basically just the Go form of Kubernetes objects, but only what we care about!
When writing controllers, we want to be declarative and tolerate changes by other components.
Server-Side Apply lets us declare the structure that we care about, and let the server take care of merging those changes into the object 8.
We also set the owner reference, so that we keep track of which Guestbook owns these objects.
In the words of Picard:
make it so!
↩
Now, we just need some wiring!
Controller Wiring 101
A controller is responsible for executing our logic. We call that logic a reconciler.
Each controller functions on (is for) a single Kind.
This kind may own other Kinds that it creates, or watch Kinds that are otherwise related.
For instance, our GuestBook controller:
is for GuestBooks
will own Services and Deployments to run and expose the frontend PHP app.
watches Redis-es 9 to see the leader and follower service names.
Redi? What’s the plural of Redis anyway? 10 points to anyone who can come for with a reasonable yet completely false linguistic explanation for the plural of their choice. ↩
Wire it up…
We’ll wrap the code from above in the Reconcile
function (which is part
of the Reconcile
interface).
We’ll provide a helper to set up the controller to run as part of a manager, which is responsible for coordinating all the controllers.
We’ll need to add an index on the RedisName
field so that we can
tell our controller how a given Redis relates back to one or more
GuestBooks in booksUsingRedis
10.
To do this, we’ll use
r.List()
with theclient.MatchingField
option, as we’ll see a bit later. ↩
…add the watch helper…
We just use our List()
method to list all GuestBook items that match
the index on redisName
.
Then, we map those GuestBook instances to reconcile Requests.
…and try it out!
Let’s give it a try: 11
(terminal 1) $
(terminal 2) $
Jump to
git checkout cbad60
to follow along ↩
Run as cluster admin – who needs permissions anyway?
Well, actually we do 🤦
We’ll use the // +kubebuilder:rbac
marker to add additional RBAC
permissions to our controller.
We’ve already got all thre permissions for our types, and we’ll need to add GET, LIST, WATCH, and PATCH 12 for Deployments and Services.
as you might’ve noticed,
PATCH
is the verb we use for Server-Side Apply ↩
My work laptop is a viable deployment platform, right?
We’ll need to build an image containing our controller manager and push it somewhere we can use it.
Then, we’ll deploy it to our cluster.
Finally, we’ll wait a bit, then open the browser with the URL from the status of our object, and we should get a working guestbook! 13
Jump to
git checkout 605890
to follow along ↩
That’s all folks!
KubeBuilder: book.kubebuilder.io
controller-runtime godocs: godoc.org/sigs.k8s.io/controller-runtime
This (and other) workshops: pres.metamagical.dev/kubecon-us-2019/code
These slides: pres.metamagical.dev/kubecon-us-2019