- Published on
Mastering Kubernetes Deployment Strategies: Recreate, Rolling Updates, Blue/Green, and Canary Explained
- Authors
Introduction
In today’s fast-paced tech environment, seamless, reliable, and flexible application deployments are essential for delivering quality user experiences. Kubernetes provides robust deployment strategies that cater to various application needs and risk levels, allowing teams to update, scale, and release applications with precision and control.
This post will explore four popular Kubernetes deployment strategies: Recreate, Rolling Update, Blue/Green, and Canary. Understanding these approaches is crucial for any DevOps team looking to optimize the deployment process, minimize downtime, and manage risk effectively.
Deploying applications isn’t just about getting new features or updates out to users; it’s also about doing so in a way that ensures stability and allows for quick rollback in case of issues. A well-suited deployment strategy can mean the difference between a smooth release and service interruptions that impact users and operations.
Kubernetes provides various deployment strategies to address different needs—from minimizing downtime to enabling gradual rollouts and testing updates on a small portion of traffic before a full release. Let’s take a closer look at each of these methods.
Setup
To make this post more practical, we'll implement each deployment strategy in a real Kubernetes environment. If you have a running Kubernetes cluster in your home lab or have access to another cluster where you can practice, feel free to use it. But not everyone has access to one of these.
In that case, we will leverage killercoda, where we can use an empty pre-made cluster as a playground. After signing up, you can visit the Kubernetes Playground, where the latest Kubernetes version is already pre-installed.
To understand each strategy, we’ll use the same scenario for each one and achieve our goal using the specific deployment method.
The scenario is:
We have a Kubernetes Deployment that controls 4 Pods that are currently running. We have to update the Deployment to use another container image.
First, let's setup our default Deployment, that serves the old container image. In the terminal, paste this command to create a Deployment whose four replicas are using the httpd:alpine container image:
kubectl create deployment myapp-v1 --image=httpd:alpine --replicas=4
You can verify if everything is up and running correctly:
kubectl get deployments
# NAME READY UP-TO-DATE AVAILABLE AGE
# myapp-v1 4/4 4 4 7s
kubectl get pods
# NAME READY STATUS RESTARTS AGE
# myapp-v1-5b4dd59d67-7k77c 1/1 Running 0 11s
# myapp-v1-5b4dd59d67-lzm8n 1/1 Running 0 11s
# myapp-v1-5b4dd59d67-mc6hq 1/1 Running 0 11s
# myapp-v1-5b4dd59d67-z4xxq 1/1 Running 0 11s
Alright! The foundation is set, now let's take a look at how we can deploy the new Pods using each strategy.
Deployment Strategies
Recreate
This strategy is the most straightforward one. Here, all running pods are first terminated, and then all new Pods are created.
First, we will terminate all running Pods with the old image. To accomplish this, we will delete the deployment myapp-v1
.
kubectl delete deployment myapp-v1
We verify now that everything is deleted properly:
kubectl get deployments
# No resources found in default namespace.
kubectl get pods
# No resources found in default namespace.
Now, let's create a new deployment v2
that runs our new image nginx:alpine:
kubectl create deployment myapp-v2 --image=nginx:alpine --replicas=4
kubectl get deployments
# NAME READY UP-TO-DATE AVAILABLE AGE
# myapp-v2 4/4 4 4 5s
kubectl get pods
# NAME READY STATUS RESTARTS AGE
# myapp-v2-bbf4659cd-cf7hh 1/1 Running 0 8s
# myapp-v2-bbf4659cd-cv7kk 1/1 Running 0 8s
# myapp-v2-bbf4659cd-s2dxr 1/1 Running 0 8s
# myapp-v2-bbf4659cd-wrmhm 1/1 Running 0 8s
To verify that one of our Pods is running the correct image, we can use this command:
kubectl describe pod myapp-v2-bbf4659cd-cf7hh | grep "Image:"
# Image: nginx:alpine
IMPORTANT: The name of the Pod will be different for you.
Great! This was fairly easy on the one side but comes with a big trade-off on the other side: DOWNTIME.
Since all old instances are removed at once, users may experience a temporary service disruption. This strategy is often best suited for applications that don’t require high availability or where short, controlled downtimes are acceptable.
We have implemented this strategy manually to understand what's happening here. But in the real world, we would specify it in the Deployment manifest under spec.strategy.type
:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-v1
spec:
replicas: 4
strategy:
type: Recreate
...
Before we start with the next one, let's set everything up again to have our default Deployment up and running:
kubectl delete deployment myapp-v2
kubectl create deployment myapp-v1 --image=httpd:alpine --replicas=4
Rolling Update
This strategy is one of the most common Kubernetes deployment strategies, offering a balanced approach by gradually replacing old instances of an application with new ones.
Kubernetes launches the new version in batches (e.g., two new Pods at the same time) and waits for each batch to become healthy before moving on to the next. This reduces the risk of downtime because some instances of the previous version remain active throughout the process.
The Rolling Update is the default behavior when updating a Deployment in Kubernetes.
In our scenario, we're going to update our existing deployment with the new container image and apply those changes.
First, let's edit our existing deployment:
kubectl set image deployment/myapp-v1 httpd=nginx:alpine
Let's observe the rolling update process:
kubectl get pods -w
You can see that Kubernetes first creates the new batch of Pods, and only if they're healthy, the first batch of the old Pods are terminated. After that, the next batch will be created, and so on until we reach our desired replicas count.
This reduces the risk of downtime because some instances of the previous version remain active throughout the process.
Rolling Updates are ideal for teams that want to deploy with minimal service interruption and provide a smooth transition for users.
maxSurge
and maxUnavailable
Control the rolling update with When we're using this strategy, we can specify how many new Pods can be created and how many can be unavailable during the update. The options maxSurge
and maxUnavailable
are our friends in that case.
Both can be specified under spec.strategy.rollingUpdate
in a Deployment manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-v1
spec:
replicas: 4
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
...
The values for these options can be an absolute number (5) or percentage (10%). The default value for both is 25% when you don't specify it in your Deployment manifest.
maxUnavailable
defines the amount of Pods that can be unavailable during a rolling update.
If it is set to 30% and you have ten replicas, Kubernetes will ensure that at least 70% of the desired replicas (7 Pods) remain available to handle traffic during the update. Old Pods will be terminated in phases, and new Pods will start while maintaining that minimum threshold of 7 running Pods.
This setting is crucial for controlling the availability of your application during updates, especially in scenarios where you need a certain percentage of your Pods to be available at all times.
maxSurge
specifies the maximum number of Pods that can be created over the desired replica count during a rolling update.
When it's set to 50%, and your deployment defines ten replicas, Kubernetes allows up to 15 Pods (10 desired + 5 extra) to be running temporarily during the update. This lets the deployment start new Pods before terminating old ones, providing extra capacity to speed up the update.
Overall, maxSurge
allows for faster deployments by enabling a higher number of Pods to handle traffic while new ones come online, reducing the time it takes to achieve full replication of the new version.
Awesome! Before we continue, let's create our default environment again:
kubectl set image deployment/myapp-v1 httpd=httpd:alpine
Blue/Green
In this deployment strategy, two identical environments (Blue and Green) are used to host the old and new versions of the application. The active version (let’s say Blue) serves all traffic, while the new version (Green) is deployed and tested separately.
Once the Green version is confirmed to be stable, traffic is switched from Blue to Green. If any issues arise, traffic can easily be rolled back to the Blue environment, minimizing risk.
In our case, we will create a Service first that redirects traffic to the current Deployment. After that, we will add a new Deployment with the new image.
When all Pods are up and running, we will test them. For example, we could add a new Service for the Deployment that gets exposed via a custom route in the ingress, or we could forward a port of one of the new Pods.
If the tests are all good and the new version is deployed correctly, we adjust the new Deployment to receive the traffic (we'll see how we achieve it) and delete the old Deployment, which also kills the ReplicaSet it controls and the corresponding Pods.
First, we create a new Service which is connected to the Pods from the myapp-v1
Deployment:
nano myapp-service.yaml
Add the following code to the newly created yaml and save it:
apiVersion: v1
kind: Service
metadata:
labels:
app: myapp
name: myapp
spec:
selector:
version: v1
ports:
- nodePort: 30080
port: 80
protocol: TCP
targetPort: 80
type: NodePort
As you can see in the manifest, we're connecting to a Deployment with the label version: v1
.
To make it work, we have to give this label to the Pods of our Deployment that is up and running:
kubectl patch deployment myapp-v1 -p '{"spec": {"template": {"metadata": {"labels": {"version": "v1"}}}}}'
This will restart the existing Pods with a Rolling Update. To confirm that every pod is labeled correctly:
kubectl get pods --show-labels
# NAME READY STATUS RESTARTS AGE LABELS
# myapp-v1-74cffc64c9-2xsc6 1/1 Running 0 13s app=myapp-v1,pod-template-hash=74cffc64c9,version=v1
# myapp-v1-74cffc64c9-8gkr7 1/1 Running 0 10s app=myapp-v1,pod-template-hash=74cffc64c9,version=v1
# myapp-v1-74cffc64c9-jcbft 1/1 Running 0 13s app=myapp-v1,pod-template-hash=74cffc64c9,version=v1
# myapp-v1-74cffc64c9-lttfc 1/1 Running 0 10s app=myapp-v1,pod-template-hash=74cffc64c9,version=v1
Okay, this worked! Now, apply the Service:
kubectl apply -f myapp-service.yaml
Alright. We can now send traffic to the Service and see if we receive the correct response from one of the myapp-v1
Pods. To achieve this, we spin up a temporary container which serves a nginx image and makes a request to the internal URL of the Service:
k run temp --image=nginx --restart=Never -i --rm -- curl http://myapp.default.svc.cluster.local
# % Total % Received % Xferd Average Speed Time Time Time Current
# Dload Upload Total Spent Left Speed
# 100 45 100 45 0 0 975 0 --:--:-- --:--:-- --:--:-- 1000
# <html><body><h1>It works!</h1></body></html>
This is the response we wanted to receive!
Our application is now ready for a blue/green deployment, but it still serves the old image.
Let's fix that and create a new Deployment first, which serves up the new container image:
kubectl create deployment myapp-v2 --image=nginx:alpine --replicas=4
Now, add a new label called version: v2
to the Pods:
kubectl patch deployment myapp-v2 -p '{"spec": {"template": {"metadata": {"labels": {"version": "v2"}}}}}'
Let's take a look at all running Pods and verify we've got 8 Pods running in total (4 for v1
and 4 for v2
):
kubectl get pods
# NAME READY STATUS RESTARTS AGE
# myapp-v1-74cffc64c9-2xsc6 1/1 Running 0 20m
# myapp-v1-74cffc64c9-8gkr7 1/1 Running 0 20m
# myapp-v1-74cffc64c9-jcbft 1/1 Running 0 20m
# myapp-v1-74cffc64c9-lttfc 1/1 Running 0 20m
# myapp-v2-55fd57c4c6-7ps2t 1/1 Running 0 46s
# myapp-v2-55fd57c4c6-jn5wc 1/1 Running 0 47s
# myapp-v2-55fd57c4c6-mgd79 1/1 Running 0 45s
# myapp-v2-55fd57c4c6-xgkpk 1/1 Running 0 45s
Right now, we're in the phase where the Pods could be tested to see if everything is running properly. In our scenario, we'll spin up a temporary Pod again and route traffic to one of the new Pods.
First, let's check the IP addresses for our v2
Pods:
kubectl get pods -l version=v2 -o wide
# NAME READY STATUS RESTARTS AGE IP NODE
# myapp-v2-57dd85dcd7-9h8gt 1/1 Running 0 21m 192.168.0.15 controlplane
# myapp-v2-57dd85dcd7-sqfh4 1/1 Running 0 21m 192.168.0.17 controlplane
# myapp-v2-57dd85dcd7-tzt6s 1/1 Running 0 21m 192.168.0.16 controlplane
# myapp-v2-57dd85dcd7-xqld2 1/1 Running 0 21m 192.168.0.14 controlplane
After that, we spin up our temporary Pod again and test the connection:
kubectl run temp --image=nginx --restart=Never -i --rm -- curl http://192.168.0.15
# % Total % Received % Xferd Average Speed Time Time Time Current
# Dload Upload Total Spent Left Speed
# 100 615 100 615 0 0 98k 0 --:--:-- --:--:-- --:--:-- 100k
# <!DOCTYPE html>
# <html>
# <head>
# <title>Welcome to nginx!</title>
# <style>
# html { color-scheme: light dark; }
# body { width: 35em; margin: 0 auto;
# font-family: Tahoma, Verdana, Arial, sans-serif; }
# </style>
# </head>
# <body>
# <h1>Welcome to nginx!</h1>
# <p>If you see this page, the nginx web server is successfully installed and
#working. Further configuration is required.</p>
# <p>For online documentation and support, please refer to
# <a href="http://nginx.org/">nginx.org</a>.<br/>
# Commercial support is available at
# <a href="http://nginx.com/">nginx.com</a>.</p>
# <p><em>Thank you for using nginx.</em></p>
# </body>
# </html>
Great! This looks like the default response from nginx. In a real-world application, we could test the application a bit more, of course. For example, if the new features are working properly or if a critical bug is fixed correctly.
We're ready now to route our application traffic to the new Pods and terminate all old Pods.
Achieving this is fairly easy. All we have to do now is to switch spec.selector.version
to v2
in your Service:
kubectl patch service myapp -p '{"spec": {"selector": {"version": "v2"}}}'
This tells the Service now to route all incoming traffic to the Pods with the label version v2
. Let's verify it:
kubectl run temp --image=nginx --restart=Never -i --rm -- curl http://myapp.default.svc.cluster.local
# % Total % Received % Xferd Average Speed Time Time Time Current
# Dload Upload Total Spent Left Speed
# 100 615 100 615 0 0 98k 0 --:--:-- --:--:-- --:--:-- 100k
# <!DOCTYPE html>
# <html>
# <head>
# <title>Welcome to nginx!</title>
# <style>
# html { color-scheme: light dark; }
# body { width: 35em; margin: 0 auto;
# font-family: Tahoma, Verdana, Arial, sans-serif; }
# </style>
# </head>
# <body>
# <h1>Welcome to nginx!</h1>
# <p>If you see this page, the nginx web server is successfully installed and
#working. Further configuration is required.</p>
# <p>For online documentation and support, please refer to
# <a href="http://nginx.org/">nginx.org</a>.<br/>
# Commercial support is available at
# <a href="http://nginx.com/">nginx.com</a>.</p>
# <p><em>Thank you for using nginx.</em></p>
# </body>
# </html>
Amazing! We're now receiving the default response from nginx, which means that the blue/green deployment was successful. We can now delete the old Deployment, which also removes the associated resources (ReplicaSet and Pods):
kubectl delete deployment myapp-v1
As you have seen, this strategy allows for thorough testing in production-like conditions before fully switching users to the new version. It’s ideal for critical updates where a fail-safe rollback mechanism is necessary.
Before we move to the next strategy, let's clean everything up and create our starting position:
kubectl delete svc myapp
kubectl create deployment myapp-v1 --image=httpd:alpine --replicas=4
Canary
The Canary deployment strategy is highly flexible and allows for a gradual rollout of new features or updates. In a Canary deployment, a small portion of user traffic is directed to the new version while most users continue using the old version.
This approach is great for testing updates on a subset of real user traffic and monitoring their impact. If the canary release performs well, it can gradually be scaled up to full deployment; if not, it’s easy to revert to the previous version with minimal user impact.
So, you're testing the waters carefully before rolling it completely out.
The term canary comes from canary birds that were used in coal mines to detect toxic gases. In software development, a canary deployment is similar to this because it's a way to detect errors in a new version.
In our scenario, we would like to route 25% (1 Pod) of the traffic to the new version of our application and 75% (3 Pods) to the old. This will be a starting point for further tests to see if the new version is running correctly.
First, let's create the deployment that serves the nginx:alpine image with one replica:
kubectl create deployment myapp-v2 --image=nginx:alpine --replicas=1
To hit the percentages from above, we have to scale our myapp-v1
Deployment down to 3 replicas:
kubectl scale --replicas=3 deployment/myapp-v1
When we now check our running Pods, it should look like this:
kubectl get pods
# NAME READY STATUS RESTARTS AGE
# myapp-v1-74cffc64c9-2xsc6 1/1 Running 0 20m
# myapp-v1-74cffc64c9-8gkr7 1/1 Running 0 20m
# myapp-v1-74cffc64c9-jcbft 1/1 Running 0 20m
# myapp-v2-55fd57c4c6-7ps2t 1/1 Running 0 46s
Now, only a smaller amount of the traffic is redirected to the v2
version of our application. In a real-world scenario, this would be the phase now where we check if the new version is performing and running properly. We could check the KPIs (Key Performance Indicators) in our monitoring tools and receive user feedback about the new version, for example.
A typical workflow could look like this:
- The KPIs (Key Performance Indicators), such as response time and error rates, are measured
- Newly measured performance is compared with that of previous versions
- If the new version performs well, traffic to the new version is gradually increased (e.g. 10%, 25%, 50%, etc.)
- If the new version still performs well and no significant errors are found, all traffic is now directed to the new version
- If bugs are found at any time or poorer performance is detected, traffic is rolled back to a previous stable version
Tools like ArgoCD support this kind of strategy natively. For example, you can specify a Rollout
Manifest that would look like this in our scenario:
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: myapp
spec:
replicas: 4
strategy:
canary:
steps:
- setWeight: 25 # 👈 Route 25% of traffic to the new version
pause:
duration: 1h # 👈 Pause for 1 hour
- setWeight: 50 # 👈 Increase to 50% of traffic
pause:
duration: 1h # 👈 Pause for 1 hour
- setWeight: 100 # 👈 Route all traffic to the new version
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: nginx:alpine
This automatically increases the traffic to the new version until all traffic is redirected to it. You can specify pause times and percentages on how much traffic should be redirected. You can read more about it in the official docs.
In summary, canary deployments are popular for applications that require frequent updates and for teams that want real-world validation of changes before a full rollout.
Conclusion
Choosing the right Kubernetes deployment strategy is a key step in managing and scaling applications efficiently while minimizing downtime and ensuring a smooth user experience.
Each deployment strategy—whether Recreate, Rolling Update, Blue/Green, or Canary—comes with unique strengths and is suited to specific use cases, depending on factors like application type, user impact tolerance, and deployment frequency.
Understanding these deployment strategies equips teams with the flexibility to make informed decisions based on their specific operational needs and risk thresholds. In today’s fast-evolving tech landscape, selecting the right deployment approach can make all the difference in delivering seamless updates, enhancing application resilience, and ensuring a positive experience for end users.
By leveraging Kubernetes’ powerful deployment options, you can continuously innovate with confidence, knowing you have control over both the speed and stability of their release process.
I hope you've had as much fun as I have! Can't wait to see you in the next blog post.