<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Jean Mainguy on Medium]]></title>
        <description><![CDATA[Stories by Jean Mainguy on Medium]]></description>
        <link>https://medium.com/@jhandguy?source=rss-60d6fa56fda3------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*sfhTYdciua8QI7eGchvH7w.jpeg</url>
            <title>Stories by Jean Mainguy on Medium</title>
            <link>https://medium.com/@jhandguy?source=rss-60d6fa56fda3------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Mon, 13 Jul 2026 13:37:01 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@jhandguy/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Farewell, ECS! Hello, EKS: A Kubernetes Migration Story]]></title>
            <link>https://code.egym.com/farewell-ecs-hello-eks-a-kubernetes-migration-story-0b89f95fbf0a?source=rss-60d6fa56fda3------2</link>
            <guid isPermaLink="false">https://medium.com/p/0b89f95fbf0a</guid>
            <category><![CDATA[cloud-computing]]></category>
            <category><![CDATA[ek]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[ec]]></category>
            <category><![CDATA[kubernetes]]></category>
            <dc:creator><![CDATA[Jean Mainguy]]></dc:creator>
            <pubDate>Wed, 28 Feb 2024 10:21:37 GMT</pubDate>
            <atom:updated>2024-05-04T18:47:31.407Z</atom:updated>
            <content:encoded><![CDATA[<h4>Learn how we migrated EGYM’s production workloads on AWS from ECS to EKS with zero downtime!</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/982/1*iC1KjhDGa65fB163I9zj6g.png" /><figcaption>Kubernetes, Kubernetes everywhere</figcaption></figure><blockquote><strong>Pssst</strong>! I started my own <a href="https://jhandguy.github.io/"><strong>blog</strong></a>!<br>You can read this <a href="https://jhandguy.github.io/posts/farewell-ecs-hello-eks"><strong>very same article</strong></a> over there too!<br>No paywall, no ad, no Javascript — no bullshit, just pure content.<br>See you there!</blockquote><p>Saying goodbye to old systems can sound scary, but for us at EGYM, migrating from Elastic Container Service (ECS) to Elastic Kubernetes Service (EKS) was a journey of growth and excitement.</p><p>🎬 Hi there, I’m Jean!</p><p>In this blog post, I’ll share the inside scoop on our successful migration from ECS to EKS, why we did it and how, complete with the magic formula for <strong>zero downtime</strong>!</p><h4>Starting with the Why</h4><p>Let’s kick it off with a famous phrase in engineering:</p><blockquote>If it ain’t broke, don’t fix it.</blockquote><p>Although this holds true in most cases, there are some situations where the costs of keeping the lights on for “what isn’t broken”, can no longer be justified.</p><p>Originally, EGYM’s entire infrastructure was hosted on a single Cloud, Google Cloud Platform (GCP), and all of our workloads were running in Google Kubernetes Engine (GKE). This held true until EGYM acquired a company, which made us embrace multi-Cloud by adding 3 ECS clusters hosted on Amazon Web Services (AWS).</p><p>While the merger itself was more or less successful from a business perspective (depending who you ask), maintaining, coaching and hiring engineers with the necessary skills to work with 2 Clouds, fundamentally different yet doing pretty much the same thing, became challenging and most importantly: costly.</p><p>With Kubernetes being Cloud-agnostic by design, we soon realized there could be potential for porting most of the core components we already used in GKE, to EKS. This fact alone made the case for moving away from ECS towards EKS much easier to sell to upper management.</p><p>Very soon, we all unanimously agreed in the SRE team that migrating to EKS was the way forward, and that we needed to evaluate its feasibility.</p><h4>Evaluating Feasibility with a Proof of Concept (PoC)</h4><p>One of the main goals of this PoC, was to design an EKS cluster from the ground up with all the core infrastructure components that our micro-services would need to serve traffic reliably (load balancing, auto-scaling, monitoring, etc).</p><p>From all the technical choices we had to make, deciding which Load Balancer to pick was by far the most challenging. <a href="https://github.com/kubernetes/ingress-nginx">Ingress NGINX</a> seemed like an obvious candidate for its Cloud-agnostic design, yet <a href="https://github.com/kubernetes-sigs/aws-load-balancer-controller">AWS Load Balancer Controller</a> also caught our eye for its native integration with AWS Load Balancers.</p><p>AWS Load Balancer Controller is not a Load Balancer per se, but the Controller that provisions them. One can either provision a Network Load Balancer (NLB) using the <a href="https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.7/guide/service/annotations/">Service annotations</a> or an Application Load Balancer (ALB) using the <a href="https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.7/guide/ingress/annotations/">Ingress annotations</a>.</p><p>Unless explicitly configured otherwise, AWS Load Balancer Controller will provision an AWS Load Balancer per Service/Ingress resource, which can be a driver for higher costs.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PzolZ8l_GtZJ4Ppt10tKdw.png" /><figcaption>Diagram of AWS Load Balancer Controller in EKS</figcaption></figure><p>Ingress NGINX is fundamentally different in the fact that it does not only act as a Controller, but also as an L7 Load Balancer: it is responsible for both satisfying Ingress rules and load balancing HTTP traffic to the corresponding Services.</p><p>While AWS Load Balancer Controller can be used to provision both ALBs (L7 Load Balancers) and NLBs (L4 Load Balancers), Ingress NGINX needs to be itself load balanced by an L4 Load Balancer (NLB), before taking over and load balance traffic on the application layer.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9a9Z_64SAMiK_uvIRMVU2w.png" /><figcaption>Diagram of NGINX Ingress Controller in EKS</figcaption></figure><h4>Testing Staging Workloads in EKS</h4><p>As soon as our EKS clusters architecture was designed and showcased in a PoC, it was time to port the staging workloads to a new shiny EKS test cluster and make sure they function equally well in ECS and EKS.</p><p>While this part was mostly smooth sailing, here are a few issues that we faced along the way:</p><ul><li>Many of our micro-services hosted on AWS are Java services, which are using the <a href="https://aws.amazon.com/sdk-for-java/">AWS SDK for Java</a> to interact with AWS services. On paper this should not be a problem however, the authentication method used in EKS (<a href="https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html">AssumeRoleWithWebIdentity</a>) is different than in ECS (<a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html">AmazonEC2ContainerServiceforEC2Role</a>) and sometimes, based on how the SDK was utilized or whether the AWS STS package was found in the classpath, the default authentication chain provider would choose the wrong authentication method in EKS and fail to authenticate.</li><li>Some of our micro-services perform asynchronous processing (also known as event messaging), which meant we had to ensure each event consumption was done in an idempotent operation or else be at risk of data duplication/corruption during the migration.</li></ul><p>Once the staging workloads were successfully deployed and tested, it was time to migrate the production workloads to EKS as well!</p><h4>Deploying Production Workloads to EKS</h4><p>To ensure maximum availability during the migration, we decided to run all our workloads twice: one deployment remained in the existing ECS production cluster and a new replica set was deployed in the new EKS production cluster.</p><p>This approach had the following advantages:</p><ul><li><strong>Isolated Testing</strong>: Since our ECS cluster was already serving user traffic, it was easy to deploy and test all production services via a separate domain and Ingress in the EKS cluster without affecting users.</li><li><strong>Blue-Green Deployment</strong>: By far the biggest advantage of running the EKS cluster along side the ECS one, was the ability to route user traffic to EKS gradually and to quickly roll it back to ECS in case things went south.</li></ul><p>However, running things twice also had some drawbacks:</p><ul><li><strong>Added Costs</strong>: Duplicating the replica sets also meant doubling the costs for a short period of time, which was a conscious investment we made to guarantee high availability during the entirety of the migration.</li><li><strong>Increased Connections</strong>: With twice more workloads, twice more connections were opened to our data infrastructure such as MySQL databases, Redis instances and ActiveMQ brokers, which meant raising the connection limit temporarily in some cases.</li></ul><p>Eventually, all EGYM’s production workloads were deployed to EKS and tested successfully, in isolation from the services deployed in ECS.</p><p>The next step was to execute our rollout plan, which consisted of 3 phases:</p><ol><li><strong>Enabling Asynchronous Processing in EKS</strong></li><li><strong>Routing User Traffic Gradually to EKS</strong></li><li><strong>Scaling down Workloads in ECS</strong></li></ol><h4>Enabling Asynchronous Processing in EKS</h4><p>While deploying EGYM’s micro-services to EKS, we took the precautionary measure of disabling asynchronous processing (PubSub, SQS/SNS, etc.) for all workloads deployed in EKS.</p><p>This way, we ensured all upstream connections were ready before we would enable event processing gradually, giving us plenty of time to observe and ensure events were processed properly.</p><p>Once asynchronous processing was enabled on all replica sets in EKS, it was finally time for the most exciting yet also treacherous part of the migration: routing user traffic.</p><h4>Routing User Traffic Gradually to EKS</h4><p>In order to gradually pour traffic in the new EKS cluster, we took advantage of 2 routing mechanisms:</p><ul><li><a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/continuous-deployment.html"><strong>CloudFront Staging Distribution</strong></a><strong><br></strong>With most of our AWS traffic being served on CloudFront’s edges, one easy and powerful way of routing traffic to EKS gradually, was using CloudFront Staging Distribution.<br>Simply put, a Staging Distribution is like a blue-green deployment but for Content Delivery Networks (CDN): when deploying a new proxy configuration, one can choose to deploy it only to a subset of users and roll it out gradually to observe the change while minimizing risks.<br>The advantage of a Staging Distribution is that it is directly deployed on the edges, meaning that rolling back in case of an incident is almost instant.<br>The drawback on the other hand, is that it is currently only possible to route up to 15% of user traffic to a Staging Distribution before promoting it to a Production Distribution.</li><li><a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy-weighted.html"><strong>Route 53 Weighted Routing</strong></a><strong><br></strong>This is where Route 53 Weighted Routing comes in: for routing more than 15% of user traffic to EKS, we had to temporarily route user traffic away from the CDN, and directly towards the NLB.<br>With Weighted Routing, one has complete control of how much traffic is sent to either one of the origins however, the Time To Live (TTL) of the record (60 seconds for A records) must be taken into account when rolling back with DNS.</li></ul><p>By exploiting both <strong>CloudFront Staging Distribution</strong> and <strong>Route 53 Weighted Routing</strong> routing mechanisms, we were able to gradually transfer traffic from ECS to EKS while benefiting from the incredible rollback speed of CloudFront for the first 15% of routed traffic, thus ensuring the highest availability possible.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*i8pMaVy5mg3eCHAtU9dL4Q.png" /><figcaption>Example of 50/50 traffic split using Route 53 Weighted Routing and CloudFront Staging Distribution</figcaption></figure><p>We proceeded with a gradual rollout of roughly 2 weeks, starting at 1%, followed by 5%, 10%, 15%, 25%, 35%, 50%, 65%, 80% and finally 100%.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_BW8lysCTkg5aHrNB54WdA.png" /><figcaption>NGINX Traffic Distribution between ECS and EKS during migration</figcaption></figure><p>For the first 15%, traffic routed to EKS was entirely served on CloudFront’s edges, and above the 15%, weighted routing was handled on DNS level via Route 53.</p><p>Once arrived at 80% of traffic routed to EKS, the last bump to 100% was achieved, not by routing traffic exclusively from Route 53, but by promoting the CloudFront Staging Distribution to Production Distribution instead and transfer all user traffic back to CloudFront.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kwEJ_m5NlFzFLrM4ifklHQ.png" /><figcaption>CloudFront Request Throughput and Latency during migration</figcaption></figure><p>As more traffic entered the EKS cluster, we were able to cautiously observe the Horizontal Pod Autoscalers and ensure all deployments were scaling properly as the load increased.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DvF6bId3jerrOrMtaLqRPw.png" /><figcaption>Ingress NGINX Request Success Share and Average Latency during migration</figcaption></figure><p>Ultimately, with the EKS cluster reporting stable and the availability of the services left intact, we were ready to proceed with the ECS scale-down.</p><h4>Scaling down Workloads in ECS</h4><p>Sunsetting workloads in ECS was the final moment of truth, leading to the fateful question: did we miss anything?</p><p>As we scaled down each service in ECS one by one, we paid special attention to asynchronous processing and event messaging until…</p><blockquote>“The last ECS Task, a digital phoenix battling resource constraints, choked on its final heartbeat, its containerized spirit evaporating into the cloud.”<br><em>Gemini, February 2024</em></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xjwkPWk5VD7FyswfJvBjkg.png" /><figcaption>Empty Production ECS Cluster</figcaption></figure><h4>Acknowledgements</h4><p>I would like to thank Kolawole for helping me in carrying this project to its completion, Marty for trusting me in leading the project, as well as Viktor, Serhii, Roma and Eugene for their continued support throughout the migration.</p><p>That’s it! I hope this experience was valuable to you, if so:</p><ol><li>Let me know in the comments below! 👇</li><li>Don’t forget to hit that <a href="https://medium.com/@jhandguy">subscribe</a> button! ✅</li><li>Follow me on <a href="https://twitter.com/jhandguy">Twitter</a>, I’ll be happy to answer any of your questions and you’ll be the first ones to know when a new article comes out! 👌</li></ol><p>Bye-bye! 👋</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0b89f95fbf0a" width="1" height="1" alt=""><hr><p><a href="https://code.egym.com/farewell-ecs-hello-eks-a-kubernetes-migration-story-0b89f95fbf0a">Farewell, ECS! Hello, EKS: A Kubernetes Migration Story</a> was originally published in <a href="https://code.egym.com">EGYM Software Development</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Vertical Pod Autoscaler in Kubernetes]]></title>
            <link>https://code.egym.com/vertical-pod-autoscaler-in-kubernetes-b12a5c61393f?source=rss-60d6fa56fda3------2</link>
            <guid isPermaLink="false">https://medium.com/p/b12a5c61393f</guid>
            <category><![CDATA[metrics-server]]></category>
            <category><![CDATA[vertical-scaling]]></category>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[vertical-pod-autoscaler]]></category>
            <category><![CDATA[autoscaling]]></category>
            <dc:creator><![CDATA[Jean Mainguy]]></dc:creator>
            <pubDate>Tue, 08 Nov 2022 16:23:08 GMT</pubDate>
            <atom:updated>2023-12-03T10:58:20.106Z</atom:updated>
            <content:encoded><![CDATA[<h4>Learn how to use Vertical Pod Autoscaler (VPA) to vertically scale services in Kubernetes automatically based on resource metrics.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*-sLCyA5H72U9H6JR" /><figcaption>Photo by <a href="https://unsplash.com/@tvick?utm_source=medium&amp;utm_medium=referral">Taylor Vick</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><blockquote><strong>Pssst</strong>! I started my own <a href="https://jhandguy.github.io/"><strong>blog</strong></a>!<br>You can read this <a href="https://jhandguy.github.io/posts/vertical-pod-autoscaler/"><strong>very same article</strong></a> over there too!<br>No paywall, no ad, no Javascript — no bullshit, just pure content.<br>See you there!</blockquote><p>In Kubernetes, we usually think about the Horizontal Pod Autoscaler (HPA) when referring to autoscaling. In most cases, it will be the preferred way of scaling services, based on CPU usage, memory usage, or custom metrics.</p><blockquote>If you haven’t already, go read <a href="https://medium.com/@jhandguy/horizontal-pod-autoscaler-in-kubernetes-part-1-simple-autoscaling-using-metrics-server-929e96cc2ab2"><strong>Horizontal Pod Autoscaler in Kubernetes (Part 1) — Simple Autoscaling using Metrics Server</strong></a> and learn how to implement a Horizontal Pod Autoscaler using Metrics Server!</blockquote><p>However, while HPA can scale up and down replicas based on the current load, it is not capable of optimizing resource usage over the long term: This is where the Vertical Pod Autoscaler (VPA) comes in.</p><blockquote>The VPA can be leveraged to optimize resource usage over time, based on mid to long-term observation.</blockquote><p>Please note that to avoid a race condition, the VPA should only be used together with HPAs that are based on custom metrics. In addition, the VPA should not be used with JVM-based services due to limited visibility into the actual memory usage of the workload (learn more about its limitations <a href="https://cloud.google.com/kubernetes-engine/docs/concepts/verticalpodautoscaler#limitations">here</a>).</p><blockquote>If you haven’t already, go read <a href="https://medium.com/@jhandguy/horizontal-pod-autoscaler-in-kubernetes-part-2-advanced-autoscaling-using-prometheus-adapter-d2b8bc3a019d"><strong>Horizontal Pod Autoscaler in Kubernetes (Part 2) — Advanced Autoscaling using Prometheus Adapter</strong></a> and learn how to implement a Horizontal Pod Autoscaler using Prometheus Adapter!</blockquote><p>🎬 Hi there, I’m Jean!</p><p>In this article, we’re going to learn how to use <strong>Vertical Pod Autoscaler (VPA)</strong> to vertically scale services in Kubernetes automatically based on resource metrics! 💪</p><h4>Requirements</h4><p>Before we start, make sure you have the following tools installed:</p><ul><li><a href="https://kind.sigs.k8s.io/docs/user/quick-start/#installation">Kind</a></li><li><a href="https://kubernetes.io/docs/tasks/tools/">Kubectl</a></li><li><a href="https://helm.sh/docs/intro/install/">Helm</a></li><li><a href="https://k6.io/docs/getting-started/installation/">K6</a></li></ul><blockquote><em>Note: for MacOS users or Linux users using Homebrew, simply run: </em><em>brew install kind kubectl helm k6</em></blockquote><p>All set? Let’s go! 🏁</p><h4>Creating Kind Cluster</h4><p><a href="https://kind.sigs.k8s.io/docs/user/quick-start/#installation">Kind</a> is a tool for running local Kubernetes clusters using Docker container “nodes”. It was primarily designed for testing Kubernetes itself, but may be used for local development or CI.</p><p>I don’t expect you to have a demo project in handy, so <a href="https://github.com/jhandguy/vertical-pod-autoscaler">I built one</a> for you.</p><pre>git clone <a href="https://github.com/jhandguy/vertical-pod-autoscaler.git">https://github.com/jhandguy/vertical-pod-autoscaler.git</a></pre><pre>cd vertical-pod-autoscaler</pre><p>Alright, let’s spin up our Kind cluster! 🚀</p><pre>➜ kind create cluster --image kindest/node:v1.27.3 --config=kind/cluster.yaml</pre><pre>Creating cluster &quot;kind&quot; ...<br> ✓ Ensuring node image (kindest/node:v1.27.3) 🖼<br> ✓ Preparing nodes 📦<br> ✓ Writing configuration 📜<br> ✓ Starting control-plane 🕹️<br> ✓ Installing CNI 🔌<br> ✓ Installing StorageClass 💾</pre><pre>Set kubectl context to &quot;kind-kind&quot;<br>You can now use your cluster with:</pre><pre>kubectl cluster-info --context kind-kind</pre><pre>Have a question, bug, or feature request? Let us know! <a href="https://kind.sigs.k8s.io/#community">https://kind.sigs.k8s.io/#community</a> 🙂</pre><h4>Installing cert-manager</h4><p><a href="https://github.com/cert-manager/cert-manager">cert-manager</a> is a Kubernetes addon that automates the management and issuance of TLS certificates from various issuing sources. It ensures certificates are valid and up to date periodically, and attempts to renew certificates at an appropriate time before expiry.</p><p>cert-manager can be installed via its <a href="https://github.com/cert-manager/cert-manager/tree/master/deploy/charts/cert-manager">Helm chart</a>.</p><pre>helm repo add jetstack https://charts.jetstack.io</pre><pre>helm install jetstack/cert-manager --name-template cert-manager --create-namespace -n cert-manager --values kind/cert-manager-values.yaml --version 1.13.2 --wait</pre><p>If everything went fine, you should be able to see three newly spawned Deployments with the READY state!</p><pre>➜ kubectl get deploy -n cert-manager<br>NAME                      READY   UP-TO-DATE   AVAILABLE   AGE<br>cert-manager              1/1     1            1           6m27m<br>cert-manager-cainjector   1/1     1            1           6m27m<br>cert-manager-webhook      1/1     1            1           6m27m</pre><h4>Installing NGINX Ingress Controller</h4><p><a href="https://github.com/kubernetes/ingress-nginx">NGINX Ingress Controller</a> is one of the <a href="https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/#additional-controllers">many available Kubernetes Ingress Controllers</a>, which acts as a load balancer and satisfies routing rules specified in <a href="https://kubernetes.io/docs/concepts/services-networking/ingress/#what-is-ingress">Ingress</a> resources, using the <a href="https://nginx.org/en/">NGINX reverse proxy</a>.</p><p>NGINX Ingress Controller can be installed via its <a href="https://github.com/kubernetes/ingress-nginx/tree/main/charts/ingress-nginx">Helm chart</a>.</p><pre>helm repo add ingress-nginx <a href="https://kubernetes.github.io/ingress-nginx">https://kubernetes.github.io/ingress-nginx</a></pre><pre>helm install ingress-nginx/ingress-nginx --name-template ingress-nginx --create-namespace -n ingress-nginx --values kind/ingress-nginx-values.yaml --version 4.8.3 --wait</pre><p>Now, if everything goes according to plan, you should be able to see the <strong>ingress-nginx-controller</strong> Deployment running.</p><pre>➜ kubectl get deploy -n ingress-nginx<br>NAME                       READY   UP-TO-DATE   AVAILABLE   AGE<br>ingress-nginx-controller   1/1     1            1           4m35s</pre><h4>Installing Metrics Server</h4><p><a href="https://github.com/kubernetes-sigs/metrics-server">Metrics Server</a> is a source of container resource metrics, which collects them from Kubelets and exposes them in Kubernetes API server through <a href="https://github.com/kubernetes/metrics">Metrics API</a> for use by <a href="https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/">Horizontal Pod Autoscaler</a> and <a href="https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler/">Vertical Pod Autoscaler</a>.</p><p>Metrics Server can be installed via its <a href="https://github.com/kubernetes-sigs/metrics-server/tree/master/charts/metrics-server">Helm chart</a>.</p><pre>helm repo add metrics-server <a href="https://kubernetes-sigs.github.io/metrics-server">https://kubernetes-sigs.github.io/metrics-server</a></pre><pre>helm install metrics-server/metrics-server --name-template metrics-server --create-namespace -n metrics-server --values kind/metrics-server-values.yaml --version 3.11.0 --wait</pre><p>Now, if everything goes well, you should see a <strong>metrics-server </strong>Deployment running.</p><pre>➜ kubectl get deploy -n metrics-server<br>NAME             READY   UP-TO-DATE   AVAILABLE   AGE<br>metrics-server   1/1     1            1           3m21s</pre><h4>Installing Vertical Pod Autoscaler</h4><p><a href="https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler">Vertical Pod Autoscaler</a> (VPA) is a component of the <a href="https://github.com/kubernetes/autoscaler">Kubernetes Autoscaler</a> that frees users from the necessity of setting up-to-date resource limits and requests for the containers in their pods.</p><p>When configured, it will set the requests automatically based on usage and thus allow proper scheduling onto nodes so that the appropriate resource amount is available for each pod. It will also maintain ratios between limits and requests that were specified in the initial container configuration.</p><p>It can both down-scale pods that are over-requesting resources, and also up-scale pods that are under-requesting resources based on their usage over time.</p><blockquote>Note: VPA is still in its beta phase, using it for production is at your own risk.</blockquote><p>As of this writing, Kubernetes does not provide an official Helm chart, so I went ahead and built one!</p><pre>helm install helm-chart --name-template vertical-pod-autoscaler --create-namespace -n vertical-pod-autoscaler --wait</pre><p>If everything goes fine, you should eventually see three Deployments with the READY state!</p><pre>➜ kubectl get deploy -n vertical-pod-autoscaler<br>NAME                               READY UP-TO-DATE AVAILABLE AGE<br>vert...scaler-admission-controller 1/1   1          1         2m32s<br>vert...scaler-recommender          1/1   1          1         2m32s<br>vert...scaler-updater              1/1   1          1         2m32s</pre><p>As you can observe, the VPA is split into 3 separate components:</p><ul><li>The <a href="https://github.com/kubernetes/autoscaler/blob/master/vertical-pod-autoscaler/pkg/recommender">Recommender</a> computes the recommended resource requests for pods based on historical and current usage of the resources. The current recommendations are then put in the status of the VPA resource, where they can be inspected;</li><li>The <a href="https://github.com/kubernetes/autoscaler/blob/master/vertical-pod-autoscaler/pkg/updater">Updater</a> decides which pods should be restarted based on resource allocation recommendations calculated by Recommender. If a pod should be updated, Updater will try to evict the pod. It respects the pod disruption budget, by using the Eviction API to evict pods. Updater does not perform the actual resources update but relies on Admission Controller to update pod resources when the pod is recreated after eviction.</li><li>The <a href="https://github.com/kubernetes/autoscaler/blob/master/vertical-pod-autoscaler/pkg/admission-controller">Admission Controller</a> will get a request from the API server for each pod creation and will either decide there’s no matching VPA configuration or find the corresponding one and use the current recommendation to set resource requests in the pod.</li></ul><h4>Configuring Vertical Pod Autoscaler</h4><p>Now that the Vertical Pod Autoscaler is up and running, let’s get to it, shall we? 🧐</p><pre>helm install sample-app/helm-chart --name-template sample-app --create-namespace -n sample-app --wait</pre><p>If everything goes fine, you should eventually see one Deployment with the READY state.</p><pre>➜ kubectl get deploy -n sample-app<br>NAME         READY   UP-TO-DATE   AVAILABLE   AGE<br>sample-app   3/3     3            3           58s</pre><p>Alright, now let’s have a look at the VPA!</p><pre>➜ kubectl describe vpa -n sample-app<br>...<br>Spec:<br>  Resource Policy:<br>    Container Policies:<br>      Container Name:  sample-app<br>      <strong>Controlled Resources:<br>        cpu<br>        memory<br>      Max Allowed:<br>        Cpu:     100m<br>        Memory:  200Mi<br>      Min Allowed:<br>        Cpu:     10m<br>        Memory:  20Mi</strong><br>  Target Ref:<br>    API Version:  apps/v1<br>    Kind:         Deployment<br>    Name:         sample-app<br>  Update Policy:<br>    <strong>Update Mode:  Auto</strong></pre><p>As you can see, this VPA is configured to scale the service based on its CPU and memory resources. Its <strong>spec</strong> states that the minimum allowed CPU/memory is <em>10m/20Mi</em> and the maximum is <em>100m/200Mi</em>.</p><p>Finally, its <strong>update policy</strong> is in “Auto” mode, meaning that VPA assigns resource requests on pod creation as well as updates them on existing pods using the preferred update mechanism.</p><p>Currently, both the resource requests and limits are matching the VPA’s minimum allowance.</p><pre>➜ kubectl get pods -n sample-app -o yaml | grep -A 6 &#39;resources:&#39;<br>      resources:<br>        limits:<br><strong>          cpu: 10m<br>          memory: 20Mi</strong><br>        requests:<br><strong>          cpu: 10m<br>          memory: 20Mi</strong><br>--<br>      resources:<br>        limits:<br><strong>          cpu: 10m<br>          memory: 20Mi</strong><br>        requests:<br><strong>          cpu: 10m<br>          memory: 20Mi</strong><br>--<br>      resources:<br>        limits:<br><strong>          cpu: 10m<br>          memory: 20Mi</strong><br>        requests:<br><strong>          cpu: 10m<br>          memory: 20Mi</strong></pre><p>Now, let’s give some load to our service and see what happens!</p><p>For Load Testing, I really recommend <a href="https://k6.io/docs/">k6</a> from the Grafana Labs team. It is a dead-simple yet super powerful tool with very extensive documentation.</p><p>See for yourself!</p><pre>k6 run k6/script.js</pre><p>While k6 is gradually increasing strain on the pods&#39; CPU usage, let’s watch out for any EvictedByVPA events in a second tab: eventually, you should see all 3 pods get evicted simultaneously!</p><pre>➜ kubectl get events -n sample-app -w | grep EvictedByVPA<br>... <strong>Pod was evicted by VPA Updater to apply resource recommendation.</strong><br>... <strong>Pod was evicted by VPA Updater to apply resource recommendation.</strong><br>... <strong>Pod was evicted by VPA Updater to apply resource recommendation.</strong></pre><p>As soon as this happens, have a look at the updated pods’ resource requests and limits: the CPU/memory requests/limits should have doubled in value (CPU from 10m to 20m and memory from 10Mi to 20Mi).</p><pre>➜ kubectl get pods -n sample-app -o yaml | grep -A 6 &#39;resources:&#39;<br>      resources:<br>        limits:<br>          <strong>cpu: 20m<br>          memory: &quot;20971520&quot;</strong><br>        requests:<br>          <strong>cpu: 20m<br>          memory: &quot;20971520&quot;</strong><br>--<br>      resources:<br>        limits:<br>          <strong>cpu: 20m<br>          memory: &quot;20971520&quot;</strong><br>        requests:<br>          <strong>cpu: 20m<br>          memory: &quot;20971520&quot;</strong><br>--<br>      resources:<br>        limits:<br>          <strong>cpu: 20m<br>          memory: &quot;20971520&quot;</strong><br>        requests:<br>          <strong>cpu: 20m<br>          memory: &quot;20971520&quot;</strong></pre><p>This means Vertical Pod Autoscaler successfully evicted the pods in order to increase their resource requests and limits! 🎉</p><p>Once k6 is done, have a look at the Load Test summary and the result of the status code counter metric in particular.</p><pre>          /\      |‾‾| /‾‾/   /‾‾/<br>     /\  /  \     |  |/  /   /  /<br>    /  \/    \    |     (   /   ‾‾\<br>   /          \   |  |\  \ |  (‾)  |<br>  / __________ \  |__| \__\ \_____/ .io</pre><pre>  execution: local<br>     script: k6/script.js<br>     output: -</pre><pre>  scenarios: (100.00%) 1 scenario, 200 max VUs, 10m30s max duration (incl. graceful stop):<br>           * load: Up to 40.00 iterations/s for 10m0s over 3 stages (maxVUs: 200, gracefulStop: 30s)</pre><pre><strong>     ✗ status code is 200<br>      ↳  97% — ✓ 17724 / ✗ 365</strong><br>     ✗ node is kind-control-plane<br>      ↳  97% — ✓ 17724 / ✗ 365<br>     ✗ namespace is sample-app<br>      ↳  97% — ✓ 17724 / ✗ 365<br>     ✗ pod is sample-app-*<br>      ↳  97% — ✓ 17724 / ✗ 365</pre><pre>   ✓ checks.........................: 97.98% ✓ 70896     ✗ 1460<br>     data_received..................: 4.2 MB 7.1 kB/s<br>     data_sent......................: 2.1 MB 3.5 kB/s<br>     http_req_blocked...............: avg=18.39µs  min=2µs   med=8µs    max=2.93ms  p(90)=17µs     p(95)=20µs<br>     http_req_connecting............: avg=5.71µs   min=0s    med=0s     max=2.74ms  p(90)=0s       p(95)=0s<br>   ✓ http_req_duration..............: avg=188.13ms min=491µs med=4.57ms max=59.99s  p(90)=269.43ms p(95)=646.78ms<br>       { expected_response:true }...: avg=129.55ms min=491µs med=4.62ms max=7.3s    p(90)=261.24ms p(95)=602.03ms<br>     http_req_failed................: 2.01%  ✓ 365       ✗ 17724<br>     http_req_receiving.............: avg=98.96µs  min=0s    med=75µs   max=4.27ms  p(90)=159µs    p(95)=209µs<br>     http_req_sending...............: avg=49.92µs  min=7µs   med=34µs   max=14.39ms p(90)=72µs     p(95)=93µs<br>     http_req_tls_handshaking.......: avg=0s       min=0s    med=0s     max=0s      p(90)=0s       p(95)=0s<br>     http_req_waiting...............: avg=187.99ms min=451µs med=4.38ms max=59.99s  p(90)=269.31ms p(95)=646.69ms<br>     http_reqs......................: 18089  30.147771/s<br>     iteration_duration.............: avg=188.59ms min=666µs med=5.11ms max=1m0s    p(90)=270.13ms p(95)=647.49ms<br>     iterations.....................: 18089  30.147771/s<br>     vus............................: 0      min=0       max=145<br>     vus_max........................: 200    min=200     max=200</pre><pre>running (10m00.0s), 000/200 VUs, 18089 complete and 0 interrupted iterations<br>load ✓ [======================================] 000/200 VUs  10m0s  00.41 iters/s</pre><p>Uh-oh… It looks like we had some downtime! 😱</p><p>Thankfully, our service was able to restart relatively fast and <strong>365</strong> out of <strong>17724</strong> requests failed. But for a service with a slower startup time, this could have led to an incident! 🚨</p><p>This is due to the fact that vertical scaling, in essence, cannot happen without a restart: a pod’s CPU and/or memory cannot be increased in place. Instead, the pod must be terminated and a new one created with increased resources.</p><p>So how do we ensure the availability of our service during vertical autoscaling then?</p><blockquote>This is where the Pod Disruption Budget (PDB) comes in!</blockquote><p>We’ll get to that in a minute, let’s uninstall our Helm release first! (we won’t be needing this one anymore)</p><pre>helm uninstall sample-app -n sample-app</pre><h4>Configuring Pod Disruption Budget</h4><p>To prevent downtimes during pod disruption such as the one we previously experienced, a Pod Disruption Budget (PDB) can be configured.</p><p>A PDB limits the number of pods that are down simultaneously from voluntary disruptions. It can be configured to sustain either a minimum amount of available pods (minAvailable), or a maximum amount of unavailable pods (maxUnavailable).</p><p>Let’s see what happens if we try to autoscale vertically the same application but with a Pod Disruption Budget with maxUnavailable: 1.</p><pre>helm install sample-app/helm-chart --name-template sample-app --set podDisruptionBudget.enabled=true --create-namespace -n sample-app --wait</pre><p>Once again, you should eventually see one Deployment with the READY state.</p><pre>➜ kubectl get deploy -n sample-app<br>NAME         READY   UP-TO-DATE   AVAILABLE   AGE<br>sample-app   3/3     3            3           32s</pre><p>Alright, now let’s have a look at the PDB!</p><pre>➜ kubectl describe pdb -n sample-app<br>Name:             sample-app<br>Namespace:        sample-app<br><strong>Max unavailable:  1</strong><br>Selector:         app=sample-app<br>Status:<br>    Allowed disruptions:  1<br>    Current:              3<br>    Desired:              2<br>    Total:                3</pre><p>As you can see, this PDB is configured to prevent more than 1 pod to be unavailable during a voluntary pod disruption (such as pod eviction by VPA).</p><p>Now, let’s see how our service is going to behave under load with a PDB!</p><pre>k6 run k6/script.js</pre><p>Once again, while k6 is gradually increasing strain on the pods’ CPU usage, let’s watch out for any EvictedByVPA events in a second tab: eventually, you should see all 3 pods get evicted but this time, only one by one!</p><pre>➜ kubectl get events -n sample-app -w | grep EvictedByVPA<br>... <strong>Pod was evicted by VPA Updater to apply resource recommendation.</strong><br>... <strong>Pod was evicted by VPA Updater to apply resource recommendation.</strong><br>... <strong>Pod was evicted by VPA Updater to apply resource recommendation.</strong></pre><p>Once k6 is done, have a look at the Load Test summary and the result of the status code counter metric in particular.</p><pre>          /\      |‾‾| /‾‾/   /‾‾/<br>     /\  /  \     |  |/  /   /  /<br>    /  \/    \    |     (   /   ‾‾\<br>   /          \   |  |\  \ |  (‾)  |<br>  / __________ \  |__| \__\ \_____/ .io</pre><pre>  execution: local<br>     script: k6/script.js<br>     output: -</pre><pre>  scenarios: (100.00%) 1 scenario, 200 max VUs, 10m30s max duration (incl. graceful stop):<br>           * load: Up to 40.00 iterations/s for 10m0s over 3 stages (maxVUs: 200, gracefulStop: 30s)</pre><pre><strong>     ✓ status code is 200</strong><br>     ✓ node is kind-control-plane<br>     ✓ namespace is sample-app<br>     ✓ pod is sample-app-*</pre><pre>   ✓ checks.........................: 100.00% ✓ 72356     ✗ 0<br>     data_received..................: 4.2 MB  7.0 kB/s<br>     data_sent......................: 2.1 MB  3.5 kB/s<br>     http_req_blocked...............: avg=19.7µs   min=2µs      med=7µs    max=2.95ms  p(90)=16µs     p(95)=21µs<br>     http_req_connecting............: avg=6.86µs   min=0s       med=0s     max=2.24ms  p(90)=0s       p(95)=0s<br>   ✓ http_req_duration..............: avg=103.43ms min=452µs    med=6.4ms  max=5.29s   p(90)=259.88ms p(95)=484.22ms<br>       { expected_response:true }...: avg=103.43ms min=452µs    med=6.4ms  max=5.29s   p(90)=259.88ms p(95)=484.22ms<br>     http_req_failed................: 0.00%   ✓ 0         ✗ 18089<br>     http_req_receiving.............: avg=99.34µs  min=8µs      med=77µs   max=5.42ms  p(90)=166µs    p(95)=212µs<br>     http_req_sending...............: avg=51.86µs  min=9µs      med=33µs   max=18.76ms p(90)=71µs     p(95)=96.59µs<br>     http_req_tls_handshaking.......: avg=0s       min=0s       med=0s     max=0s      p(90)=0s       p(95)=0s<br>     http_req_waiting...............: avg=103.28ms min=418µs    med=6.2ms  max=5.29s   p(90)=259.8ms  p(95)=483.99ms<br>     http_reqs......................: 18089   30.148343/s<br>     iteration_duration.............: avg=103.9ms  min=625.62µs med=6.98ms max=5.29s   p(90)=260.24ms p(95)=485.04ms<br>     iterations.....................: 18089   30.148343/s<br>     vus............................: 0       min=0       max=69<br>     vus_max........................: 200     min=200     max=200</pre><pre>running (10m00.0s), 000/200 VUs, 18089 complete and 0 interrupted iterations<br>load ✓ [======================================] 000/200 VUs  10m0s  00.41 iters/s</pre><p>Yay! 🎉<br>This time, our service handled pod disruption beautifully and not a single request failed!</p><p>Thanks to the Pod Disruption Budget, a pod can only be evicted if all other pods are up, ensuring that at least 2 pods are available to handle the traffic.</p><p>This is what we call: <a href="https://en.wikipedia.org/wiki/High_availability">High availability</a>! 🚀</p><h4>Wrapping up</h4><p>That’s it! You can now stop and delete your Kind cluster.</p><pre>kind delete cluster</pre><p>To summarize, using Vertical Pod Autoscaler (VPA) we were able to:</p><ul><li>Autoscale vertically our service, based on resource metrics;</li><li>Prevent downtime during pod eviction thanks to Pod Disruption Budget.</li></ul><p>Was it worth it? Did that help you understand how to implement Vertical Pod Autoscaler in Kubernetes?</p><p>If so:</p><ol><li>Let me know in the comments below! 👇</li><li>Don’t forget to hit that <a href="https://medium.com/@jhandguy">subscribe</a> button! ✅</li><li>Follow me on <a href="https://twitter.com/jhandguy">Twitter</a>, I’ll be happy to answer any of your questions and you’ll be the first ones to know when a new article comes out! 👌</li></ol><p>Bye-bye! 👋</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b12a5c61393f" width="1" height="1" alt=""><hr><p><a href="https://code.egym.com/vertical-pod-autoscaler-in-kubernetes-b12a5c61393f">Vertical Pod Autoscaler in Kubernetes</a> was originally published in <a href="https://code.egym.com">EGYM Software Development</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Horizontal Pod Autoscaler in Kubernetes (Part 2) — Advanced Autoscaling using Prometheus Adapter]]></title>
            <link>https://code.egym.com/horizontal-pod-autoscaler-in-kubernetes-part-2-advanced-autoscaling-using-prometheus-adapter-d2b8bc3a019d?source=rss-60d6fa56fda3------2</link>
            <guid isPermaLink="false">https://medium.com/p/d2b8bc3a019d</guid>
            <category><![CDATA[horizontal-pod-autoscaler]]></category>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[horizontal-scaling]]></category>
            <category><![CDATA[autoscaling]]></category>
            <category><![CDATA[prometheus]]></category>
            <dc:creator><![CDATA[Jean Mainguy]]></dc:creator>
            <pubDate>Wed, 03 Aug 2022 12:15:30 GMT</pubDate>
            <atom:updated>2023-12-03T10:52:20.444Z</atom:updated>
            <content:encoded><![CDATA[<h3>Horizontal Pod Autoscaler in Kubernetes (Part 2) — Advanced Autoscaling using Prometheus Adapter</h3><h4>Learn how to use Prometheus Adapter to horizontally scale services in Kubernetes automatically based on Prometheus metrics.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*sp_hNxI5QXdACXpC" /><figcaption>Photo by <a href="https://unsplash.com/@julianhochgesang?utm_source=medium&amp;utm_medium=referral">Julian Hochgesang</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><blockquote><strong>Pssst</strong>! I started my own <a href="https://jhandguy.github.io/"><strong>blog</strong></a>!<br>You can read this <a href="https://jhandguy.github.io/posts/advanced-horizontal-autoscaling/"><strong>very same article</strong></a> over there too!<br>No paywall, no ad, no Javascript — no bullshit, just pure content.<br>See you there!</blockquote><p>The Horizontal Pod Autoscaler (HPA) is a fundamental feature of Kubernetes. It enables automatic scale-up and scale-down of containerized applications based on CPU usage, memory usage, or custom metrics.</p><p>Traditionally, when scaling software, we first think of vertical scaling: the CPU and the RAM are increased so the application consuming them can perform better. While this seems like a flawless mechanism on paper, it actually comes with many drawbacks.</p><p>First, upgrading the CPU or RAM on a physical machine (or VM) requires downtime and unless a Pod Disruption Budget (PDB) is used to handle <a href="https://kubernetes.io/docs/concepts/workloads/pods/disruptions">disruptions</a>, all pods will be evicted and recreated in the new resized node.</p><p>Nodes resource usage is also not optimized, as scaling vertically means requiring sufficient resources in a single node, while horizontal scaling may have the same amount of resources distributed across multiple nodes.</p><p>Additionally, vertical scaling is not as resilient as horizontal scaling, as fewer replicas mean higher risks of disruptions in case of node failure.</p><p>Finally, reaching a certain threshold, scaling only vertically becomes very expensive and most importantly, isn’t limitless. In fact, there is only so much CPU and RAM a physical machine(or VM) alone can handle.<br>This is where horizontal scaling comes into play!</p><blockquote>Eventually, it is more efficient to duplicate an instance, than increase its resources.</blockquote><p>🎬 Hi there, I’m Jean!</p><p>In this 2 parts series, we’re going to explore several ways to scale services horizontally in Kubernetes, and the second one is…<br>🥁<br>… using<strong> Prometheus Adapter</strong>! 🎊</p><h4>Requirements</h4><p>Before we start, make sure you have the following tools installed:</p><ul><li><a href="https://kind.sigs.k8s.io/docs/user/quick-start/#installation">Kind</a></li><li><a href="https://kubernetes.io/docs/tasks/tools/">Kubectl</a></li><li><a href="https://helm.sh/docs/intro/install/">Helm</a></li><li><a href="https://k6.io/docs/getting-started/installation/">K6</a></li></ul><blockquote><em>Note: for MacOS users or Linux users using Homebrew, simply run: </em><em>brew install kind kubectl helm k6</em></blockquote><p>All set? Let’s go! 🏁</p><h4>Creating Kind Cluster</h4><p><a href="https://kind.sigs.k8s.io/docs/user/quick-start/#installation">Kind</a> is a tool for running local Kubernetes clusters using Docker container “nodes”. It was primarily designed for testing Kubernetes itself, but may be used for local development or CI.</p><p>I don’t expect you to have a demo project in handy, so <a href="https://github.com/jhandguy/horizontal-pod-autoscaler">I built one</a> for you.</p><pre>git clone <a href="https://github.com/jhandguy/horizontal-pod-autoscaler.git">https://github.com/jhandguy/horizontal-pod-autoscaler.git</a></pre><pre>cd horizontal-pod-autoscaler</pre><p>Alright, let’s spin up our Kind cluster! 🚀</p><pre>➜ kind create cluster --image kindest/node:v1.27.3 --config=kind/cluster.yaml</pre><pre>Creating cluster &quot;kind&quot; ...<br> ✓ Ensuring node image (kindest/node:v1.27.3) 🖼<br> ✓ Preparing nodes 📦<br> ✓ Writing configuration 📜<br> ✓ Starting control-plane 🕹️<br> ✓ Installing CNI 🔌<br> ✓ Installing StorageClass 💾</pre><pre>Set kubectl context to &quot;kind-kind&quot;<br>You can now use your cluster with:</pre><pre>kubectl cluster-info --context kind-kind</pre><pre>Have a question, bug, or feature request? Let us know! <a href="https://kind.sigs.k8s.io/#community">https://kind.sigs.k8s.io/#community</a> 🙂</pre><h4>Installing NGINX Ingress Controller</h4><p><a href="https://github.com/kubernetes/ingress-nginx">NGINX Ingress Controller</a> is one of the <a href="https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/#additional-controllers">many available Kubernetes Ingress Controllers</a>, which acts as a load balancer and satisfies routing rules specified in <a href="https://kubernetes.io/docs/concepts/services-networking/ingress/#what-is-ingress">Ingress</a> resources, using the <a href="https://nginx.org/en/">NGINX reverse proxy</a>.</p><p>NGINX Ingress Controller can be installed via its <a href="https://github.com/kubernetes/ingress-nginx/tree/main/charts/ingress-nginx">Helm chart</a>.</p><pre>helm repo add ingress-nginx <a href="https://kubernetes.github.io/ingress-nginx">https://kubernetes.github.io/ingress-nginx</a></pre><pre>helm install ingress-nginx/ingress-nginx --name-template ingress-nginx --create-namespace -n ingress-nginx --values kind/ingress-nginx-values.yaml --version 4.8.3 --wait</pre><p>Now, if everything goes according to plan, you should be able to see the <strong>ingress-nginx-controller</strong> Deployment running.</p><pre>➜ kubectl get deploy -n ingress-nginx<br>NAME                       READY   UP-TO-DATE   AVAILABLE   AGE<br>ingress-nginx-controller   1/1     1            1           4m35s</pre><h4>Installing Prometheus</h4><p>Prometheus can be installed via its community <a href="https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack">Helm chart</a>, which also provides <a href="https://grafana.com/grafana/">Grafana</a> out of the box.</p><pre>helm repo add prometheus-community <a href="https://prometheus-community.github.io/helm-charts">https://prometheus-community.github.io/helm-charts</a></pre><pre>helm install prometheus-community/kube-prometheus-stack --name-template prometheus --create-namespace -n prometheus --version 54.2.2 --wait</pre><p>If everything went fine, you should be able to see three newly spawned deployments with the READY state!</p><pre>➜ kubectl get deploy -n prometheus<br>NAME                                  READY   UP-TO-DATE   AVAILABLE   <br>prometheus-grafana                    1/1     1            1           <br>prometheus-kube-prometheus-operator   1/1     1            1           <br>prometheus-kube-state-metrics         1/1     1            1</pre><h4>Installing Prometheus Adapter</h4><p><a href="https://github.com/kubernetes-sigs/prometheus-adapter">Prometheus Adapter</a> is a source of custom metrics, which collects them from Prometheus and exposes them in Kubernetes API server through <a href="https://github.com/kubernetes/metrics">Metrics API</a> for use by <a href="https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/">Horizontal Pod Autoscaler</a> and <a href="https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler/">Vertical Pod Autoscaler</a>.</p><p>Unlike <a href="https://github.com/kubernetes-sigs/metrics-server">Metrics Server</a> which is limited to resource metrics, Prometheus Adapter exposes custom metrics measurable from within a Pod’s container, such as memory usage, GC duration, but also request throughput, latency, etc.</p><blockquote><em>If you haven’t already, go read </em><a href="https://medium.com/p/929e96cc2ab2"><strong>Horizontal Pod Autoscaler in Kubernetes (Part 1) — Simple Autoscaling using Metrics Server</strong></a> <em>and learn how to implement a Horizontal Pod Autoscaler using Metrics Server!</em></blockquote><p>Prometheus Adapter can be installed via its <a href="https://github.com/prometheus-community/helm-charts/tree/main/charts/prometheus-adapter">Helm chart</a>.</p><pre>helm install prometheus-community/prometheus-adapter --name-template prometheus-adapter --create-namespace -n prometheus-adapter --values kind/prometheus-adapter-values.yaml --version 4.9.0 --wait</pre><p>Now, if all goes well, you should see the <strong>prometheus-adapter</strong> Deployment running with the READY state.</p><pre>➜ kubectl get deploy -n prometheus-adapter<br>NAME                 READY   UP-TO-DATE   AVAILABLE   AGE<br>prometheus-adapter   1/1     1            1           52s</pre><h4>Autoscaling Services based on Prometheus Metrics</h4><p>Now that Prometheus Adapter is up and running, let’s get to it, shall we? 🧐</p><pre>helm install golang-sample-app/helm-chart --name-template sample-app --create-namespace -n sample-app --set prometheus.enabled=true --wait</pre><p>If everything goes fine, you should eventually see one Deployment with the READY state.</p><pre>➜ kubectl get deploy -n sample-app<br>NAME         READY   UP-TO-DATE   AVAILABLE   AGE<br>sample-app   2/2     2            2           28s</pre><p>Alright, now let’s have a look at the HPA!</p><pre>➜ kubectl describe hpa -n sample-app<br>...<br>Metrics: ( current / target )<br>  <strong>&quot;golang_sample_app_requests_per_second&quot; on pods:  &lt;unknown&gt; / 10</strong><br>Min replicas:                                       2<br>Max replicas:                                       8</pre><p>As you can see, this HPA is configured to scale the service based on requests per second (rps), with an average of <strong>10</strong>.</p><blockquote>Note: as you probably have noticed, the current value for the request throughput is <strong>unknown</strong>, this is expected as the service hasn’t served any requests yet, thus no timeseries for this metric are present in Prometheus.</blockquote><p>This means that as soon as the request throughput breaches the <strong>10rps</strong> threshold, the HPA will trigger an upscale.</p><p>Under minimal load, the HPA will still retain a replica count of <strong>2</strong>, while the maximum amount of Pods the HPA is allowed to spin up under high load is <strong>8</strong>.</p><blockquote>Note: in a production environment, it is recommended to have a minimum replica count of at least 3, to guarantee maintained availability in the case of <a href="https://kubernetes.io/docs/concepts/scheduling-eviction/_print/#pod-disruption">Pod Disruption</a>.</blockquote><p>Now, this is the moment you’ve certainly expected… It’s Load Testing time! 😎</p><p>For Load Testing, I really recommend <a href="https://k6.io/docs/">k6</a> from the Grafana Labs team. It is a dead-simple yet super powerful tool with very extensive documentation.</p><p>See for yourself!</p><pre>k6 run k6/script.js</pre><p>While the load test is running, I suggest watching the HPA in a separate tab.</p><pre>kubectl get hpa -n sample-app -w</pre><p>As the load test progresses and the 2 starting Pods start to handle more than 10rps each, you should see the Prometheus metric’s target increasing, and ultimately, the <strong>replica count reaching its maximum</strong>!</p><pre>Deployment/sample-app   &lt;unknown&gt;/10   2         8         <strong>2</strong><br>Deployment/sample-app   4360m/10       2         8         <strong>2</strong><br>Deployment/sample-app   6236m/10       2         8         <strong>2</strong><br>Deployment/sample-app   12471m/10      2         8         <strong>2</strong><br>Deployment/sample-app   15577m/10      2         8         <strong>3</strong><br>Deployment/sample-app   21231m/10      2         8         <strong>3</strong><br>Deployment/sample-app   21231m/10      2         8         <strong>5</strong><br>Deployment/sample-app   16752m/10      2         8         <strong>5</strong><br>Deployment/sample-app   18362m/10      2         8         <strong>5</strong><br>Deployment/sample-app   15921m/10      2         8         <strong>6</strong><br>Deployment/sample-app   15543m/10      2         8         <strong>6</strong><br>Deployment/sample-app   16530m/10      2         8         <strong>8</strong><br>Deployment/sample-app   15397m/10      2         8         <strong>8</strong><br>Deployment/sample-app   14428m/10      2         8         <strong>8</strong><br>Deployment/sample-app   11897m/10      2         8         <strong>8</strong><br>Deployment/sample-app   12370m/10      2         8         <strong>8</strong><br>Deployment/sample-app   12423m/10      2         8         <strong>8</strong><br>Deployment/sample-app   12414m/10      2         8         <strong>8</strong><br>Deployment/sample-app   12423m/10      2         8         <strong>8</strong><br>Deployment/sample-app   10962m/10      2         8         <strong>8</strong></pre><p>Now, let’s quickly have a look at the Load Test summary and the result of the http_req_duration metric in particular!</p><pre>          /\      |‾‾| /‾‾/   /‾‾/<br>     /\  /  \     |  |/  /   /  /<br>    /  \/    \    |     (   /   ‾‾\<br>   /          \   |  |\  \ |  (‾)  |<br>  / __________ \  |__| \__\ \_____/ .io</pre><pre>  execution: local<br>     script: k6/script.js<br>     output: -</pre><pre>  scenarios: (100.00%) 1 scenario, 100 max VUs, 5m30s max duration (incl. graceful stop):<br>           * load: Up to 100.00 iterations/s for 5m0s over 2 stages (maxVUs: 100, gracefulStop: 30s)</pre><pre>     ✓ status code is 200<br>     ✓ node is kind-control-plane<br>     ✓ namespace is sample-app<br>     ✓ pod is sample-app-*</pre><pre>   ✓ checks.........................: 100.00% ✓ 60356     ✗ 0<br>     data_received..................: 3.5 MB  12 kB/s<br>     data_sent......................: 1.7 MB  5.8 kB/s<br>     http_req_blocked...............: avg=18.43µs min=1µs     med=8µs    max=3.9ms  p(90)=17µs    p(95)=20µs<br>     http_req_connecting............: avg=5.41µs  min=0s      med=0s     max=3.7ms  p(90)=0s      p(95)=0s<br><strong>   ✓ http_req_duration..............: avg=16.74ms min=498µs   med=2.77ms max=1.48s  p(90)=14.52ms p(95)=64.78ms</strong><br>       { expected_response:true }...: avg=16.74ms min=498µs   med=2.77ms max=1.48s  p(90)=14.52ms p(95)=64.78ms<br>     http_req_failed................: 0.00%   ✓ 0         ✗ 15089<br>     http_req_receiving.............: avg=97.15µs min=9µs     med=74µs   max=3.33ms p(90)=151µs   p(95)=197µs<br>     http_req_sending...............: avg=48.11µs min=6µs     med=34µs   max=3.12ms p(90)=69µs    p(95)=86µs<br>     http_req_tls_handshaking.......: avg=0s      min=0s      med=0s     max=0s     p(90)=0s      p(95)=0s<br>     http_req_waiting...............: avg=16.59ms min=452µs   med=2.61ms max=1.48s  p(90)=14.32ms p(95)=64.69ms<br>     http_reqs......................: 15089   50.297179/s<br>     iteration_duration.............: avg=17.18ms min=610.5µs med=3.21ms max=1.48s  p(90)=15.08ms p(95)=65.33ms<br>     iterations.....................: 15089   50.297179/s<br>     vus............................: 0       min=0       max=18<br>     vus_max........................: 100     min=100     max=100</pre><pre>running (5m00.0s), 000/100 VUs, 15089 complete and 0 interrupted iterations<br>load ✓ [======================================] 000/100 VUs  5m0s  000.65 iters/s</pre><p>As you can observe, our service has performed very well under heavy load, with a Success Share of 100%, a median latency of ~3ms, and a 95th percentile latency of ~50ms!</p><p>We have the HPA to thank for that, as it scaled the Deployment from 2 to 8 Pods swiftly and automatically, based on request throughput!</p><p>We definitely would not have had the same results without an HPA… Actually, why don’t you try it yourself? 😉</p><p>Just delete the HPA (kubectl delete hpa sample-app -n sample-app), run the load test again (k6 run k6/script.js) and see what happens! (spoiler alert: it’s not pretty 😬)</p><h4>Wrapping up</h4><p>That’s it! You can now stop and delete your Kind cluster.</p><pre>kind delete cluster</pre><p>To summarize, using Prometheus Adapter we were able to:</p><ul><li>Autoscale horizontally our service, based on Prometheus metrics.</li></ul><p>Was it worth it? Did that help you understand how to implement Horizontal Pod Autoscaler in Kubernetes using Prometheus Adapter?</p><p>If so:</p><ol><li>Let me know in the comments below! 👇</li><li>Don’t forget to hit that <a href="https://medium.com/@jhandguy">subscribe</a> button! ✅</li><li>Follow me on <a href="https://twitter.com/jhandguy">Twitter</a>, I’ll be happy to answer any of your questions and you’ll be the first ones to know when a new article comes out! 👌</li></ol><p>Bye-bye! 👋</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d2b8bc3a019d" width="1" height="1" alt=""><hr><p><a href="https://code.egym.com/horizontal-pod-autoscaler-in-kubernetes-part-2-advanced-autoscaling-using-prometheus-adapter-d2b8bc3a019d">Horizontal Pod Autoscaler in Kubernetes (Part 2) — Advanced Autoscaling using Prometheus Adapter</a> was originally published in <a href="https://code.egym.com">EGYM Software Development</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Horizontal Pod Autoscaler in Kubernetes (Part 1) — Simple Autoscaling using Metrics Server]]></title>
            <link>https://code.egym.com/horizontal-pod-autoscaler-in-kubernetes-part-1-simple-autoscaling-using-metrics-server-929e96cc2ab2?source=rss-60d6fa56fda3------2</link>
            <guid isPermaLink="false">https://medium.com/p/929e96cc2ab2</guid>
            <category><![CDATA[metrics-server]]></category>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[horizontal-scaling]]></category>
            <category><![CDATA[horizontal-pod-autoscaler]]></category>
            <category><![CDATA[autoscaling]]></category>
            <dc:creator><![CDATA[Jean Mainguy]]></dc:creator>
            <pubDate>Wed, 06 Jul 2022 12:59:45 GMT</pubDate>
            <atom:updated>2023-12-03T10:49:23.467Z</atom:updated>
            <content:encoded><![CDATA[<h3>Horizontal Pod Autoscaler in Kubernetes (Part 1) — Simple Autoscaling using Metrics Server</h3><h4>Learn how to use Metrics Server to horizontally scale native and JVM services in Kubernetes automatically based on resource metrics.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*rUgJMAG4ZbAzYTeu" /><figcaption>Photo by <a href="https://unsplash.com/@christianem?utm_source=medium&amp;utm_medium=referral">Christian Englmeier</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><blockquote><strong>Pssst</strong>! I started my own <a href="https://jhandguy.github.io/"><strong>blog</strong></a>!<br>You can read this <a href="https://jhandguy.github.io/posts/simple-horizontal-autoscaling/"><strong>very same article</strong></a> over there too!<br>No paywall, no ad, no Javascript — no bullshit, just pure content.<br>See you there!</blockquote><p>The Horizontal Pod Autoscaler (HPA) is a fundamental feature of Kubernetes. It enables automatic scale-up and scale-down of containerized applications based on CPU usage, memory usage, or custom metrics.</p><p>Traditionally, when scaling software, we first think of vertical scaling: the CPU and the RAM are increased so the application consuming them can perform better. While this seems like a flawless mechanism on paper, it actually comes with many drawbacks.</p><p>First, upgrading the CPU or RAM on a physical machine (or VM) requires downtime and unless a Pod Disruption Budget (PDB) is used to handle <a href="https://kubernetes.io/docs/concepts/workloads/pods/disruptions">disruptions</a>, all pods will be evicted and recreated in the new resized node.</p><p>Nodes resource usage is also not optimized, as scaling vertically means requiring sufficient resources in a single node, while horizontal scaling may have the same amount of resources distributed across multiple nodes.</p><p>Additionally, vertical scaling is not as resilient as horizontal scaling, as fewer replicas mean higher risks of disruptions in case of node failure.</p><p>Finally, reaching a certain threshold, scaling only vertically becomes very expensive and most importantly, isn’t limitless. In fact, there is only so much CPU and RAM a physical machine(or VM) alone can handle.<br>This is where horizontal scaling comes into play!</p><blockquote>Eventually, it is more efficient to duplicate an instance, than increase its resources.</blockquote><p>🎬 Hi there, I’m Jean!</p><p>In this 2 parts series, we’re going to explore several ways to scale services horizontally in Kubernetes, and the first one is…<br>🥁<br>… using<strong> Metrics Server</strong>! 🎊</p><h4>Requirements</h4><p>Before we start, make sure you have the following tools installed:</p><ul><li><a href="https://kind.sigs.k8s.io/docs/user/quick-start/#installation">Kind</a></li><li><a href="https://kubernetes.io/docs/tasks/tools/">Kubectl</a></li><li><a href="https://helm.sh/docs/intro/install/">Helm</a></li><li><a href="https://k6.io/docs/getting-started/installation/">K6</a></li></ul><blockquote><em>Note: for MacOS users or Linux users using Homebrew, simply run: </em><em>brew install kind kubectl helm k6</em></blockquote><p>All set? Let’s go! 🏁</p><h4>Creating Kind Cluster</h4><p><a href="https://kind.sigs.k8s.io/docs/user/quick-start/#installation">Kind</a> is a tool for running local Kubernetes clusters using Docker container “nodes”. It was primarily designed for testing Kubernetes itself, but may be used for local development or CI.</p><p>I don’t expect you to have a demo project in handy, so <a href="https://github.com/jhandguy/horizontal-pod-autoscaler">I built one</a> for you.</p><pre>git clone <a href="https://github.com/jhandguy/horizontal-pod-autoscaler.git">https://github.com/jhandguy/horizontal-pod-autoscaler.git</a></pre><pre>cd horizontal-pod-autoscaler</pre><p>Alright, let’s spin up our Kind cluster! 🚀</p><pre>➜ kind create cluster --image kindest/node:v1.27.3 --config=kind/cluster.yaml</pre><pre>Creating cluster &quot;kind&quot; ...<br> ✓ Ensuring node image (kindest/node:v1.27.3) 🖼<br> ✓ Preparing nodes 📦<br> ✓ Writing configuration 📜<br> ✓ Starting control-plane 🕹️<br> ✓ Installing CNI 🔌<br> ✓ Installing StorageClass 💾</pre><pre>Set kubectl context to &quot;kind-kind&quot;<br>You can now use your cluster with:<br><br>kubectl cluster-info --context kind-kind</pre><pre>Have a question, bug, or feature request? Let us know! <a href="https://kind.sigs.k8s.io/#community">https://kind.sigs.k8s.io/#community</a> 🙂</pre><h4>Installing NGINX Ingress Controller</h4><p><a href="https://github.com/kubernetes/ingress-nginx">NGINX Ingress Controller</a> is one of the <a href="https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/#additional-controllers">many available Kubernetes Ingress Controllers</a>, which acts as a load balancer and satisfies routing rules specified in <a href="https://kubernetes.io/docs/concepts/services-networking/ingress/#what-is-ingress">Ingress</a> resources, using the <a href="https://nginx.org/en/">NGINX reverse proxy</a>.</p><p>NGINX Ingress Controller can be installed via its <a href="https://github.com/kubernetes/ingress-nginx/tree/main/charts/ingress-nginx">Helm chart</a>.</p><pre>helm repo add ingress-nginx <a href="https://kubernetes.github.io/ingress-nginx">https://kubernetes.github.io/ingress-nginx</a></pre><pre>helm install ingress-nginx/ingress-nginx --name-template ingress-nginx --create-namespace -n ingress-nginx --values kind/ingress-nginx-values.yaml --version 4.8.3 --wait</pre><p>Now, if everything goes according to plan, you should be able to see the <strong>ingress-nginx-controller</strong> Deployment running.</p><pre>➜ kubectl get deploy -n ingress-nginx<br>NAME                       READY   UP-TO-DATE   AVAILABLE   AGE<br>ingress-nginx-controller   1/1     1            1           4m35s</pre><h4>Installing Metrics Server</h4><p><a href="https://github.com/kubernetes-sigs/metrics-server">Metrics Server</a> is a source of container resource metrics, which collects them from Kubelets and exposes them in Kubernetes API server through <a href="https://github.com/kubernetes/metrics">Metrics API</a> for use by <a href="https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/">Horizontal Pod Autoscaler</a> and <a href="https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler/">Vertical Pod Autoscaler</a>.</p><p>Metrics Server can be installed via its <a href="https://github.com/kubernetes-sigs/metrics-server/tree/master/charts/metrics-server">Helm chart</a>.</p><pre>helm repo add metrics-server <a href="https://kubernetes-sigs.github.io/metrics-server">https://kubernetes-sigs.github.io/metrics-server</a></pre><pre>helm install metrics-server/metrics-server --name-template metrics-server --create-namespace -n metrics-server --values kind/metrics-server-values.yaml --version 3.11.0 --wait</pre><p>Now, if everything goes according to plan, you should be able to see the <strong>metrics-server</strong> Deployment running.</p><pre>➜ kubectl get deploy -n metrics-server<br>NAME             READY   UP-TO-DATE   AVAILABLE   AGE<br>metrics-server   1/1     1            1           38s</pre><h4>Horizontal Pod Autoscaler</h4><p>Before we dive in, let’s quickly remind ourselves of what a <a href="https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/">Horizontal Pod Autoscaler</a> in Kubernetes actually is:</p><blockquote>A <a href="https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/">HorizontalPodAutoscaler</a> (HPA for short) automatically updates a workload resource (such as a <a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/">Deployment</a> or <a href="https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/">StatefulSet</a>), with the aim of automatically scaling the workload to match demand.</blockquote><blockquote>Horizontal scaling means that the response to increased load is to deploy more <a href="https://kubernetes.io/docs/concepts/workloads/pods/">Pods</a>. This is different from <em>vertical</em> scaling, which for Kubernetes would mean assigning more resources (for example: memory or CPU) to the Pods that are already running for the workload.</blockquote><blockquote>If the load decreases, and the number of Pods is above the configured minimum, the HorizontalPodAutoscaler instructs the workload resource (the Deployment, StatefulSet, or other similar resource) to scale back down.</blockquote><p>Now that we know what an HPA is, let’s get started, shall we? 🧐</p><h4>Autoscaling Native Services based on CPU and Memory Usage</h4><p>A native service is a piece of software that does not require a virtual environment in order to run across different OSs and CPU architectures. This is the case for C/C++, Golang, Rust, and more: those are languages that compile into a binary, that is directly executable by the Pod.<br>This means that native services can utilize all of the CPU and memory available from the Pod they run in, without an intermediary environment.</p><p>Let’s try it out with a Golang service!</p><pre>helm install golang-sample-app/helm-chart --name-template sample-app --create-namespace -n sample-app --wait</pre><p>If everything goes fine, you should eventually see one Deployment with the READY state.</p><pre>➜ kubectl get deploy -n sample-app<br>NAME         READY   UP-TO-DATE   AVAILABLE   AGE<br>sample-app   2/2     2            2           44s</pre><p>Once the Pods are running, Metrics Server will start collecting the Pods resource metrics from the node’s Kubelet and expose them in Kubernetes through <a href="https://github.com/kubernetes/metrics">Metrics API</a> for use by <a href="https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/">Horizontal Pod Autoscaler</a> and <a href="https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler/">Vertical Pod Autoscaler</a>.</p><p>Let’s see what the resource usage for those Pods currently is!</p><pre>➜ kubectl top pods -n sample-app<br>NAME                          CPU(cores)   MEMORY(bytes)<br>sample-app-6bcbfc8b49-j6xmq   1m           1Mi<br>sample-app-6bcbfc8b49-wtd8g   1m           1Mi</pre><p>Pretty low, right? 🤔 <br>This is obviously expected since our Go service currently isn’t handling any load.</p><p>Alright, now let’s have a look at the HPA!</p><pre>➜ kubectl describe hpa -n sample-app<br>...<br>Metrics: ( current / target )<br>  <strong>resource memory on pods (as a percentage of request):  8% / 50%<br>  resource cpu on pods (as a percentage of request):     10% / 50%</strong><br>Min replicas:                                            2<br>Max replicas:                                            8<br>...</pre><p>As you can see, this HPA is configured to scale the service based on both CPU and memory, with average utilization of <strong>50%</strong> each.</p><p>This means that as soon as either the CPU or memory utilization breaches the <strong>50%</strong> threshold, the HPA will trigger an upscale.</p><p>Under minimal load, the HPA will still retain a replica count of <strong>2</strong>, while the maximum amount of Pods the HPA is allowed to spin up under high load is <strong>8</strong>.</p><blockquote>Note: in a production environment, it is recommended to have a minimum replica count of at least 3, to guarantee maintained availability in the case of <a href="https://kubernetes.io/docs/concepts/scheduling-eviction/_print/#pod-disruption">Pod Disruption</a>.</blockquote><p>Now, this is the moment you’ve certainly expected… It’s Load Testing time! 😎</p><p>For Load Testing, I really recommend <a href="https://k6.io/docs/">k6</a> from the Grafana Labs team. It is a dead-simple yet super powerful tool with very extensive documentation.</p><p>See for yourself!</p><pre>k6 run k6/script.js</pre><p>While the load test is running, I suggest watching the HPA in a separate tab.</p><pre>kubectl get hpa -n sample-app -w</pre><p>As the load test progresses and the 2 starting Pods struggle to handle incoming requests, you should see both CPU and memory targets increasing, and ultimately, the <strong>replica count reaching its maximum</strong>!</p><pre>Deployment/sample-app   16%/50%, 10%/50%   2         8         <strong>2</strong><br>Deployment/sample-app   16%/50%, 15%/50%   2         8         <strong>2</strong><br>Deployment/sample-app   17%/50%, 40%/50%   2         8         <strong>2</strong><br>Deployment/sample-app   18%/50%, 50%/50%   2         8         <strong>2</strong><br>Deployment/sample-app   19%/50%, 60%/50%   2         8         <strong>2</strong><br>Deployment/sample-app   22%/50%, 75%/50%   2         8         <strong>3</strong><br>Deployment/sample-app   27%/50%, 85%/50%   2         8         <strong>3</strong><br>Deployment/sample-app   24%/50%, 80%/50%   2         8         <strong>4</strong><br>Deployment/sample-app   27%/50%, 80%/50%   2         8         <strong>5</strong><br>Deployment/sample-app   22%/50%, 72%/50%   2         8         <strong>5</strong><br>Deployment/sample-app   23%/50%, 70%/50%   2         8         <strong>6</strong><br>Deployment/sample-app   25%/50%, 64%/50%   2         8         <strong>7</strong><br>Deployment/sample-app   24%/50%, 61%/50%   2         8         <strong>7</strong><br>Deployment/sample-app   25%/50%, 61%/50%   2         8         <strong>7</strong><br>Deployment/sample-app   27%/50%, 60%/50%   2         8         <strong>8</strong><br>Deployment/sample-app   28%/50%, 60%/50%   2         8         <strong>8</strong><br>Deployment/sample-app   27%/50%, 57%/50%   2         8         <strong>8</strong></pre><p>When relying on multiple targets for a single HPA, you can find out which of those have triggered the up/downscale by consulting Kubernetes events.</p><pre>➜ kubectl get events -n sample-app<br><strong>...<br>New size: 3</strong>; reason: cpu resource utilization (percentage of request) above target<br>Scaled up replica set sample-app-6bcbfc8b49 to 3<br><strong>New size: 4</strong>; reason: cpu resource utilization (percentage of request) above target<br>Scaled up replica set sample-app-6bcbfc8b49 to 4<br><strong>New size: 5</strong>; reason: cpu resource utilization (percentage of request) above target<br>Scaled up replica set sample-app-6bcbfc8b49 to 5<br><strong>New size: 6</strong>; reason: cpu resource utilization (percentage of request) above target<br>Scaled up replica set sample-app-6bcbfc8b49 to 6<br><strong>New size: 7</strong>; reason: cpu resource utilization (percentage of request) above target<br>Scaled up replica set sample-app-6bcbfc8b49 to 7<br><strong>New size: 8</strong>; reason: cpu resource utilization (percentage of request) above target<br>Scaled up replica set sample-app-6bcbfc8b49 to 8<br>...</pre><p>Now, let’s quickly have a look at the Load Test summary and the result of the http_req_duration metric in particular!</p><pre>          /\      |‾‾| /‾‾/   /‾‾/<br>     /\  /  \     |  |/  /   /  /<br>    /  \/    \    |     (   /   ‾‾\<br>   /          \   |  |\  \ |  (‾)  |<br>  / __________ \  |__| \__\ \_____/ .io</pre><pre>  execution: local<br>     script: k6/script.js<br>     output: -</pre><pre>  scenarios: (100.00%) 1 scenario, 100 max VUs, 5m30s max duration (incl. graceful stop):<br>           * load: Up to 100.00 iterations/s for 5m0s over 2 stages (maxVUs: 100, gracefulStop: 30s)</pre><pre>     ✓ status code is 200<br>     ✓ node is kind-control-plane<br>     ✓ namespace is sample-app<br>     ✓ pod is sample-app-*</pre><pre>   ✓ checks.........................: 100.00% ✓ 60360    ✗ 0<br>     data_received..................: 3.5 MB  12 kB/s<br>     data_sent......................: 1.7 MB  5.8 kB/s<br>     http_req_blocked...............: avg=17.57µs  min=1µs      med=9µs    max=5.48ms  p(90)=17µs    p(95)=21µs<br>     http_req_connecting............: avg=4.36µs   min=0s       med=0s     max=5.26ms  p(90)=0s      p(95)=0s<br><strong>   ✓ http_req_duration..............: avg=17.63ms  min=496µs    med=3.16ms max=1.76s   p(90)=11.38ms p(95)=51.18ms</strong><br>       { expected_response:true }...: avg=17.63ms  min=496µs    med=3.16ms max=1.76s   p(90)=11.38ms p(95)=51.18ms<br>     http_req_failed................: 0.00%   ✓ 0        ✗ 15090<br>     http_req_receiving.............: avg=107.72µs min=10µs     med=78µs   max=7.62ms  p(90)=164µs   p(95)=214µs<br>     http_req_sending...............: avg=53.87µs  min=5µs      med=35µs   max=15.33ms p(90)=73µs    p(95)=95µs<br>     http_req_tls_handshaking.......: avg=0s       min=0s       med=0s     max=0s      p(90)=0s      p(95)=0s<br>     http_req_waiting...............: avg=17.47ms  min=423µs    med=2.99ms max=1.76s   p(90)=11.08ms p(95)=51.02ms<br>     http_reqs......................: 15090   50.29863/s<br>     iteration_duration.............: avg=18.11ms  min=626.66µs med=3.64ms max=1.76s   p(90)=12.03ms p(95)=51.78ms<br>     iterations.....................: 15090   50.29863/s<br>     vus............................: 0       min=0      max=18<br>     vus_max........................: 100     min=100    max=100</pre><pre>running (5m00.0s), 000/100 VUs, 15090 complete and 0 interrupted iterations<br>load ✓ [======================================] 000/100 VUs  5m0s  000.65 iters/s</pre><p>As you can observe, our Golang service has performed very well under heavy load, with a Success Share of 100%, a median latency of ~3ms, and a 95th percentile latency of ~50ms!</p><p>We have the HPA to thank for that, as it scaled the Deployment from 2 to 8 Pods swiftly and automatically, based on the Pods resource usage!</p><p>We definitely would not have had the same results without an HPA… Actually, why don’t you try it yourself? 😉</p><p>Just delete the HPA (kubectl delete hpa sample-app -n sample-app), run the load test again (k6 run k6/script.js) and see what happens! (spoiler alert: it’s not pretty 😬)</p><p>Once you are done, don’t forget to uninstall the Helm release! (we won’t be needing this one anymore)</p><pre>helm uninstall sample-app -n sample-app</pre><h4>Autoscaling JVM Services based on CPU Usage</h4><p>While native services run as executable binaries, JVM services need an extra environment layer in order to run on various OSs and CPU architectures: the Java Virtual Machine (JVM).</p><p>This creates an issue for Pod Autoscaling, as the JVM pre-allocates more memory than it actually needs from the Pod’s resources, for its Garbage Collector (GC). This makes memory altogether an unreliable metric to use for autoscaling a JVM-based service in Kubernetes via Metrics Server.</p><p>Thus, in the case of Java or other JVM-based services, when utilizing Metrics Server for HPA, one can only rely on the CPU metric for autoscaling.</p><p>Let’s experience it with a Kotlin/JVM service!</p><pre>helm install kotlin-sample-app/helm-chart --name-template sample-app --create-namespace -n sample-app --wait</pre><p>If everything goes fine, you should eventually see one Deployment with the READY state.</p><pre>➜ kubectl get deploy -n sample-app<br>NAME         READY   UP-TO-DATE   AVAILABLE   AGE<br>sample-app   2/2     2            2           52s</pre><p>Let’s see what the resource usage for those Pods running a JVM currently is!</p><pre>➜ kubectl top pods -n sample-app<br>NAME                         CPU(cores)   MEMORY(bytes)<br>sample-app-8df8cfcd4-lg9s8   7m           105Mi<br>sample-app-8df8cfcd4-r9fjh   7m           105Mi</pre><p>Interesting! As you can see, while being idle, both pods consume ~100Mi (~104Mb) of memory, which is almost 50% of the Pods memory limit! 😱<br>As previously stated, this is due to the JVM pre-allocating memory for its Garbage Collector (GC).</p><p>Alright, now let’s have a look at the HPA!</p><pre>➜ kubectl describe hpa -n sample-app<br>...<br>Metrics: ( current / target )<br>  <strong>resource cpu on pods (as a percentage of request):  10% / 50%</strong><br>Min replicas:                                         2<br>Max replicas:                                         8<br>...</pre><p>As announced, this time the HPA only relies on one resource metric: the CPU.</p><p>Alright, let’s give our favorite Load Testing tool another go! 🚀</p><pre>k6 run k6/script.js</pre><p>As previously mentioned, I suggest watching the HPA in a separate tab.</p><pre>kubectl get hpa -n sample-app -w</pre><p>As the load test progresses and the 2 starting Pods struggle to handle incoming requests, you should see the CPU target increasing, and ultimately, the <strong>replica count reaching its maximum</strong>!</p><pre>Deployment/sample-app   10%/50%   2         8         <strong>2</strong><br>Deployment/sample-app   15%/50%   2         8         <strong>2</strong><br>Deployment/sample-app   36%/50%   2         8         <strong>2</strong><br>Deployment/sample-app   64%/50%   2         8         <strong>2</strong><br>Deployment/sample-app   37%/50%   2         8         <strong>3</strong><br>Deployment/sample-app   41%/50%   2         8         <strong>3</strong><br>Deployment/sample-app   51%/50%   2         8         <strong>3</strong><br>Deployment/sample-app   99%/50%   2         8         <strong>3</strong><br>Deployment/sample-app   56%/50%   2         8         <strong>6</strong><br>Deployment/sample-app   50%/50%   2         8         <strong>6</strong><br>Deployment/sample-app   76%/50%   2         8         <strong>6</strong><br>Deployment/sample-app   74%/50%   2         8         <strong>8</strong><br>Deployment/sample-app   61%/50%   2         8         <strong>8</strong><br>Deployment/sample-app   58%/50%   2         8         <strong>8</strong></pre><p>Now, let’s quickly have a look at the Load Test summary and the result of the http_req_duration metric in particular!</p><pre>          /\      |‾‾| /‾‾/   /‾‾/<br>     /\  /  \     |  |/  /   /  /<br>    /  \/    \    |     (   /   ‾‾\<br>   /          \   |  |\  \ |  (‾)  |<br>  / __________ \  |__| \__\ \_____/ .io</pre><pre>  execution: local<br>     script: k6/script.js<br>     output: -</pre><pre>  scenarios: (100.00%) 1 scenario, 100 max VUs, 5m30s max duration (incl. graceful stop):<br>           * load: Up to 100.00 iterations/s for 5m0s over 2 stages (maxVUs: 100, gracefulStop: 30s)</pre><pre>     ✓ status code is 200<br>     ✓ node is kind-control-plane<br>     ✓ namespace is sample-app<br>     ✓ pod is sample-app-*</pre><pre>   ✓ checks.........................: 100.00% ✓ 60360     ✗ 0<br>     data_received..................: 3.3 MB  11 kB/s<br>     data_sent......................: 1.7 MB  5.8 kB/s<br>     http_req_blocked...............: avg=18.56µs  min=1µs    med=9µs    max=2.37ms p(90)=17µs    p(95)=20µs<br>     http_req_connecting............: avg=5.52µs   min=0s     med=0s     max=1.65ms p(90)=0s      p(95)=0s<br><strong>   ✓ http_req_duration..............: avg=13.4ms   min=864µs  med=3.7ms  max=1.96s  p(90)=11.43ms p(95)=43.16ms</strong><br>       { expected_response:true }...: avg=13.4ms   min=864µs  med=3.7ms  max=1.96s  p(90)=11.43ms p(95)=43.16ms<br>     http_req_failed................: 0.00%   ✓ 0         ✗ 15090<br>     http_req_receiving.............: avg=101.68µs min=10µs   med=79µs   max=5.31ms p(90)=167µs   p(95)=217µs<br>     http_req_sending...............: avg=47.68µs  min=4µs    med=37µs   max=5.87ms p(90)=70µs    p(95)=88µs<br>     http_req_tls_handshaking.......: avg=0s       min=0s     med=0s     max=0s     p(90)=0s      p(95)=0s<br>     http_req_waiting...............: avg=13.25ms  min=803µs  med=3.54ms max=1.96s  p(90)=11.25ms p(95)=43.05ms<br>     http_reqs......................: 15090   50.306331/s<br>     iteration_duration.............: avg=13.84ms  min=1.06ms med=4.17ms max=1.96s  p(90)=12.02ms p(95)=43.55ms<br>     iterations.....................: 15090   50.306331/s<br>     vus............................: 0       min=0       max=13<br>     vus_max........................: 100     min=100     max=100</pre><pre>running (5m00.0s), 000/100 VUs, 15090 complete and 0 interrupted iterations<br>load ✓ [======================================] 000/100 VUs  5m0s  000.65 iters/s</pre><p>As you can observe, our Kotlin/JVM service has performed very well under heavy load, with a Success Share of 100%, a median latency of ~3ms, and a 95th percentile latency of ~50ms!</p><p>Once again, the HPA was able to scale the Deployment from 2 to 8 Pods swiftly and automatically, based on the Pods CPU usage alone!</p><blockquote>Note: if you keep the Deployment idle for a few minutes, you should see the HPA gradually scaling back down to 2 Pods, due to low CPU usage.</blockquote><h4>Wrapping up</h4><p>That’s it! You can now stop and delete your Kind cluster.</p><pre>kind delete cluster</pre><p>To summarize, using Metrics Server we were able to:</p><ul><li>Autoscale horizontally our native service written in Golang, based on both CPU and memory usage;</li><li>Autoscale horizontally our JVM service written in Kotlin/JVM, based on CPU usage.</li></ul><p>Was it worth it? Did that help you understand how to implement Horizontal Pod Autoscaler in Kubernetes using Metrics Server?</p><p>If so:</p><ol><li>Let me know in the comments below! 👇</li><li>Don’t forget to hit that <a href="https://medium.com/@jhandguy">subscribe</a> button! ✅</li><li>Follow me on <a href="https://twitter.com/jhandguy">Twitter</a>, I’ll be happy to answer any of your questions and you’ll be the first ones to know when a new article comes out! 👌</li></ol><p>See you next month, for Part 2 of my series <strong>Horizontal Pod Autoscaler in Kubernetes</strong>!</p><p>Bye-bye! 👋</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=929e96cc2ab2" width="1" height="1" alt=""><hr><p><a href="https://code.egym.com/horizontal-pod-autoscaler-in-kubernetes-part-1-simple-autoscaling-using-metrics-server-929e96cc2ab2">Horizontal Pod Autoscaler in Kubernetes (Part 1) — Simple Autoscaling using Metrics Server</a> was originally published in <a href="https://code.egym.com">EGYM Software Development</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Incremental Mobile Force Update using Ingress NGINX and Firebase Remote Config]]></title>
            <link>https://code.egym.com/incremental-mobile-force-update-using-ingress-nginx-and-firebase-remote-config-3ae552ed7ea9?source=rss-60d6fa56fda3------2</link>
            <guid isPermaLink="false">https://medium.com/p/3ae552ed7ea9</guid>
            <category><![CDATA[force-update]]></category>
            <category><![CDATA[firebase]]></category>
            <category><![CDATA[remote-config]]></category>
            <category><![CDATA[canary-deployments]]></category>
            <category><![CDATA[nginx-ingress]]></category>
            <dc:creator><![CDATA[Jean Mainguy]]></dc:creator>
            <pubDate>Thu, 10 Mar 2022 11:02:41 GMT</pubDate>
            <atom:updated>2023-11-30T08:39:34.051Z</atom:updated>
            <content:encoded><![CDATA[<h4>Learn how to conduct a mobile force update incrementally with a simple canary deployment using Ingress NGINX and Firebase Remote Config.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*0MlbZRaWA3bR8GuJ" /><figcaption>Photo by <a href="https://unsplash.com/@mike_van_den_bos?utm_source=medium&amp;utm_medium=referral">Mike van den Bos</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><blockquote><strong>Pssst</strong>! I started my own <a href="https://jhandguy.github.io/"><strong>blog</strong></a>!<br>You can read this <a href="https://jhandguy.github.io/posts/incremental-mobile-force-update/"><strong>very same article</strong></a> over there too!<br>No paywall, no ad, no Javascript — no bullshit, just pure content.<br>See you there!</blockquote><p>Mobile <a href="https://betterprogramming.pub/force-update-your-apps-74de57523650">force updates</a> occur when old versions of an app are no longer compatible with the APIs they consume. Until the app is updated to the required version, the UI blocks further usage. This is usually materialized as a system popup that will redirect users to the respective Store and disappear only once they have updated to the latest version. This is often considered bad practice as it deteriorates the UX of an app drastically, yet there are situations (mostly breaking API changes) when it is unavoidable.</p><p>Such updates are quite risky, as once an app is updated in the stores, there is no going back unless the previous version of the app is released again under a different version number. Web applications do not face such risks as they are easily rolled back. Also, just like for backend APIs, a web app can incrementally roll out such changes using a canary deployment.</p><blockquote>If you haven’t already, go read <a href="https://medium.com/@jhandguy/canary-deployment-in-kubernetes-part-1-simple-canary-deployment-using-ingress-nginx-f8f5da2b0f38"><strong>Canary Deployment in Kubernetes (Part 1) — Simple Canary Deployment using Ingress NGINX</strong></a> and learn how to implement a Simple Canary Deployment using Ingress NGINX!</blockquote><p>However, this doesn’t mean that there are no solutions. In fact, it is possible to leverage backend canary deployments in order to incrementally roll out a mobile force update. Ingress NGINX, for instance, only provides traffic splitting based on canary-weight but it also features traffic routing based on canary-by-header which can be used for this purpose.</p><blockquote>What if we could route user traffic based on the version of the app they are using?</blockquote><p>🎬 Hi there, I’m Jean!</p><p>In this article, we’re going to learn how to leverage a simple <strong>canary deployment</strong> using <strong>Ingress NGINX</strong> and <strong>Firebase Remote Config</strong> to safely conduct an incremental <strong>mobile force update</strong>! 💪</p><h3>Smart Traffic Routing with Firebase Remote Config</h3><p><a href="https://firebase.google.com/docs/remote-config">Firebase Remote Config</a> is a powerful Cloud service from Google that enables frontend applications to store their configuration properties in Google’s Cloud and access them remotely via a dedicated Firebase SDK (available in <a href="https://github.com/firebase/firebase-ios-sdk">iOS</a>, <a href="https://github.com/firebase/firebase-android-sdk">Android</a>, <a href="https://github.com/firebase/firebase-cpp-sdk">C++</a>, and <a href="https://github.com/firebase/firebase-js-sdk">Javascript</a>).</p><p>While editing those configuration properties would normally require a frontend release, with Firebase Remote Config, developers can simply change the properties values in Firebase’s UI and the frontend will update itself automatically on the fly.</p><p>The most powerful feature of Firebase Remote Config though is that configuration properties can have <strong>dynamic values</strong>, based on app data (or even user data from <a href="https://firebase.google.com/docs/analytics">Google Analytics</a>). 🤯</p><p>For instance, say we must make a non-backward-compatible change to our Backend API, this means all our mobile apps must conduct a <a href="https://betterprogramming.pub/force-update-your-apps-74de57523650">force update</a>. Thus, if users are using version <strong>v1</strong>, they will be prompted to update their app to version <strong>v2</strong> so that they can keep on consuming the Backend API. <br>But what if this non-backward-compatible change is a faulty one? How do we revert such a change? Well, in fact, we can’t: once a user has updated their mobile app, there is no going back. So how do we solve this then?</p><p>Well, to solve the force update problem, we could theoretically define a Remote Config called UseCanary and give it a <strong>dynamic value</strong> based on the app version.</p><p>Specifically:</p><ul><li>If AppVersion &lt; 2.0.0, UseCanary = never;</li><li>If AppVersion ≥ 2.0.0, UseCanary = always.</li></ul><p>Then, when the app fetches its Remote Config, it can add to its requests the famous X-Canary header we talked about in <a href="https://medium.com/@jhandguy/canary-deployment-in-kubernetes-part-1-simple-canary-deployment-using-ingress-nginx-f8f5da2b0f38"><strong>Simple Canary Deployment using Ingress NGINX</strong></a> (remember? 😉) like so: X-Canary: &lt;UseCanary&gt;.</p><p>This will result in:</p><ul><li>If AppVersion &lt; 2.0.0, request header = X-Canary: never;</li><li>If AppVersion ≥ 2.0.0, request header = X-Canary: always.</li></ul><p>In Firebase Remote Config, this is called a <strong>condition. </strong>A condition is a logical block allowing Remote Config properties (called parameters) to have <strong>dynamic values</strong> based on various app or user data, such as the app version.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RROULB20VG5j-DhQSOvWOw.png" /></figure><p>Then, creating a dynamic parameter simply boils down to specifying its name, condition(s), and associated values.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XM8EbgcNhfNAC-KuiraHNg.png" /></figure><p>Now we made sure that only users using the <strong>v2</strong> version of the app get routed to the canary deployment, we can roll out our non-backward-compatible change to canary and start doing some smoke tests, in complete isolation from the rest of the userbase!</p><h3>Incrementally Rolling Out Mobile Releases</h3><p>Once ready to go live, the mobile engineering team can start incrementally rolling out their new <strong>v2</strong> version of the app in the stores:</p><ul><li>In iOS, using the <a href="https://help.apple.com/app-store-connect/#/dev3d65fcee1">Phased Release</a> feature of the App Store ;</li><li>In Android, using the <a href="https://support.google.com/googleplay/android-developer/answer/6346149?hl=en">Staged Rollout</a> feature of the Play Store.</li></ul><p>As users update their app, traffic to the canary deployment will start coming in and if things suddenly go south, the Phased Release or Staged Rollout can be easily paused, until a fix is deployed to the canary deployment.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kF4dIxVdHvJk2pTJHQSOhg.png" /></figure><p>Once the rollout of the non-backward-compatible change in canary is considered successful, the mobile engineering team can proceed with the actual force update and the stable deployment will soon stop receiving traffic altogether. This means that all users have now migrated to the <strong>v2</strong> version of the app, the canary rollout is therefore complete and can be rolled over to the stable deployment!</p><h3>Wrapping up</h3><p>To summarize, using Ingress NGINX in tandem with Firebase Remote Config, we were able to:</p><ul><li>Route user traffic to a canary deployment based on the version of the app they are using;</li><li>Incrementally rollout a non-backward-compatible change to minimize risk before conducting a force update.</li></ul><p>Was it worth it? Did that help you understand how to incrementally conduct a mobile force update using Ingress NGINX and Firebase Remote Config?</p><p>If so:</p><ol><li>Let me know in the comments below! 👇</li><li>Don’t forget to hit that <a href="https://medium.com/@jhandguy">subscribe</a> button! ✅</li><li>Follow me on <a href="https://twitter.com/jhandguy">Twitter</a>, I’ll be happy to answer any of your questions and you’ll be the first ones to know when a new article comes out! 👌</li></ol><p>Bye-bye! 👋</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3ae552ed7ea9" width="1" height="1" alt=""><hr><p><a href="https://code.egym.com/incremental-mobile-force-update-using-ingress-nginx-and-firebase-remote-config-3ae552ed7ea9">Incremental Mobile Force Update using Ingress NGINX and Firebase Remote Config</a> was originally published in <a href="https://code.egym.com">EGYM Software Development</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Canary Deployment in Kubernetes (Part 3) — Smart Canary Deployment using Argo Rollouts and…]]></title>
            <link>https://code.egym.com/canary-deployment-in-kubernetes-part-3-smart-canary-deployment-using-argo-rollouts-and-47992d72222c?source=rss-60d6fa56fda3------2</link>
            <guid isPermaLink="false">https://medium.com/p/47992d72222c</guid>
            <category><![CDATA[canary-deployments]]></category>
            <category><![CDATA[prometheus]]></category>
            <category><![CDATA[nginx-ingress]]></category>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[argo-rollouts]]></category>
            <dc:creator><![CDATA[Jean Mainguy]]></dc:creator>
            <pubDate>Wed, 02 Feb 2022 10:42:41 GMT</pubDate>
            <atom:updated>2023-12-03T10:44:21.228Z</atom:updated>
            <content:encoded><![CDATA[<h3>Canary Deployment in Kubernetes (Part 3) — Smart Canary Deployment using Argo Rollouts and Prometheus</h3><h4>Learn how to use Argo Rollouts with Prometheus to automate the detection and rollback of faulty deployments using AnalysisTemplates.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*k7-SDkM7iB4ukpuj" /><figcaption>Photo by <a href="https://unsplash.com/@mateusmaia?utm_source=medium&amp;utm_medium=referral">Mateus Maia</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><blockquote><strong>Pssst</strong>! I started my own <a href="https://jhandguy.github.io/"><strong>blog</strong></a>!<br>You can read this <a href="https://jhandguy.github.io/posts/smart-canary-deployment/"><strong>very same article</strong></a> over there too!<br>No paywall, no ad, no Javascript — no bullshit, just pure content.<br>See you there!</blockquote><p>Deploying to production in Kubernetes can be quite stressful. Even after meaningful and reliable automated tests have successfully passed, there is still room for things to go wrong and lead to a nasty incident when pressing the final button.</p><p>Thankfully, Kubernetes is made to be resilient to this kind of scenario, and rolling back is a no-brainer. But still, rolling back means that, at least for some time, <strong>all of the users</strong> were negatively impacted by the faulty change…</p><p>What if we could smoke test our change in production <strong>before</strong> it actually hits real users? What if we could roll out a change incrementally to <strong>some users</strong> instead of all of them at once? What if we could detect a faulty deployment and roll it back automatically?<br>Well, that, my friend, is what Canary Deployment is all about!</p><blockquote>Minimizing the impact on real users while deploying a risky change to production.</blockquote><p>🎬 Hi there, I’m Jean!</p><p>In this 3 parts series, we’re going to explore several ways to do Canary Deployment in Kubernetes, and the last one is…<br>🥁<br>… using <strong>Argo Rollouts and Prometheus</strong>! 🎊</p><h3>Requirements</h3><p>Before we start, make sure you have the following tools installed:</p><ul><li><a href="https://kind.sigs.k8s.io/docs/user/quick-start/#installation">Kind</a></li><li><a href="https://kubernetes.io/docs/tasks/tools/">Kubectl</a></li><li><a href="https://argoproj.github.io/argo-rollouts/installation/#kubectl-plugin-installation">Argo Rollouts Kubectl Plugin</a></li><li><a href="https://helm.sh/docs/intro/install/">Helm</a></li><li><a href="https://k6.io/docs/getting-started/installation/">K6</a></li></ul><blockquote>Note: for MacOS users or Linux users using Homebrew, simply run:<br><em>brew install kind kubectl argoproj/tap/kubectl-argo-rollouts helm k6</em></blockquote><p>All set? Let’s go! 🏁</p><h3>Creating Kind Cluster</h3><p><a href="https://kind.sigs.k8s.io/docs/user/quick-start/#installation">Kind</a> is a tool for running local Kubernetes clusters using Docker container “nodes”. It was primarily designed for testing Kubernetes itself, but may be used for local development or CI.</p><p>I don’t expect you to have a demo project in handy, so <a href="https://github.com/jhandguy/canary-deployment">I built one</a> for you.</p><pre>git clone <a href="https://github.com/jhandguy/canary-deployment.git">https://github.com/jhandguy/canary-deployment.git</a></pre><pre>cd canary-deployment</pre><p>Alright, let’s spin up our Kind cluster! 🚀</p><pre>➜ kind create cluster --image kindest/node:v1.27.3 --config=kind/cluster.yaml</pre><pre>Creating cluster &quot;kind&quot; ...<br> ✓ Ensuring node image (kindest/node:v1.27.3) 🖼<br> ✓ Preparing nodes 📦<br> ✓ Writing configuration 📜<br> ✓ Starting control-plane 🕹️<br> ✓ Installing CNI 🔌<br> ✓ Installing StorageClass 💾</pre><pre>Set kubectl context to &quot;kind-kind&quot;<br>You can now use your cluster with:</pre><pre>kubectl cluster-info --context kind-kind<br><br>Have a question, bug, or feature request? Let us know! <a href="https://kind.sigs.k8s.io/#community">https://kind.sigs.k8s.io/#community</a> 🙂</pre><h3>Using Argo Rollouts with Prometheus</h3><p><a href="https://argoproj.github.io/argo-rollouts/">Argo Rollouts</a> is a <a href="https://kubernetes.io/docs/concepts/architecture/controller/">Kubernetes controller</a> and set of <a href="https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/">CRDs</a> which provide advanced deployment capabilities to Kubernetes such as blue-green, canary, canary analysis, experimentation, and progressive delivery.</p><p>In combination with <a href="https://prometheus.io/">Prometheus</a>, Argo Rollouts can automatically roll back a Canary Deployment based on Prometheus metrics, which means it can theoretically handle an incremental rollout without human intervention.</p><h4>Installing NGINX Ingress Controller</h4><blockquote>If you haven’t already, go read <a href="https://medium.com/@jhandguy/canary-deployment-in-kubernetes-part-1-simple-canary-deployment-using-ingress-nginx-f8f5da2b0f38"><strong>Canary Deployment in Kubernetes (Part 1) — Simple Canary Deployment using Ingress NGINX</strong></a> and learn how to implement a Simple Canary Deployment using Ingress NGINX!</blockquote><p><a href="https://github.com/kubernetes/ingress-nginx">NGINX Ingress Controller</a> is one of the <a href="https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/#additional-controllers">many available Kubernetes Ingress Controllers</a>, which acts as a load balancer and satisfies routing rules specified in <a href="https://kubernetes.io/docs/concepts/services-networking/ingress/#what-is-ingress">Ingress</a> resources, using the <a href="https://nginx.org/en/">NGINX reverse proxy</a>.</p><p>NGINX Ingress Controller can be installed via its <a href="https://github.com/kubernetes/ingress-nginx/tree/main/charts/ingress-nginx">Helm chart</a>.</p><pre>helm repo add ingress-nginx <a href="https://kubernetes.github.io/ingress-nginx">https://kubernetes.github.io/ingress-nginx</a></pre><pre>helm install ingress-nginx/ingress-nginx --name-template ingress-nginx --create-namespace -n ingress-nginx --values kind/ingress-nginx-values.yaml --version 4.8.3 --wait</pre><p>Now, if everything goes according to plan, you should be able to see the <strong>ingress-nginx-controller</strong> Deployment running.</p><pre>➜ kubectl get deploy -n ingress-nginx<br>NAME                       READY   UP-TO-DATE   AVAILABLE   AGE<br>ingress-nginx-controller   1/1     1            1           4m35s</pre><h4>Installing Argo Rollouts</h4><blockquote>If you haven’t already, go read <a href="https://medium.com/@jhandguy/canary-deployment-in-kubernetes-part-2-automated-canary-deployment-using-argo-rollouts-8a3550d5a434"><strong>Canary Deployment in Kubernetes (Part 2) — Automated Canary Deployment using Argo Rollouts</strong></a> and learn how to implement a Canary Deployment using Argo Rollouts!</blockquote><p>Argo Rollouts can be installed via its <a href="https://github.com/argoproj/argo-helm/tree/master/charts/argo-rollouts">Helm chart</a>.</p><pre>helm repo add argo <a href="https://argoproj.github.io/argo-helm">https://argoproj.github.io/argo-helm</a></pre><pre>helm install argo/argo-rollouts --name-template argo-rollouts --create-namespace -n argo-rollouts --set dashboard.enabled=true --version 2.32.5 --wait</pre><p>If all goes well, you should see two newly spawned deployments with the READY state!</p><pre>➜ kubectl get deploy -n argo-rollouts<br>NAME                      READY   UP-TO-DATE   AVAILABLE   AGE<br>argo-rollouts             1/1     1            1           13m<br>argo-rollouts-dashboard   1/1     1            1           13m</pre><h4>Installing Prometheus</h4><p>Prometheus can be installed via its community <a href="https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack">Helm chart</a>, which also provides <a href="https://grafana.com/grafana/">Grafana</a> out of the box.</p><pre>helm repo add prometheus-community <a href="https://prometheus-community.github.io/helm-charts">https://prometheus-community.github.io/helm-charts</a></pre><pre>helm install prometheus-community/kube-prometheus-stack --name-template prometheus --create-namespace -n prometheus --version 54.2.2 --wait</pre><p>If everything went fine, you should be able to see three newly spawned deployments with the READY state!</p><pre>➜ kubectl get deploy -n prometheus<br>NAME                                  READY   UP-TO-DATE   AVAILABLE   <br>prometheus-grafana                    1/1     1            1           <br>prometheus-kube-prometheus-operator   1/1     1            1           <br>prometheus-kube-state-metrics         1/1     1            1           </pre><h3>Automating Rollbacks using AnalysisTemplates</h3><p>In <a href="https://medium.com/@jhandguy/canary-deployment-in-kubernetes-part-2-automated-canary-deployment-using-argo-rollouts-8a3550d5a434">Part 2</a>, we learned how to configure a Rollout and how to automate the Traffic Routing increments via Rollout steps. <br>Yet another amazing feature of Argo Rollouts is the ability to leverage Prometheus metrics in order to detect faulty deployments (i.e. based on request Success Share or Latency).</p><p>To this end, Argo Rollouts provides another <a href="https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/">Custom Resource</a> called <strong>AnalysisTemplate</strong>.<br>Alright! Let’s explore what this is!</p><pre>helm install sample-app/helm-charts/argo-rollouts --name-template sample-app --create-namespace -n sample-app --set prometheus.enabled=true --wait</pre><p>If everything goes fine, you should eventually see one Rollout with the READY state.</p><pre>➜ kubectl get rollout sample-app -n sample-app<br>NAME         DESIRED   CURRENT   UP-TO-DATE   AVAILABLE<br>sample-app   1         1         1            1</pre><p>Alright, let’s have a look at the analysistemplate.yaml and the rollout.yaml inside the templates folder!</p><pre>➜ ls -1 sample-app/helm-charts/argo-rollouts/templates<br>analysistemplate.yaml<br>canary<br>ingress.yaml<br>rollout.yaml<br>service.yaml<br>serviceaccount.yaml<br>servicemonitor.yaml</pre><p>An <strong>AnalysisTemplate</strong> is a template <em>spec</em> that defines how to perform a canary analysis. It consists of a Prometheus metric which is being evaluated, at a given interval, against a given success condition.</p><p>In this example, the Prometheus metric is a <em>Success Share</em>, with a minimum threshold of 99% and an interval of 1 minute. The failure limit is 0, meaning that as soon as it fails, the Rollout will be aborted. Since it is the canary Deployment that we are trying to evaluate, the PromQL query must target it specifically via the <em>service</em> label.</p><pre>---<br>apiVersion: argoproj.io/v1alpha1<br>kind: AnalysisTemplate<br>...<br>spec:<br>  metrics:<br>    - name: <strong>success-share</strong><br>      interval: <strong>1m</strong><br>      successCondition: len(result) == 0 || <strong>result[0] &gt;= 0.99</strong><br>      failureLimit: <strong>0</strong><br>      provider:<br>        prometheus:<br>          address: {{ .Values.prometheus.address }}<br>          query: |<br>            sum(rate(<br>              sample_app_requests_count{service=&quot;{{ .Release.Name }}-canary&quot;, success=&quot;true&quot;}[1m])<br>            ) by (service)<br>            /<br>            sum(rate(<br>              sample_app_requests_count{service=&quot;{{ .Release.Name }}-canary&quot;}[1m])<br>            ) by (service)<br>            unless sum(rate(<br>              sample_app_requests_count{service=&quot;{{ .Release.Name }}-canary&quot;}[1m])<br>            ) by (service) == 0</pre><p>The instantiation of an <strong>AnalysisTemplate</strong> is called an <strong>AnalysisRun</strong>. It is like a <em>Job</em>, meaning it eventually completes, with a result of either Successful, Failed, or Inconclusive. If successful, the rollout continues, if failed, it is aborted and if inconclusive, it is paused.</p><p>There are two ways to define an <strong>AnalysisTemplate</strong>:</p><ol><li><strong>Background Analysis</strong><br>This Analysis runs in the background, while the Rollout is progressing through its <em>steps</em>. If the <strong>AnalysisRun</strong> fails, the Rollout is aborted. On the other hand, if the <strong>AnalysisRun</strong> succeeds or all the Rollout <em>steps</em> are finished before it completes, the Rollout is considered successful.</li><li><strong>Inline Analysis</strong><br>This Analysis runs as part of the Rollout <em>steps</em>. This means that the Rollout will wait for the <strong>AnalysisRun</strong> to complete, before proceeding to the next step, or aborting the rollout depending on the outcome.</li></ol><p>In our case, we will be using the Background Analysis, as it will detect a faulty deployment sooner than an Inline Analysis would.</p><pre>---<br>apiVersion: argoproj.io/v1alpha1<br>kind: Rollout<br>...<br>spec:<br>  ...<br>  strategy:<br>    canary:<br>      ...<br>      steps:<br>        - setWeight: 25<br>        - pause: {}<br>        - setWeight: 50<br>        - pause:<br>            duration: 5m<br>        - setWeight: 75<br>        - pause:<br>            duration: 5m<br>      <strong>analysis</strong>:<br>        templates:<br>          - templateName: {{ .Release.Name }}<br>        startingStep: 2<br>      ...<br>  ...</pre><p>As you can see, the Analysis has been scheduled to start after step 2. <br>Meaning, once the Rollout is promoted, the canary-weight will be set to 50 and an <strong>AnalysisRun</strong> will start. It will determine later if the Rollout succeeds or not: if the Prometheus metric falls below 99% Success Share, the Rollout will be aborted immediately. On the other hand, if the Success Share stays above 99% for 10 minutes, the Rollout will complete successfully.</p><h3>Detecting and Aborting Rollouts Automatically</h3><p>Now that we’ve seen how an <strong>AnalysisRun</strong> works in theory, let’s see it in practice, shall we?! 🧐</p><pre>kubectl argo rollouts dashboard -n argo-rollouts &amp;</pre><p>If you now head to <a href="http://localhost:3100/rollout/sample-app">http://localhost:3100/rollout/sample-app</a>, you should see a shiny dashboard showing the state of the sample-app Rollout.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CF3wmKHB3rdwNR0GyE989A.png" /></figure><p>Cool! So far, we can observe only one <strong>revision</strong> labeled as stable, serving 100% of the traffic.</p><p>Now, let’s set a new image for the container!</p><pre>kubectl argo rollouts set image sample-app sample-app=ghcr.io/jhandguy/canary-deployment/sample-app:latest -n sample-app</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oYLZJiRtrsdnMcW4cScyAA.png" /></figure><p>Tada! The Rollout has just completed step 1: setting the canary-weight to 25%.</p><p>As you can see, the <strong>AnalysisRun</strong> has not yet started, this will only happen once we start step 2. Let’s proceed then!</p><pre>kubectl argo rollouts promote sample-app -n sample-app</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*45mJ5LEz95k2RhCmzz4MgA.png" /></figure><p>Awesome! The canary-weight is now at 50% and an <strong>AnalysisRun</strong> has just started.</p><pre>➜ kubectl get analysisrun -n sample-app<br>NAME                      STATUS<br>sample-app-5c9fc8b7d4-2   Running</pre><p>Let’s have a deeper look at it!</p><pre>➜ kubectl describe analysisrun -n sample-app<br>Name:         sample-app-5c9fc8b7d4-2<br>Namespace:    sample-app<br>Labels:       rollout-type=Background<br>              rollouts-pod-template-hash=5c9fc8b7d4<br>Annotations:  rollout.argoproj.io/revision: 2<br>API Version:  argoproj.io/v1alpha1<br>Kind:         AnalysisRun<br>...<br>Spec:<br>  Metrics:<br>    Failure Limit:  0<br>    Interval:       1m<br>    Name:           success-share<br>    Provider:<br>      Prometheus:<br>        Address:  <a href="http://prometheus-operated.prometheus.svc.cluster.local:9090">http://prometheus-operated.prometheus.svc.cluster.local:9090</a><br>        Query:    sum(rate(<br>                    sample_app_requests_count{service=&quot;sample-app-canary&quot;, success=&quot;true&quot;}[1m])<br>                  ) by (service)<br>                  /<br>                  sum(rate(<br>                    sample_app_requests_count{service=&quot;sample-app-canary&quot;}[1m])<br>                  ) by (service)<br>                  unless sum(rate(<br>                    sample_app_requests_count{service=&quot;sample-app-canary&quot;}[1m])<br>                  ) by (service) == 0<br>    Success Condition:  len(result) == 0 || result[0] &gt;= 0.99<br>Status:<br>  Metric Results:<br>    <strong>Count:  1</strong><br>    Measurements:<br>      <strong>Finished At:  2022-01-29T18:37:49Z<br>      Phase:        Successful<br>      Started At:   2022-01-29T18:37:49Z<br>      Value:        []</strong><br>    Name:           success-share<br>    Phase:          Running<br>    <strong>Successful:     1</strong><br>  Phase:            Running<br>  Started At:       2022-01-29T18:37:49Z<br>Events:             &lt;none&gt;</pre><p>As expected, the <em>Spec</em> part of the <strong>AnalysisRun</strong> is the same one we’ve specified in the <strong>AnalysisTemplate</strong>. <br>To monitor the progress of the Rollout, however, it is the <em>Status</em> and the <em>Events</em> that we will be looking at.</p><p>Right now, the measurement’s value is empty ([]), this is because the service has not recorded any metrics yet. Let’s change that and send some successful requests for Prometheus to scrape! 🚀</p><blockquote>Since the PromQL query targets the canary Service specifically, we must make sure to always land in the canary Deployment using the Ingress canary-by-header annotation (learn more about it in <a href="https://medium.com/@jhandguy/canary-deployment-in-kubernetes-part-1-simple-canary-deployment-using-ingress-nginx-f8f5da2b0f38">Part 1</a>).</blockquote><pre>curl localhost/success -H &quot;Host: sample.app&quot; -H &quot;X-Canary: always&quot;</pre><p>After firing some requests and waiting about a minute for Prometheus to scrape the metrics and the <strong>AnalysisRun</strong> to measure the success-share metric, you should see one of the measurements with a value of [1], meaning that the Success Share has been successfully measured at 100%! 💯</p><pre>➜ kubectl describe analysisrun -n sample-app<br>...<br>Status:<br>  Metric Results:<br>    <strong>Count:  5</strong><br>    Measurements:<br>      ...<br>      <strong>Finished At:  2022-01-29T18:41:49Z<br>      Phase:        Successful</strong><br>      <strong>Started At:   2022-01-29T18:41:49Z<br>      Value:        [1]</strong><br>    Name:           success-share<br>    Phase:          Running<br>    <strong>Successful:     5</strong><br>  Phase:            Running<br>  Started At:       2022-01-29T18:37:49Z<br>Events:             &lt;none&gt;</pre><p>The best part of this is, that while all of this was happening, the Rollout kept on progressing and has probably already reached the next step: increasing the canary-weight to 75%.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XMpt-RYOx0_sXbHFjjoWZA.png" /></figure><p>That’s great! But let’s make things more interesting: what happens once the Success Share falls below 99%? 😱</p><p>Let’s go ahead and simulate a 50% Success Share by sending several requests and alternating between successes and errors!</p><pre>curl localhost/success -H &quot;Host: sample.app&quot; -H &quot;X-Canary: always&quot;<br>curl localhost/error -H &quot;Host: sample.app&quot; -H &quot;X-Canary: always&quot;</pre><p>After a short while, you should notice that something quite magical has happened… 🪄</p><pre>➜ kubectl describe analysisrun -n sample-app<br>...<br>Status:<br>  <strong>Message:  metric &quot;success-share&quot; assessed Failed due to failed (1) &gt; failureLimit (0)</strong><br>  Metric Results:<br>    <strong>Count:   9<br>    Failed:  1</strong><br>    Measurements:<br>      ...<br>      <strong>Finished At:  2022-01-29T18:45:49Z<br>      Phase:        Failed<br>      Started At:   2022-01-29T18:45:49Z<br>      Value:        [0.5]</strong><br>    Name:           success-share<br>    <strong>Phase:          Failed</strong><br>    Successful:     8<br>  Phase:            Failed<br>  Started At:       2022-01-29T18:37:49Z<br>Events:<br>  Type     Reason             Age   From                 Message<br>  ----     ------             ----  ----                 -------<strong><br>  Warning  MetricFailed       17s   rollouts-controller  metric &#39;success-share&#39; completed Failed<br>  Warning  AnalysisRunFailed  17s   rollouts-controller  analysis completed Failed</strong></pre><p>As soon as the <strong>AnalysisRun</strong> recorded a measurement breaching below the 99% Success Share threshold, the Rollout was immediately aborted.</p><p>Let’s confirm that from the Rollout’s status real quick!</p><pre>➜ kubectl argo rollouts status sample-app -n sample-app<br>Degraded<br>Error: The rollout is in a degraded state with message: <strong>RolloutAborted: Rollout aborted update to revision 2: metric &quot;success-share&quot; assessed Failed due to failed (1) &gt; failureLimit (0)</strong></pre><p>Indeed, even the Dashboard highlights that the Rollout is in a <em>Degraded</em> state, and consequently the canary-weight has been taken down to 0%.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WYPjH5cVlSDxqx89lkRY_Q.png" /></figure><p>This would, in practice, give us time to investigate the issue, and once fixed, <strong>retry</strong> the Rollout, either via the CLI or the Dashboard’s <em>RETRY</em> button.</p><pre>kubectl argo rollouts retry rollout sample-app -n sample-app<br>kubectl argo rollouts promote sample-app -n sample-app</pre><p>Now that the Rollout has restarted and reached step 2, a new <strong>AnalysisRun</strong> has started and the incremental rollout is back on track! 🛤</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WjURmJQTRhyrqq-JBtlbng.png" /></figure><h4>Load Testing with k6</h4><p>Instead of running some curl commands like we previously did, how about we execute a Load Test instead?</p><p>For Load Testing, I really recommend <a href="https://k6.io/docs/">k6</a> from the Grafana Labs team. It is a dead-simple yet super powerful tool with very extensive documentation.</p><p>See for yourself!</p><pre>k6 run k6/script.js</pre><p>After about 1 minute, k6 should be done executing the load test and show you the results.</p><pre>          /\      |‾‾| /‾‾/   /‾‾/<br>     /\  /  \     |  |/  /   /  /<br>    /  \/    \    |     (   /   ‾‾\<br>   /          \   |  |\  \ |  (‾)  |<br>  / __________ \  |__| \__\ \_____/ .io</pre><pre>  execution: local<br>     script: k6/script.js<br>     output: -</pre><pre>  scenarios: (100.00%) 1 scenario, 20 max VUs, 1m30s max duration (incl. graceful stop):<br>           * load: Up to 20.00 iterations/s for 1m0s over 2 stages (maxVUs: 20, gracefulStop: 30s)</pre><pre>     ✓ status code is 200<br>     ✓ node is kind-control-plane<br>     ✓ namespace is sample-app<br>     ✓ pod is sample-app-*<br>     ✓ deployment is stable or canary</pre><pre>   ✓ checks.........................: 100.00% ✓ 3095      ✗ 0<br>     data_received..................: 157 kB  2.6 kB/s<br>     data_sent......................: 71 kB   1.2 kB/s<br>     http_req_blocked...............: avg=41.24µs min=3µs    med=8µs    max=3.39ms  p(90)=19µs   p(95)=55.19µs<br>     http_req_connecting............: avg=21.96µs min=0s     med=0s     max=2.71ms  p(90)=0s     p(95)=0s<br>   ✓ http_req_duration..............: avg=3.75ms  min=940µs  med=2.96ms max=17.22ms p(90)=6.99ms p(95)=9.37ms<br>       { expected_response:true }...: avg=3.75ms  min=940µs  med=2.96ms max=17.22ms p(90)=6.99ms p(95)=9.37ms<br>     http_req_failed................: 0.00%   ✓ 0         ✗ 619<br>     http_req_rate..................: 50.00%  ✓ 619       ✗ 619<br><strong>     ✓ { deployment:canary }........: 49.91%  ✓ 309       ✗ 310<br>     ✓ { deployment:stable }........: 50.08%  ✓ 310       ✗ 309</strong><br>     http_req_receiving.............: avg=107.1µs min=24µs   med=88µs   max=2.09ms  p(90)=165µs  p(95)=189.09µs<br>     http_req_sending...............: avg=50.78µs min=14µs   med=36µs   max=837µs   p(90)=77µs   p(95)=114.19µs<br>     http_req_tls_handshaking.......: avg=0s      min=0s     med=0s     max=0s      p(90)=0s     p(95)=0s<br>     http_req_waiting...............: avg=3.6ms   min=857µs  med=2.78ms max=17.09ms p(90)=6.81ms p(95)=9.1ms<br>     http_reqs......................: 619     10.316653/s<br>     iteration_duration.............: avg=4.31ms  min=1.09ms med=3.53ms max=21.84ms p(90)=7.82ms p(95)=10.34ms<br>     iterations.....................: 619     10.316653/s<br>     vus............................: 0       min=0       max=0<br>     vus_max........................: 20      min=20      max=20</pre><pre>running (1m00.0s), 00/20 VUs, 619 complete and 0 interrupted iterations<br>load ✓ [======================================] 00/20 VUs  1m0s  00.71 iters/s</pre><p>That sounds about right!<br>Depending on which step the Rollout is at, the Traffic Distribution shown in k6’s output should be either 50/50 or 75/25.</p><p>Same as before, if you now describe the <strong>AnalysisRun</strong>, you should see some successful measurements were measured.</p><pre>➜ kubectl describe analysisrun -n sample-app<br>...<br>Status:<br>  Metric Results:<br>    Count:  5<br>    Measurements:<br>      ...<br>      <strong>Phase:        Successful<br>      Started At:   2022-01-30T14:34:07Z<br>      Value:        [1]</strong><br>    Name:           success-share<br>    Phase:          Running<br>    Successful:     5<br>  Phase:            Running<br>  Started At:       2022-01-30T14:32:07Z<br>Events:             &lt;none&gt;</pre><p>Eventually, the Rollout should complete successfully and the new revision should become the new stable Deployment! 🎉</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6OsrNhTuy3JA2t602Y284Q.png" /></figure><h3>Wrapping up</h3><p>That’s it! You can now stop and delete your Kind cluster!</p><pre>kind delete cluster</pre><p>To summarize, using Argo Rollouts and Prometheus we were able to:</p><ul><li>Detect when a Prometheus metric breaches a given threshold and abort the faulty Rollout automatically, thanks to the <strong>AnalysisRun</strong> CRD;</li><li>Restart the aborted Rollout effortlessly once the issue is fixed.</li></ul><p>Was it worth it? Did that help you understand how to implement Canary Deployment in Kubernetes using Argo Rollouts and Prometheus?</p><p>If so:</p><ol><li>Let me know in the comments below! 👇</li><li>Don’t forget to hit that <a href="https://medium.com/@jhandguy">subscribe</a> button! ✅</li><li>Follow me on <a href="https://twitter.com/jhandguy">Twitter</a>, I’ll be happy to answer any of your questions and you’ll be the first ones to know when a new article comes out! 👌</li></ol><p>Bye-bye! 👋</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=47992d72222c" width="1" height="1" alt=""><hr><p><a href="https://code.egym.com/canary-deployment-in-kubernetes-part-3-smart-canary-deployment-using-argo-rollouts-and-47992d72222c">Canary Deployment in Kubernetes (Part 3) — Smart Canary Deployment using Argo Rollouts and…</a> was originally published in <a href="https://code.egym.com">EGYM Software Development</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Canary Deployment in Kubernetes (Part 2) — Automated Canary Deployment using Argo Rollouts]]></title>
            <link>https://code.egym.com/canary-deployment-in-kubernetes-part-2-automated-canary-deployment-using-argo-rollouts-8a3550d5a434?source=rss-60d6fa56fda3------2</link>
            <guid isPermaLink="false">https://medium.com/p/8a3550d5a434</guid>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[nginx-ingress]]></category>
            <category><![CDATA[argo-rollouts]]></category>
            <category><![CDATA[canary-deployments]]></category>
            <category><![CDATA[nginx-ingress-controller]]></category>
            <dc:creator><![CDATA[Jean Mainguy]]></dc:creator>
            <pubDate>Tue, 25 Jan 2022 09:12:32 GMT</pubDate>
            <atom:updated>2023-12-03T10:43:26.760Z</atom:updated>
            <content:encoded><![CDATA[<h3>Canary Deployment in Kubernetes (Part 2) — Automated Canary Deployment using Argo Rollouts</h3><h4>Learn how to use Argo Rollouts to improve a Canary Deployment setup using Ingress NGINX and automate incremental rollouts.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*TtWUKBuj4F5j3s_Y" /><figcaption>Photo by <a href="https://unsplash.com/@dozy_de?utm_source=medium&amp;utm_medium=referral">Christian Ladewig</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><blockquote><strong>Pssst</strong>! I started my own <a href="https://jhandguy.github.io/"><strong>blog</strong></a>!<br>You can read this <a href="https://jhandguy.github.io/posts/automated-canary-deployment/"><strong>very same article</strong></a> over there too!<br>No paywall, no ad, no Javascript — no bullshit, just pure content.<br>See you there!</blockquote><p>Deploying to production in Kubernetes can be quite stressful. Even after meaningful and reliable automated tests have successfully passed, there is still room for things to go wrong and lead to a nasty incident when pressing the final button.</p><p>Thankfully, Kubernetes is made to be resilient to this kind of scenario, and rolling back is a no-brainer. But still, rolling back means that, at least for some time, <strong>all of the users</strong> were negatively impacted by the faulty change…</p><p>What if we could smoke test our change in production <strong>before</strong> it actually hits real users? What if we could roll out a change incrementally to <strong>some users</strong> instead of all of them at once? What if we could detect a faulty deployment and roll it back automatically?<br>Well, that, my friend, is what Canary Deployment is all about!</p><blockquote>Minimizing the impact on real users while deploying a risky change to production.</blockquote><p>🎬 Hi there, I’m Jean!</p><p>In this 3 parts series, we’re going to explore several ways to do Canary Deployment in Kubernetes, and the second one is…<br>🥁<br>… using <strong>Argo Rollouts</strong>! 🎊</p><h3>Requirements</h3><p>Before we start, make sure you have the following tools installed:</p><ul><li><a href="https://kind.sigs.k8s.io/docs/user/quick-start/#installation">Kind</a></li><li><a href="https://kubernetes.io/docs/tasks/tools/">Kubectl</a></li><li><a href="https://argoproj.github.io/argo-rollouts/installation/#kubectl-plugin-installation">Argo Rollouts Kubectl Plugin</a></li><li><a href="https://helm.sh/docs/intro/install/">Helm</a></li><li><a href="https://k6.io/docs/getting-started/installation/">K6</a></li></ul><blockquote>Note: for MacOS users or Linux users using Homebrew, simply run: <em>brew install kind kubectl argoproj/tap/kubectl-argo-rollouts helm k6</em></blockquote><p>All set? Let’s go! 🏁</p><h3>Creating Kind Cluster</h3><p><a href="https://kind.sigs.k8s.io/docs/user/quick-start/#installation">Kind</a> is a tool for running local Kubernetes clusters using Docker container “nodes”. It was primarily designed for testing Kubernetes itself, but may be used for local development or CI.</p><p>I don’t expect you to have a demo project in handy, so <a href="https://github.com/jhandguy/canary-deployment">I built one</a> for you.</p><pre>git clone <a href="https://github.com/jhandguy/canary-deployment.git">https://github.com/jhandguy/canary-deployment.git</a></pre><pre>cd canary-deployment</pre><p>Alright, let’s spin up our Kind cluster! 🚀</p><pre>➜ kind create cluster --image kindest/node:v1.27.3 --config=kind/cluster.yaml</pre><pre>Creating cluster &quot;kind&quot; ...<br> ✓ Ensuring node image (kindest/node:v1.27.3) 🖼<br> ✓ Preparing nodes 📦<br> ✓ Writing configuration 📜<br> ✓ Starting control-plane 🕹️<br> ✓ Installing CNI 🔌<br> ✓ Installing StorageClass 💾</pre><pre>Set kubectl context to &quot;kind-kind&quot;<br>You can now use your cluster with:</pre><pre>kubectl cluster-info --context kind-kind<br><br>Have a question, bug, or feature request? Let us know! <a href="https://kind.sigs.k8s.io/#community">https://kind.sigs.k8s.io/#community</a> 🙂</pre><h3>Using Argo Rollouts with NGINX Ingress Controller</h3><p><a href="https://argoproj.github.io/argo-rollouts/">Argo Rollouts</a> is a <a href="https://kubernetes.io/docs/concepts/architecture/controller/">Kubernetes controller</a> and set of <a href="https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/">CRDs</a> which provide advanced deployment capabilities to Kubernetes such as blue-green, canary, canary analysis, experimentation, and progressive delivery.</p><p>In combination with the <a href="https://argoproj.github.io/argo-rollouts/features/traffic-management/nginx/">NGINX Ingress Controller</a>, Argo Rollouts achieves the same as a <a href="https://medium.com/@jhandguy/canary-deployment-in-kubernetes-part-1-simple-canary-deployment-using-ingress-nginx-f8f5da2b0f38">Simple Canary Deployment using Ingress NGINX</a>, but better.</p><h4>Installing NGINX Ingress Controller</h4><blockquote>If you haven’t already, go read <a href="https://medium.com/@jhandguy/canary-deployment-in-kubernetes-part-1-simple-canary-deployment-using-ingress-nginx-f8f5da2b0f38"><strong>Canary Deployment in Kubernetes (Part 1) — Simple Canary Deployment using Ingress NGINX</strong></a> and learn how to implement a Simple Canary Deployment using Ingress NGINX!</blockquote><p><a href="https://github.com/kubernetes/ingress-nginx">NGINX Ingress Controller</a> is one of the <a href="https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/#additional-controllers">many available Kubernetes Ingress Controllers</a>, which acts as a load balancer and satisfies routing rules specified in <a href="https://kubernetes.io/docs/concepts/services-networking/ingress/#what-is-ingress">Ingress</a> resources, using the <a href="https://nginx.org/en/">NGINX reverse proxy</a>.</p><p>NGINX Ingress Controller can be installed via its <a href="https://github.com/kubernetes/ingress-nginx/tree/main/charts/ingress-nginx">Helm chart</a>.</p><pre>helm repo add ingress-nginx <a href="https://kubernetes.github.io/ingress-nginx">https://kubernetes.github.io/ingress-nginx</a></pre><pre>helm install ingress-nginx/ingress-nginx --name-template ingress-nginx --create-namespace -n ingress-nginx --values kind/ingress-nginx-values.yaml --version 4.8.3 --wait</pre><p>Now, if everything goes according to plan, you should be able to see the <strong>ingress-nginx-controller</strong> Deployment running.</p><pre>➜ kubectl get deploy -n ingress-nginx<br>NAME                       READY   UP-TO-DATE   AVAILABLE   AGE<br>ingress-nginx-controller   1/1     1            1           4m35s</pre><h4>Installing Argo Rollouts</h4><p>Argo Rollouts can be installed via its <a href="https://github.com/argoproj/argo-helm/tree/master/charts/argo-rollouts">Helm chart</a>.</p><pre>helm repo add argo <a href="https://argoproj.github.io/argo-helm">https://argoproj.github.io/argo-helm</a></pre><pre>helm install argo/argo-rollouts --name-template argo-rollouts --create-namespace -n argo-rollouts --set dashboard.enabled=true --version 2.32.5 --wait</pre><p>If all goes well, you should see two newly spawned Deployments with the READY state.</p><pre>➜ kubectl get deploy -n argo-rollouts<br>NAME                      READY   UP-TO-DATE   AVAILABLE   AGE<br>argo-rollouts             1/1     1            1           13m<br>argo-rollouts-dashboard   1/1     1            1           13m</pre><h3>Configuring Rollout Resources</h3><p>One of the limitations of using Ingress NGINX alone is that it requires the duplication of quite a few Kubernetes resources. If you remember in <a href="https://medium.com/@jhandguy/canary-deployment-in-kubernetes-part-1-simple-canary-deployment-using-ingress-nginx-f8f5da2b0f38">Part 1</a>, the <strong>Deployment</strong>, <strong>Service,</strong> and <strong>Ingress</strong> resources were almost identically cloned. <br>With Argo Rollouts, the duplication of Kubernetes resources is reduced to only one: the <strong>Service</strong> resource.</p><p>Another limitation of using plain Ingress NGINX is that the incremental rollout of the Canary Deployment (via canary-weight) has to be done manually by annotating the canary Ingress. <br>Argo Rollouts created its own <a href="https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/">Custom Resource</a> to remediate that problem: the <a href="https://argoproj.github.io/argo-rollouts/features/specification/"><strong>Rollout</strong></a> resource.</p><p>Alrighty then! Let’s get to it! 🧐</p><pre>helm install sample-app/helm-charts/argo-rollouts --name-template sample-app --create-namespace -n sample-app --wait</pre><p>If everything goes fine, you should eventually see one Rollout with the READY state.</p><pre>➜ kubectl get rollout sample-app -n sample-app<br>NAME         DESIRED   CURRENT   UP-TO-DATE   AVAILABLE<br>sample-app   1         1         1            1</pre><p>Alright, let’s take a look at what’s under all this!</p><pre>➜ ls -1 sample-app/helm-charts/argo-rollouts/templates<br>analysistemplate.yaml<br>canary<br>ingress.yaml<br>rollout.yaml<br>service.yaml<br>serviceaccount.yaml<br>servicemonitor.yaml</pre><pre>➜ ls -1 sample-app/helm-charts/argo-rollouts/templates/canary<br>service.yaml</pre><blockquote>Note: ignore the analysistemplate.yaml and the servicemonitor.yaml for now and focus on the rest of the resources.</blockquote><p>As you can see, there is only one resource that has been duplicated in the canary folder: the service.yaml.</p><p>Now you might wonder, where did the <strong>Deployment</strong> resource go?! 🤔</p><p>In Argo Rollouts, the <strong>Deployment</strong> resource has been replaced with the <a href="https://argoproj.github.io/argo-rollouts/features/specification/"><strong>Rollout</strong></a> resource. It is pretty much the same though, except it contains some extra specifications.</p><p>In fact, if you compare the deployment.yaml from <a href="https://medium.com/@jhandguy/canary-deployment-in-kubernetes-part-1-simple-canary-deployment-using-ingress-nginx-f8f5da2b0f38">Part 1</a> with the rollout.yaml, you will find that they are very similar, except for the strategy spec which is very unique to the <strong>Rollout</strong> resource.</p><pre>---<br>apiVersion: argoproj.io/v1alpha1<br>kind: Rollout<br>...<br>spec:<br>  ...<br>  <strong>strategy</strong>:<br>    canary:<br>      canaryService: {{ .Release.Name }}-canary<br>      stableService: {{ .Release.Name }}<br>      canaryMetadata:<br>        labels:<br>          deployment: canary<br>      stableMetadata:<br>        labels:<br>          deployment: stable<br>      trafficRouting:<br>        nginx:<br>          stableIngress: {{ .Release.Name }}<br>          additionalIngressAnnotations:<br>            canary-by-header: X-Canary<br>      steps:<br>        - setWeight: 25<br>        - pause: {}<br>        - setWeight: 50<br>        - pause:<br>            duration: 5m<br>        - setWeight: 75<br>        - pause:<br>            duration: 5m<br>  ...</pre><p>Now, let’s break down what is happening here:</p><ul><li>Argo Rollouts Controller needs to know which Service is the canaryService and which is the stableService so it can update them to select their corresponding pods on the fly;</li><li>In order for us to know which pods are being selected by which service, we can specify a label or annotation to be attached to the Pods metadata via canaryMetadata and stableMetadata;</li><li>Argo Rollouts Controller also needs to know which Ingress is the stableIngress so it can create automatically the canary Ingress from it;</li><li>Finally, in order to avoid us from manually editing the Ingress’s canary-weight annotation, the Argo Rollouts Controller will be able to do it for us automatically, thanks to the steps specification.</li></ul><p>A step can be one of three things:</p><ol><li>pause: this will put the Rollout in a <em>Paused</em> state. It can either be a timed pause (i.e. duration: 5m) or an indefinite pause, until it is manually resumed;</li><li>setWeight: this will increase the canary Ingress’s canary-weight annotation to the desired weight (i.e. setWeight: 25);</li><li>setCanaryScale: this will increase the number of replicas or pods which are selected by the canary Service (i.e. setCanaryScale: 4).</li></ol><h3>Automating Incremental Rollouts</h3><p>Awesome! Now that our Rollout is up and we understand how it works under the hood, let’s take it for a spin! 🚗</p><p>But first, let’s have a look at the dashboard!</p><pre>kubectl argo rollouts dashboard -n argo-rollouts &amp;</pre><p>If you now head to <a href="http://localhost:3100/rollout/sample-app">http://localhost:3100/rollout/sample-app</a>, you should see a shiny dashboard showing the state of the sample-app Rollout.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lHcxowFlaZndAGxcCIiI6w.png" /></figure><p>Cool! So far, we can observe only one <strong>revision</strong> labeled as stable, serving 100% of the traffic.</p><p>Now, let’s set a new image for the container and see what happens!</p><pre>kubectl argo rollouts set image sample-app sample-app=ghcr.io/jhandguy/canary-deployment/sample-app:latest -n sample-app</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Gs_wq9wFaMV4RC0mWoiEmw.png" /></figure><p>Aha! A new <strong>revision</strong> has been created and was labeled as canary. The weight for the canary Ingress has been set to 25% and the <strong>Rollout</strong> has now entered the <em>Paused</em> state.</p><p>Let’s verify this real quick!</p><pre>➜ kubectl get ingress -n sample-app<br>NAME                           CLASS    HOSTS        ADDRESS     ...   <br>sample-app                     &lt;none&gt;   sample.app   localhost   ...  <br><strong>sample-app-sample-app-canary</strong>   &lt;none&gt;   sample.app   localhost   ...</pre><pre>➜ kubectl describe ingress <strong>sample-app-sample-app-canary</strong> -n sample-app<br>...<br>Annotations:  kubernetes.io/ingress.class: nginx<br>              nginx.ingress.kubernetes.io/canary: true<br>              nginx.ingress.kubernetes.io/canary-by-header: X-Canary<br>              nginx.ingress.kubernetes.io/canary-weight: <strong>25</strong><br>...</pre><p>Indeed! An extra Ingress resource was automatically created, and given a canary-weight of 25.</p><p>Let’s see if it works!</p><pre>curl localhost/success -H &quot;Host: sample.app&quot;</pre><p>As you can see, out of 4 requests, one came back from the canary pod.</p><pre>➜ curl localhost/success -H &quot;Host: sample.app&quot;<br>{&quot;node&quot;:&quot;kind-control-plane&quot;,&quot;namespace&quot;:&quot;sample-app&quot;,&quot;pod&quot;:&quot;sample-app-6bf6dfdbb4-xmqdw&quot;,&quot;deployment&quot;:&quot;<strong>stable</strong>&quot;}</pre><pre>➜ curl localhost/success -H &quot;Host: sample.app&quot;<br>{&quot;node&quot;:&quot;kind-control-plane&quot;,&quot;namespace&quot;:&quot;sample-app&quot;,&quot;pod&quot;:&quot;sample-app-6bf6dfdbb4-xmqdw&quot;,&quot;deployment&quot;:&quot;<strong>stable</strong>&quot;}</pre><pre>➜ curl localhost/success -H &quot;Host: sample.app&quot;<br>{&quot;node&quot;:&quot;kind-control-plane&quot;,&quot;namespace&quot;:&quot;sample-app&quot;,&quot;pod&quot;:&quot;sample-app-6bf6dfdbb4-xmqdw&quot;,&quot;deployment&quot;:&quot;<strong>stable</strong>&quot;}</pre><pre>➜ curl localhost/success -H &quot;Host: sample.app&quot;<br>{&quot;node&quot;:&quot;kind-control-plane&quot;,&quot;namespace&quot;:&quot;sample-app&quot;,&quot;pod&quot;:&quot;sample-app-5c9fc8b7d4-wdrck&quot;,&quot;deployment&quot;:&quot;<strong>canary</strong>&quot;}</pre><p>And, the same way as in <a href="https://medium.com/@jhandguy/canary-deployment-in-kubernetes-part-1-simple-canary-deployment-using-ingress-nginx-f8f5da2b0f38">Part 1</a>, we can easily <strong>Smoke Test</strong> our change in isolation by leveraging the Ingress canary-by-header annotation.</p><pre>curl localhost/success -H &quot;Host: sample.app&quot; -H &quot;X-Canary: always&quot;</pre><p>Aha! We are now consistently hitting the canary pod.</p><pre>➜ curl localhost/success -H &quot;Host: sample.app&quot; -H &quot;X-Canary: always&quot;<br>{&quot;node&quot;:&quot;kind-control-plane&quot;,&quot;namespace&quot;:&quot;sample-app&quot;,&quot;pod&quot;:&quot;sample-app-5c9fc8b7d4-wdrck&quot;,&quot;deployment&quot;:&quot;<strong>canary</strong>&quot;}</pre><pre>➜ curl localhost/success -H &quot;Host: sample.app&quot; -H &quot;X-Canary: always&quot;<br>{&quot;node&quot;:&quot;kind-control-plane&quot;,&quot;namespace&quot;:&quot;sample-app&quot;,&quot;pod&quot;:&quot;sample-app-5c9fc8b7d4-wdrck&quot;,&quot;deployment&quot;:&quot;<strong>canary</strong>&quot;}</pre><pre>➜ curl localhost/success -H &quot;Host: sample.app&quot; -H &quot;X-Canary: always&quot;<br>{&quot;node&quot;:&quot;kind-control-plane&quot;,&quot;namespace&quot;:&quot;sample-app&quot;,&quot;pod&quot;:&quot;sample-app-5c9fc8b7d4-wdrck&quot;,&quot;deployment&quot;:&quot;<strong>canary</strong>&quot;}</pre><h4>Load Testing with k6</h4><p>Now we’ve made a few requests, let’s see how it behaves under load and whether or not it indeed guarantees a rough 25/75 traffic split!</p><p>For Load Testing, I really recommend <a href="https://k6.io/docs/">k6</a> from the Grafana Labs team. It is a dead-simple yet super powerful tool with very extensive documentation.</p><p>See for yourself!</p><pre>k6 run k6/script.js</pre><p>After about 1 minute, k6 should be done executing the load test and show you the results.</p><pre>          /\      |‾‾| /‾‾/   /‾‾/<br>     /\  /  \     |  |/  /   /  /<br>    /  \/    \    |     (   /   ‾‾\<br>   /          \   |  |\  \ |  (‾)  |<br>  / __________ \  |__| \__\ \_____/ .io</pre><pre>  execution: local<br>     script: k6/script.js<br>     output: -</pre><pre>  scenarios: (100.00%) 1 scenario, 20 max VUs, 1m30s max duration (incl. graceful stop):<br>           * load: Up to 20.00 iterations/s for 1m0s over 2 stages (maxVUs: 20, gracefulStop: 30s)</pre><pre>     ✓ status code is 200<br>     ✓ node is kind-control-plane<br>     ✓ namespace is sample-app<br>     ✓ pod is sample-app-*<br>     ✓ deployment is stable or canary</pre><pre>   ✓ checks.........................: 100.00% ✓ 3100      ✗ 0<br>     data_received..................: 157 kB  2.6 kB/s<br>     data_sent......................: 71 kB   1.2 kB/s<br>     http_req_blocked...............: avg=42.6µs   min=2µs    med=15µs   max=2.44ms  p(90)=23µs   p(95)=36.29µs<br>     http_req_connecting............: avg=20.73µs  min=0s     med=0s     max=1.49ms  p(90)=0s     p(95)=0s<br>   ✓ http_req_duration..............: avg=4.73ms   min=829µs  med=4.09ms max=36.81ms p(90)=7.89ms p(95)=10.14ms<br>       { expected_response:true }...: avg=4.73ms   min=829µs  med=4.09ms max=36.81ms p(90)=7.89ms p(95)=10.14ms<br>     http_req_failed................: 0.00%   ✓ 0         ✗ 620<br>     http_req_rate..................: 50.00%  ✓ 620       ✗ 620<br><strong>     ✓ { deployment:canary }........: 28.70%  ✓ 178       ✗ 442<br>     ✓ { deployment:stable }........: 71.29%  ✓ 442       ✗ 178</strong><br>     http_req_receiving.............: avg=129.17µs min=21µs   med=128µs  max=2.05ms  p(90)=186µs  p(95)=214.04µs<br>     http_req_sending...............: avg=65.53µs  min=11µs   med=62µs   max=1.96ms  p(90)=92.1µs p(95)=104.04µs<br>     http_req_tls_handshaking.......: avg=0s       min=0s     med=0s     max=0s      p(90)=0s     p(95)=0s<br>     http_req_waiting...............: avg=4.53ms   min=774µs  med=3.88ms max=36.6ms  p(90)=7.7ms  p(95)=9.73ms<br>     http_reqs......................: 620     10.333024/s<br>     iteration_duration.............: avg=5.4ms    min=1.07ms med=4.81ms max=38.46ms p(90)=8.76ms p(95)=10.76ms<br>     iterations.....................: 620     10.333024/s<br>     vus............................: 0       min=0       max=1<br>     vus_max........................: 20      min=20      max=20</pre><pre>running (1m00.0s), 00/20 VUs, 620 complete and 0 interrupted iterations<br>load ✓ [======================================] 00/20 VUs  1m0s  00.71 iters/s</pre><p>That sounds about right!<br>Out of 620 requests, 178 (28%) were served by the canary Deployment while 442 (72%) were served by the stable one. Pretty good!</p><h4>Promoting Rollouts</h4><p>Once feeling confident enough to proceed to the next step, we can now <strong>promote</strong> our Rollout, either from the dashboard via the <em>PROMOTE</em> button, or via the Kubectl Plugin.</p><pre>kubectl argo rollouts promote sample-app -n sample-app</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1BtRVYazk0jBdxeuZOfoVw.png" /></figure><blockquote>Note: while the rollout is ongoing, keep firing some curl requests or run the k6 Load Test script again to observe the change in traffic distribution!</blockquote><p>And if feeling so confident that we want to skip all the Rollout steps and get straight to 100% canary traffic, then we can <strong>fully promote</strong> the Rollout, also either from the dashboard via the <em>PROMOTE-FULL</em> button, or from the command line.</p><pre>kubectl argo rollouts promote sample-app -n sample-app --full</pre><p>But if on the contrary, we found an issue while in the middle of rolling out a change, then we can easily <strong>abort</strong> the Rollout and return to 100% stable traffic, either from the dashboard via the <em>ABORT</em> button, or from the CLI.</p><pre>kubectl argo rollouts abort sample-app -n sample-app</pre><p>Finally, once the Rollout is complete and if something were to go wrong, just like one would do with a standard Deployment, we can easily <strong>undo</strong> a Rollout and roll it back to the previous revision, also either from the dashboard via the <em>ROLLBACK</em> button, or via Kubectl.</p><pre>kubectl argo rollouts undo sample-app -n sample-app</pre><h3>Wrapping up</h3><p>That’s it! You can now stop and delete your Kind cluster.</p><pre>kind delete cluster</pre><p>To summarize, using Argo Rollouts we were able to:</p><ul><li>Improve our Canary Deployment setup, by leveraging the Rollout custom resource, without which duplicated Deployment and Ingress resources would have to be created;</li><li>Automate the Traffic Routing increments via Rollout steps, which would otherwise have to be done manually by editing the canary Ingress annotation directly.</li></ul><p>Was it worth it? Did that help you understand how to implement Canary Deployment in Kubernetes using Argo Rollouts?</p><p>If so:</p><ol><li>Let me know in the comments below! 👇</li><li>Don’t forget to hit that <a href="https://medium.com/@jhandguy">subscribe</a> button! ✅</li><li>Follow me on <a href="https://twitter.com/jhandguy">Twitter</a>, I’ll be happy to answer any of your questions and you’ll be the first ones to know when a new article comes out! 👌</li></ol><p>See you next week, for Part 3 of my series <strong>Canary Deployment in Kubernetes</strong>!</p><p>Bye-bye! 👋</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8a3550d5a434" width="1" height="1" alt=""><hr><p><a href="https://code.egym.com/canary-deployment-in-kubernetes-part-2-automated-canary-deployment-using-argo-rollouts-8a3550d5a434">Canary Deployment in Kubernetes (Part 2) — Automated Canary Deployment using Argo Rollouts</a> was originally published in <a href="https://code.egym.com">EGYM Software Development</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Canary Deployment in Kubernetes (Part 1) — Simple Canary Deployment using Ingress NGINX]]></title>
            <link>https://code.egym.com/canary-deployment-in-kubernetes-part-1-simple-canary-deployment-using-ingress-nginx-f8f5da2b0f38?source=rss-60d6fa56fda3------2</link>
            <guid isPermaLink="false">https://medium.com/p/f8f5da2b0f38</guid>
            <category><![CDATA[canary-deployments]]></category>
            <category><![CDATA[nginx]]></category>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[nginx-ingress]]></category>
            <category><![CDATA[nginx-ingress-controller]]></category>
            <dc:creator><![CDATA[Jean Mainguy]]></dc:creator>
            <pubDate>Tue, 18 Jan 2022 08:50:51 GMT</pubDate>
            <atom:updated>2023-12-03T10:43:55.606Z</atom:updated>
            <content:encoded><![CDATA[<h3>Canary Deployment in Kubernetes (Part 1) — Simple Canary Deployment using Ingress NGINX</h3><h4>Learn how to use Ingress NGINX to set up a Canary Deployment allowing isolated Smoke Testing and incremental rollout to minimize risk.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*JHvvEidOE6JTWt8m" /><figcaption>Photo by <a href="https://unsplash.com/@jelletaman?utm_source=medium&amp;utm_medium=referral">Jelle Taman</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><blockquote><strong>Pssst</strong>! I started my own <a href="https://jhandguy.github.io/"><strong>blog</strong></a>!<br>You can read this <a href="https://jhandguy.github.io/posts/simple-canary-deployment/"><strong>very same article</strong></a> over there too!<br>No paywall, no ad, no Javascript — no bullshit, just pure content.<br>See you there!</blockquote><p>Deploying to production in Kubernetes can be quite stressful. Even after meaningful and reliable automated tests have successfully passed, there is still room for things to go wrong and lead to a nasty incident when pressing the final button.</p><p>Thankfully, Kubernetes is made to be resilient to this kind of scenario, and rolling back is a no-brainer. But still, rolling back means that, at least for some time, <strong>all of the users</strong> were negatively impacted by the faulty change…</p><p>What if we could smoke test our change in production <strong>before</strong> it actually hits real users? What if we could roll out a change incrementally to <strong>some users</strong> instead of all of them at once? What if we could detect a faulty deployment and roll it back automatically?<br>Well, that, my friend, is what Canary Deployment is all about!</p><blockquote>Minimizing the impact on real users while deploying a risky change to production.</blockquote><p>🎬 Hi there, I’m Jean!</p><p>In this 3 parts series, we’re going to explore several ways to do Canary Deployment in Kubernetes, and the first one is…<br>🥁<br>… using <strong>Ingress NGINX</strong>! 🎊</p><h3>Requirements</h3><p>Before we start, make sure you have the following tools installed:</p><ul><li><a href="https://kind.sigs.k8s.io/docs/user/quick-start/#installation">Kind</a></li><li><a href="https://kubernetes.io/docs/tasks/tools/">Kubectl</a></li><li><a href="https://helm.sh/docs/intro/install/">Helm</a></li><li><a href="https://k6.io/docs/getting-started/installation/">K6</a></li></ul><blockquote>Note: for MacOS users or Linux users using Homebrew, simply run: brew install kind kubectl helm k6</blockquote><p>All set? Let’s go! 🏁</p><h3>Creating Kind Cluster</h3><p><a href="https://kind.sigs.k8s.io/docs/user/quick-start/#installation">Kind</a> is a tool for running local Kubernetes clusters using Docker container “nodes”. It was primarily designed for testing Kubernetes itself, but may be used for local development or CI.</p><p>I don’t expect you to have a demo project in handy, so <a href="https://github.com/jhandguy/canary-deployment">I built one</a> for you.</p><pre>git clone <a href="https://github.com/jhandguy/canary-deployment.git">https://github.com/jhandguy/canary-deployment.git</a></pre><pre>cd canary-deployment</pre><p>Alright, let’s spin up our Kind cluster! 🚀</p><pre>➜ kind create cluster --image kindest/node:v1.27.3 --config=kind/cluster.yaml</pre><pre>Creating cluster &quot;kind&quot; ...<br> ✓ Ensuring node image (kindest/node:v1.27.3) 🖼<br> ✓ Preparing nodes 📦<br> ✓ Writing configuration 📜<br> ✓ Starting control-plane 🕹️<br> ✓ Installing CNI 🔌<br> ✓ Installing StorageClass 💾</pre><pre>Set kubectl context to &quot;kind-kind&quot;<br>You can now use your cluster with:<br><br>kubectl cluster-info --context kind-kind</pre><pre>Have a question, bug, or feature request? Let us know! <a href="https://kind.sigs.k8s.io/#community">https://kind.sigs.k8s.io/#community</a> 🙂</pre><h3>Installing NGINX Ingress Controller</h3><p><a href="https://github.com/kubernetes/ingress-nginx">NGINX Ingress Controller</a> is one of the <a href="https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/#additional-controllers">many available Kubernetes Ingress Controllers</a>, which acts as a load balancer and satisfies routing rules specified in <a href="https://kubernetes.io/docs/concepts/services-networking/ingress/#what-is-ingress">Ingress</a> resources, using the <a href="https://nginx.org/en/">NGINX reverse proxy</a>.</p><p>NGINX Ingress Controller can be installed via its <a href="https://github.com/kubernetes/ingress-nginx/tree/main/charts/ingress-nginx">Helm chart</a>.</p><pre>helm repo add ingress-nginx <a href="https://kubernetes.github.io/ingress-nginx">https://kubernetes.github.io/ingress-nginx</a></pre><pre>helm install ingress-nginx/ingress-nginx --name-template ingress-nginx --create-namespace -n ingress-nginx --values kind/ingress-nginx-values.yaml --version 4.8.3 --wait</pre><p>Now, if everything goes according to plan, you should be able to see the <strong>ingress-nginx-controller</strong> Deployment running.</p><pre>➜ kubectl get deploy -n ingress-nginx<br>NAME                       READY   UP-TO-DATE   AVAILABLE   AGE<br>ingress-nginx-controller   1/1     1            1           4m35s</pre><h3>Configuring Ingress Canary Annotations</h3><p>Now that our NGINX Ingress Controller is up and running, let’s get into the thick of it, shall we? 🧐</p><pre>helm install sample-app/helm-charts/ingress-nginx --name-template sample-app --create-namespace -n sample-app --wait</pre><p>If everything goes fine, you should eventually see two Deployments with the READY state.</p><pre>➜ kubectl get deploy -n sample-app<br>NAME                READY   UP-TO-DATE   AVAILABLE   AGE<br>sample-app          1/1     1            1           100s<br>sample-app-canary   1/1     1            1           100s</pre><p>Alright, let’s take a look at what’s under all this!</p><pre>➜ ls -1 sample-app/helm-charts/ingress-nginx/templates<br>canary<br>deployment.yaml<br>ingress.yaml<br>service.yaml<br>serviceaccount.yaml</pre><pre>➜ ls -1 sample-app/helm-charts/ingress-nginx/templates/canary<br>deployment.yaml<br>ingress.yaml<br>service.yaml</pre><p>As you can see, most of the resources have been duplicated, except for the serviceaccount.yaml:</p><ul><li>The deployment.yaml and the canary/deployment.yaml are strictly the same, apart from the name and the image tag.</li><li>The service.yaml and the canary/service.yaml are also very much the same, except for the name and the label selector.</li></ul><p>This is actually for a simple reason: in the end, the stable and the canary deployments are meant to be very much alike, apart from the image built inside the container, exactly like for a <a href="https://www.redhat.com/en/topics/devops/what-is-blue-green-deployment">blue/green deployment</a>.</p><p>Now, the ingress.yaml and the canary/ingress.yaml are similar, but not very much the same though: you’ll notice some extra <a href="https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/">annotations</a> in the canary/ingress.yaml.</p><pre>---<br>apiVersion: networking.k8s.io/v1<br>kind: Ingress<br>metadata:<br>  ...<br>  annotations:<br>    ...<br>    <strong>nginx.ingress.kubernetes.io/canary: &quot;true&quot;<br>    nginx.ingress.kubernetes.io/canary-weight: &quot;{{ .Values.canary.weight }}&quot;<br>    nginx.ingress.kubernetes.io/canary-by-header: &quot;X-Canary&quot;</strong><br>...</pre><blockquote>This is where the magic happens! 🪄</blockquote><p>Let’s dive into each of these <a href="https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/">annotations</a> real quick:</p><ul><li><a href="https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#canary"><strong>nginx.ingress.kubernetes.io/canary</strong></a><strong><br></strong>When set to &quot;true&quot;, this will let the Ingress Controller know that this is the Ingress routing traffic to the canary Deployment.</li><li><a href="https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#canary"><strong>nginx.ingress.kubernetes.io/canary-weight</strong></a><br>When given a value, this will tell the Ingress Controller how to split traffic between the stable and the canary Deployments.<br>For instance, a weight of 50 would result in a 50/50 split.</li><li><a href="https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#canary"><strong>nginx.ingress.kubernetes.io/canary-by-header</strong></a><br>When given a key, this will allow us to force a request into hitting the canary or the stable deployment, no matter the canary-weight.<br>By using the header value always, the request will always land in the canary Deployment, and when using the value never, the request will always land in the stable one.</li></ul><p>Right now, the canary-weight is 0 and the canary-by-header is X-Canary. This means that normal requests will never land in the canary Deployment unless a header X-Canary: always is passed to the request.</p><pre>➜ kubectl describe ingress sample-app-canary -n sample-app<br>...<br>Annotations:  ...<br>              nginx.ingress.kubernetes.io/canary: true<br>              nginx.ingress.kubernetes.io/canary-by-header: X-Canary<br>              nginx.ingress.kubernetes.io/canary-weight: <strong>0</strong><br>...</pre><p>Let’s give it a try!</p><pre>curl localhost/success -H &quot;Host: sample.app&quot;</pre><p>As you can see, no matter how many times you run this command, the pod will always be the same. In fact, this is the stable pod.</p><pre>➜ curl localhost/success -H &quot;Host: sample.app&quot;<br>{&quot;node&quot;:&quot;kind-control-plane&quot;,&quot;namespace&quot;:&quot;sample-app&quot;,&quot;pod&quot;:&quot;sample-app-6bd9dc6d5d-jstn2&quot;,&quot;deployment&quot;:&quot;<strong>stable</strong>&quot;}</pre><pre>➜ curl localhost/success -H &quot;Host: sample.app&quot;<br>{&quot;node&quot;:&quot;kind-control-plane&quot;,&quot;namespace&quot;:&quot;sample-app&quot;,&quot;pod&quot;:&quot;sample-app-6bd9dc6d5d-jstn2&quot;,&quot;deployment&quot;:&quot;<strong>stable</strong>&quot;}</pre><pre>➜ curl localhost/success -H &quot;Host: sample.app&quot;<br>{&quot;node&quot;:&quot;kind-control-plane&quot;,&quot;namespace&quot;:&quot;sample-app&quot;,&quot;pod&quot;:&quot;sample-app-6bd9dc6d5d-jstn2&quot;,&quot;deployment&quot;:&quot;<strong>stable</strong>&quot;}</pre><p>This is due to the fact that the current canary-weight is 0 thus 100% of the traffic will normally go to the stable Deployment.</p><p>Now let’s try with the X-Canary: always header!</p><pre>curl localhost/success -H &quot;Host: sample.app&quot; -H &quot;X-Canary: always&quot;</pre><p>Aha! We are now consistently hitting the canary pod.</p><pre>➜ curl localhost/success -H &quot;Host: sample.app&quot; -H &quot;X-Canary: always&quot;<br>{&quot;node&quot;:&quot;kind-control-plane&quot;,&quot;namespace&quot;:&quot;sample-app&quot;,&quot;pod&quot;:&quot;sample-app-canary-b559b9d75-gfvxp&quot;,&quot;deployment&quot;:&quot;<strong>canary</strong>&quot;}</pre><pre>➜ curl localhost/success -H &quot;Host: sample.app&quot; -H &quot;X-Canary: always&quot;<br>{&quot;node&quot;:&quot;kind-control-plane&quot;,&quot;namespace&quot;:&quot;sample-app&quot;,&quot;pod&quot;:&quot;sample-app-canary-b559b9d75-gfvxp&quot;,&quot;deployment&quot;:&quot;<strong>canary</strong>&quot;}</pre><pre>➜ curl localhost/success -H &quot;Host: sample.app&quot; -H &quot;X-Canary: always&quot;<br>{&quot;node&quot;:&quot;kind-control-plane&quot;,&quot;namespace&quot;:&quot;sample-app&quot;,&quot;pod&quot;:&quot;sample-app-canary-b559b9d75-gfvxp&quot;,&quot;deployment&quot;:&quot;<strong>canary</strong>&quot;}</pre><p>Did you pay attention to what we just did? 😉<br>This has a name: <strong>Smoke Testing</strong>! 🔥</p><p>That’s right, we just smoke-tested a change, in production, while isolating it from the rest of the users as normal requests are routed to the stable Deployment thanks to the canary-weight. However, we bypassed that during Smoke Testing by utilizing the canary-by-header to force the request into hitting the canary Deployment.<br>Isn’t that awesome?! No more panic attacks when smoke tests fail on Canary, your users aren’t affected. 😌</p><h3>Rolling-out Canary Deployments Incrementally</h3><p>So we have solved one problem, yet one remains: How do we expose a change to our real users incrementally, to minimize the impact if something goes south?</p><p>Well, you’ve probably guessed it, canary-weight to the rescue! 🚀</p><p>We can simply slowly increase the canary-weight in order to let <strong>some</strong> users being routed to the canary Deployment. Let’s give it a try!</p><pre>helm upgrade sample-app sample-app/helm-charts/ingress-nginx -n sample-app --reuse-values --set canary.weight=50 --wait</pre><p>Let’s verify that our canary Ingress now has a canary-weight of 50!</p><pre>➜ kubectl describe ingress sample-app-canary -n sample-app<br>...<br>Annotations:  ...<br>              nginx.ingress.kubernetes.io/canary: true<br>              nginx.ingress.kubernetes.io/canary-by-header: X-Canary<br>              nginx.ingress.kubernetes.io/canary-weight: <strong>50</strong><br>...</pre><p>All good, let’s test it out!</p><pre>curl localhost/success -H &quot;Host: sample.app&quot;</pre><p>As you can see, traffic is now equally split between the stable and canary Deployments.</p><pre>➜ curl localhost/success -H &quot;Host: sample.app&quot;<br>{&quot;node&quot;:&quot;kind-control-plane&quot;,&quot;namespace&quot;:&quot;sample-app&quot;,&quot;pod&quot;:&quot;sample-app-6bd9dc6d5d-jstn2&quot;,&quot;deployment&quot;:&quot;<strong>stable</strong>&quot;}</pre><pre>➜ curl localhost/success -H &quot;Host: sample.app&quot;<br>{&quot;node&quot;:&quot;kind-control-plane&quot;,&quot;namespace&quot;:&quot;sample-app&quot;,&quot;pod&quot;:&quot;sample-app-canary-5754f4bbc7-lvthj&quot;,&quot;deployment&quot;:&quot;<strong>canary</strong>&quot;}</pre><pre>➜ curl localhost/success -H &quot;Host: sample.app&quot;<br>{&quot;node&quot;:&quot;kind-control-plane&quot;,&quot;namespace&quot;:&quot;sample-app&quot;,&quot;pod&quot;:&quot;sample-app-6bd9dc6d5d-jstn2&quot;,&quot;deployment&quot;:&quot;<strong>stable</strong>&quot;}</pre><pre>➜ curl localhost/success -H &quot;Host: sample.app&quot;<br>{&quot;node&quot;:&quot;kind-control-plane&quot;,&quot;namespace&quot;:&quot;sample-app&quot;,&quot;pod&quot;:&quot;sample-app-canary-5754f4bbc7-lvthj&quot;,&quot;deployment&quot;:&quot;<strong>canary</strong>&quot;}</pre><h4>Load Testing with k6</h4><p>Now we’ve made a few requests, let’s see how it behaves under load and whether or not it indeed guarantees about 50% traffic distribution!</p><p>For Load Testing, I really recommend <a href="https://k6.io/docs/">k6</a> from the Grafana Labs team. It is a dead-simple yet super powerful tool with very extensive documentation.</p><p>See for yourself!</p><pre>k6 run k6/script.js</pre><p>After about 1 minute, k6 should be done executing the load test and show you the results.</p><pre>          /\      |‾‾| /‾‾/   /‾‾/<br>     /\  /  \     |  |/  /   /  /<br>    /  \/    \    |     (   /   ‾‾\<br>   /          \   |  |\  \ |  (‾)  |<br>  / __________ \  |__| \__\ \_____/ .io</pre><pre>  execution: local<br>     script: k6/script.js<br>     output: -</pre><pre>  scenarios: (100.00%) 1 scenario, 20 max VUs, 1m30s max duration (incl. graceful stop):<br>           * load: Up to 20.00 iterations/s for 1m0s over 2 stages (maxVUs: 20, gracefulStop: 30s)</pre><pre>     ✓ status code is 200<br>     ✓ node is kind-control-plane<br>     ✓ namespace is sample-app<br>     ✓ pod is sample-app-*<br>     ✓ deployment is stable or canary</pre><pre>   ✓ checks.........................: 100.00% ✓ 3095      ✗ 0<br>     data_received..................: 160 kB  2.7 kB/s<br>     data_sent......................: 71 kB   1.2 kB/s<br>     http_req_blocked...............: avg=48.21µs  min=3µs    med=18µs   max=2.38ms  p(90)=23µs   p(95)=30.09µs<br>     http_req_connecting............: avg=24.56µs  min=0s     med=0s     max=1.48ms  p(90)=0s     p(95)=0s<br>   ✓ http_req_duration..............: avg=4.87ms   min=800µs  med=4.24ms max=34.52ms p(90)=7.85ms p(95)=9.88ms<br>       { expected_response:true }...: avg=4.87ms   min=800µs  med=4.24ms max=34.52ms p(90)=7.85ms p(95)=9.88ms<br>     http_req_failed................: 0.00%   ✓ 0         ✗ 619<br>     http_req_rate..................: 50.00%  ✓ 619       ✗ 619<br><strong>     ✓ { deployment:canary }........: 47.65%  ✓ 295       ✗ 324<br>     ✓ { deployment:stable }........: 52.34%  ✓ 324       ✗ 295</strong><br>     http_req_receiving.............: avg=132.17µs min=24µs   med=136µs  max=610µs   p(90)=179µs  p(95)=213.09µs<br>     http_req_sending...............: avg=70.69µs  min=15µs   med=72µs   max=736µs   p(90)=94.2µs p(95)=101µs<br>     http_req_tls_handshaking.......: avg=0s       min=0s     med=0s     max=0s      p(90)=0s     p(95)=0s<br>     http_req_waiting...............: avg=4.67ms   min=719µs  med=4.02ms max=34.2ms  p(90)=7.64ms p(95)=9.65ms<br>     http_reqs......................: 619     10.316511/s<br>     iteration_duration.............: avg=5.59ms   min=1.08ms med=5.02ms max=41.22ms p(90)=8.68ms p(95)=10.52ms<br>     iterations.....................: 619     10.316511/s<br>     vus............................: 0       min=0       max=0<br>     vus_max........................: 20      min=20      max=20</pre><pre>running (1m00.0s), 00/20 VUs, 619 complete and 0 interrupted iterations<br>load ✓ [======================================] 00/20 VUs  1m0s  00.71 iters/s</pre><p>That sounds about right!<br>Out of 619 requests, 295 (48%) were served by the canary Deployment while 324 (52%) were served by the stable one. Pretty good!</p><h3>Wrapping up</h3><p>That’s it! You can now stop and delete your Kind cluster.</p><pre>kind delete cluster</pre><p>To summarize, using Ingress NGINX we were able to:</p><ul><li>Smoke test our change in complete isolation from the rest of the users;</li><li>Incrementally expose our change to <strong>some</strong> users to minimize risk.</li></ul><p>Was it worth it? Did that help you understand how to implement Canary Deployment in Kubernetes using Ingress NGINX?</p><p>If so:</p><ol><li>Let me know in the comments below! 👇</li><li>Don’t forget to hit that <a href="https://medium.com/@jhandguy">subscribe</a> button! ✅</li><li>Follow me on <a href="https://twitter.com/jhandguy">Twitter</a>, I’ll be happy to answer any of your questions and you’ll be the first ones to know when a new article comes out! 👌</li></ol><p>See you next week, for Part 2 of my series <strong>Canary Deployment in Kubernetes</strong>!</p><p>Bye-bye! 👋</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f8f5da2b0f38" width="1" height="1" alt=""><hr><p><a href="https://code.egym.com/canary-deployment-in-kubernetes-part-1-simple-canary-deployment-using-ingress-nginx-f8f5da2b0f38">Canary Deployment in Kubernetes (Part 1) — Simple Canary Deployment using Ingress NGINX</a> was originally published in <a href="https://code.egym.com">EGYM Software Development</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A Path to CI / CD Nirvana in iOS]]></title>
            <link>https://code.egym.com/a-path-to-ci-cd-nirvana-in-ios-a631fcdffb43?source=rss-60d6fa56fda3------2</link>
            <guid isPermaLink="false">https://medium.com/p/a631fcdffb43</guid>
            <category><![CDATA[fastlane]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[ci-cd-pipeline]]></category>
            <category><![CDATA[ui-testing]]></category>
            <dc:creator><![CDATA[Jean Mainguy]]></dc:creator>
            <pubDate>Thu, 12 Sep 2019 09:04:26 GMT</pubDate>
            <atom:updated>2022-08-17T17:56:27.323Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*gDYaXyHx-8GxWA_b" /></figure><blockquote><strong>Pssst</strong>! I started my own <a href="https://jhandguy.github.io/"><strong>blog</strong></a>!<br>You can read this <a href="https://jhandguy.github.io/posts/path-to-cicd-nirvana-ios/"><strong>very same article</strong></a> over there too!<br>No paywall, no ad, no Javascript — no bullshit, just pure content.<br>See you there!</blockquote><p>For many companies, testing and releasing are still very blurry processes, which don’t seem to work as they should. Testing is mostly manual, slow and error-prone. Releasing is usually also manual and slow, making its frequency hard to maintain on a sprint-basis. Continuous Integration (CI) and Continuous Deployment (CD) are all about automating both testing and releasing, making it possible for a team to release every week or two. At EGYM, we have achieved just that and I am going to tell you all about it!</p><blockquote>Testing &amp; Automating is easy, but doing it at scale isn’t.</blockquote><p>🎬 Hi there, I’m Jean!</p><p>In this article, I want to share with you my experience in our Path to CI / CD Nirvana in iOS, at EGYM! 😃</p><h3>Continuous Integration (CI)</h3><p>CI is the process of ensuring that a new change added on top of previous ones won’t break what has been built so far, making developments smooth and flawless. 🏎</p><p>That pretty much means most of the time “Testing before Merging”. ✅</p><p>Do you remember the testing pyramid? Well, I won’t go through it in detail, but we have pretty much 3 layers, Unit Testing, Integration Testing and finally UI Testing. All of those are crucial for ensuring the quality of your app, but the one, in particular, I want to focus with you today is <strong>UI Testing</strong>. 📱</p><p>We all know it, UI Testing is a pain when it comes to stability and maintainability, but don’t you worry, I might just have what you are looking for! 🧐</p><h4>UI Testing in iOS - The Ultimate Guide</h4><p>That’s right, I made a complete guide for you to get started with stable, maintainable and scalable UI testing in iOS! 🎉</p><p><strong>🔌 </strong><a href="https://medium.com/@jhandguy/painless-ui-testing-in-ios-part-1-mocking-the-network-ffbd6ab4809a"><strong>Mocking the Network</strong></a></p><p>The first thing you need to take care of is <strong>Mocking the Network</strong>. You don’t want to let your network requests hit an actual network and there are mainly two reasons for that.</p><p><strong>Isolation</strong>. Of course, what if your backend friends break their test server? Well, your UI Tests will fail as well, even though nothing is wrong with the app 😱 So now you’ll say “Well let&#39;s deploy a dedicated environment for it then!”. And whose ownership will that be? iOS devs? Or Backend devs? Isn’t that a little bit over-engineering? 🤔 Mocking the Network allows you to avoid unnecessary headaches and “Just re-trigger it!” kind of workflows. 😆</p><p><strong>Monitoring</strong>. Well yes, if you mock or stub the network, that means you need to predict exactly how many calls are going to be made during your UI test and that’s extremely valuable because you might sometimes find out that your app is actually making unnecessary calls which are eating your user’s data! 😮</p><p>Overall, Mocking the Network gives you full control and helps to keep your UI tests stability to a very high standard! 💪</p><p>If you want to read more about <strong>Mocking the Network</strong>, go ahead and check out my dedicated <a href="https://medium.com/@jhandguy/painless-ui-testing-in-ios-part-1-mocking-the-network-ffbd6ab4809a"><strong>article</strong></a>! 👀</p><p><strong>🚦 </strong><a href="https://medium.com/@jhandguy/painless-ui-testing-in-ios-part-2-stubbing-the-navigation-c5984e728f7e"><strong>Stubbing the Navigation</strong></a></p><p>The second thing you want to address is <strong>Stubbing the Navigation</strong>. One major pain point with iOS UI Testing is that every test will always relaunch the app entirely. This means that you are forced to go through several screens manually before getting to the one you actually want to test. That breaks the isolation principle, making UI tests hard to debug. 😕</p><p>One way of solving this is to inject a stub that will route your navigation in the exact screen you want to end up on. Of course, that also means that your navigation logic must be centralised in a dedicated layer like the <a href="https://https:/medium.com/@jhandguy/lightweight-design-patterns-in-ios-part-3-coordinator-fbc011f7f5f5"><strong>Coordinator</strong></a> for instance! 🚦</p><p>That way, your UI Tests will suddenly become blazingly fast, but also more isolated, stable and easier to maintain! 🚀</p><p>If you want to read more about <strong>Stubbing the Navigation</strong>, go ahead and check out my dedicated <a href="https://medium.com/@jhandguy/painless-ui-testing-in-ios-part-2-stubbing-the-navigation-c5984e728f7e"><strong>article</strong></a>! 👀</p><p><strong>📺 </strong><a href="https://medium.com/@jhandguy/painless-ui-testing-in-ios-part-3-disabling-animations-175529533523"><strong>Disabling Animations</strong></a></p><p>The third action you might want to take is <strong>Disabling Animations</strong>. This one is a quick and easy win, simply disabling animations while UI testing will make your tests not only faster but more stable as animations won’t be a potential risk for your assertions. 👍</p><p>If you want to read more about <strong>Disabling Animations</strong>, go ahead and check out my dedicated <a href="https://medium.com/@jhandguy/painless-ui-testing-in-ios-part-3-disabling-animations-175529533523"><strong>article</strong></a>! 👀</p><p><strong>🔮 </strong><a href="https://medium.com/@jhandguy/ui-testing-in-ios-generating-accessibility-identifiers-using-reflection-6568dff3dbf4"><strong>Generating Accessibility Identifiers using Reflection</strong></a></p><p>Until now, Accessibility Identifiers have always been assigned manually. This is a pain and most importantly unnecessary noise in your code so you should avoid doing this. <strong>Generating Accessibility Identifiers using Reflection</strong> is a great way to get rid of all this manual work, but instead, let your code generate it for you at runtime! 🤯</p><p>If you want to read more about <strong>Generating Accessibility Identifiers using Reflection</strong>, go ahead and check out my dedicated <a href="https://medium.com/@jhandguy/ui-testing-in-ios-generating-accessibility-identifiers-using-reflection-6568dff3dbf4"><strong>article</strong></a>! 👀</p><p><strong>🤖 </strong><a href="https://medium.com/@jhandguy/ui-testing-in-ios-robot-pattern-6e0dfb72514d"><strong>Implementing the Robot Pattern</strong></a></p><p>Finally, UI Tests are not only meant to be verifying that your app still behaves as expected, but they also can serve as implicit documentation of how your app works, what the main features are, for your new hires who do not have a clue about the product yet! 😮</p><p><strong>Implementing the Robot Pattern</strong> will not only help you separate the <em>What</em> from the <em>How</em> of your UI Tests and make screen assertions reusable across UI Tests, but also provide a more human-readable way of specifying tests! 😎</p><p>If you want to read more about <strong>Implementing the Robot Pattern</strong>, go ahead and check out my dedicated <a href="https://medium.com/@jhandguy/ui-testing-in-ios-robot-pattern-6e0dfb72514d"><strong>article</strong></a>! 👀</p><h3>Continuous Deployment (CD)</h3><p>CD is the process of deploying a new version of the product, for every new change that is made, making deployments incremental meaning, less risky than releases sent-out in big chunks. This process should ideally be fully automated, just like CI. 🚀</p><h4>Fastlane Swift</h4><p>When it comes to automation for iOS we, at EGYM, use <a href="https://docs.fastlane.tools/getting-started/ios/fastlane-swift/"><strong>Fastlane Swift</strong></a><strong>.</strong> <br>It is <a href="https://docs.fastlane.tools/"><strong>Fastlane</strong></a> but in <strong>Swift</strong>! 😱🍾</p><p>The choice of Fastlane was simple, we must be able to trigger automated tasks remotely as well as locally. And using <strong>Fastlane Swift</strong>, you can democratise your CI / CD automation to the whole iOS team, as they don’t need to learn Ruby anymore: their extensive Swift knowledge will do! 👌</p><h4>Automate Everything</h4><p>Currently, we have a total of <strong>12 lanes</strong>:</p><p><strong>✅ Run Tests (Unit / Integration / UI)</strong></p><p>Obviously, running tests is the first process you might want to automate. 🤖<br>In Fastlane Swift, this is easy, simply call therunTests action fromFastfile.swift and you’re all set! 🚀</p><p>At EGYM, we run the tests on every Pull Request.</p><p><strong>📸 Capture Screenshots</strong></p><p>Now, the capture screenshots lane is pretty similar to the run tests one, simply call the action captureScreenshots fromFastfile.swift and if you want to be fancy, also call the frameScreenshots action to add the device’s frame around your screenshots! 🖼</p><p>At EGYM, we use this lane to capture, frame and share our localised screenshots with our designers so they can reuse them for marketing purposes, this saves them a lot of time and they’re really grateful for it! 🤗</p><p><strong>✈️ Upload To Testflight</strong></p><p>Of course, uploading to Testflight is also crucial to your development team. 👨‍✈<br>It requires a little more logic though, so hold on to your seats! 💺</p><p>Here are the actions you need to sequentially use:</p><ul><li>Get the latest Testflight build number usinglatestTestflightBuildNumber;</li><li>Increment the build number usingincrementBuildNumber;</li><li>Build the app usingbuildApp;</li><li>Upload a new build to Testflight usinguploadToTestflight;</li><li>Download.dSYMsusingdownloadDsyms;</li><li>Upload.dSYMsto Crashlytics usinguploadSymbolsToCrashlytics;</li></ul><p>At EGYM, we automatically submit a new Testflight build after every Git merge to ourdevelopbranch.</p><p><strong>📱 Upload To AppStore</strong></p><p>Now, uploading to AppStore is a whole different story. 😆</p><p>Overall, here are the steps we follow at EGYM:</p><ul><li>Generate Metadata from ourAppStore.stringslocalization file ;</li><li>Download Screenshots from a Google Cloud Storage bucket ;</li><li>Upload a new build to Testflight ;</li><li>Upload to AppStore the metadata, screenshots and link the Testflight build ;</li><li>Increment the version number (patch, minor or major) ;</li><li>Add, commit and push the changes (build increment) ;</li><li>Create Pull Requests ;</li><li>Track Deployment Frequency.</li></ul><p>At EGYM, our AppStore releases are triggered manually from Bitrise, but once triggered, everything else is automated, including sending for Apple Review and releasing using Phased Rollout! 🚀</p><p><strong>🐛 Upload To Crashlytics Beta</strong></p><p>Unfortunately, AppStoreConnect’s build processing can take quite some time (~20 minutes); therefore, we also like to use Crashlytics Beta to distribute our development builds. 👍</p><p>At EGYM, we automatically submit a new Crashlyitics Beta build on every Git push.</p><p><strong>📝 Sync Code Signing</strong></p><p>Syncing Code Signing Certificates and Profiles is a very trivial process using Fastlane, simply call the actionsyncCodeSigningfromFastfile.swiftand you are good to go! 🚗</p><p>At EGYM, we mostly use this lane for downloading development certificates.</p><p><strong>🛬 Download Translations</strong></p><p>Remembering the exact CLI parameters can be a pain sometimes, that is why we also have a lane for managing our Lokalise translations! 🔡</p><p>At EGYM, our Download Translations lane simply calls thelokaliseCLI under the hood.</p><p><strong>🛫 Upload Translations</strong></p><p>Similarly to Downloading Translations to Lokalise, we also have a lane for uploading them. Same way as the downloading lane, the upload one also uses thelokaliseCLI under the hood. 🔤</p><p><strong>🖨 Generate Metadata</strong></p><p><a href="https://docs.fastlane.tools/actions/deliver/#available-metadata-folder-options">Fastlane Metadata</a> is a folder/file representation of what information AppStore Connect expects from your app when releasing. 🚀</p><p>At EGYM, we’ve automated the generation of Metadata by creating a Swift script expecting a localisation file(AppStore.strings)as input! 🤖</p><p><strong>✨ Update Dependencies</strong></p><p>Updating dependencies automatically is very helpful, its main purpose is to let you know when a dependency has a new version, by creating a Pull Request when something has changed in any of your.lock or .resolvedfiles. 🔐</p><p>Simply create a lane that executes for instancebundle updateorcarthage updateand submits a Pull Request! 📝</p><p>At EGYM, we use it to update ourGemfile.lockandCartfile.resolvedfiles! 👌</p><p><strong>🖼 Update Screenshots</strong></p><p>As capturing screenshots can take quite some time, doing it as part of the release process might not be very wise. 🤔</p><p>Instead, it might be more efficient to upload the screenshots inside a Google Cloud Storage bucket and update them on a weekly / monthly basis! 💡</p><p>At EGYM, we currently host about 220 screenshots inside a dedicated Google Cloud Storage bucket, in 2 different languages for 3 different devices! 🤯</p><p><strong>🎫 Move Ticket to Ready for Acceptance Testing</strong></p><p>As I said earlier, we automate everything. And that includes moving tickets in our Sprint Dashboard! 😆</p><p>Whenever we send a build to Testflight after a merge, once the build processing has been completed, we move the corresponding ticket to “Ready for Acceptance Testing” and send a message on Slack to our QA testers! 🚀</p><p>I know, that’s babysitting at this point but well, our QA testers love it! 😍</p><h4>Fully Automated Release - The Holy Grail</h4><p>That’s right, we made it, the holy grail: a fully automated release! 👑</p><p>This was a long journey, but by combining the power of both Bitrise and Fastlane, we’ve managed to submit our app for AppStore release, create version bump Pull Requests and track our Deployment Frequency, with a simple click of a button! 😱</p><p>Here is the full sequence of actions we perform automatically:</p><ol><li><strong>Create branch “release-\(version)” (from develop)</strong></li><li><strong>Download AppStore.strings localisation file from Lokalise</strong></li><li><strong>Generate metadata from AppStore.strings localisation file</strong></li><li><strong>Download screenshots from Google Cloud Storage bucket</strong></li><li><strong>Upload new build to Testflight</strong></li><li><strong>Upload DSYM to Crashlytics</strong></li><li><strong>Release to AppStore with metadata, screenshots and Testflight build</strong></li><li><strong>Add Git tag “releases/\(version)”</strong></li><li><strong>Increment Version Number (patch, minor, major)</strong></li><li><strong>Commit and Push</strong></li><li><strong>Create Pull Requests (develop, master)</strong></li><li><strong>Track Deployment Frequency in Deployment Logbook</strong></li></ol><p>All we need to do is trigger our UploadToAppStore workflow on Bitrise from the branch develop and a few minutes later, we get a Slack message confirming that the release was successful, along with 2 freshly created Pull Requests, one for the actual release (master) and one for the Version Bump (develop)! 🚀</p><h3>Conclusion</h3><p>It’s been a long journey, but definitely worthwhile! 🎉<br>Our workflows work so well, that we are able to catch bugs early on in our development process, as well as keeping a high Deployment Frequency without compromising on our developments speed! ✈️</p><p>Worth a try? Would you like to follow our path to CI / CD Nirvana? If so:</p><ol><li>Let me know in the comments below how are you doing CI / CD in your company! 👇</li><li>Don’t forget to hit that <a href="https://medium.com/@jhandguy">subscribe</a> button to be notified of my future articles about Fastlane Swift! ✅</li><li>Follow me on <a href="https://twitter.com/jhandguy">Twitter</a>, I’ll be happy to answer any of your questions and you’ll be the first ones to know when a new article comes out! 👌</li></ol><p>Until next time, happy automating! 🤖</p><p>Bye-bye! 👋</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a631fcdffb43" width="1" height="1" alt=""><hr><p><a href="https://code.egym.com/a-path-to-ci-cd-nirvana-in-ios-a631fcdffb43">A Path to CI / CD Nirvana in iOS</a> was originally published in <a href="https://code.egym.com">EGYM Software Development</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[UI Testing in iOS - Robot Pattern]]></title>
            <link>https://code.egym.com/ui-testing-in-ios-robot-pattern-6e0dfb72514d?source=rss-60d6fa56fda3------2</link>
            <guid isPermaLink="false">https://medium.com/p/6e0dfb72514d</guid>
            <category><![CDATA[mobile-app-development]]></category>
            <category><![CDATA[ui-testing]]></category>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[robots]]></category>
            <dc:creator><![CDATA[Jean Mainguy]]></dc:creator>
            <pubDate>Fri, 03 May 2019 13:31:38 GMT</pubDate>
            <atom:updated>2022-06-05T09:42:06.229Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*lNNSqxzhRNJeEZSJ" /></figure><blockquote><strong>Pssst</strong>! I started my own <a href="https://jhandguy.github.io/"><strong>blog</strong></a>!<br>You can read this <a href="https://jhandguy.github.io/posts/robot-pattern-ios/"><strong>very same article</strong></a> over there too!<br>No paywall, no ad, no Javascript — no bullshit, just pure content.<br>See you there!</blockquote><p>Most of the time, UI Testing gets abandoned because it becomes harder to maintain over time and one reason for that is: <strong>readability</strong>. We also tend to forget that UI tests should not only serve the purpose of verification but provide meaningful documentation that everybody (including non-developers) can understand. Alright, let’s get right into it, fellas!</p><blockquote>What if we could write UI Tests in a human-readable language, that even a Product Owner could understand?</blockquote><p>🎬 Hi there, I’m Jean!</p><p>In this article, I am going to introduce a better way to write UI Tests using XCTest! 😃 <br><em>Note: third-party libraries such as Appium or Calabash won’t be part of this post.</em></p><h3>The usual way</h3><p>Usually, all UI Testing logic is bundled in one test function. This inevitably leads to this…</p><pre>func uglyUITest() {<br>    ...<br>    app.launch()<br>    let title = app.navigationBars.firstMatch.otherElements.firstMatch<br>    XCTAssert(title.isHittable)<br>    XCTAssertEqual(title.label, &quot;U.S. - Politics&quot;)<br>    let stackView = app.otherElements[&quot;StoryView.stackView&quot;]<br>    let multimediaImage = stackView.images[&quot;StoryView.multimediaImageView&quot;]<br>    XCTAssert(multimediaImage.isHittable)<br>    let titleLabel = stackView.staticTexts[&quot;StoryView.titleLabel&quot;]<br>    XCTAssertEqual(titleLabel.label, &quot;A Trump Endorsement Can Decide a Race. Here’s How to Get One.&quot;)<br>    let abstractLabel = stackView.staticTexts[&quot;StoryView.abstractLabel&quot;]<br>    XCTAssertEqual(abstractLabel.label, &quot;The president’s grip on G.O.P. primary voters is as strong as it has been since he seized the party’s nomination.&quot;)<br>    let bylineLabel = stackView.staticTexts[&quot;StoryView.bylineLabel&quot;]<br>    XCTAssertEqual(bylineLabel.label, &quot;By JONATHAN MARTIN and MAGGIE HABERMAN&quot;)<br>    snapshot(&quot;Story&quot;)<br>}</pre><p>I am sure you didn’t even finish reading that you already jumped to this line.<br>And to be fair, I’m not angry at you; this UI Test was fairly <strong>unreadable</strong>! 😵</p><p>And the main reason for that is:</p><blockquote>Common UI Tests are mixing the What and the How altogether, in one place.</blockquote><p>Well, that’s obviously a problem, as this isn’t following the <a href="https://clean-code-developer.de/die-grade/orangener-grad/#Separation_of_Concerns_SoC">Separation of Concern</a> <strong>Clean Code</strong> teaches us… 🤔</p><h3>Beep… Beep… Boop… 🤖</h3><p>That’s right! Introducing, the <strong>Robot Pattern</strong> by <a href="https://jakewharton.com/testing-robots/">Jake Wharton</a>! 👏<br>The idea Jake had for the Android community is to separate in a UI Test the <em>What</em> from the <em>How</em>. <br>And to achieve this, he introduced a new layer called: <strong>Robot</strong>.</p><p>Transcribed to iOS (Swift), our UI Test now looks like this…</p><pre>func prettyUITest() {<br>    ...<br>    StoryRobot(app)<br>        .start()<br>        .checkTitle(contains: &quot;U.S. - Politics&quot;)<br>        .checkStoryImage()<br>        .checkStoryTitle(contains: &quot;A Trump Endorsement Can Decide a Race. Here’s How to Get One.&quot;)<br>        .checkStoryAbstract(contains: &quot;The president’s grip on G.O.P. primary voters is as strong as it has been since he seized the party’s nomination.&quot;)<br>        .checkStoryByline(contains: &quot;By JONATHAN MARTIN and MAGGIE HABERMAN&quot;)<br>        .takeScreenshot(named: &quot;Story&quot;)<br>}</pre><p>How about that, didn’t it feel right to read the whole function this time? 😍<br>Exactly, in the UI Test itself, we are now left only with the <em>What</em> and the <em>How</em> is delegated to the <strong>Robot</strong>. Meaning: anybody can read and understand it! 👍</p><h3>The Robot in action!</h3><p>First and foremost, we are going to implement a base <strong>Robot</strong>. <br>This class will take care of setting up Fastlane’s Snapshot, launching the app and providing a nice panel of util functions, such as tapping on elements, making assertions and taking screenshots… 👌</p><pre>class Robot {<br><br>    private static let defaultTimeout: Double = 30<br><br>    var app: XCUIApplication<br><br>    lazy var navigationBar       = app.navigationBars.firstMatch<br>    lazy var navigationBarButton = navigationBar.buttons.firstMatch<br>    lazy var navigationBarTitle  = navigationBar.otherElements.firstMatch<br><br>    init(_ app: XCUIApplication) {<br>        self.app = app<br>        setupSnapshot(app)<br>    }<br><br>    @discardableResult<br>    func start(timeout: TimeInterval = Robot.defaultTimeout) -&gt; Self {<br>        app.launch()<br>        assert(app, [.exists], timeout: timeout)<br><br>        return self<br>    }<br><br>    @discardableResult<br>    func tap(_ element: XCUIElement, timeout: TimeInterval = Robot.defaultTimeout) -&gt; Self {<br>        assert(element, [.isHittable], timeout: timeout)<br>        element.tap()<br><br>        return self<br>    }<br><br>    @discardableResult<br>    func assert(_ element: XCUIElement, _ predicates: [Predicate], timeout: TimeInterval = Robot.defaultTimeout) -&gt; Self {<br>        let expectation = XCTNSPredicateExpectation(predicate: NSPredicate(format: predicates.map { $0.format }.joined(separator: &quot; AND &quot;)), object: element)<br>        guard XCTWaiter.wait(for: [expectation], timeout: timeout) == .completed else {<br>            XCTFail(&quot;[\(self)] Element \(element.description) did not fulfill expectation: \(predicates.map { $0.format })&quot;)<br>            return self<br>        }<br><br>        return self<br>    }<br><br>    @discardableResult<br>    func checkTitle(contains title: String, timeout: TimeInterval = Robot.defaultTimeout) -&gt; Self {<br>        assert(navigationBar, [.isHittable], timeout: timeout)<br>        assert(navigationBarTitle, [.contains(title)], timeout: timeout)<br><br>        return self<br>    }<br><br>    @discardableResult<br>    func takeScreenshot(named name: String, timeout: TimeInterval = Robot.defaultTimeout) -&gt; Self {<br>        snapshot(name, timeWaitingForIdle: timeout)<br><br>        return self<br>    }<br><br>    @discardableResult<br>    func back(timeout: TimeInterval = Robot.defaultTimeout) -&gt; Self {<br>        tap(navigationBarButton, timeout: timeout)<br><br>        return self<br>    }<br>}</pre><p>Simple, right?<br>Oh, by the way, here is the Predicate enum I am using for making assertions, you’ll find it quite handy! 😎</p><pre>enum Predicate {<br>    case contains(String), doesNotContain(String)<br>    case exists, doesNotExist<br>    case isHittable, isNotHittable<br><br>    var format: String {<br>        switch self {<br>        case .contains(let label):<br>            return &quot;label == &#39;\(label)&#39;&quot;<br>        case .doesNotContain(let label):<br>            return &quot;label != &#39;\(label)&#39;&quot;<br>        case .exists:<br>            return &quot;exists == true&quot;<br>        case .doesNotExist:<br>            return &quot;exists == false&quot;<br>        case .isHittable:<br>            return &quot;isHittable == true&quot;<br>        case .isNotHittable:<br>            return &quot;isHittable == false&quot;<br>        }<br>    }<br>}</pre><p>Alright! Now our Base <strong>Robot</strong> class is ready, let’s inherit from it in the <em>StoryRobot</em><strong> </strong>and see how it plays out! 🤖</p><pre>final class StoryRobot: Robot {<br><br>    private lazy var stackView       = app.otherElements[&quot;StoryView.stackView&quot;]<br>    private lazy var multimediaImage = stackView.images[&quot;StoryView.multimediaImageView&quot;]<br>    private lazy var titleLabel      = stackView.staticTexts[&quot;StoryView.titleLabel&quot;]<br>    private lazy var abstractLabel   = stackView.staticTexts[&quot;StoryView.abstractLabel&quot;]<br>    private lazy var bylineLabel     = stackView.staticTexts[&quot;StoryView.bylineLabel&quot;]<br><br>    @discardableResult<br>    func checkStoryImage() -&gt; Self {<br>        assert(multimediaImage, [.exists])<br><br>        return self<br>    }<br><br>    @discardableResult<br>    func checkStoryTitle(contains storyTitle: String) -&gt; Self {<br>        assert(titleLabel, [.isHittable, .contains(storyTitle)])<br><br>        return self<br>    }<br><br>    @discardableResult<br>    func checkStoryAbstract(contains storyAbstract: String) -&gt; Self {<br>        assert(abstractLabel, [.isHittable, .contains(storyAbstract)])<br><br>        return self<br>    }<br><br>    @discardableResult<br>    func checkStoryByline(contains storyByline: String) -&gt; Self {<br>        assert(bylineLabel, [.isHittable, .contains(storyByline)])<br><br>        return self<br>    }<br>}</pre><p>That’s it! We’ve just implemented the <em>How</em> of our UI Test! 🎊<br>What’s also really nice about <strong>Robots</strong>, is that they can cross-call themselves without breaking the continuity of our UI Tests! 🤯</p><p>For instance, let’s say we want to open Safari in the <em>StoryUITest</em>.<br>All we need to do is create a <em>SafariRobot</em> which will take care of UI Testing the browser and create a function inside the <em>StoryRobot</em> that will open Safari as well as return the <em>SafariRobot</em>! 😮</p><pre>lazy var urlButton = stackView.buttons[&quot;StoryView.urlButton&quot;]</pre><pre>@discardableResult<br>func openSafari() -&gt; SafariRobot {<br>    tap(urlButton)</pre><pre>    return SafariRobot(app)<br>}</pre><p>And as you can see now, the continuity of our UI Test isn’t disturbed…</p><pre>final class StoryUITest: XCTestCase {<br><br>    private lazy var app = XCUIApplication()<br><br>    func testStory() {<br>        StoryRobot(app)<br>            .start()<br>            .checkTitle(contains: &quot;U.S. - Politics&quot;)<br>            .checkStoryImage()<br>            .checkStoryTitle(contains: &quot;A Trump Endorsement Can Decide a Race. Here’s How to Get One.&quot;)<br>            .checkStoryAbstract(contains: &quot;The president’s grip on G.O.P. primary voters is as strong as it has been since he seized the party’s nomination.&quot;)<br>            .checkStoryByline(contains: &quot;By JONATHAN MARTIN and MAGGIE HABERMAN&quot;)<br>            .takeScreenshot(named: &quot;Story&quot;)<br>            .openSafari()<br>            .checkURL(contains: &quot;https://www.nytimes.com/2018/08/27/us/politics/trump-endorsements.html&quot;)<br>            .closeSafari()<br>    }<br>}</pre><p>Boom! 💥 <br>We just implemented the <strong>Robot Pattern</strong>! 🎉<br>UI Testing is now easy, elegant and readable! 💪</p><p>Worth a try? Did you regain faith in UI Testing? If so:</p><ol><li>Let me know in the comments below! 👇</li><li>Don’t forget to hit that <a href="https://medium.com/@jhandguy">subscribe</a> button! ✅</li><li>Follow me on <a href="https://twitter.com/jhandguy">Twitter</a>, I’ll be happy to answer any of your questions and you’ll be the first ones to know when a new article comes out! 👌</li></ol><p>Want to read more about UI Testing? <br>Check out <a href="https://medium.com/@jhandguy/ui-testing-in-ios-generating-accessibility-identifiers-using-reflection-6568dff3dbf4">UI Testing in iOS - Generating Accessibility Identifiers using Reflection</a>, as well as my 3 parts series:</p><ol><li><a href="https://medium.com/@jhandguy/painless-ui-testing-in-ios-part-1-mocking-the-network-ffbd6ab4809a">Painless UI Testing in iOS (Part 1) — Mocking the Network</a></li><li><a href="https://medium.com/@jhandguy/painless-ui-testing-in-ios-part-2-stubbing-the-navigation-c5984e728f7e">Painless UI Testing in iOS (Part 2) — Stubbing the Navigation</a></li><li><a href="https://medium.com/@jhandguy/painless-ui-testing-in-ios-part-3-disabling-animations-175529533523">Painless UI Testing in iOS (Part 3) — Disabling Animations</a></li></ol><p>If you’d like to see a <strong>real-life example of an app doing extensive UI Testing</strong>, don’t hesitate to check out my <a href="https://github.com/jhandguy/SwiftKotlination">Github</a> repo! 📦</p><p>Until next time, happy testing! 🤖</p><p>Bye-bye! 👋</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6e0dfb72514d" width="1" height="1" alt=""><hr><p><a href="https://code.egym.com/ui-testing-in-ios-robot-pattern-6e0dfb72514d">UI Testing in iOS - Robot Pattern</a> was originally published in <a href="https://code.egym.com">EGYM Software Development</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>