Kubernetes Operator Best Practices – Kubebuilder Deep Dive

#Kubernetes Operators #Cube Builder #Conflict Resolution #Retry Mechanisms #Parallel Processing #Reconciliation Loops #Custom Resources #ResourceVersion #Generation #Error Handling #Kubernetes
💬 Chat with this Video
Ask anything about this video…
Learn the best practices for writing Kubernetes operators using Cube Builder. This includes strategies for managing multiple controllers for parallelism, resolving conflicts for custom resource updates, implementing efficient retries, and avoiding infinite reconciliation loops. These resources provide guidance on scaling operations to support significantly more users effectively. I usually start with a problem statement in my videos but this time I want to do something different. Um let's first learn a concept and then we will go to a problem statement and then we will see um what we need to do. So um let me show you something really quickly. Um I have a cluster here and I do have my EC2 instance um with a certain spec like this. So this is something we have uh seen in the video where we talked about how to uh implement the status update, how do you work with a generation, how do you work with spec. It's just a continuation of off of that. I will put the link of the video uh in the description. Now what happens is what I wanted to show you is every time you make changes to a resource, the generation changes. If you have made changes in the spec and also every resource in Kubernetes has this thing called a resource version. This resource version is how CD knows about um your object. This is how uh it is reflected in the HCD and this is irres this is going to change irrespective of whether you updated your spec whether you updated your label you updated your annotations you put any changes in your object whether it is spec or anywhere um this is going to be changed whether the generation will only be changed when you actually update um the spec of your operator of your resource. So let's do uh some changes here. I'm going to say uh testing and uh this is a change. Now if I save and quit this, you see what happened? We went from generation four to five and there was a different number for the resource version. Now to show you something that it does not really matter uh what change was there on the on the object. If I just make a new label and I would say um cloud uh GCP. Now this is to show you that we are not making any changes on the spec but rather a metadata change but even that if I save and quit you see um this changed the resource version did change It did not change the generation because we didn't make changes to the spec of our object but the resource version changed. Just to quickly show you something else. Um environment and uh I would say production. Now right now the resource version is 42331 but if I save and continue uh that's going to change to something else and it is 42 345. And that's something I wanted to show you. It does not matter where you make what change in your object. If it has changed, it's going to get a new, you know, the change is going to be uh committed to HCD and every commit is going to have a new resource version. So now that out of the way, um let's move to the problem which I was facing and that got me to record this video. Um now imagine this is what you were doing and this is something what I did. I felt the problem and imagine this is what you are also doing. So you have a requirement. Let's let's start with it now. You have a requirement where you want to make an operator, right? You want to make an operator that is going to work and that's eventually going to give you a VM running somewhere. It could be u in Amazon uh it could be uh in GCP. It doesn't really matter. What this VM is going to do is eventually this virtual machine is going to be uh added in your cluster as a worker. So think of this as you're making a scaler. You're you're building your own custom autoscaler. And this is something what I felt when I was working on a similar project. Now when you are writing an operator you know you will be writing some custom resources and for that you will be needing some custom resource definitions. So when I was working on it I decided my custom resource definition to very simply uh look like this. So the kind was a node pool um and and this would define the number of nodes that I can have. So a node pool resource uh would be defining um how many maximum nodes can I get? What is my target node? What's the name of my node pool? And also a very simple um configuration like how big the node should be, how much disk do I need and how much memory um am I going to need in that particular node pool. You can think of this. This node poolool is going to create me a node which is going to be in one of the cloud providers somewhere and that is going to be then registered in my cluster. The thing is a node pool can be created manually when I create this custom resource. Let's say I have built my operator um you know my operator has some knowledge of creating a virtual machine. my operator has the knowledge of registering that into my cluster. Now, if you really want to start using it, when you create this custom resource, you can make changes on these target nodes. You could say from zero to one and your operator is reacting on that change. So, what it sees is it sees right now I've got like zero. Let me change uh to a different color. Right now I've got zero nodes of this configuration. The user has asked me to create one node. One node and I'm going to go and I will give them a node. This is what a very simple operator that you wrote. You also have not just this spec here but you also decided to give a status of uh you know uh to someone who is looking at your node pool. So this status has a key called scale operation and this is usually inactive if your operator is not doing anything which means if your operator is not busy creating a node or registering that to the cluster it doesn't change from inactive. However, this scale operation changes. This scale operation changes when um you know when you are actively creating a VM in a cloud provider from inactive to active. And this is what you wrote an operator for. This is something similar to what we did in my um Kubernetes video of how to build your own operator that manages EC2 instances. similar to what I was working on a project in my company. The thing is you were able to create these resources manually which is a node because you have an operator you deploy or you create this custom resource based on the custom resource your operator runs and it creates you a node and everything is happy and healthy. However, you decided to take this thing one step further. You said that instead of me deploying the custom resource and then uh I to do do cube ctl edit node pool and then I will go from one and change this to two and save and quit and eventually there goes another node. Uh this was node number one. This was node number two. I don't want to do this. This is a scaler. This is not an autocaler. So what you decided was um that this cubectl edit node pool or in the end the increment to this target node, you know, this to this target node should actually happen on a certain event and and you decided that this event should be a pod being in a pending state um because of CPU you or memory. Now you know you know that a pod could be in a pending state for so many reasons. The pod could be in a pending state because there's no secret that the pod is referencing to or a persistent volume is not available that the pod needs. The thing is you have the logic where you list on all the pending pods. You find the ones which are actually pending because of CPU or because of memory. you see you know uh if the memory that the P is asking for let's say if the P is asking for memory uh which is around 28 GB um and then you have the logic that yes because the pod is asking for memory that is less than this 96 GB if I create a node the node will be enough for my pod to be deployed you wanted to have this trigger So you have this as an autoscaler. Now what you do for all this, let me quickly go and uh delete this because we don't need it. What you decided was that I'm going to write another um you know controller. So there are two controllers right now which are working in your operator. I think it makes more sense. Let me just quickly say you can have one single operator which can have multiple custom resource uh multiple custom resource definitions and also multiple controllers there. So you said right now I have uh one custom resource definition which is called a node pool and there is an operator that reacts on top of that and that's going to be the node pool operator which is eventually the one that creates you a virtual machine or a or a VM you know somewhere in the cloud. You then decided because you wanted to make this an autoscaler, you say I'm going to have a new custom resource definition and that's called um you know uh that's called an autoscaler. Let's say you create a new custom resource definition and you name this as autoscaler um and then there is a auto scalar um config um autoscaler operator. This autoscaler custom resource is very simple. It just says um how many or you know how long to wait before checking for pending pods. Let's say frequency. For example, you can say that my my autoscaler operator should look for pods every five minutes. You know this is the frequency that you are configuring in your custom resource definition. So this might look something like uh let me just quickly go uh and write a YAML for this. This would make more sense. Um so your kind will be um autocaler uh version one. Um then we have the meta data where wait am I doing something wrong here. Um let me quickly just get the API version and kind and metadata. So this is what I was missing because every resource needs this. So I have my API version as compute.ample.com example.com vi1 kind is autoscaler and then we have the metadata the name my autoscaler and then you could have a spec essentially when you are working with cube builder um you create the spec you define the spec uh of your of your custom resource in this case I would have the list of name spaces to target you know these are my uh prod name space. Then I have my search nameace and then I have uh my um um real money maker namespace. These are the name spaces in which I want to target. If there is a pending pod in these name spaces then only I want to scale because there could be people creating dev projects and I don't really want to care about if there's a pending pod because of CPU or memory in the dev project. But if these projects are there that has a pending board we want to be um creating or reacting on that event then you could say uh frequency and you can say 5 minutes. So every 5 minute your autoscaler controller will check the pending ports in the list of name spaces to target and it will react on top of that. That's simple. So you decided that I'm going to write these two custom resources and I'm going to write these two different operators. This operator creates the VM. This second operator just finds the pending pod. You know it just finds the pending pods. Now because what what would you want to do if you have found a pending pod? you would want to create a VM, right? Because there's a pod pending, you need compute and you would want to create a VM. The thing here to notice is many times you might find yourself in a situation where you have custom resources, you have multiple operators and they talk to each other's custom resource, not just their own but each other's resource. In this case, for example, the autoscaler operator once it has found some pods, what this would do is it rather than this creating a virtual machine, it updates the custom resource called node pool. Essentially, think of this as cube edit node pool and then you update the number of target nodes. Let's say whatever the number was uh two you change this to three and because you update the custom resource uh called node poolool the node poolool controller will see this change and it will eventually create you a VM and then uh your VM will be registered as a worker. The thing here to understand is a different operator which is our autoscaler operator is using a different custom resource. This is not you know uh only working on its own custom resource definition and you will find this happening a lot and that's where things get interesting. So let's draw a timeline on what actually happened. So you start from the very beginning uh imagine a cluster where you have your operator your resources all running. Now what you do is you install or you create a YAML for the node pool which is essentially uh something like this which we already have here. So I'm going to go uh copy this and let's paste it over here. Oh, okay. That was something I wasn't expecting. Let me clear that. Uh so this becomes a bit more um easier to work with. Uh should have copied this somewhere. Um okay. So let me quickly just copy that and keep a fresh copy of this in case I need it in the future. So see what happened. You created a node pool. You created a node poolool resource here. and you define uh the target nodes as node zero but you define the target node as one um or let's say it's zero to begin with it is zero all right so you're not um doing anything you just say I want the resource to be there in my cluster I don't want it to do anything but let's let it be there um and let me quickly go ahead and get this here. Um I think we can just get from carbon.now.sh here. Uh this is my YAML. I'm going to copy this image and here is my custom resource definition or rather the custom resource for uh the autoscaler. So these are two uh CRs that you have created. I hope this is making uh you know this is this is going to be clear. So do custom resources eventually uh because you have two custom resources you also have two operators but the operators are not doing anything. Um so the reason why they're not doing anything for this one you have the node pool operator for this one you have the autoscaling operator they're not doing anything because first your target node is zero so nothing needs to be done and also because you don't have any pending pods in these name spaces so nothing is being done now let's say you manually went you know uh and you said that I'm going to create one new node from the node poolool operator. What you do, the first thing you go ahead and you do is cubectl create um cubectl uh say edit uh my node pool which is short form np and you change this target node from uh you know you change this target node from one uh 0 to one. All good. This operator starts working. It is working and because you made a change because you made a change in the spec um a new version would be there a new uh you know for the same object the generation is going to change from 1 to two and this is also going to be updated to a number x y z that's all good um this would be updated now coincidentally while this operator is doing its job. You found a pod which was pending in the broad name space. See what happens. You found a pod which was pending in the broad name space and you have configured your autoscaler operator that if pending is found you know you're going to update uh the custom resource uh you're going to update the custom resource which is in this case the node pool. So if you found a pending pod, you update the custom resource and just the same what you're doing. Uh cube ctl edit node poolool um yeah cubect node poolool. Keep this in mind that this all happened while your operator was working. While your operator was working. Now what you do is you edit the node pool. Now this is going to move from one to two because uh one from 0 to one was you did this manually cubed edit node pool and from 1 to two was done by this particular operator while this was running. Okay, while this was running, now what's going to happen? Not just the target node is changed, but the generation also changes three. And this goes from XY Z to A B C. Okay. Um, and this is all done. It's it's done. It's working. Once this operator will finish, once this operator will finish, you might want to update this custom resource back because when you start working, you you know, you change this from inactive to active because your operator was running. And when the operator is done creating a virtual machine, registering that to your worker uh to your to your cluster, you might want to update the object from active back to inactive because it's not technically doing anything. And that's where you're going to have a problem. That's where you're going to have an issue. See when your node pool operator started every time uh and this is this is how it's going to make more sense. Let's say you have a custom resource. Uh it's u resource version is one and you start an operator based on uh this custom resource with a resource version of one. Every time your operator sees that there was a change on my custom resource, the first thing it does is it downloads or it caches the object. So if your custom resource had resource version one in HCD here as well uh your operator knows that the version latest is one and the operator starts doing its work whatever it is doing. Now someone or you or another operator, another controller, it updates this particular custom resource and this changes from 1 to two. Your operator still thinks that the latest version is one because it hasn't gone to HCD to get the latest copy. It it did not it didn't know that uh you know the operator version has changed. it doesn't even have an idea about it yet because it's still running. So while your operator has an older version of the resource um you went ahead and you updated this custom resource in HCD. Now the resource version is two. Now when your operator is done and usually you might want to update the object imagine um this was a custom resource for the EC2 instance um use case that we have been talking about. Now you got the spec the the resource version was one. You create the VM. You got the public IP. Maybe you want to add the public IP in the status of that object because every object has spec and then the status. You want to update the public IP in the status. You try to update the object for which the resource version of the object was still one. And that's where the problem happens because when you try to update the object, you essentially want to commit into HCD. So you are telling HCD that hey or hey CD I want you to update an object um whose name is so and so and so. HCD doesn't care about the name. It cares about the resource version. So you are saying update the object uh with a resource version one. NH3D says there is no such object whose resource version is one. I have some whose resource version is two. So it looks like you either have an older copy of the object or you are clearly doing something wrong. And that's what you need to fix. And this is the problem that happens when you are working with more than one controllers where each controller is you know uh touching a different uh custom resource and this is called a conflict. You might have seen this error some you know at some places you might have seen this that the object that you are trying uh you know u is old get the latest cop copy of that object. So essentially if you really want to update this object what you would do is before writing to HCD you know your because your object update has failed what you technically do you get the newer copy from HCD you get the newer copy from HCD and now your resource version is two and then you will then update you will try retry try the uh the update um object um task or you will try to update the object again and this time because you are making a change in HCD for that object its resource version will now change from two to three see every time anytime anyone makes any changes you're going to have a new resource version when you create the resource for the first time. Let's say the resource version was one and that's what your operator started with. Now this guy made some changes to the resource. The resource version went to two. So you try to update on an older version where in city there was a newer version it failed. You fetch the new copy and then you try to update and because the last resource resource version was two and you are doing an update this changes to three. And that is the whole story of me trying to explain and come up with this situation. I hope this has made some sense. If not the comment section is open. You can always ask these questions. But this problem that can happen to you um and and this happens because you're trying to update an older version of the object where the object has already been updated by something someone external but your operator did not know about this. So to fix this there are two different ways in which you can you know uh in which you can fix this problem. Um the first way is a simple way or I like to call that a lazy way of doing it and the second one is a pro way of doing that. Now it doesn't make sense for me to talk about these simple or lazy or pro but rather let's go into the code and let's see what different ways are there where you can make your operator intelligent that okay the object that I'm trying to create it has been updated by someone else somewhere let me get a new copy and then I will try to make my changes on the new copy so let's go into the code and then let's let's see that how it works. All right. So, uh if you look at the code here, um let me just quickly walk you through how we are building our API. And in this case, I have a very simple API, a custom resource type which is called EC2 instance. This is something that I always use in all of my examples. So, if you are working on this best practices series, you already would be very familiar with how do you build an operator? Um, and there's a whole course on cube simplify of how do you build this on scratch. We'll probably put the link in the description here. And uh, here you can see I have a very simple custom resource called fu and bar. It has a spec which is just fu and bar. And this is just a very simple string. Look at the operator. Um, this reconciled function is the main function that runs whenever you make changes to your custom resources. This is the one that tells you what to do. Um, here's a bit of a fancy print statement. So, we can we can see in the logs of when the reconcile is started from the beginning. Uh, and we get the object that is being worked upon into a variable of type EC2 instance. Now every object that you have it has a spec it has a status that is something you can define in um in here for uh for my case the spec just contains fu and it contains bar which is another um key in my object. Um however every object gets um some metadata. So every object would have a UID, every object would have some labels that you do not set in the beginning. This is something which is coming from the Kubernetes API server. Similar to that, every object has a generation and a resource version. So while the resource version, think of this as the version of the object in HCD and the object generation is the resource um uh it's a deployment version of the object. So when you create the object for the first time its generation will be one. Any change in the spec that you make increments the generation and that's how you can know uh what is the old version and what is a newer version um that is being worked upon. Now in in this function which is a reconcile function it's fairly simple. I'm just trying to simulate a time-taking task that would be done. Usually your operator which is in case the EC2 operator in here um it is supposed to be creating an EC2 instance on Amazon. So usually this would go to AWS make API calls using the SDK written in Golang for AWS. It will authenticate to Amazon. It will give the information that I want to create an EC2 instance with this name the security group. Um, it should have these tags, these labels, this is the AMI ID, all of this information and eventually you're going to get back uh some response from Amazon. This is something we already talked about in the in the course for building operators from scratch. But I'm not doing this all in here. I'm just counting from 1 to 10 and I'm printing I'm doing a task and the number of the task and I'm sleeping uh time sleep. Now, this is where the fun part begins. thing while you are working in your um operator while the reconcile function is running. So see so what happens there's a object right there's an object let's say you just create this object its generation would be one because it is just created and its resource version would be some random number which is given by the cube API server. Now your object was created and the operator fetches this object uh and it stores it into the cache of the object and this is what's being done here. So this is the part which stores the object into the cache of the reconciler. Right? You start and you make some changes. Now the operator runs and it keeps on making the changes. However, what we discussed is that you can have multiple operators trying to write on the same resource. So there could be um another operator or a human that goes and does cubectl edit um EC2 instance and it goes ahead and probably change some aspects of the custom resource. Now when you save uh and quit you know when you save and quit your changes the version is going to be the generation is going to be from 1 to two. Here the old object is with generation one and it's XY Z and this one's going to be A B C and um the old uh the old object is generation one XY Z resource version. This one is generation 2 and ABC resource version. Now what happens? Your operator is going to make a change. Maybe it's going to update the spec or maybe it's going to update the status. Let's put it this way. You know, your call to AWS to create an instance gives you a public IP. Maybe you want to put this public IP in the object status. But this is where your um you know your operator is going to cry about when you say that update when you run the update function or you make the update call what you're telling to Kubernetes that I want to put this update onto an object whose resource version is XY Z and its generation is one because that's what you are aware of. Your operator does not know that a change has been done outside of its scope. It doesn't know that. And when it goes to do it here, when you try to do an update, you get an error. It says the object that you're trying to talk about no longer exists. I don't have an object with this resource version or this generation. There's a newer object. There's there's a change in that object that you do not know about. And this is where um it gets interesting. So see what's what's happening here. we try to run some steps in the operator trying to uh run some steps and I have a very simple scanal in so I can wait um this is going to run and let me uh make my changes outside of the operator with cubectl edit so this would be similar to what any other um another operator would make changes to the same object and until I press enter this just gives me a little bit of a time until I press enter it will not move ahead. So when I do enter uh what's going to happen is I'm updating my spec.bar to a new string or to a different string and when I try to update this is where it's going to fail because I have already made changes to my object outside the version has changed. The version that operator knows right now is the old one and this is where it's going to be a problem. Now if any error happens in your operator, you usually want to retry that. You usually want to retry this. So this is where the return function um comes into the picture. The reconcile uh this this reconcile function it has two return types or it returns two values. One is the result. The other one that we are more concerned about and this is the lazy way of doing things is the error. So when you have a reconciler, see the way it works is when you have a reconcile function, if it's going to return an error, you mean I mean if the error that uh is returned is not nil, Kubernetes is going to see okay the reconciler wanted to do something but it could not do that because uh and that is why it returned an error. I would try to run this reconciler on the object again and this is the self-healing mechanism of Kubernetes. Maybe you could not create the Amazon instance because there was a network problem you know and when your reconciler exits it says the work hasn't been done. Kubernetes will try that again and this time the network calls would go through or maybe you had some permission issues and you fix the permissions and this is how the self-healing works with Kubernetes. In our case, if we could not update the status uh the oper the the custom resource, it's going to return an error. And this is the lazy way. The reason why if there's no error, don't return anything. The work has already been done. We don't return anything. If nothing is returned, the the reconciler does not run again. It just waits for a new change to a to a object that it is responsible for. But see what would happen because we will not be able to update. There's an error. The reconciler will run again from the beginning and it will go through all these 10 tasks. It will go through printing the object and the resource version. But this time it's going to get the newer version because it's this get object uh this get um function gets the latest object that is being worked upon. And this time it's going to be version two which is some change that was made outside the operator. And this time uh it's going to get the object print the version which is new this time and run through all the steps of your operator which it should not be doing. There's no reason for doing that. But because you returned an error, it says I'm going to run through everything in the reconciler. And this is why I have been saying that your reconciler need to be a tempotent. If no changes are required you know to to be made no changes should be made uh by the reconciler. Now this time everything will be fine and because there's the same version of the uh same generation and resource version because you get the latest uh one that is available you would be able to update the spec. you would be able to update the spec and without any errors you will be returning a nil and that's the lazy way because you run your reconciler loop again just to get the new version you're spending us uh you're spending compute cycles you're spending a lot of time um you're running your reconciler you're wasting your compute capacity for no reason instead of managing the error here itself you return the error you let Kubernetes run your reconilider can get the new version and uh eventually move on um move on with the with the update and this is what I can uh I want to show you. Now before I can show you I also want to talk about um I have changed my uh development environment. Before I was using kind but now I'm using wind or wind as some people would call that which is vcluster in docker and that is going to be the base of every um operator that I build every um development environment that I ever would be needing. bind is really really cool and it gives you a lot of functionality which I'm sure there are other videos on cube simplify which you can look at um but uh for for a very simple example you could just do vcluster create and it's going to give you a vcluster in docker so exactly drop in replacement for kind and I have my vclustery file which if I can show you it looks something like this so I'm going to get my worker nodes as the docker container. VIN also gives me flexibility to create an Amazon EC2 instance and attach that to my local VIN cluster. Um I'm going to be deploying the default Flannel Celium um Flannel CNI and the Kubernetes version that I'm using is 1.3.4 core and you can do vcluster create the name of the cluster in my case this is zry the driver would be docker and this works very well on mac with obstack in case you're using this you can you can totally use that uh I already have this um cluster running from from wind and you can see I've got these containers and this is where my kubernetes cluster is running so um we have a we have a working Kubernetes cluster. We also need our custom resource definitions installed on the cluster. And because we are using cube builder and this is a make file, it has some make targets make manifest, make generate and make install. What this is going to do is it's going to create the CRD. it's going to uh apply them on my cluster and I would be able to do uh k get EC2 instances because this was my um custom resource definition or rather explain uh EC2 instances and here you can see uh it knows about this particular resource and if I do spec you will see the fu and the bar uh examples here and this is the spec that I that I wanted to do. Now let me quickly show you uh this in demo and let me run my um let me see I should be empty okay good I'm running my main go and this is running the operator on the vclustluster um in in docker now see what happens as soon as I create an object of type EC2 instance let me just quickly show you how does it look like so um it would be config examples or config samples compute. This is my EC2 instance which I am creating right. So let's do that. If I do create what's going to happen is my reconcile is started on the left side. You can see the object the generation is one because it's the first time this has been created and the resource version is something different over here. Right? The resource version is something different over here. Um, however, it looks like it should be stopping. Um, probably. Oh, yes. So, what happened? This was supposed to stop here so that I could go ahead and make the changes myself. But because I hit an enter, um, it was given as an input and nothing uh, waited for me. It just moved ahead. But let me quickly do a delete. Uh, and then let's rerun this again. So this time I'm going to make sure I do not give any enter. I want this to wait for me so I can make changes myself to uh the object from outside the operator. So if I do a create now see what happens if I do k get ec2 instances hyphen o yaml this is the generation and that's the same generation my operator knows about. So that's good that's perfect and the resource version also is what it knows. So right now both of them are knowing the same object. But what I'm going to do is I'm going to edit my EC2 instance and I'm going to say this change is made while the operator was working on the resource something like that. Now it's edited and if you look at this again this time the generation is incremented by one and also the resource version is changed. This tells you that Kubernetes or API server or HCD sees this object differently than what my operator holds here right now. Now when I press enter what's going to happen is uh it will be at this stage when I press enter it will try to update my bar it will it will change uh whatever string is there right now for bar which is testing uh it will replace it with this string comes from operator and when this tries to update it's going to fail. So let's press enter and now we will see. Now see what happens. This is the problem. It says um operation cannot be fulfilled on EC2 instances. E2 instances the object has been modified. Please apply your changes to the latest version and try again. Try again. The way you are trying again now and this is a stack trace of the of the error. The way you are doing a retry now is that you simply say panic. I'm returning an error. And the way reconciler works is if you return the error, the whole pro uh the whole loop would be running again. And this is what you see here. The reconciler started. This time it got the new version which is version two. Right? It got the resource version which is resource version here. It ran through all these tasks. It ran through all three all of these tasks and eventually it now knows the latest version that that there is. And here you can see when I press enter it did update the object which is which is you know it it went through because nobody else updated the object while the controller was running. It did not change the version. Um this time when you did an update a new generation was u was was changed or rather a new generation was incremented which is you see here any change on the spec updates the generation. So when you create the object for the first time that was the generation I make the uh generation one I made some changes manually that incremented the generation two and this time it went to generation three because I was able to update my object using um using the the controller that we are writing right now. This is a lazy way of doing things. However, there's a better way of handling errors so that you do you don't just simply say, "Hey, there's an error." You don't do that. Rather, Kubernetes gives you a package called errors. So if I show you um go package um Kubernetes package errors and here's this errors package and this is where Kubernetes has defined or yeah Kubernetes has defined all the errors that you can get. A lot of times you might see problems like unauthorized maybe you say it's forbidden it's a new conflict that you have this um this object already exists so I can't create that all of these are already defined in the package uh in the API uh called errors and this is something that we can use to figure out what type of error it was and what to do on top of that. So let's look at this errors and see how we can make our operator smarter. So instead of quitting and rerunning from the beginning, it gets uh the latest uh version here itself. When it fails to update, it says okay, looks like the operator outside, looks like the object outside has been changed. Let me quickly go and get the latest version right here instead of running the reconciler all uh again. So let's take a look and see how this is uh this is done. All right. So what we're looking into in here is to try to update um our object if there was a error. You know we want to retry if there was an error trying to update our custom resource because the version has been updated or modified somewhere external to our operator. The first thing we need to know is how can we find out what was the error. So this update function here it does return an error but it is on you to control that error. It is on you to define how or what that error was. It could be that the object was no longer available. It could be that you did not have permission issues or you did not have permission. It could be uh there was a 500 error that you got. How do you classify these errors that you are getting when you are trying to update the resource and for that is what we talked about the error package? You could do if the error is a conflict error something like this is how you can say that if I got an error you know I'm using the errors package and if it was a conflict this error type is already defined by Kubernetes in the errors package. So if I show you that go package errors here you can see this package implements uh the functionality to classify the error. Um now this isn't the one uh it's for um kubernetes one this one. So this defines um already given scenarios in which you can run into and you can probably be given an error. For example, you try to make an object which is already available with that name. You know, when you try to do cubectl create on a resource and you try that again, that's what you get returned. And this is what the error is that it already is available. One of the other one that we're looking into is the is conflict function. It checks that the error that you got when you're trying to update your object if it was indeed conflict issue. you know, so you print out um that the conflict error was there. Maybe you would want to send an email to the team that hey, I tried to run my operator and I tried to run the controller and when I tried to update my object, there was an error or you might want to uh handle uh it here, right? You might have another errors as well. So, it's not the only reason that you could not update the object. Uh could be because of conflict. it might as well be that the object you are trying to work on it has been deleted uh from you know uh from Kubernetes itself. So when you try to update that object which no longer exists you're going to get an error. And again here we can see if errors is not found something like this. And here is how you can make you know um here's how you can make your um operator more smarter in terms of how to do error handling, how to do error management. In our case, if there is a conflict, we want to retry. We don't want to quit the reconciler. We don't want to do that. We want to stop uh there and retry rather than exiting the reconciler returning an error. and Kubernetes will see, hey, this operator did not work properly. It is returning me an error. So, I'm going to rerun all the steps again just to be able to update the function. Essentially, at this stage, when you got a conflict error, you want to get the new object version and store in the cache. and then uh you update um the object with the new version right that's what you want to do and for that to do there is a fairly simple function and there's actually a package that kubernetes gives you so that you can implement this retry for without even that you could do very simply so it's going to be the same code that I try to get my object right if there was an error. Let's get that again and let's update. And this would be very very simple. This is how you can implement uh an error um conflict functionality. However, the thing is you can again get an error trying to update your object. It's not like you got an error once and the next time it's going to work fine. You know, you might still get an error. You might still get an error here. And in the end, you're doing the same thing. You are trying to just exit the reconciler because you're returning an error because you caught an error when trying to update. And it's kind of like you're going into a loop here. So, Kubernetes has a retry package which I can probably show you. Kubernetes go retry package. It's somewhere around here. It's in the client go. And you can use these two functions available. So one of them is on error. Um the other one is retry on conflict which is something we want to only try if there was a conflict. The the first one is pretty generic on any error you want to try that again. This one we want to try on conflict and it has certain uh options to control how do we want to retry. Um so this is something that we can use. I would say retry error and I'm going to use the function retry dot return on conflict. You can see I've already got the tab completion and I'm not sure which function which uh model am I using but this is cursor um and let me just quickly do uh this retry error uh and um here we go. So this bit makes your retrying a lot more uh you know uh smarter rather than just manually getting because we are still doing the same we are getting the object the new one from the cube API server we try to update this is exactly what we did even without this function but what does it give it to you the one thing that I really like about the retry package and especially the retry on conflict is that it does give two inputs to this function. The first one is some default back off or it is uh it is a set of configuration that tells Kubernetes or tells your reconciler how many times do you want to retry before failing. So if you look on the default backoff it has steps as four. That being said it is going to retry four times before giving up and telling to Kubernetes hey I tried I could not update it. So this is kind of like a retry within the retry um on conflict function. That's what it gives you how many times do you want to try again. The duration is the initial time before which your retry function will wait to try for the first time and then the subsequent times. For example, um it's 10 milliseconds. So for me, it's going to start at 0 to 10 milliseconds to retry. If it fails again then take another 10 milliseconds then another 10 then another 10. So 10 20 30 40 milliseconds is how it's going to be. The factor controls what is going to be your duration or how effectively you're going to retry. So in this case the duration uh and the factor it's kind of like a multiplier. So if I have a duration of 10 seconds and the factor is five for the first time when my operator starts it's going to try on the 10th millisecond then on the 50th millisecond then on the 250 millisecond then after 1.25 uh seconds is is it going to try? So it's multiplying your duration with the factor kind of like giving you multiple tries within exponential back off. Within every try you're going to stop a little bit more and try again. And this jitter is 0.1 up to one. That's the maximum value. This gives you the randomness between the time on which you're going to uh retry your uh update statement. This is especially useful when you have more than one controllers. They all have the same duration. They all start at the same time with the same factor. They all will try to write the object at the same time. And this is something we do not want. So jitter will actually be um uh changing the effective time. So if you would see the duration and factor this would be something like if your duration uh if your duration is 10 and your factor is let's say five it would have been um it's going to do 10 milliseconds then after 50 milliseconds then after 250 mconds you're multiplying that by the factor. But when you have jitter uh which is uh jitter would be uh 0.1 or it's 10% uh that the randomness that you are involving. So it would be 10% of 10 milliseconds within that it's going to start right. So it's not going to be exactly 10 millconds but it's going to be probably 8 millisecond 9 millisecond. Then for 50 it would be 47 48 mconds. a randomness is added. So it is not exactly this particular time on which you are retrying. This helps you when you have more than one controllers running at the same time uh and they all have the same factor and the same duration. They try to hit your cube API server at the same time for updates and this randomness would be uh allowing them not to do that at one go. So this retryon conflict function it takes two inputs. One of them is the configuration of your backoff which is the step and what we talked about. The another one is a function without any inputs and this returns an error. Essentially the return on conflict function itself it returns an error. You can see here in the function definition this is the error it is returning and this error comes from u this function which essentially does nothing but it tries to get the object right. it gets the object and uh then tries to update your operator or your custom resource again. You don't have to have this line because it's the same update that you want from this from um you know the spec that you have changed and then you try to do an update. You return uh the error if if it's if there is any because update function also returns an error. So essentially we are trying to get and update the resource and we're returning uh an error if there is any to the return to the retry error uh variable here and this is where we can see this bit goes into a loop for your number of steps. It waits for the the randomness uh it waits for the duration. Then it goes on every factor and added the randomness. It defines the time for four four uh steps or four retries. This is something you can also very well configure. You can give your own configuration. Maybe you want to try for eight times before you say okay I give up now. There was an error and despite trying for all these uh retries the steps that you have like in our case is four. If it is still not nil that you know you can only retry. It's not like it's a guarantee that your update is going to go through, but you are retrying. You tried again. You tried again. Maybe 10 times you tried. And here you can say um print ln. Uh I tried I tried four times. Something is wrong with the cubernetes cluster or API server because that's is what you're trying to hit with the with the r.update function, right? And then you can simply say uh do you want if you return an error the reconciler will run again. That's usually you can do that. It depends on how do you want to choose to build your operator. If I tried many times already there is an error. Uh I probably will just simply say nil that I don't want my uh reconciler to exit and retry again because I just just retrieded. I would rather send something as in send email because it is something bigger. You know, you retry then again and again and again that you couldn't update. Maybe the API server is no longer responsive. Um, someone needs to take a look into this. Um, so here you can see uh what's what's being done. We will get rid of the is not found object. And here it's sending an email because something is bigger. So if you could retry um you know if if you could retry then we just uh ignore this. We don't want to do anything because if you could retry uh this auto control. Oh all right. So in here you uh were not able to update the object and you send an email. However, if everything goes fine, you know, you could say successfully updated the object after the conflict. And because you changed the object, see this is very important. Every time you make changes to your object, the reconciler remembers it. And when this one loop, this one reconciler function exits. It sees that the object was updated, it will run again. And that's what we are saying because the update uh because the update was done to the object a new reconciler will be triggered and we exit gracefully without giving any errors giving another run to the reconciler because the operator because the object uh the object was updated properly. um if everything was fine. So this is uh this is the condition if the error was something else you know because you could have um any other problems. This is where you can also control this um that uh fmt.print ln there was an error other than the conflict. I'm going to return the error. Um again you could handle different cases. One of the case was if there was a conflict, you could handle other cases like uh if the object was not found, maybe it was deleted. That could be another reason um why you might want to uh just um you know not give an error. So you could say if errors uh do is not found. If it's not found, then I don't want to do anything. You know, if it's not found, I can't do anything. So I'm just returning gracefully. We are not complaining. We're not sending out an error from the reconciler. We are not telling Kubernetes try again because the object does not exist. There's nothing to try. If we don't have the object, we tried to update, we failed. What can we do? We can just simply ignore the failure. You know, this is nothing that our operator can control. So you can have more than one um conditions. And here also you can kind of like have a catch all for errors. Uh and here you can say um there was an error other than the conflict or is not found and uh you just then in this case you really want to return an error and you really want to retry the whole loop for the reconciler because you already have two cases covered. One of them was if it was not found you do not do anything. Nothing needs to be done. If it was a conflict, we retry. And if we couldn't retry properly, then also we send an email. We don't really rerun that again. But if if we could update uh you know, if we could update our object, we simply say, "Yep, the update went through. When we retried, everything is fine. We don't need to rerun the reconciler because the object was already updated." And in the end if there was no errors at all you can see here um we just uh exit gracefully. So there are many reasons or many places you will find yourself. Do you want to return an error? Do you want to return gracefully? What do you want to do? Every place has its own value. In this case we try to update and if there was no error we simply return because there was no error. Nothing needs to be done. If in case was there any error that is when you are returning at different places and handling that error differently. But if there was no error in the first time nothing needs to be done. We can just simply exit out of the reconider and wait for a new object change detected by Kubernetes. So um if I show you this in uh in action now I'm just going to clean this up a little bit. I don't want to look into is not found errors and let's just do a go run main go. I don't have any object right now that this operator would be looking into. So it's just going to wait and I will do um create. As you can see as soon as you create the object the version is uh the object generation is one and the resource version is 80,748. It's going to wait because we ask it to wait. What I'm going to do is simulate that my object was changed outside. So I would say this is now changed and you can then see uh its version or generation is two. The version is also different. If I try again here you see what happened. It found out that trying to update the object there was a there was a you know a conflict error and it's it tries to retry there right then and there it did not exit the whole reconciler what you are seeing this one is a new run because you update the object in here and this is what it says there was a conflict error it was retrying successfully updated object after retrying on conflict because update was to the object object, a new reconciler will be triggered. And this again it makes sense because um you did update the object and here you can see you got the latest version and everything uh worked as expected. So this is something that you would be actually doing and this is how you can make your operator more smarter in terms of not just retry but how do you handle errors because there could be errors when your operator when your reconciler is running. So I think this is a bit longer video but I really wanted to give you an idea of why would you want to do something uh especially when you have multiple things juggling the same object at the same time. uh in case of u when you have more than one operators trying to work on the object at one time uh you it's always a good idea whenever you are updating your object you know whether it is a spec or a status it's always a good idea to have a retry when you think that this object could be modified by some other controller if it's not there if your operator owns that object nobody else is changing it or no other process. No other controller is updating that particular object. You don't have to do that. There's no need to deal with it. But especially in in systems where there are multiple actors running on the same object, it is a very good idea to implement this retry like we did. So, and even then it's a good idea to handle errors rather than just panicking and telling Kubernetes please try again. I mean rather than just returning an error and let Kubernetes do it, it's it's a good idea that you can do it on yourself, right? When you try to update, if there was an error, you handle that. If there was a conflict, you retry. Hey everyone, welcome to this video about um operator best practices in the series. And today we are going to talk about how can you make your operator work a lot faster compared to what you might have been doing before. And for this let's talk a little bit about what do I mean when I say make the operator work faster. Rather than saying we want to uh make our operator work faster, it should be we want to make our operator work in parallel. This best practice does not talk about how long does your operator take to perform an operation. Um by now if you have been following the codes and if you have following uh you know the best practices you would be knowing that we always talk about this custom resource called EC2 um EC2 instance right this is instance there we go right so we talk about a resource a custom resource that we created in the course which was EC2 instance and our operator which was running in this Kubernetes cluster was responsible to reconcile on this particular object. It goes to AWS. It creates the instance. It gives you the information about that instance such as the private IP, the name of that instance uh the the public IP that the VM has uh has been given and it essentially gives it back to you and you update that to the status of your custom resource and everything is happy. When I talk about mating making making your operator faster, what we are not talking about is this process of delivering your customers the virtual machine or whatever time it takes uh for you to do uh the task that your operator is supposed to do. In my case, if the time taken by my operator to talk to Amazon is maybe let's say um 10 milliseconds, it makes a call to Amazon. Then Amazon does its magic and gives me a virtual machine in 10 seconds. Let's say it takes 10 seconds for my VM to be really in the running and ready state and essentially then I get back my response another say 10 millisecond. This is not the time we're talking about to reduce this. This is something which is very um programmatic. This is not uh how it is related to an operator. This is related to your own coding paradigms. Maybe you are using an inefficient call to Amazon which takes longer. Maybe you are doing long polling. Um again it doesn't change the time it takes for the for the operation to work. But uh there are ways in which you can make your operations take less time but that's not the the feature of this video. What we want to talk about is when you develop an operator, you usually do this because there are people who want to use a product. Imagine this that you are working in a company and there are people you know there's one guy there's another guy and both of them they are coming to you and they are saying that hey Shbam they come to me and they say hey Shabbam can you please give me an EC2 instance and I think about that that maybe these guys would be you know more more benefited if I give them uh a selfservice kind of portal and that's where this is my Kubernetes cluster where I create them a custom resource which is again called the EC2 instance I give them just enough data that they need to populate the custom resource with the name the service account uh that they want to use the IM role they want the EC2 instance to have um the the private key they want in the instance to be added so they can SSH to the machine and essentially um what would happen then my operator will take this object. This is the operator right? This is the operator. It will take this object and this is going to create me an Amazon um machine and I would give it back to the user whoever was requesting this virtual machine and everything would be happy. The problem starts that the the problem starts when you are growing in terms of the usage of your object. What do I mean by that? When you have one person asking for the instance, you're one operator. You have one operator and this is running and that's fine. What happens when you have multiple people from different teams coming all to you and they say, "Hey, you know what? Um, we need an instance as well." So imagine there are 10 people. Imagine there are 10 people who submit a request for an object called EC2 instance and then again you have an operator that reacts on this. Essentially what happens you have a working Q. So you add 10 objects of kind EC2 instance and then your operator reacts on top of that and it goes ahead and it creates the virtual machine create the VM. It gives it back to uh it updates the status of this object. Then after it has processed one out of these 10, this is interesting. You have 10 resources to create. You have one operator and every operator has got something called a worker and this worker is the one responsible to look into the queue and see how many objects do I have. If I have one, cool, I will take it. I will process that and I would move on. If I have 10 then also it does the same thing. It looks on the the queue of your objects that are being updated. It takes one. It does not look at how many it has. It takes one, processes it, and it then moves on to the second operator object, then the third operator object. Because it works as long as there are objects in this queue. um it it's going to do that. But this happens sequentially. Imagine if here's a counter. Imagine if here's a counter. Where are these there are some objects, you know, and there are also there's another counter. You need to move this object from here to this place. But you only have one person working. And this guy, even though there are multiple uh objects available on your left counter, he can only take one, then he goes back and it places it on the right counter. Right? This is what your operator is doing. It keeps a look, it keeps watching this particular slab and it sees, oh, there are objects on top of that, but I can only carry one at one time. And this is the reason why this happens is because your operator when it is created it only has one worker. Right? So if somehow you can tell Kubernetes that you know what I assume my object is going to be used a lot. I I assume that there are going to be many people trying to make changes to my object trying to delete that trying to create that. I don't want just one person who's doing the the lift thing. I might need multiple. And that's where you can tell Kubernetes operators which is we are using cube builder as a framework. You can tell cube builder that please go ahead and create me um maybe two workers or or three workers or four workers. Essentially there's no limit to how many workers you can create. I mean there's always a limit because every worker consumes uh compute and memory. Uh so you don't want your operator to be using gigabytes of memory and cores of of CPU. You don't want like more workers than the requirement because it doesn't make sense. It's not a right optimization. But uh you would be increasing the number of workers, right? And this is something you can do in your operator itself in the reconciler where you run your operator manager. That's something where we can configure this. But let but let's look at that in action. How does it look like when I have 10 instances and I only have one worker? How long does it take for um you know um my operator to be to be working? So this is the this the code that you have uh been looking at in every video and you are very you should be very familiar with that. This is our reconciler function. If I quickly show you the API uh that we created it was EC2 instances and here you can see every object has got um the metadata. It has got the object metadata the type meta and then it has the spec and also it has the status. It's fairly simple. There's no magic going on. We only have two keys. One of them is fu. The other one is bar. We don't have anything in our status because it's not something where we are looking forward in this video. Now the controller here is also very simple. This is the reconcile function and this reconcile function is the one that is running every time your object gets updated or created. The object in this case is the EC2 instance um custom resource that we are working with. So what we do is we register the time at which my operator started and we're just printing it. So we have a bit more verbosity that uh the time it was started was time dot now. So this is the start time of the operator. the start time of the operator. Let's say not the operator but the reconciler. That makes more sense. The reconciliation and um right so we print out that we started our reconciliation at this time and we reg instance. This is nothing new. You have been seeing me doing this for a for a very long time if you have been following along along on the videos. For operators, we create an we create a variable of type EC2 instance which you can see it only has a spec and it has a status. The spec only has two things fu and bar. We get the object from the reconciler request because whenever your object gets updated, a request is sent to your reconciler. So we use the the the reconciler and the get function to get the object which is of type uh which is coming from the request. We get the namespace name. Essentially we get two things from this. We get the name and the object um the name space and the object name. Uh from the request we store that into our EC2 instance. And here's where you will be uh simulating simulating a task that takes 5 seconds for example to complete. Think of this as uh think of this as creating an EC2 instance on Amazon. Right? So I'm using cursor which is pretty good in terms of uh helping you with the development. But imagine each object that your people are working with that your customers are using it takes around five seconds to complete right around 5 seconds. I'm not saying exactly five but let's say for to keep it simple for the video we say every anytime someone creates an object which is of kind EC2 instance this will create an Amazon machine and this particular operation takes 5 seconds now let's let's try to do this let's try to run this and then we are just very simply printing that the total time taken by the reconciliation is we get the time dots sinceense from I think it makes sense to have it as a variable because we want to store the time rather than calculating it here because it will be zero cuz time since now is is nothing it's zero so let's do that start time and essentially that is what we wanted to do so we very simply print out the time it has taken since the starting of our reconciler and then we return because we are not really making any changes. We are just seeing how the reconciler function how the operator works if we change some object and here's a same setup with manager because we have an operator manager and we set up our reconciler with that manager. This is something which is pretty standard. And if I show you the object that we're going to work with, this would be something in config sample. And here you go. So you can see this is kind of how I'm simulating that multiple people are trying to interact with my resource. So this could be a person from team A who gives the object that I want to create an EC2 instance. This could be from team B, team C, team D, team E. Could be multiple files, could be same file. The only important thing is all of these objects that I'm giving to my Kubernetes cluster at the same time is because this is how people would be using your object. There may be two people, three people, four people in a team who would be trying to talk to your operator at the same time. No, the the only thing difference is the names are not clashing because we don't want to keep the names clashing because this is not a name score namespace resource. If I have instance one, if I try to create that again, I'm going to get a problem that you're trying to create a resource which is already existing. We don't want that. Kubernetes 101. And here you can see instance two. And here you can see three and the same four and it's five. They have different names. they have different data. The data could be anything. It could be the same. It doesn't really matter. But the name should be unique because it's not not it's not a namespaced scope uh object. What I'm trying to do here is just trying to see or trying to tell you is there would be imagine this is being done by different people, right? So let's run our operator. I will see first if I have any EC2 instances in my cluster. I do have one. I will delete that. Um I will delete that and I will just install my custom resource definition again. Um because I was working on multiple versions of the operator but this is the one that I want to use and I'm using cluster um platform uh to run my local development environment. So I could say um go run main.go. So this is running our operator and that's where you will see this line which is very important. It is starting the controller. It is then starting your worker and you only have one worker for the controller kind of EC2 instance. That means if someone creates an EC2 instance, even if they give like 10 instances, which is what I'm doing in in this example, it is only going to have it is only going to create one instance at one time. And this is this is exactly what you want. See, the thing is you do not want um more than one people. So imagine this that this guy called A and this is called B. If A was already working on this object, if A was already trying to uh move it to destination, you don't want B to uh interact with him. Right? This is the same thing that happens when in cricket you have a cricket ball and there are two people trying to catch that ball and they just collide into each other and the the catch drops, right? You don't want that. What you want is if there were two balls, which in cricket is not a good example, but if there are two balls, each one should be only looking at their ball at the same time because you don't want either of the balls to be dropped on the ground. That is what we want. We want to make our operator faster so that not in terms of how long does it take to create the WM but how long does it take for the operator to react on those 10 available objects. So we only have one worker you can see and I'm going to create all these five instances and you will see how this is behaving. So if I do uh create you can see here start time of the reconciliation is this and you see I created five instances but what's happening is it's being done one by one and the operator is smart the the workers are smart they know that there were five instances that were changed I need to work on five instances but I can only take one at a time I cannot take multiple at one time and you don't also want back. So you see what's happening is every time the reconciler starts it calculates it prints out the time and it prints out the time taken for that reconciliation. Every time the reconciler runs it runs because an object was updated. It runs because an object was changed. You would be if you were thinking that every time you see I'm taking five seconds for one uh instance I have five instances my my reconciler will take 25 seconds for all five. That's not how it works. Yes, for my loop to run, if you will just count them, five five five signs, it essentially took 25 seconds. But one worker takes 5 seconds um and five times is is when it's added to 25. It doesn't change the time taken per reconciliation. It still takes 5 seconds for one object. Now what you can do in this case is imagine if the time taken was longer you know if if your operator takes more time let's say 2 minutes or which is not too bad but it's not also not great not terrible quoting shernobbile here however let's do let's kick things up a notch and I would say uh you know it doesn't sleep for uh 1 second it sleeps for 10 seconds every time you are going to because I'm creating five instances. So just trying to simulate that now it takes 10 seconds for each object and I've got five. So I'm just printing it five times. So it's going to be actually 50 seconds which is not what we want. I want two into 10. So five times it's going to run 2 seconds. So 10 seconds in total is what it's waiting for. If you stop it and if I run that again, you're going to see the same thing. See, now for each instance, it hasn't even started one of the other instances yet. It's just using instance number one. Then it moves on to instance number two. This is a serial um work of your operator on your resources because you only have one worker. The worker is busy uh here with instance one. Then it moves on to instance two then on to three. People who have created their object before the others it it's going to be um you know um given priority to them. Actually this doesn't make sense because uh it is not really um 1 2 3 4. It's not by the the creation time stamp. But the thing is your worker is using one object at one time and this is not what an efficient use of the operator framework is. You could say that in my company I see a lot more usage. So if I have like 10 objects it makes sense to create three or four workers because then they can have things in parallel. They will still be completely separate from each other. If I give 10 EC2 instances, they're going to be creating 10 different instances completely separated because they do not work with each other. All they do is they grab an object and they do what they have been told to do separately without working with other other worker processes. And this single thing can really help you um to to work with your operators much more efficiently. And that can be done in the setup with manager function. What you can do is you could say that um for the EC2 instance that is what my operator should be reacting on. I want it to react with the options and then you have options within your controller. And here's you can see there are options and one option is max recurrent reconcilers. Re reconciles that means it's essentially a fancy word for number of workers. How many maximum um instances of the reconciler you can run and because we know that one or one worker runs one reconciler at one time. If you have 10 then there will be 10 workers. They can reconcile 10 times or they can run this function at one time uh 10 times. And that bit is going to make huge difference for you. Let me put it back to where it was for one second, right? Because we wanted it to be 1 second. And now you can see how it works as a difference. What difference does it make when we run this again? Now you can see I run my operator and because I now have 10 workers you can see it's already doing things in parallel. we get I'm starting with um a reconciliation time and you can see this is instance 2 3 2 5 it all you know each of them took 5 seconds because we are not reducing the time it takes individually but your other objects that were there they are not waiting to be picked up by an operator because your operator has number of workers which are all busy they they will be taking up the object when it's We don't want that. Think of this practice as when you have a lot of use for your um for your custom resource. For example, pods. If I was to create 100 pods and only one could be created at one time, you're going to be waiting before your pod could even be scheduled on a node before it could it was even picked by the operator. So number of workers increasing that it really helps your operator being a lot more parallel. It helps you to pick those objects much faster and essentially then whatever time it's going to take they will because we can't stop that. As I said if they're going to take 5 seconds in each iteration for each object they will do that. That's not the point of the video. You could you could do this by saying okay um if I wanted to reduce the time they take within each reconciler for each worker I could say millisecond you know this is I make my program more efficient and they're going to now take one millisecond but the point is if you have a lot of operate if you have a lot of objects that you're working with and you do not want to wait for them to at least be picked up and get started because imagine Imagine uh this case this is for humans that let's say on an axis of time um here time being zero and here time being infinity if creation of one instance right if creation of one instance takes 10 minutes I'm just giving you a very simple example if this takes 10 minutes it's bad uh Um again it it depends on what you're trying to create and where you are trying to create that. But let's say um this one is 1 p.m. So 1300 hours is here and then there's probably like 2300. If I was to start my operator creation from 1300, I would be getting my instance at 1310 because it takes 10 minutes for my instance to be created. However, if there's a flood of objects, right? If you give like 10 objects and each one takes 10 minutes. So your instance which was probably taken the last it would be starting 10 minutes 10 minutes 10 minutes 10 minutes 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 and then here you would be starting your your request would even be picked up uh after like um you know it takes one instance 10 minutes or 100 minutes after an hour or more than an hour more than one and a half hour later your instance request would be picked up. So it would be around 14 um uh would be about 1430 90 minutes or 100 minutes would be so 1440 is when your instance would be picked up but you made the request at 13 at 1 p.m. requested and your request was started to be picked up at 2 hour 40 minutes and you know PM and then you would get your instance in the same 10 minutes because everyone got their instances in 10 minutes but you had to wait because there were only one worker. If only you have like 10 workers and if only you have 10 objects all these objects would be picked up at the same time. They would be worked on by one one worker and you would get your object. All of you who have requested for the object, they will be getting their object after like 10 minutes because it takes 10 minutes to create one instance. The whole idea is yes there are ways in which you can make your reconciler efficient which is what I did from time second to time millisecond that's always always possible you know again it depends how you have coded but the the main uh motive of this video is to make your operator behave in parallelism so you as a user are not waiting for someone to be free and pick up your request and start working on top of that. And that could be done with the controller options uh using the max current reconciles. That means um how many concurrent um instances of this reconcile function could be running. And because one worker will only work on one object, if you have 10, you could give 10. There's no limit to how many workers you can have. Of course, it's also a bad design. If you have like 100 workers but only at a time you get five requests, 10 requests, 10 object changes, creations on your custom resource, that's also a bad decision. There's no one stopping you from doing that. But you wouldn't be making 100 rooms for uh for people, you know, who are only maximum 10 at one time. So the decisions like these would actually help you to make your operators much more faster in terms of parallelism, right? And that's the object that's the the the objective of this video. If I would run that again now, you can see it's efficient in terms of how long does it take for the instance creation. It's like 5 milliseconds, but it still picked up all of your instances at parallel because you now have the worker count to be 10. Small things like this can really make a difference when you are using your object when you're using your operator at scale. All right. So for this best practice before I talk about what we are going to do here I rather want to um draw a picture. So if you have gone through my previous course of how to build your own uh Kubernetes operator from scratch um you must have heard me saying this many many times that every time you make an update to your object um you are triggering a reconciler loop and I've said this extensively in the video you run the reconciler loop again and this is Why you need to make things ident so that if the external state that your object uh is managing or your operator is managing if it matches the desired state that the user has asked for you simply do not do anything. Think about uh anible in this case you write a playbook that installs a server uh that's installs an engineext uh application server on on a Linux machine. it starts the service and you run that again. It does not reinstall EngineX. It does it does not restart the service. It just simply says things are exactly how you want me um to make and I'm not going to make any changes. The same thing happens in your reconciler. So any update that you are making whether it was uh an AI u you know tool that did it for you whether you did this with cube c or whether it was kit ops that makes the changes for whatever reason there's an update to your object an object has two fields which is spec and the status either one that you update This triggers a new reconciler loop and this is why um you know um your reconciler should be ademptent that if no change is needed I do not do anything. I have talked about this extensively uh in the in the YouTube video where we talk about how do you build an operator from scratch and I will be putting a link of that in the description. Now let me uh let me draw a picture. uh we made an EC2 operator that operator creates an instance for you. So in that operator we had an object um and we had a simple spec you know things that you need when you are creating um an instance which is uh with uh in Amazon. So, so you had basic things like you had the instance name that you want. Um the AMI ID um that you wanted to use. Uh you also had then the security group. Um you then talked about the storage that you want to use. Um and you could be as uh descriptive or as you know uh options you can give as you want, but I'm just going to keep these simple. This was a spec and the status pad. couple of fields like the public IP, what is the public IP of my instance and it could be um the creation timestamp. Now these things you will get or you will you know uh use to create an Amazon instance. And here is your EC2 instance. Now imagine you wanted to make this operator a bit more reliable or you wanted to make this operator a bit more um intelligent. What you thought of is I have the public IP, I have the creation timestamp. Um I want to periodically check if my instance is there or not to do some sort of drift detection. and you wanted the user to know when the last check was made. So you introduce a new field in the status. See, you're not changing the instance. You're not changing the instance's name. You're not changing its AMI ID. You're not changing anything anything on the instance. Nothing is being changed. The spec is exactly the same. What you are changing or rather introducing a new field which is called last checked at all you have made in your operator is it goes to Amazon with that instance's name sees this instance is there if it's there it does nothing it only updates your reconciler it does not do anything if it's there and running because that's what you want this is your desired state. If it's not running, you can start it. If it's deleted, you can maybe send an email from your reconciler. That's all uh you can do when it's not there. But if it's there, all you want it to do is to update your status of last checked app. Uh and this could be, you know, uh 5:00 p.m. um 5 p.m. 10 minutes and 20 seconds. This is when you made the change and you want to do this periodically every every 2 minutes. Let's say you want to make a check on Amazon that if the instance is there. Every 2 minute your reconciler is running not doing anything on the spec because you don't want to uh change anything on the spec but it goes ahead and checks your um you know it checks your instance and every 2 minutes it's updating this specification here which is last check. The thing is you are updating the status the resource is already in that state. Now what what's kind of happening is what I told you that when you make an update to your object when you make an an update to your object um it triggers a reconiler loop. We talked about this and if you are making changes to your object in the reconciler loop you will be rerunning you know you may update the object the reconciler loop runs you update the object the reconciler loop runs it does not matter if the object was updated externally from someone who's using cubectdl manually or it was updated in the reconciling loop itself within the reconciler the only thing Kubernetes cares about object was changed. I will rerun the reconciler loop which is then you have the steps for uh creating the instance. Then you have um you know um maybe sending an email to the team with the instance IP address um and all all of the steps that you need to do when you are creating an EC2 instance. The problem is these steps should only be running when you actually want to make a change whether it is creating a new instance which is in this case it just creates an instance just because you updated your object or I should say just because you updated your status that the instance was there you need not to run the whole reconciler loop you should run the reconciler loop again if I change the AMI ID in that case your operator should you know delete the instance and recreate that again you need to make some changes in the environment but if I just go ahead and check hey if the instance is there if it's there I just want to update the metadata I did not change the name of the instance I did not change the AMI ID of the instance no security groups are changed. I didn't increase the storage from 50 to 100 gigs. Nothing is changing. So why are you running your reconciled loop and wasting your precious CPU cycles? This is where you are trapped in a loop where you update the status which was supposed to be the metadata. The reconciler runs again and this loop continues forever. So this is what I kind of wanted to um show you. You know when you are creating the instance you have a spec here and then you have the status which is where we have the last check that your operator goes to Amazon sees if the instance is there if it's there we don't make changes to the spec but we make changes to the status where we update it with the last check that and the time the thing that happens is because you updated your object there was a watch event which your operator always uh watches on um you know um your controller always watches on the operator. What am I saying? Your controller always watches on the object that is changed and you rerun again even though you did not really want to make any change. your reconciled logic should have just skipped it that hey you know what guys the user did not ask me to make any changes to the WM why why am I even running okay I I know I have to be a dempotent so that if I run I should not make any changes but why run in the first place if I was not you know changing the spec if the user did not change the spec there's no reason for me to run the change was only at a status level which was only the metadata and this is the problem with uh the reconciler or not the problem but the way the reconcilers are created because Kubernetes does not differentiate if the change was in the spec which you most care about or was it in the status which you care about less to rerun your reconciler it only says the object was updated rerun the uh reconcil loop that's it so I hope you got the problem you're updating the status of the object and this triggers an update. Um, and this triggers a reconiler and within the reconiler you updated the status. You then got a update event. The reconiler runs again. You again go update the status. You then kubernetes says hey an update is there. the reconciler runs and this is going to make your operator a living hell. Yes. I mean see the thing is you could say um Shubhham it's not a problem even if my operate e even if my reconciler runs you told me to make it Impo. So even if it runs, nothing's going to be um changed in my Amazon instance. And that is correct. But that's the difference between you and a staff engineer or a principal engineer who would stop you from running that operator even in the first place because you're going to be using CPU of your cluster and if you do not design things properly that you could eventually uh you know uh waste your resources. So this is what we have been doing. So the user he makes a change to the API server. Um there's a there's a cop there's a component in your operator um which is called a reflector and this reflector is responsible to watch um your op your uh object. We are not going to go into details of what and how reflectors are implemented. Think about this as the deflector is the one responsible to keep a keep an eye on your object in the API server and it catches these events. It's the one that updates the informer. So you go ahead and you update the cache with the latest object that was updated by the user and then uh the event handler is the one responsible to add it to the working queue where you add what object has been changed and the reconil gets the object from this cache. So it doesn't go to the API server to get it. And what it does is it does an update of the status. Eventually, here's where the loop gets created, which is I just showed you above. That's the loop that you're making. We're just going into a bit more detail here. So every time you make a change to the object, the reflector gets it. The reflector sees that there was a change. The reflector talks to the informer which gets the object and stores it into the cache memory or the cache of the operator. Then the event handler is is is you know um informed that hey you know what this object has been changed please add it to the working queue. And the reconciler workers are looking at the working queue seeing that new object has been changed. It has been added to the working queue. I need to start and run uh my reconciler. Then in the reconciler you update the status and this whole story uh you know the whole story goes on. I think it makes more sense if I show this to you in a demo. So let me go to uh my trusted uh cursor and let me show you how this looks like. So this is standard cube builder. I'm sure you have gone through the video so you are familiar with uh how the cube builder u you know um how the cube builder structure looks like. The most important things that I wanted to show you is my API and this is what I have created. So I have a very simple um new custom resource called EC2 instance. It has two things. It has a spec of type EC2 instance spec and then it has a status of type EC2 instance status. Now this now the spec is very simple. It's just fu and bar. It is just the two fu and bar keys that I want to use. We're not creating a fully functional operator in this video. We did this in a previous course. This is a continuation from that course. The status is what we are more interested in. And here we have the last updated at which is a type of metav.micro time. This um this is a strct that lets you add a time up to microsconds. We will talk about that. And this is my simple uh EC2 instance custom resource. It just has a spec of bar and a status which is last updated at. Now how does my reconciler look? So if I go to my reconciler here, you can see um there are a couple of things that I do not need. Um uh wait uh I don't need the math library as well. So here's my reconciler. See this reconciler does something very very simple. It does something very simple. it gets um uh it creates a variable of type EC2 instance. So this is where we will be storing the object that we get from Kubernetes in this line here. This course is this code is something that I've already explained extensively uh into our uh Kubernetes operator video. So I'm just going to go through that very quickly and show you essentially what's happening. We create a variable of type EC2 instance which is our strct which is our custom resource. We get the object from the request that was made to my reconciler. You know API server said hello this object has been updated. I get the object. I stored it this into a variable and I'm reading that variable and as such. So f value is instance.spec.fu two and bar value is instance.spec.bar. Now you can see here so far I've just got the object and iterated over its values. Now here's where the interesting thing um here's where uh things gets interesting. What I wanted to make is I wanted to do an update. See I have this uh I have this EC2 instance custom resource. It has two fields spec and a status. I am only reading on the spec. I did not make any changes on the spec. What I am making the change on is the status and the field called last updated at. So I want to set the last updated as field uh to the current time till uh to to the microsconds of the current time. And this is where we get the time uh in UTC. And then we convert that or rather we format it uh into a um time which is microsconds. I think it it shows microsconds and then I'm doing a status.date function. What this does this line here updates your object. So essentially what's happening is you start your uh reconciler, you get the object. Okay. Uh here's where you got the object. Here you got the time and then here's where you read um or they could be interchanged, but that's not something we need to worry about. What's essentially happening here is where you update the status and because you updated the status with the R dot status dot update function. You're going to put this back to the working Q work Q. Think of this working queue as a place where once your reconciler is done he goes and sees is there something and yes you added your object key the name of the object that was updated here. Now what's going to happen? You update the status and then we are printing this function last update is you know uh you print something that last update is done blah blah blah and then you return. Now as soon as as soon as you do a return your operator is going to look in the working queue. He's going to read from the cube and this is going to write to the cube which is the name of the object that was updated. You read it, you rerun the whole reconciler from the beginning and here you see you update it again. You write it, you finish, you go back to the working queue, you run from the beginning, you read it, you write it. This is the loop that I've been talking about. And let me show you this in um in action. So here you can see we have uh I so there is no EC2 instances right now. I have already installed this in my cluster using cube builder and you can simply use the make generate make manifest um and make install. I've just uh uh done a clean installation. So I'm always using uninstall and install because I was making changes to my spec my status but it's installed. So you can do cubectl explain EC2 instance status and here you can see the status field has last updated app and spec has got bar and fu which is exactly the spec that we are creating. All of my terminals are connected to the same cluster where we have installed these custom resource definitions. Now if I wanted to show you what we are creating, you can see here it's a very simple EC2 instance. It just has a bar for testing. It has two for testing. Nothing nothing fancy. So I'll run my operator and because there is no EC2 uh resource uh there's no EC2 instance custom resource, it's just watching. It just says okay I'm waiting for any changes to happen on your operator on your custom resource whether you update it or you create that. Now I'm going to create this in my cluster and you're going to see something interesting which I've been trying to show you. Let's create that and you see what's happening. You see my operator is going crazy. It's running and running and here you can see the status update is happening which I wanted this but my operator keeps on running. My my reconciler has gone crazy. It's gone into a loop where it runs it updates the status then um it registers in the working queue. It exits, sees in the working queue, runs it again, updates the status, puts it into a working queue, exits, sees the working queue. And this is the whole shenanigan that's been going through. And this is something you absolutely do not want. You don't want to rerun your reconcile it just because you made a metadata change, which is the status. Why would you want to do that? Now something that I also want to u you know um put into here is something called generation. See as your operator is making changes to your resources um you do update the last update act because this is what you want to do but Kubernetes also updates something called a resource version. This is the version of the resource which was there in HCD. So you always um have a resource version which is telling you the latest version uh that your operator is uh in HCD. There's also something called generation. Now you can see my operator has been running for so long and I've got about so many resource version changes. However, I only have generation as one. Now let me show you something really interesting. If I stop my operator and I do a cubectl edit EC2 instances, I do a bar and I say new testing data. I save and quit that. See what happens. The generation changes uh this time not just with the resource version. Resource version changes anytime you make an object change. But generation changes only when you update the spec of Kubernetes of your object not the status because we kept running our operator for a very long time which is you can see again you can see here my status is changing my resource version changes but the generation never changes however as soon as I do a change in this case I do testing you see my generation is incremented by one. So every time you update your status, the generation does not change, but every time you update your spec, the generation is incremented. And that's something you can make use of when you are writing your operators to not go into this forever running loop um where you uh you know you update the object then you read you update the status you rerun and and getting yourself um throwing those precious CPU cycles away and here's what's going to happen. See when you are working with um with status updates you just see there was a change you don't care about if the change was in status you don't care about if the change was in spec or Kubernetes does not do that your operator does not care about it right now however in cube builder in controller runtime there is an option where you can say guys I only want to trigger a rerun of my reconciler if the generation was changed. That means if the spec was changed, you always want to rerun if the spec has changed. Because when you change the spec, that is generally to reflect the state that you want in here. If I let's say um you know um think about this if I have a deployment and this creates me it has um spec template.spec it has replicas. If I have five replicas I'm going to get five pods. If I change this to 10, I need to um take an action which is incrementing to five more pods. So I need to do something externally. I mean not external to Kubernetes in this case, but you want to make some changes. In case you want to add a new environment variable, you need to rerun your pods because the environment variable has been added there or you change the value to the environment variable. Um the pods have to be recreated. Some some change has to happen whether it is um recreating the pods, whether it is recreating the config map, whatever it is, you need to react on that change. But if you update the status, you do not need to react. Many of the times you do not need to react on uh these uh you know these these uh updates of the status and this is exactly what you can um you're going to be in a problem and the solution for that is to better manage your status updates. So the same flow continues. Somebody makes changes, user or your own operator makes changes via the API server to the object, you get storing this into HCD. Um, from there the reflector watches this event, it gets the data, gives it to the informer and you store that into your cache. Now because the operator was updated your reconciler has to run. Now what happens is you give it to the event handler which again was responsible to add this object into the queue. But now you have a predicate filter. You're making your operator a bit more smarter. what you say, hey event, uh, hey event handler, I want you to add this into the working queue only if there was a change in the spec, not if there was a change on the status. So I want you to ignore the noise or I want you to simply skip running the reconciler or rather one step further I don't want you to be putting this object into the working queue because once it gets into into the working queue your reconciler will take and run. So you're simply telling with this predicate that guys I only care about the changes in the spec. I do not care about uh the changes in the status. That means if the generation was changed then do your thing. If the generation did not change do not rerun that again. And there are there are some other predicates as well. The one we are talking about is the generation changed predicate. It simply checks if the you know um if the generation has changed. that means the spec has changed. That means I want to run it. But if the generation did not change, that means the spec did not change. Probably it was just a status update and do nothing. And that line, that one single configuration is going to stop you from wasting your precious cycles, your CPU cycles because um you don't want to just write an operator as a junior engineer. You want to write an operator thinking it's not just running, it is running efficiently. Because if you if you really go into uh operators, you want to keep them as efficient as possible. Otherwise, your cluster will be your resources are going to be wasting or wasted on the operators themselves rather than the data that they are managing. So you have to be um not careful but smart about it this time. That's why it's called a best practice. So, how do you how do you work with that? And here's where the magic happens. When you are setting up your controller with your manager, I think let me just quickly talk about that. You have a manager and within a manager you can have multiple APIs. Usually you have an API and for that you have a controller. The same thing happens here. Um let me copy that here and let me copy this. So you can make multiple APIs with cube builder in the same project. Um, for example, I could have my cube builder domain as cloud.com and I can have APIs as uh EC2 instance.cloud.com. Uh, I can have one object is is EC2 instance. I can have so this is going to be my API number one here. So, one controller that manages the EC2 instance for me. Um, the other one manages buckets. So buckets docloud.com. The other one could be storage u or it could be you know uh ebs.sto.cloud.com and then you can have uh esfs.sto.cloud.com. You can you can make multiple. The thing is all of these controllers have to be registered with the manager. This manager is responsible for running multiple workers threads um routines for your controllers. So this is the one which is doing the heavy lifting. We attach our controller um to the manager uh using this function setup with manager. we define which manager we want to use. Um and essentially we are telling that um for this object you know for this object I want you to attach to my manager I want you to do this with an event filter and here's the one that is going to make your ch your life a lot more easier. So what you're telling is I want to filter on some events previously. Previously you were just updating um whether it you were updating my working queue even if it was a spec change or either it was a status change. But this time I don't want you to do that. I want you to see if that was a spec change. I want you to see if the generation has changed. um then add this to my working queue. If it hasn't changed, then don't do that. Do not add it to the working queue. Do not let my reconciler rerun even if it's identic. Your playbook is topnotch. uh it is ident. But why would you even want to run this in the first place if there was no need you know um if no need don't run it if it runs it doesn't change okay that's a good argument but the better argument is if not needed do not run it um because even if you run it even if no changes were made you're wasting your compute and that's what we want to stop so with this event filter uh we have couple of predicates available and this generation change predicate It simply says um if your uh generation did not change uh don't do anything. So here is what you can see. Now if I run my controller now if I run my controller the first time my controller runs it will update the status because I want my status to be updated. The next time because there was a loop you saw before. the next time we are you know our controller is going to see hey you know what the generation did not change that change was only in the status so I'm not going to rerun that I'll just wait so let's do that now I hope this is going to work just as we wanted you see we did update our last updated at but that's it our operator is is active uh is running but not doing anything because of this one predicate because my generation did not change. If I want to show you that again you see uh right now our microsconds is 108. Uh just to show you that the changes are actually being made. This is what we have currently uh right now. And if I rerun my operator this will be updated. So you can see here now um the time has changed the status has been updated but now see what happened it's here. So see what happened um you updated your object. Okay you updated your object you updated this into the cache. You have stored this into the cache. Then the event handler before it added this back to the working queue, it saw that the generation has actually not changed and that's why it did not add it back to the working queue and the reconciler did not run. Even though the reconciler did update the status but it did not do anything at all apart from that. And that's what we wanted to do. We wanted to light our operators so that they do care about spec, but they do not care much about the changes on the status. This lets you have a free hand of any sort of metadata that you want to put into your status. You want it to be more descriptive. You want to have more fields in the status to make it as more descriptive as possible. But you were worrying or you were managing how do I not rerun my reconciler if I'm doing a status change. I have to be very careful. But this one single predicate does it for you. It says if no change in the generation I'm not going to make I'm not going to rerun that again. It did run the first time. You see we did read the value of it. We did read the value of it. Um and here is my is my update. So this is what I wanted to show you uh how the generation works, how the how the resource version works and especially the generation was changed when you update the spec in Kubernetes. Uh the the generation does not change when you change the status of your operator uh in Kubernetes. So I hope this has been really helpful and this is going to help you create operators better not create operators because that's the course that helps you to do this but this is going to help you write operators thinking from a point of view of a platform engineer who's not just a junior but uh a staff engineer or a principal engineer. Um these things are really mattering when you write operators and this is how you should write this in a better way. So, I hope this has been useful.

Generated algorithmically for Search Engine Indexing.

Summarize Another Video