Autopilot: an operator framework for building workflows on top of service mesh
Autopilot is a recently announced and open-sourced project from Solo.io that gives you a framework for building opinionated operators for automated workflows on top of a service mesh. These types of workflows typically take signals or telemetry from the environment to decide what action to take next. Just like a “pilot” observes their surrounding and makes decisions on how best to guide the air plane, an autopilot automates those decisions.
At Solo.io, we believe the true power in service mesh comes from their respective programmable interfaces. Autopilot allows you to automate the service mesh interface to do interesting things like canary automation, chaos experimentation, adaptive security, and more. In the past, doing so would have been brittle, hand-crafted and bespoke. Let’s take a closer look.
Autopilot in action
Autopilot lets you define the states for your automated workflow, and generates the scaffolding for the controller that lets you plug in your business logic. With this new project, you define a new Custom Resource Definition (CRD) which will be used to configure the controller.
Autopilot init
The best way to understand Autopilot is by example. Download one of the latest releases and follow along using ap
CLI.
The first thing we need to do is initialize our new project with ap init
ceposta@postamaclab(src) $ ap init example && cd example
INFO[0000] Creating Project Config: kind:"Example" apiVersion:"example.io/v1" operatorName:"example-operator" phases:<name:"Initializing" description:"Example has begun initializing" initial:true outputs:"virtualservices" > phases:<name:"Processing" description:"Example has begun processing" inputs:"metrics" outputs:"virtualservices" > phases:<name:"Finished" description:"Example has finished" final:true > go: creating new go.mod: module example
This init
step created a couple initial resources that are used to define the state-machine for our controller loop:
ceposta@postamaclab(example) $ ls -l
total 152
-rw-r--r-- 1 ceposta staff 119 Nov 17 09:49 autopilot-operator.yaml
-rw-r--r-- 1 ceposta staff 376 Nov 17 09:49 autopilot.yaml
-rw-r--r-- 1 ceposta staff 2133 Nov 17 09:49 go.mod
-rw-r--r-- 1 ceposta staff 63818 Nov 17 09:49 go.sum
The key file from this list is the autopilot.yaml
file which defines the Autopilot “phases” or set of states the control lop can be in. Let’s take a look:
apiVersion: example.io/v1 kind: Example operatorName: example-operator phases:
- description: Example has begun initializing initial: true name: Initializing outputs: - virtualservices
- description: Example has begun processing inputs: - metrics name: Processing outputs: - virtualservices
- description: Example has finished final: true name: Finished
Here we see three phases: Initializing, Processing, and Finished. If you were building a control loop for driving a canary release, you might have something like: Initializing, Waiting, Promoting, Rollback, etc with transitions from one to the other as well between each other where appropriate.
Notice in the phase definition, we specify what the inputs and outputs that will drive the parameter set that will drive the business logic behind each phase.
Autopilot generate
Once we’ve defined the phases for our control loop, we can generate the rest of the scaffolding for our project.
ceposta@postamaclab(example) $ ap generate
This should give our directory some generated code that becomes our controller:
ceposta@postamaclab(example) $ ls -l total 152 -rw-r--r-- 1 ceposta staff 119 Nov 17 09:49 autopilot-operator.yaml -rw-r--r-- 1 ceposta staff 376 Nov 17 09:49 autopilot.yaml drwxr-xr-x 4 ceposta staff 128 Nov 17 10:06 build drwxr-xr-x 3 ceposta staff 96 Nov 17 10:06 cmd drwxr-xr-x 12 ceposta staff 384 Nov 17 10:06 deploy -rw-r--r-- 1 ceposta staff 2133 Nov 17 09:49 go.mod -rw-r--r-- 1 ceposta staff 64681 Nov 17 10:06 go.sum drwxr-xr-x 4 ceposta staff 128 Nov 17 10:06 hack drwxr-xr-x 7 ceposta staff 224 Nov 17 10:06 pkg
There are a couple important concepts to know about once the code has been generated:
- the Spec defining your CRD
- workers for each of the defined phases
- the scheduler that coordinates the workers
In the next section, we’ll take a look at these concepts.
Implementing the brains of our controller
At this point, we have a fully functioning auto-pilot controller, but it doesn’t do much yet. The first thing we will want to do is define what the Custom Resource Definition should look like. For example, when building a Canary automation system, you may want something that specifies how to unroll the canary deployment including how frequently to do so, what telemetry to observe, and what success looks like.
Building the custom resource definition
Take, for example, the following CRD that defines some basic canary-release configurations:
apiVersion: autopilot.examples.io/v1 kind: CanaryDeployment metadata: name: example spec: ports: - 9898 successThreshold: 100 measurementInterval: 1m analysisPeriod: 10s deployment: example
To add this CRD to our project, we need to fill in the $BASE/pkg/apis/example/v1/spec.go
file. Right now it looks like this:
package v1
type ExampleSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster }
To build the CanaryDeployment
CRD, we could add something like this:
package v1
type ExampleSpec struct { v1.DeploymentSpec
// ports for which traffic should be split (between primary and canary) Ports []int32 `json:"ports,omitempty"`
// Over what interval should we measure the success rate? MeasurementInterval metav1.Duration `json:"measurementInterval"`
// Canary must maintain [a success rate metric]() or for the given analysisPeriod SuccessThreshold float64 `json:"successThreshold,omitempty"`
// How long should we process the canary for before promoting? AnalysisPeriod metav1.Duration `json:"analysisPeriod,omitempty"` }
Building the workers
Now that we’ve specified the CRD that will drive the canary automation control loop, we need to fill in the custom code for each one of the phases and the transitions between phases.
If we look in $BASE/pkg/workers
we see packages for each of the phases we defined in the autopilot.yaml
before generating the code for our project. Once we generated the code, we have stubs for the workers. For example, in the worker for the Initializing
phase, we see:
package initializing
import ( "context"
"github.com/go-logr/logr" "github.com/solo-io/autopilot/pkg/ezkube"
v1 "example/pkg/apis/examples/v1" )
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
type Worker struct { Client ezkube.Client Logger logr.Logger }
func (w *Worker) Sync(ctx context.Context, example *v1.Example) (Outputs, v1.ExamplePhase, *v1.ExampleStatusInfo, error) {
panic("implement me!") }
We can fill in the details of our workers making sure to respect the inputs and outputs that we defined in the autopilot.yaml
file. When we return from a worker, we also want to pass back the next phase that should be triggered, or a reference to the current phase to indicate not transition needs to take place.
In the accompanying video demo, we explore building the workers.
Exploring the scheduler
The scheduler implements the controller-runtime Reconcile()
function which watches the CRD and runs a reconciliation against any of the changes. The scheduler determines the current phase and calls the workers. The workers return the next phase, if applicable, and the scheduler calls the next worker. This continues until the control loop reaches the final phase. Here’s an example of the auto-generated scheduler for the Initializing
phase:
switch example.Status.Phase { case "", v1.ExamplePhaseInitializing: // begin worker phase logger.Info("Syncing Example in phase Initializing", "name", example.Name)
worker := &initializing.Worker{ Client: client, Logger: logger, } outputs, nextPhase, statusInfo, err := worker.Sync(s.ctx, example) if err != nil { return result, fmt.Errorf("failed to run worker for phase Initializing: %v", err) } for _, out := range outputs.VirtualServices.Items { if err := client.Ensure(s.ctx, example, &out); err != nil { return result, fmt.Errorf("failed to write output VirtualService<%v.%v> for phase Initializing: %v", out.GetNamespace(), out.GetName(), err) } }
// update the Example status with the worker's results example.Status.Phase = nextPhase if statusInfo != nil { logger.Info("Updating status of primary resource") example.Status.ExampleStatusInfo = *statusInfo }
Note, the scheduler is auto-generated and is not intended to be edited by had. The code is influenced by the autopilot.yaml
inputs and outputs specified for each phase.
Once we’ve filled in the workers, we can build and deploy the project with ap build
and ap deploy
. See the accompanying videos to see how this works.
Follow along with demo!
In this series of videos, we walk through each of these steps and build a new controller using auto-pilot that builds a Canary Automation controller for Istio:
https://www.youtube.com/watch?v=cD74L8cPeBY
https://www.youtube.com/watch?v=5DPs9zksBJg
https://www.youtube.com/watch?v=hcfSHFslo14
https://www.youtube.com/watch?v=h3HUFMP_ej8
For more:
See the following resources for more: