![]() |
From Kubernetes in Action by Marko Lukša
This article discusses deploying and updating applications in a Kubernetes cluster. |
Save 37% on Kubernetes in Action. Just enter code fccluksa into the discount code box at checkout at manning.com.
Using deployments for managing apps declaratively
A Deployment is a high-level construct used for running and updating applications instead of doing the same through a replication controller or a replica set, which are both more low-level objects.
When you create a Deployment, a replica set is created underneath. The pods defined in a Deployment object are created and supervised by the Deployment’s replica set, as shown in figure 1.
Figure 1 A deployment is backed by a ReplicaSet, which supervises the deployment’s pods
You might be wondering why you’d want to complicate things by introducing another object on top of a replication controller or replica set, when they’re sufficient to keep a set of pod instances running. The reason lies in the fact that when you’re ready to update the app some time later, you need to introduce an additional replication controller and get the two controllers to dance around each other without stepping on each other’s toes. There needs to be something coordinating this dance. A Deployment object takes care of that.
Using a Deployment instead of a lower-level construct makes updating an app a lot easier, because, you’re only dealing with a single deployment object instead of multiple replication controllers, each managing their own individual pod versions, as we’ll see in the next few pages.
Creating a Deployment
Creating a Deployment isn’t much different from creating a replication controller or a replica set. A deployment is also composed of a label selector, desired replica count, and a pod template; but, in addition to that, it also contains a configurable deployment strategy, which defines how an update should be performed when the deployment is modified.
Let’s create a Deployment object and see how it compares to a replication controller. Here’s the YAML:
apiVersion: extensions/v1beta1 ❶ kind: Deployment ❷ metadata: name: kubia ❸ spec: replicas: 3 template: metadata: name: kubia labels: app: kubia spec: containers: - image: luksa/kubia:v1 name: nodejs
❶ Deployments are currently part of the extensions API group, version v1beta1
❷ Instead of ReplicationController, the kind is Deployment
❸ There’s no need to include the version in the name of the deployment
Because a replication controller usually manages a specific version of the pods, we usually include the version designator in its name (for example, a controller managing the first version of our app would be called kubia-v1). A Deployment, on the other hand, should not include the version number in its name; since, at any point in time, it can have multiple pod versions running under it.
Let’s create the deployment now:
$ kubectl create -f kubia-deployment-v1.yaml --record deployment "kubia" created
Once created, you can use the usual kubectl get deployment and the kubectl describe deployment commands to see details of the deployment, but let me point you to an additional command, which is made specifically for checking a deployment’s status:
$ kubectl rollout status deployment kubia deployment kubia successfully rolled out
The report states that the deployment has been successfully rolled out, and we should see the three pod replicas up and running. Let’s have a look:
$ kubectl get po NAME READY STATUS RESTARTS AGE kubia-1506449474-otnnh 1/1 Running 0 14s kubia-1506449474-vmn7s 1/1 Running 0 14s kubia-1506449474-xis6m 1/1 Running 0 14s
Take note of the names of these pods. Earlier, when we used a replication controller to create pods, their names were composed of the name of the controller plus a randomly generated string. The three pods created by the deployment now include an additional numeric value in the middle of their names. So, what is that exactly?
The number is actually the hashed value of the pod template in the replica set managing these pods. Remember, a deployment doesn’t manage pods directly. Instead, it creates replica sets to do that. Let’s see the replica set created by our deployment:
$ kubectl get replicasets NAME DESIRED CURRENT AGE kubia-1506449474 3 3 10s
The replica set’s name also contains the hash value of its pod template. A deployment creates multiple replica sets – one for each version. Using the hash value of the pod template instead of a random string allows the deployment to always use the same (possibly existing) replica set for a given version of the pod template.
With the three replicas created by this replica set running, we can now use the same service we created earlier to access them, because we’ve made the new pods’ labels match the service’s label selector.
So far, we’ve created a Deployment and it has spun up our three pods. Up until this point, we haven’t yet demonstrated a clear reason to use deployments over replication controllers. But, you’ll admit that creating a deployment hasn’t really been any harder than creating a replication controller. Now, let’s see why you should always prefer deployments over replication controller or replica sets. In the next few paragraphs, we’ll see how updating the app through a deployment compares to updating it through a replication controller.
Updating a Deployment
When you update an app being managed by a replication controller, you have to explicitly instruct Kubernetes to perform the update. Besides specifying the new image, you also have to specify the name of the new replication controller. Kubernetes then slowly replaces all the original pods with new ones and deletes the original replication controller at the end. Because the update process is orchestrated by the kubectl CLI tool, you basically have to stay at your desk until the process is completed, taking care not to accidentally close your terminal until kubectl finishes the rolling update.
Now compare this to how we’re about to update a deployment. The only thing we need to do is modify the pod template, defined in the deployment, and Kubernetes will take all the steps necessary to bring the actual state of the system to what is defined in the deployment. This is not much different than scaling up or down, but instead of modifying the replica count field, we’ll modify the reference to our container image (point to a new version of the image) and leave it up to Kubernetes to transform our system to match the new desired state.
How this new state should be achieved is governed by the deployment strategy configured in the deployment. The default strategy is to perform a rolling update. The alternative is the Recreate strategy, which first deletes all the old pods and only then creates the new ones, similar to modifying a replication controller’s pod template and then deleting all the pods (the replication controller will then bring up the new pods).
This delete-create strategy should be used when your application doesn’t support running multiple versions in parallel and requires the old version to be stopped completely before the new one is started. Of course, this strategy involves a short period of time when your app becomes completely unavailable.
The RollingUpdate strategy, on the other hand, removes old pods one-by-one, while adding new ones at the same time; this keeps the application available throughout the process and ensures that there is no drop in its capacity to handle requests. This strategy is the default. The upper and lower limits for the number of pods, above or below the desired replica count, are configurable. Since this strategy runs the new version before terminating the old one, you should only use this strategy when your app can handle running both old and new versions at the same time.
In our example, we’ll use the RollingUpdate strategy, but we’ll need to slow down the deployment process a little, to see that the update has indeed been performed in a rolling fashion. We can do that by setting the minReadySeconds attribute on the Deployment, which basically causes the rolling update process to pause for the specified number of seconds at each step. We’ll set it to 10 seconds with the patch command.
$ kubectl patch deployment kubia -p '{"spec": {"minReadySeconds": 10}}' "kubia" patched
Although we’ve modified the deployment object, our change doesn’t trigger any kind of update, since we haven’t changed the pod template in any way. Changing deployment properties that don’t directly affect its pods (this includes the desired replica count or the deployment strategy) doesn’t trigger a rollout.
If you’d like to see the update process from the clients’ perspective, run the following curl loop in another terminal before you trigger the rollout (replace the IP with the actual external IP of your service):
$ while true; do curl http://130.211.109.222; done
To trigger the actual rollout, we’ll change the image used in the pod’s single container to luksa/kubia:v2. There’s a dedicated kubectl command for doing exactly that and saves us from having to edit the whole YAML of the deployment object. The kubectl set image command allows changing the image of any resource that contains a container (pods, replication controllers, deployments, etc.). We’ll use it to modify our deployment like this:
$ kubectl set image deployment kubia nodejs=luksa/kubia:v2 deployment "kubia" image updated
With this command, we’re updating the kubia deployment by changing the nodejs container’s image to luksa/kubia:v2 (from :v1). This is shown in figure 2.
Figure 2 Updating a deployment’s pod template to point to a new image
Instead of the kubectl set image command, we could have used any of the other ways of modifying an existing object. Let’s see what those are:
Table 1 Modifying an existing resource in Kubernetes
Method | What it does |
kubectl edit |
Opens the object’s manifest in your default editor. After making changes, saving the file and exiting the editor, the object is updated.
Example: kubectl edit deployment kubia |
kubectl patch |
Modifies individual properties of an object.
Example: |
kubectl apply |
Modifies the object by applying property values from a full YAML or JSON file. If the object specified in the YAML/JSON doesn’t exist yet, it is created.
Example: |
kubectl replace |
Replaces the object with a new one from a YAML/JSON file. In contrast to the apply command above, this command requires the object to exist, otherwise it prints an error.
Example: |
kubectl set image |
Changes the container image defined in a pod, replication controller’s template, deployment, daemon set, job, or replica set.
Example: |
The above methods are all equivalent, as far as pointing a deployment to a different container image goes. What they do is simply modify the deployment object. This change then triggers the rollout process.
If you’ve run the curl loop, you’ll see requests initially hitting only the v1 pods, then more and more of them start hitting the v2 pods until, finally, every request hits the remaining v2 pods (when all v1 pods are deleted).
Let’s think about what we’ve just done and what happened behind the scenes. By changing the pod template inside the deployment, we’ve updated our app to a newer version – by only changing a single field! The controllers running as part of the Kubernetes control plane performed the update. The process wasn’t performed by the kubectl client, like it was when we used kubectl rolling-update. You’ll agree that this is much better than having to tell Kubernetes what to do and then waiting around for the process to be completed.
So, what actually happened below the surface? Basically, it’s not much different from what happened during the kubectl rolling-update. An additional ReplicaSet was created and scaled up slowly, while the previous ReplicaSet was scaled down to zero (the initial and final state is shown in figure 3).
Figure 3 Deployment during a rolling update
We can still see the old ReplicaSet next to the new one if we list them:
$ kubectl get rs NAME DESIRED CURRENT AGE kubia-1506449474 0 0 24m kubia-1581357123 3 3 23m
Like with replication controllers, all our new pods are now being supervised by the new ReplicaSet. Unlike before, the old replica set is still there (whereas the old replication controller was deleted at the end of the rolling-update process). The inactive replica set still has a purpose, but we shouldn’t really concern ourselves with ReplicaSets, since we didn’t create them directly. All we should care about is the Deployment object and take the underlying replica sets as an implementation detail. It should be clear that managing a single Deployment object is much easier than keeping track of and dealing with multiple replication controllers.
This difference may not be apparent when everything goes well with a rollout, but it becomes much more obvious when you hit a problem during the rollout process.
For more, check out the whole book on liveBook here.