3. What is an Operator?
“An Operator is a method of packaging,
deploying and managing a Kubernetes
application.” [1]
“Conceptually, an Operator takes human
operational knowledge and encodes it
into software that is more easily packaged
and shared with consumers.” [1]
[1] https://coreos.com/operators
[2] https://github.com/operator-framework/awesome-operators
7. Basic Building Block
implicit val system: ActorSystem = ActorSystem("Operator")
implicit val mat: ActorMaterializer = ActorMaterializer()
implicit val ec: ExecutionContext = system.dispatcher
val k8s: client.RequestContext = k8sInit
val stream: Future[(UniqueKillSwitch, Future[Done])] =
k8s.list[PredictionDeployment].map { l =>
k8s.watchAllContinuously[PredictionDeployment](Some(l.resourceVersion))
.viaMat(KillSwitches.single)(Keep.right)
.groupBy(10, we => we._object.name)
.map{ we =>
we._type match {
case ADDED => reconcile(we)
case MODIFIED => reconcile(we)
case DELETED => we
case ERROR => we
}
}.async.mergeSubstreams
.toMat(Sink.ignore)(Keep.both)
.run()
}
Determine
starting point
Create a Akka
Stream Source of
watch events
Process events
SKUBER
Create Sub Streams to
process in parallel
8. On Startup - Committer
implicit val system: ActorSystem = ActorSystem("Operator")
implicit val mat: ActorMaterializer = ActorMaterializer()
implicit val ec: ExecutionContext = system.dispatcher
val streamId = StreamId("pd")
val k8s: client.RequestContext = k8sInit
val committer: Committer = buildCommiter()
val stream = committer.retrieve[PredictionDeployment](streamId,Position.Latest).map(offset =>
k8s.watchAllContinuously[PredictionDeployment](offset)
.viaMat(KillSwitches.single)(Keep.right)
.groupBy(10, we => we._object.name)
.map { we =>
we._type match {
case ADDED => reconcile(we)
case MODIFIED => reconcile(we)
case DELETED => we
case ERROR => we
}
}.async.mergeSubstreams.map { we =>
committer.commit(streamId, Offset(we._object.metadata.resourceVersion))
}.toMat(Sink.ignore)(Keep.both) .run()
)
Create Kafka like
committer
Retrieve offset,
defaulting to latest
position is non exist
Commit offset
9. On Startup - Alternative
implicit val system: ActorSystem = ActorSystem("Operator")
implicit val mat: ActorMaterializer = ActorMaterializer()
implicit val ec: ExecutionContext = system.dispatcher
val k8s: client.RequestContext = k8sInit
val stream= k8s.list[PredictionDeploymentList].map { l =>
Source(l.items.map(l => WatchEvent(MODIFIED, l))).concat(
k8s.watchAllContinuously[PredictionDeployment](Some(l.resourceVersion))
).viaMat(KillSwitches.single)(Keep.right)
.groupBy(10, we => we._object.name)
.map { we =>
we._type match {
case ADDED => reconcile(we)
case MODIFIED => reconcile(we)
case DELETED => we
case ERROR => we
}
}.async.mergeSubstreams
.toMat(Sink.ignore)(Keep.both).run()
}
Retrieve current state of
Prediction Deployment
resources.
Use the resource version
from the list operator to
subsequent changes
10. Reconciling
The reconciler function ensures that
the state of the system matches the
specification defined by the resource.
Derive
Expected
State
Determine
Current
State
Calculate
Difference
Apply
changes
11. Reconciling
EitherT(
k8s.create[O](resource).map(Right(_)).recoverWith {
case ex: K8SException if ex.status.code.contains(409) =>
k8s.get[O](resource.name).flatMap { existing =>
if (existing.metadata.ownerReferences.contains(ORef(pd))) {
if (!resource.subsetOf(existing)) {
k8s.update[O](existing.merge(resource))
.map(Right(_))
.recoverWith(K8sErrorHandler())
} else {
Future.successful(Right(existing))
}
} else {
Future.successful(Left(DuplicateService))
}
}
}.recoverWith(
K8sErrorHandler()
)
)
}
Optimistically attempt to
create the resource.
If that fails resolve the
conflict and update if
necessary
Subset is an implicit
function that determines
if the resource needs to
be updated.
Merge is an implicit
function that combines
the resources.
12. Working with Multiple Resource Types
val pdSource = k8s.list[PredictionDeploymentList].map { l =>
Source(l.items.map(l => WatchEvent(MODIFIED, l))).concat(
k8s.watchAllContinuously[PredictionDeployment](Some(l.resourceVersion))
)
}
val dSource = k8s.list[DeploymentList].map { l =>
k8s.watchAllContinuously[Deployment](Some(l.resourceVersion))
}
Future.sequence(List(pdSource, dSource)).map(s =>
Source.combine(s.head, s(1))(Merge(_))
.viaMat(KillSwitches.single)(Keep.right)
.groupBy(10, we => we._object.name)
.map {
case `WatchEvent[PredictionDeployment]`(we) => we
case `WatchEvent[Deployment]`(we) => we
}.async.mergeSubstreams
.toMat(Sink.ignore)(Keep.both).run()
)
Combine sources into a
single source
Use shapeless ‘TypeCase’
to avoid type erasure
Who am I?
Peter Barron
Principal Engineer @Zalando
Where do i work?
What Team am I part of?
What we are doing.
We are looking at build infrastructure for ML
In particular we are currently focused on the serving aspect of delivering ML models to production.
Nearly all of the infrastructure we are currently building is built on kubernetes and as such we are looking to build on that foundation
Who initially can up with the idea?
What is an operator?
For the most part K8s can handle the deployment service…
Point of extensibilty
What problem is being solved?
What do that do?
Installation
Upgrades
Lifecycle
Auto-pilot
What type applications use Operators
Stateful applications such as Kafka
Argo - native workflows, events, CI and CD
Kubeless - lambda for k8s
This is an example of CRD.
Provides extensibility for k8s
Describes a custom resource.
Skuber provides support for building CRDs.
An operator processes and acts on a stream of events from K8s.
Depending on the events an operator
Create/Update/remove/delete the resources in K8s
Can interact with the deployed artifacts - in this case rebalance a topic.
Other third party components - Monitoring etc..
Concepts to get across
Scala is a good fit… particularly reactive frameworks such as akka and akka streams
Why are we doing this?
Scala is the dominant language within the team/office.
There enough scala library support to process streams of events.
Need to able to two things:
Process a stream of watch events from the K8s cluster.
Act
Update/Create/Remove a K8s resource.
Reconcile differences
Make requests to deployed artifacts.
Make requests to other 3rd party systems.
Prerequisites
Service Account to give operator access
Define and Deploy a CRD to act on
Current go support for Operators.
Why are we looking to do this in Scala.
Skuber just allows you to watch a single resource Kind/Type within a single Source.
At the moment it is not possible to used Skuber to filter by label selectors. This is useful if the operator is working a single/sub collection of the resources that are deployed. But for the moment it is still possible to use.
What we have not included here is some of the boilerplate that you would normally see.
Start and stop a stream
Restarting a failed stream.
Error Handling
Configuration.
Monitoring.
Logging.
Provides at least once processing semantics
This is the basic approach used by operatorSDK from coreos
While this works well in situations where all the resources are contained within K8s
This approach falls down is if the operator is configuring resources external to k8s.
In this case there are lost delete operations that don’t get processed.
The reconciler function comes from the go clients implication.
Points to get across
Blind filter to stop event storms via the subset function
Merge function… can’t do a direct replacement as
K8s updates/adds some fields
Other operators might add labels/scale resources extra
Possible to use patch but at the moment skuber does not support that.