{"id":9655,"date":"2020-02-29T13:25:00","date_gmt":"2020-02-29T12:25:00","guid":{"rendered":"https:\/\/blog.mi.hdm-stuttgart.de\/?p=9655"},"modified":"2023-08-06T21:44:17","modified_gmt":"2023-08-06T19:44:17","slug":"image-editor-on-kubernetes-with-kompose-minikube-k3s-k3sup-and-helm-part-2","status":"publish","type":"post","link":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2020\/02\/29\/image-editor-on-kubernetes-with-kompose-minikube-k3s-k3sup-and-helm-part-2\/","title":{"rendered":"Kubernetes: from Zero to Hero with Kompose, Minikube, k3sup and Helm \u2014 Part 2: Hands-On"},"content":{"rendered":"<p>This is part two of our series on how we designed and implemented a scalable, highly-available and fault-tolerant microservice-based Image Editor. This part depicts how we went from a basic Docker Compose setup to running our application on our own \u00bbbare-metal\u00ab Kubernetes cluster.<\/p>\n<p><!--more--><\/p>\n<p><a href=\"\/index.php\/2020\/02\/28\/image-editor-on-kubernetes-with-kompose-minikube-k3s-k3sup-and-helm-part-1\/\" target=\"_blank\" rel=\"noopener noreferrer\" data-wplink-edit=\"true\">Part 1<\/a> of this series explains the design choices we&#8217;ve made to get here.<\/p>\n<p>The complete source code and configuration files along with setup instructions for our project can be <a href=\"https:\/\/gitlab.mi.hdm-stuttgart.de\/webapparch\" target=\"_blank\" rel=\"noopener noreferrer\">found in GitLab<\/a>.<\/p>\n<hr>\n<p>In this post we will dive into Kubernetes and <a href=\"https:\/\/helm.sh\/\" target=\"_blank\" rel=\"noopener noreferrer\">Helm<\/a> \u2014 <em>the<\/em> package manager for Kubernetes \u2014 and how we went from a basic microservice deployment with Docker Compose to a fully working and production-ready Kubernetes cluster with <em>K3s<\/em> and <em>k3sup<\/em>.<\/p>\n<p>Initially, while we were developing our various microservice components (see our <a href=\"\/index.php\/2020\/02\/28\/image-editor-on-kubernetes-with-kompose-minikube-k3s-k3sup-and-helm-part-1\/\" target=\"_blank\" rel=\"noopener noreferrer\">previous blog post<\/a>), we decided to package each service to a Docker container. This went quite smooth as we already had some experience in working with Docker. We ended up with 11 Dockerfiles, one for every of our microservices: <a href=\"https:\/\/gitlab.mi.hdm-stuttgart.de\/webapparch\/auth\/blob\/master\/Dockerfile\" target=\"_blank\" rel=\"noopener noreferrer\">auth manager<\/a>, <a href=\"https:\/\/gitlab.mi.hdm-stuttgart.de\/webapparch\/conversion-manager\/blob\/master\/Dockerfile\" target=\"_blank\" rel=\"noopener noreferrer\">conversion manager<\/a>, the two converters (<a href=\"https:\/\/gitlab.mi.hdm-stuttgart.de\/webapparch\/converter-deepdream\/blob\/master\/Dockerfile\" target=\"_blank\" rel=\"noopener noreferrer\">converter-deepdream<\/a> and <a href=\"https:\/\/gitlab.mi.hdm-stuttgart.de\/webapparch\/converter-basic\/blob\/master\/Dockerfile\" target=\"_blank\" rel=\"noopener noreferrer\">converter-basic<\/a>), <a href=\"https:\/\/gitlab.mi.hdm-stuttgart.de\/webapparch\/job-manager\/blob\/master\/Dockerfile\" target=\"_blank\" rel=\"noopener noreferrer\">job manager<\/a>, the <a href=\"https:\/\/gitlab.mi.hdm-stuttgart.de\/webapparch\/image-storage\/blob\/master\/Dockerfile\" target=\"_blank\" rel=\"noopener noreferrer\">image storage<\/a> and <a href=\"https:\/\/gitlab.mi.hdm-stuttgart.de\/webapparch\/storage-service\/blob\/master\/Dockerfile\" target=\"_blank\" rel=\"noopener noreferrer\">storage service<\/a>, <a href=\"https:\/\/gitlab.mi.hdm-stuttgart.de\/webapparch\/frontend\/blob\/master\/Dockerfile\" target=\"_blank\" rel=\"noopener noreferrer\">web frontend<\/a>, the <a href=\"https:\/\/gitlab.mi.hdm-stuttgart.de\/webapparch\/ingress-frontend\/blob\/master\/Dockerfile\" target=\"_blank\" rel=\"noopener noreferrer\">TLS terminating proxy<\/a>, the <a href=\"https:\/\/gitlab.mi.hdm-stuttgart.de\/webapparch\/ingress-middleware\/blob\/master\/Dockerfile\" target=\"_blank\" rel=\"noopener noreferrer\">request annotation proxy<\/a> and the <a href=\"https:\/\/gitlab.mi.hdm-stuttgart.de\/webapparch\/ingress-backend\/blob\/master\/Dockerfile\" target=\"_blank\" rel=\"noopener noreferrer\">API gateway<\/a>.<\/p>\n<p>A Dockerfile describes the steps required to build and package one or multiple apps into a Docker image. Docker images can be instantiated to a Docker container which then runs the application(s).<\/p>\n<pre style=\"font-size: 13px\"><code class=\"\" data-line=\"\">FROM golang:alpine AS builder\n\nWORKDIR \/auth\n\nRUN apk add --no-cache make\n\nCOPY .\/config\/ .\/config\/\nCOPY .\/server\/ .\/server\/\nCOPY .\/vendor\/ .\/vendor\/\nCOPY .\/go.mod .\/go.mod\nCOPY .\/go.sum .\/go.sum\nCOPY .\/main.go .\/main.go\nCOPY .\/Makefile .\/Makefile\n\nRUN make\n\n# ---\n\nFROM alpine:latest\n\nWORKDIR \/auth\n\nRUN addgroup -g 1000 -S auth &amp;&amp; \n&nbsp;&nbsp;&nbsp;&nbsp;adduser -u 1000 -S auth -G auth\n\nCOPY --from=builder \/auth\/bin\/server .\n\nUSER auth\n\nCMD [ &quot;\/auth\/server&quot; ]<\/code><\/pre>\n<p><i style=\"font-size: 13px\">(Exemplary illustration of a <code class=\"\" data-line=\"\">Dockerfile<\/code> leveraging <a href=\"https:\/\/docs.docker.com\/develop\/develop-images\/multistage-build\/\" target=\"_blank\" rel=\"noopener noreferrer\">multi-stage builds<\/a>, taken from <a href=\"https:\/\/gitlab.mi.hdm-stuttgart.de\/webapparch\/auth\/blob\/master\/Dockerfile\" target=\"_blank\" rel=\"noopener noreferrer\">auth\/Dockerfile<\/a>)<\/i><\/p>\n<p>To orchestrate our various Docker containers we decided to use Docker Compose, a tool which amongst other features helps in making some capabilities of Docker available to configure via a simple <code class=\"\" data-line=\"\">docker-compose.yml<\/code> file, for example to launch containers in a specified order, define their service dependencies, manage port mappings and implement a restart policy.<\/p>\n<pre style=\"font-size: 13px\"><code class=\"\" data-line=\"\">version: &quot;3&quot;\n\nservices:\n  auth:\n    build:\n      context: ${AUTH:-.}\n      dockerfile: Dockerfile\n    image: waa.edu.rndsh.it:5000\/auth:latest\n    restart: unless-stopped\n    ports:\n      - &quot;8443:8080\/tcp&quot;\n    env_file:\n      - ${AUTH_ENV:-.env}\n    cap_drop:\n      - ALL<\/code><\/pre>\n<p><i style=\"font-size: 13px\">(Exemplary illustration of a <code class=\"\" data-line=\"\">docker-compose.yml<\/code> file, taken from <a href=\"https:\/\/gitlab.mi.hdm-stuttgart.de\/webapparch\/auth\/blob\/master\/docker-compose.yml\" target=\"_blank\" rel=\"noopener noreferrer\">auth\/docker-compose.yml<\/a>)<\/i><\/p>\n<p>This was great for our development setup which allowed our developers to work independently of each other by running the entire application on their local development machine without any external dependencies.<\/p>\n<p>Once the backend of our application became usable with a web frontend, we decided it was time to make the app available on a public server so it could be used by our testers \u2014 finally it was time to explore Kubernetes!<\/p>\n<h1>Diving into Kubernetes<\/h1>\n<p>At first, Kubernetes was quite intimidating to us when we only knew a bare minimum of what it can be used for. It was a beast we needed to tear apart and explore the parts each on their own.<br>The official Kubernetes <a href=\"https:\/\/kubernetes.io\/docs\/home\/\" target=\"_blank\" rel=\"noopener noreferrer\">documentation<\/a> and <a href=\"https:\/\/kubernetes.io\/docs\/tutorials\/\" target=\"_blank\" rel=\"noopener noreferrer\">tutorials<\/a> already gave a good introduction to what Kubernetes does and doesn&#8217;t try to achieve.<\/p>\n<hr>\n<p>To deploy our application on Kubernetes, we first needed to move from our Docker Compose files to the equivalent <a href=\"https:\/\/kubernetes.io\/docs\/concepts\/cluster-administration\/manage-deployment\/\" target=\"_blank\" rel=\"noopener noreferrer\">Kubernetes Resources<\/a>.<\/p>\n<h2>Introducing Kompose, the official Docker Compose to Kubernetes converter<\/h2>\n<p>While exploring the repos of the <a href=\"https:\/\/github.com\/kubernetes\" target=\"_blank\" rel=\"noopener noreferrer\">Kubernetes GitHub organization<\/a>, we found a tool which helps in migrating existing Docker Compose files to Kubernetes: <a href=\"https:\/\/github.com\/kubernetes\/kompose\" target=\"_blank\" rel=\"noopener noreferrer\">Kompose<\/a>. The procedure was quite straightforward:<\/p>\n<pre style=\"font-size: 13px\"><code class=\"\" data-line=\"\">$ kompose convert --chart --controller=deployment -f docker-compose.yml<\/code><\/pre>\n<p>We ran this command for each of our 11 <code class=\"\" data-line=\"\">docker-compose.yml<\/code> files. Out came a total of around 30 Kubernetes resource files. Kompose did its job impressively well! The tool not only reliably converted the well-known Docker Compose directives <a href=\"https:\/\/docs.docker.com\/compose\/compose-file\/#image\" target=\"_blank\" rel=\"noopener noreferrer\"><code class=\"\" data-line=\"\">image<\/code><\/a>, <a href=\"https:\/\/docs.docker.com\/compose\/compose-file\/#ports\" target=\"_blank\" rel=\"noopener noreferrer\"><code class=\"\" data-line=\"\">ports<\/code><\/a> and <a href=\"https:\/\/docs.docker.com\/compose\/compose-file\/#restart\" target=\"_blank\" rel=\"noopener noreferrer\"><code class=\"\" data-line=\"\">restart<\/code><\/a>, but also converted less frequently used directives such as <a href=\"https:\/\/docs.docker.com\/compose\/compose-file\/#env_file\" target=\"_blank\" rel=\"noopener noreferrer\"><code class=\"\" data-line=\"\">env_file<\/code><\/a>, <a href=\"https:\/\/docs.docker.com\/compose\/compose-file\/#cap_add-cap_drop\" target=\"_blank\" rel=\"noopener noreferrer\"><code class=\"\" data-line=\"\">cap_drop<\/code><\/a> \/ <a href=\"https:\/\/docs.docker.com\/compose\/compose-file\/#cap_add-cap_drop\" target=\"_blank\" rel=\"noopener noreferrer\"><code class=\"\" data-line=\"\">cap_add<\/code><\/a> and even <a href=\"https:\/\/docs.docker.com\/compose\/compose-file\/#volumes\" target=\"_blank\" rel=\"noopener noreferrer\"><code class=\"\" data-line=\"\">volumes<\/code><\/a> to the equivalent Kubernetes syntax \/ resource type counterpart. It was super helpful and gave a headstart into becoming familiar with resource files \u2014 the first step into mastering the Kubernetes-Fu.<\/p>\n<p>Unfortunately, some changes to the resource files as produced by Kompose were still required. The version of Kompose we were using wasn&#8217;t completely compatible with the then-recent release of Kubernetes which we used inside Minikube. We also <a href=\"https:\/\/gitlab.mi.hdm-stuttgart.de\/webapparch\/auth\/commit\/7e4c3a43c0f25b24f83ce6c6349ee72047a1d0a9\" target=\"_blank\" rel=\"noopener noreferrer\">dropped<\/a> all references to <code class=\"\" data-line=\"\">io.kompose<\/code> \u2014 they were no longer of any to us since we were already experienced enough to manage the files on our own.<\/p>\n<p>We needed to add a <code class=\"\" data-line=\"\">imagePullSecrets<\/code> policy to every <code class=\"\" data-line=\"\">Deployment<\/code> resource and configure the referenced Docker Registry secret as we are pulling the Docker images from our own login-protected registry:<\/p>\n<pre style=\"font-size: 13px\"><code class=\"language-yaml\" data-line=\"\">imagePullSecrets:\n- name: regcred<\/code><\/pre>\n<p><i style=\"font-size: 13px\">(Excerpt of <a href=\"https:\/\/gitlab.mi.hdm-stuttgart.de\/webapparch\/auth\/blob\/master\/kubernetes\/auth\/templates\/deployment.yaml\" target=\"_blank\" rel=\"noopener noreferrer\"><code class=\"\" data-line=\"\">auth\/kubernetes\/auth\/templates\/deployment.yaml<\/code><\/a>. The two lines instruct Kubernetes to use the <code class=\"\" data-line=\"\">regcred<\/code> secret to authenticate to the Docker Registry before pulling the image.)<\/i><\/p>\n<pre style=\"font-size: 13px\"><code class=\"\" data-line=\"\">$ kubectl create secret docker-registry regcred \n&nbsp;&nbsp;--docker-server=waa.edu.rndsh.it:5000 \n&nbsp;&nbsp;--docker-username=$REGISTRY_USERNAME --docker-password=$REGISTRY_PASSWORD<\/code><\/pre>\n<p><i style=\"font-size: 13px\">(Configuring the <code class=\"\" data-line=\"\">regcred<\/code> secret via command line)<\/i><\/p>\n<p>Once the resource files were adjusted, it was time to test them \u2014 we needed a Kubernetes development cluster.<\/p>\n<h2>Setting up a private Kubernetes development cluster with Minikube<\/h2>\n<p>Minikube probably is <i>the<\/i> most widely used one-node Kubernetes cluster solution for development. And it became quite clear why: it was super easy to set up; With only a single command and a bit of time for the bootstrapping process we had a Kubernetes development cluster up and running on our local machines.<\/p>\n<pre style=\"font-size: 13px\"><code class=\"\" data-line=\"\"># Create and launch Minikube VM\n$ minikube start --memory=&#039;10240mb&#039; --cpus=8\n\ud83d\ude04 minikube v1.7.3 on Darwin\n\u2728 Automatically selected the virtualbox driver\n\ud83d\udcbf Downloading VM boot image ...\n\ud83d\udd25 Creating virtualbox VM (CPUs=8, Memory=10240MB, Disk=20000MB) ...\n\ud83d\udc33 Preparing Kubernetes v1.17.3 on Docker 19.03.6 ...\n\ud83d\udcbe Downloading kubeadm v1.17.3\n\ud83d\udcbe Downloading kubelet v1.17.3\n\ud83d\udcbe Downloading kubectl v1.17.3\n\ud83d\ude80 Launching Kubernetes ...\n\ud83c\udf1f Enabling addons: default-storageclass, storage-provisioner\n\u231b Waiting for cluster to come online ...\n\ud83c\udfc4 Done! kubectl is now configured to use &quot;minikube&quot;<\/code><\/pre>\n<p>Minikube even automatically merged its <code class=\"\" data-line=\"\">kubeconfig<\/code> to <code class=\"\" data-line=\"\">~\/.kube\/config<\/code> and switched the <code class=\"\" data-line=\"\">kubectl<\/code> context accordingly \u2014 we were ready to get started!<\/p>\n<pre style=\"font-size: 13px\"><code class=\"\" data-line=\"\"># Check cluster version\n$ kubectl version --short\nClient Version: v1.17.3\nServer Version: v1.17.3\n\n# \u2026 success!<\/code><\/pre>\n<p>As a first step, to check whether our Kubernetes resource files were producing the desired state in our cluster, we <code class=\"\" data-line=\"\">kubectl apply<\/code>&#8216;ed all of them as follows:<\/p>\n<pre style=\"font-size: 13px\"><code class=\"\" data-line=\"\">$ find . -name &#039;*.yaml&#039; -not -name &#039;Chart.yaml&#039; -not -name &#039;values*.yaml&#039; \n&nbsp;&nbsp;-exec cat {} ; -exec echo &#039;---&#039; ; | kubectl apply -f -\nconfigmap\/auth-env created\ndeployment.apps\/auth created\nservice\/auth created\ndeployment.apps\/conversion-manager created\nservice\/conversion-manager created\ndeployment.apps\/converter-basic created\nservice\/converter-basic created\nconfigmap\/converter-deepdream-env created\ndeployment.apps\/converter-deepdream created\nservice\/converter-deepdream created\ndeployment.apps\/frontend created\nservice\/frontend created\nconfigmap\/imagestorage-env created\ndeployment.apps\/imagestorage created\nservice\/imagestorage created\ndeployment.apps\/ingress-backend created\nservice\/ingress-backend created\ningress.networking.k8s.io\/ingress-frontend created\nconfigmap\/ingress-middleware-env created\ndeployment.apps\/ingress-middleware created\nservice\/ingress-middleware created\nconfigmap\/jobmanager-env created\ndeployment.apps\/jobmanager created\nservice\/jobmanager created\nconfigmap\/storageservice-env created\ndeployment.apps\/storageservice created\nservice\/storageservice created<\/code><\/pre>\n<p>We could see all our microservices spin up inside the cluster using <code class=\"\" data-line=\"\">kubectl get events --all-namespaces -w<\/code> \u2014 in real-time!<br>After a while, all deployments were <code class=\"\" data-line=\"\">READY<\/code> (verified with <code class=\"\" data-line=\"\">kubectl get all -o wide --all-namespaces<\/code>).<\/p>\n<p>To access the inside of the cluster from outside (our host machine), we needed to enable the Minikube <code class=\"\" data-line=\"\">ingress<\/code> addon via <code class=\"\" data-line=\"\">minikube addons enable ingress<\/code>. Minikube then made our application available to the world outside of the cluster.<\/p>\n<p><code class=\"\" data-line=\"\">minikube ip<\/code> shows the IP of the Minikube cluster, and <code class=\"\" data-line=\"\">curl<\/code>ing it returned our web frontend (\ud83c\udf89). To our surprise, the app<sup>0<\/sup> was already working fine \u2014 just as on our Docker Compose-managed cluster:<\/p>\n<pre style=\"font-size: 13px\"><code class=\"\" data-line=\"\">$ curl -Is $(minikube ip) | head -n1\nHTTP\/1.1 200 OK<\/code><\/pre>\n<p>It was time to extend the resource files with more awesome features.<\/p>\n<p><i style=\"font-size: 13px\"><sup>0<\/sup>: all services without additional dependencies such as a database<\/i><\/p>\n<h2>Making the Kubernetes application highly-available<\/h2>\n<p>While skimming through the whole Kubernetes <a href=\"https:\/\/kubernetes.io\/docs\/home\/\" target=\"_blank\" rel=\"noopener noreferrer\">documentation<\/a>, we compiled a list of interesting directives we wanted to implement for our resource files. Amongst them were the <a href=\"https:\/\/kubernetes.io\/docs\/tasks\/configure-pod-container\/configure-liveness-readiness-startup-probes\/\" target=\"_blank\" rel=\"noopener noreferrer\"><code class=\"\" data-line=\"\">readinessProbe<\/code><\/a> and <a href=\"https:\/\/kubernetes.io\/docs\/tasks\/configure-pod-container\/configure-liveness-readiness-startup-probes\/\" target=\"_blank\" rel=\"noopener noreferrer\"><code class=\"\" data-line=\"\">livenessProbe<\/code><\/a> directives for <code class=\"\" data-line=\"\">Deployment<\/code> resources.<\/p>\n<p>By default, when the most basic unit of Kubernetes \u2014 a pod \u2014 has launched, Kubernetes will instantly begin to route traffic to it, even if the application running inside the pod is not ready to handle any traffic yet. This would make requests reaching such an &#8220;unhealthly&#8221; pod return a <code class=\"\" data-line=\"\">502 Bad Gateway<\/code> response.<\/p>\n<p>That&#8217;s where <code class=\"\" data-line=\"\">readinessProbe<\/code> comes into play. It defines a check which needs to complete successfully before Kubernetes routes any traffic to the pod. For each of our deployments we created such a directive to instruct Kubernetes to either wait for a TCP port to become available or for a HTTP server to return a <code class=\"\" data-line=\"\">2xx<\/code> response at a specified path:<\/p>\n<pre style=\"font-size: 13px\"><code class=\"language-yaml\" data-line=\"\">readinessProbe:\n  httpGet:\n    path: \/\n    port: 8080\n    periodSeconds: 2<\/code><\/pre>\n<p><i style=\"font-size: 13px\">(Exemplary configuration of a <code class=\"\" data-line=\"\">readinessProbe<\/code> directive commanding Kubernetes to wait for a HTTP server on port 8080 of the pod to become available and answer a request to <code class=\"\" data-line=\"\">\/<\/code> with a <code class=\"\" data-line=\"\">2xx<\/code> response before beginning to route traffic to the pod. Taken from <a href=\"https:\/\/gitlab.mi.hdm-stuttgart.de\/webapparch\/frontend\/blob\/master\/kubernetes\/frontend\/templates\/deployment.yaml\" target=\"_blank\" rel=\"noopener noreferrer\">frontend\/kubernetes\/frontend\/templates\/deployment.yaml<\/a>)<\/i><\/p>\n<p>Similarly, <code class=\"\" data-line=\"\">livenessProbe<\/code>s are used to check if a pod is <em>still<\/em> running correctly. Once a liveness check fails for a specific number of times (the <code class=\"\" data-line=\"\">failureThreshold<\/code>, default: 3, <a href=\"https:\/\/kubernetes.io\/docs\/tasks\/configure-pod-container\/configure-liveness-readiness-startup-probes\/#configure-probes\" target=\"_blank\" rel=\"noopener noreferrer\">source<\/a>), Kubernetes automatically deletes (&#8220;restarts&#8221;) the pod and removes it from its internal service Load Balancer so traffic doesn&#8217;t reach it any longer.<br>Those two directives are configured in <a href=\"https:\/\/gitlab.mi.hdm-stuttgart.de\/webapparch\/frontend\/blob\/2f3e756e08eaaead73d159067324d4b39d479c30\/kubernetes\/frontend\/templates\/deployment.yaml#L39-48\" target=\"_blank\" rel=\"noopener noreferrer\">no more than 10 lines<\/a> \u2014 Kubernetes handles the rest of all the magic!<\/p>\n<p>Next, it was time to configure minimum and maximum hardware resource limits for our deployments. Setting those limits was super easy, too:<\/p>\n<pre style=\"font-size: 13px\"><code class=\"language-yaml\" data-line=\"\">resources:\n  requests:\n    memory: &quot;256Mi&quot;\n    cpu: &quot;0.4&quot;\n  limits:\n    memory: &quot;512Mi&quot;\n    cpu: &quot;0.8&quot;<\/code><\/pre>\n<p><i style=\"font-size: 13px\">(Exemplary configuration of a <code class=\"\" data-line=\"\">resources<\/code> directive, taken from <a href=\"https:\/\/gitlab.mi.hdm-stuttgart.de\/webapparch\/converter-deepdream\/blob\/e4593a5052133c9632a397df30299f684f8779a1\/kubernetes\/converter-deepdream\/templates\/deployment.yaml#L28-34\" target=\"_blank\" rel=\"noopener noreferrer\">converter-deepdream\/kubernetes\/converter-deepdream\/templates\/deployment.yaml<\/a>)<\/i><\/p>\n<p><code class=\"\" data-line=\"\">requests<\/code> specifies the minimum hardware resources required to run a pod, while <code class=\"\" data-line=\"\">limits<\/code> configures the maximum amount of hardware resources available to a pod. The values were initially either chosen arbitrarily or by looking at the output of <code class=\"\" data-line=\"\">kubectl top pods<\/code> which gave a rough estimate of how the values needed to be configured.<\/p>\n<p>The memory limit can be expressed in various units (megabyte, gigabyte, etc., see <a href=\"https:\/\/kubernetes.io\/docs\/concepts\/configuration\/manage-compute-resources-container\/#meaning-of-memory\" target=\"_blank\" rel=\"noopener noreferrer\">here<\/a>), while the definition of the <code class=\"\" data-line=\"\">cpu<\/code> unit is a bit more complex:<\/p>\n<p>Quoting the <a href=\"https:\/\/kubernetes.io\/docs\/concepts\/configuration\/manage-compute-resources-container\/#meaning-of-cpu\" target=\"_blank\" rel=\"noopener noreferrer\">documentation<\/a>, one &#8220;cpu&#8221;, in Kubernetes, is equivalent to:<br>&#8211; 1 AWS vCPU<br>&#8211; 1 GCP Core<br>&#8211; 1 Azure vCore<br>&#8211; 1 IBM vCPU<br>&#8211; 1 Hyperthread on a bare-metal Intel processor with Hyperthreading<\/p>\n<p>We played around a bit with various numbers and ended up configuring 0.1 \u2013 0.8 units of <code class=\"\" data-line=\"\">cpu<\/code> for our services.<\/p>\n<p>A scary discovery was that Kubernetes pods \u2014 just like Docker containers \u2014 are run with root privileges by default. We reconfigured our deployments accordingly to run the pods as non-root:<\/p>\n<pre style=\"font-size: 13px\"><code class=\"language-yaml\" data-line=\"\">securityContext:\n  capabilities:\n    drop:\n      - ALL\n  allowPrivilegeEscalation: false\n  runAsUser: 18443<\/code><\/pre>\n<p><i style=\"font-size: 13px\">(Excerpt of <a href=\"https:\/\/gitlab.mi.hdm-stuttgart.de\/webapparch\/auth\/blob\/master\/kubernetes\/auth\/templates\/deployment.yaml\" target=\"_blank\" rel=\"noopener noreferrer\"><code class=\"\" data-line=\"\">auth\/kubernetes\/auth\/templates\/deployment.yaml<\/code><\/a>, <code class=\"\" data-line=\"\">allowPrivilegeEscalation: false<\/code> and <code class=\"\" data-line=\"\">runAsUser: 18443<\/code> prevent running the pod with root privileges)<\/i><\/p>\n<p>As a last step, to make the number of pod replicas of a service scale up or down automatically based on their current load, we created a <a href=\"https:\/\/kubernetes.io\/docs\/tasks\/run-application\/horizontal-pod-autoscale\/\" target=\"_blank\" rel=\"noopener noreferrer\">HorizontalPodAutoscaler<\/a> resource for every microservice:<\/p>\n<pre style=\"font-size: 13px\"><code class=\"language-yaml\" data-line=\"\">apiVersion: autoscaling\/v1\nkind: HorizontalPodAutoscaler\nmetadata:\n  name: conversion-manager\nspec:\n  scaleTargetRef:\n    apiVersion: apps\/v1\n    kind: Deployment\n    name: conversion-manager\n  minReplicas: 2\n  maxReplicas: 8\n  targetCPUUtilizationPercentage: 50<\/code><\/pre>\n<p><i style=\"font-size: 13px\">(Exemplary illustration of a <code class=\"\" data-line=\"\">HorizontalPodAutoscaler<\/code> resource, taken from <a href=\"https:\/\/gitlab.mi.hdm-stuttgart.de\/webapparch\/conversion-manager\/blob\/12a556229319f01cf77e00255a6fb96b6b30cf02\/kubernetes\/conversion-manager\/templates\/hpa.yaml\" target=\"_blank\" rel=\"noopener noreferrer\"><code class=\"\" data-line=\"\">conversion-manager\/kubernetes\/conversion-manager\/templates\/hpa.yaml<\/code><\/a>)<\/i><\/p>\n<p>This basic file defines a minimum and maximum number of running replicas of a pod, and a CPU threshold (<code class=\"\" data-line=\"\">targetCPUUtilizationPercentage<\/code>) which tells Kubernetes when to launch a new, or terminate an existing pod instance.<\/p>\n<p>Now that the Kubernetes development cluster was running fine on a single node with Minikube, we needed a staging \/ production cluster with more than just one node.<\/p>\n<h2>Entering bare-metal Kubernetes cluster solutions<\/h2>\n<p>There are a number of solutions available to obtain access to a production-grade Kubernetes cluster. Probably <em>the<\/em> most basic way is to simply get a Managed Kubernetes cluster from Google Cloud&#8217;s Kubernetes Engine (<a href=\"https:\/\/cloud.google.com\/kubernetes-engine\/\" target=\"_blank\" rel=\"noopener noreferrer\">GKE<\/a>), Amazon AWS&#8217; Elastic Kubernetes Service (<a href=\"https:\/\/aws.amazon.com\/eks\/\" target=\"_blank\" rel=\"noopener noreferrer\">EKS<\/a>), Microsoft Azure&#8217;s Kubernetes Service (<a href=\"https:\/\/azure.microsoft.com\/en-us\/services\/kubernetes-service\/\" target=\"_blank\" rel=\"noopener noreferrer\">AKS<\/a>), <a href=\"https:\/\/www.ibm.com\/cloud\/container-service\/\" target=\"_blank\" rel=\"noopener noreferrer\">IBM Cloud<\/a>, <a href=\"https:\/\/www.digitalocean.com\/products\/kubernetes\/\" target=\"_blank\" rel=\"noopener noreferrer\">DigitalOcean<\/a> or one of the <a href=\"https:\/\/landscape.cncf.io\/category=special&amp;format=card-mode\" target=\"_blank\" rel=\"noopener noreferrer\">other countless providers<\/a>.<\/p>\n<p>Other solutions encompass:<\/p>\n<ul>\n<li>custom Kubernetes operating systems such as <a href=\"https:\/\/www.talos-systems.com\/\" target=\"_blank\" rel=\"noopener noreferrer\">Talos<\/a> or <a href=\"https:\/\/k3os.io\/\" target=\"_blank\" rel=\"noopener noreferrer\">k3OS<\/a><\/li>\n<li>setting up and managing a cluster with <a href=\"https:\/\/kubernetes.io\/docs\/setup\/production-environment\/tools\/kubeadm\/create-cluster-kubeadm\/\" target=\"_blank\" rel=\"noopener noreferrer\">kubeadm<\/a> \u2014 the official tool to set up Kubernetes on bare-metal<\/li>\n<li>other solutions previously <a href=\"\/index.php\/2019\/03\/15\/kubernetesk8s-everywhere-but-how\/\" target=\"_blank\" rel=\"noopener noreferrer\">talked about in another blog post<\/a><\/li>\n<\/ul>\n<p>\u2026 all of which, unfortunately, were not of an option to us. Installation of custom OS images (for <code class=\"\" data-line=\"\">Talos<\/code> and <code class=\"\" data-line=\"\">k3OS<\/code>) were not allowed on the three machines we were given access to by our university, <code class=\"\" data-line=\"\">kubeadm<\/code> was too complex as we&#8217;d need to explore Kubernetes in a depth more than desired, and managed Kubernetes cluster solutions either required a credit card, or the student accounts were too constrained for our application in regards to hardware resources. We tried hosting our cluster in an AKS \u2014 just to see how easy it would be. It was indeed quite straightforward to deploy our cluster there (using a mix of both Azure&#8217;s web UI and the official <code class=\"\" data-line=\"\">az<\/code> CLI tool), but the hardware limitation of just four <em>Azure vCores<\/em> (limit for student accounts) quickly spoiled any fun; We couldn&#8217;t even create replicas of all our services.<\/p>\n<p>In need of another solution we stumbled upon <a href=\"https:\/\/k3s.io\/\" target=\"_blank\" rel=\"noopener noreferrer\">K3s<\/a>, a lightweight, certified and API-compliant Kubernetes distribution. K3s is actively developed by <a href=\"https:\/\/rancher.com\/\" target=\"_blank\" rel=\"noopener noreferrer\">Rancher<\/a>, a company dedicated to making Kubernetes easier to use. Other software from Rancher was introduced on this blog <a href=\"\/?s=rancher\" target=\"_blank\" rel=\"noopener noreferrer\">before (click)<\/a>.<\/p>\n<h2>K3s and k3sup<\/h2>\n<p><a href=\"https:\/\/k3s.io\/\" target=\"_blank\" rel=\"noopener noreferrer\">K3s<\/a> promises to spin up a production-ready Kubernetes cluster in around 30 seconds. This looked really promising, given that setting up, maintaining and grasping the inner workings of a cluster created by <code class=\"\" data-line=\"\">kubeadm<\/code> would probably take us countless of hours or even days.<br>K3s is packaged as a single, static binary with no more than 40MB in size which can even be run on SoCs like the Raspberry Pi.<\/p>\n<p>If this wasn&#8217;t already great enough, we found a wrapper around K3s aptly named <a href=\"https:\/\/github.com\/alexellis\/k3sup\" target=\"_blank\" rel=\"noopener noreferrer\"><code class=\"\" data-line=\"\">k3sup<\/code><\/a> which made installing K3s to multiple nodes <em>even<\/em> simpler.<\/p>\n<p>We wrote a small shell script and had a 3-node Kubernetes cluster up and running \u2014 on our own machines \u2014 in just 20 lines!<\/p>\n<pre style=\"font-size: 13px\"><code class=\"\" data-line=\"\">#!\/usr\/bin\/env bash\n\nset -eufo pipefail\n\nMASTER=$(dig +short -t a sem01.mi.hdm-stuttgart.de)\nSLAVES=(\n&nbsp;&nbsp;$(dig +short -t a sem02.mi.hdm-stuttgart.de)\n&nbsp;&nbsp;$(dig +short -t a sem03.mi.hdm-stuttgart.de)\n)\n\n# Set up \u201cMaster\u201d node\nk3sup install --ip $MASTER --sudo=false --cluster \n&nbsp;&nbsp;--context k3s-sem --local-path ~\/.kube\/config --merge \n&nbsp;&nbsp;--k3s-extra-args &#039;--no-deploy traefik&#039;\n# Configure firewall to allow access with kubectl\nssh root@$MASTER -- ufw allow 6443\/tcp\n\n# Set up \u201cSlave\u201d nodes\nfor SLAVE in ${SLAVES[*]}; do\n&nbsp;&nbsp;k3sup join --ip $SLAVE --server-ip $MASTER --sudo=false\ndone\n<\/code><\/pre>\n<p>To distribute load across the three cluster nodes (<code class=\"\" data-line=\"\">sem01<\/code>, <code class=\"\" data-line=\"\">sem02<\/code>, <code class=\"\" data-line=\"\">sem03<\/code>), we installed <code class=\"\" data-line=\"\">sem00<\/code> \u2014 a load balancer running nginx as a reverse proxy \u2014 in front of them:<\/p>\n<p><a href=\"\/wp-content\/uploads\/2020\/02\/image-editor-k3s-cluster.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img decoding=\"async\" src=\"\/wp-content\/uploads\/2020\/02\/image-editor-k3s-cluster.png\"><\/a><\/p>\n<p>The <code class=\"\" data-line=\"\">k3sup install<\/code> command run above created a new context in <code class=\"\" data-line=\"\">~\/.kube\/config<\/code> which we switched to with <code class=\"\" data-line=\"\">kubectl config use-context k3s-sem<\/code>.<br>A <code class=\"\" data-line=\"\">kubectl version --short<\/code> confirmed that the cluster was running:<\/p>\n<pre style=\"font-size: 13px\"><code class=\"\" data-line=\"\">$ kubectl version --short\nClient Version: v1.17.3\nServer Version: v1.17.2+k3s1<\/code><\/pre>\n<p>Just as before with Minikube, we applied all resources files as a quick check to see whether the cluster was handling the resource files as expected. It was<sup>0<\/sup> and autoscaling was working perfectly fine too after producing some load on the cluster.<\/p>\n<p><i style=\"font-size: 13px\"><sup>0<\/sup>: after deploying an <code class=\"\" data-line=\"\">nginx<\/code> ingress controller using <code class=\"\" data-line=\"\">helm install nginx stable\/nginx-ingress --namespace kube-system --set rbac.create=true --set serviceAccount.create=true --set podSecurityPolicy.enabled=true --set controller.replicaCount=3 --set controller.autoscaling.enabled=true --set controller.autoscaling.minReplicas=3 --set controller.autoscaling.maxReplicas=9<\/code>. We decided against using <code class=\"\" data-line=\"\">traefik<\/code> as the default ingress controller installed by <em>K3s<\/em>.<\/i><\/p>\n<pre style=\"font-size: 13px\"><code class=\"\" data-line=\"\">$ curl -Is sem0{0,1,2,3}.mi.hdm-stuttgart.de 2&gt;\/dev\/null | grep HTTP\/\nHTTP\/1.1 200 OK\nHTTP\/1.1 200 OK\nHTTP\/1.1 200 OK\nHTTP\/1.1 200 OK\n\n# All nodes and the load balancer are working fine! \ud83d\udc33<\/code><\/pre>\n<h2>Helm: Managing multiple application deployments the easy way<\/h2>\n<p>We now each had a Minikube development cluster running on our local machines and a single, public production cluster running on the three <code class=\"\" data-line=\"\">sem0x<\/code> servers.<br>Maintenance tasks such as microservice upgrades would always need to be performed on both clusters the same way, so that the development and production clusters would match. After a while this became cumbersome, especially as both clusters had slightly different configurations applied \u2014 different <code class=\"\" data-line=\"\">ConfigMaps<\/code>, different autoscaling parameters and different hardware resource constraints.<\/p>\n<p>This is where <a href=\"https:\/\/helm.sh\/\" target=\"_blank\" rel=\"noopener noreferrer\">Helm<\/a> \u2014 <em>the<\/em> package manager for Kubernetes comes in handy. It extends Kubernetes resource files by the <a href=\"https:\/\/golang.org\/pkg\/text\/template\/\" target=\"_blank\" rel=\"noopener noreferrer\">Go template syntax<\/a>: hardcoded values in those resource files can be replaced by a placeholder value in <a href=\"https:\/\/golang.org\/pkg\/text\/template\/\" target=\"_blank\" rel=\"noopener noreferrer\">double-curly-brace<\/a> syntax. This placeholder will eventually be replaced by Helm&#8217;s template renderer with the appropriate values as specified in a <a href=\"https:\/\/gitlab.mi.hdm-stuttgart.de\/webapparch\/infra\/-\/blob\/master\/kubernetes\/values.yaml\" target=\"_blank\" rel=\"noopener noreferrer\"><code class=\"\" data-line=\"\">values.yaml<\/code><\/a> configuration file. Upon rendering, Helm also installs the rendered resource file to the Kubernetes cluster.<\/p>\n<p><a href=\"\/wp-content\/uploads\/2020\/02\/image-editor-helm-template.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img decoding=\"async\" src=\"\/wp-content\/uploads\/2020\/02\/image-editor-helm-template.png\"><\/a><\/p>\n<p>Helm makes it easy to upgrade or downgrade a specific service (= package = chart) to a different software release.<br>And the best: most of the work was already done for us by Kompose (see above), which created a <code class=\"\" data-line=\"\">Chart.yaml<\/code> file (as required for every Helm package \/ chart) and set up the <a href=\"https:\/\/helm.sh\/docs\/topics\/charts\/#the-chart-file-structure\" target=\"_blank\" rel=\"noopener noreferrer\">correct directory structure<\/a> for it.<\/p>\n<p>The <code class=\"\" data-line=\"\">Chart.yaml<\/code> file, amongst <a href=\"https:\/\/helm.sh\/docs\/topics\/charts\/#the-chart-yaml-file\" target=\"_blank\" rel=\"noopener noreferrer\">other directives<\/a>, must contain the name of the package name, the version of the packaged software, the version of the package itself, an optional package description and the package&#8217;s dependencies:<\/p>\n<pre style=\"font-size: 13px\"><code class=\"language-yaml\" data-line=\"\">apiVersion: v2\nname: storageservice\ndescription: Storage Service\nversion: 0.0.1\nappVersion: 0.0.1\ndependencies:\n- name: mongodb\n  version: &quot;&gt;= 7.7.0&quot;\n  repository: &quot;https:\/\/kubernetes-charts.storage.googleapis.com\/&quot;\n- name: imagestorage\n  version: &quot;*&quot;\n  repository: &quot;file:\/\/..\/..\/..\/image-storage\/kubernetes\/imagestorage&quot;<\/code><\/pre>\n<p><i style=\"font-size: 13px\">(Exemplary illustration of a Helm Chart, taken from <a href=\"https:\/\/gitlab.mi.hdm-stuttgart.de\/webapparch\/storage-service\/blob\/master\/kubernetes\/storageservice\/Chart.yaml\" target=\"_blank\" rel=\"noopener noreferrer\"><code class=\"\" data-line=\"\">storageservice\/kubernetes\/storageservice\/Chart.yaml<\/code><\/a>)<\/i><\/p>\n<p>Once the values of the resource files which we wanted to be configurable were templated, installing the entire application on our cluster became a no-brainer:<\/p>\n<pre style=\"font-size: 13px\"><code class=\"\" data-line=\"\"># Build Helm Charts (dependencies)\n$ helm dep build\n\n# View Helm&#039;s rendered resource files\n$ helm install --dry-run -f values.yaml waa .\/\n\n# Install Helm Charts\n$ helm install -f values.yaml waa .\/\n<\/code><\/pre>\n<h3>Noteworthy Helm packages<\/h3>\n<p>A lot of software packages are already available as a Helm chart. The <a href=\"https:\/\/hub.helm.sh\/\" target=\"_blank\" rel=\"noopener noreferrer\">Helm Hub<\/a> lists just a few of them.<br>We played around with some packages, all of them deployable to the cluster with a simple <code class=\"\" data-line=\"\">helm install<\/code>:<\/p>\n<ul>\n<li><a href=\"https:\/\/hub.helm.sh\/charts\/ibm-charts\/ibm-istio\" target=\"_blank\" rel=\"noopener noreferrer\">Istio<\/a>, a service mesh that connects, monitors and secures pods\/containers<\/li>\n<li><a href=\"https:\/\/hub.helm.sh\/charts\/stable\/chaoskube\" target=\"_blank\" rel=\"noopener noreferrer\">chaoskube<\/a> \u2014 which we&#8217;ll demonstrate later on \u2014 used for fault injection<\/li>\n<li><a href=\"https:\/\/hub.helm.sh\/charts\/jetstack\/cert-manager\" target=\"_blank\" rel=\"noopener noreferrer\">cert-manager<\/a>, used for automatic certificate management with issuing sources (CAs) such as Let&#8217;s Encrypt<\/li>\n<li><a href=\"https:\/\/github.com\/octarinesec\/kube-scan\" target=\"_blank\" rel=\"noopener noreferrer\">Kube-Scan<\/a>, a risk assessment tool<\/li>\n<li><a href=\"https:\/\/hub.helm.sh\/charts\/stable\/mongodb\" target=\"_blank\" rel=\"noopener noreferrer\">MongoDB<\/a> and <a href=\"https:\/\/hub.helm.sh\/charts\/stable\/postgresql\" target=\"_blank\" rel=\"noopener noreferrer\">PostgreSQL<\/a>, used to create replicated databases<\/li>\n<li><a href=\"https:\/\/hub.helm.sh\/charts\/stable\/redis\" target=\"_blank\" rel=\"noopener noreferrer\">Redis<\/a>, to deploy a replicated key-value store<\/li>\n<\/ul>\n<h2>Fault injection<\/h2>\n<p>Now that we had a supposedly production-grade cluster running (which we didn&#8217;t really believe), we tried to break it by manually and automatically injecting faults.<\/p>\n<p>The first attempt (which already made the cluster go <em>kaput<\/em>) was to simply shutdown \/ reboot one of the three <code class=\"\" data-line=\"\">sem0x<\/code> nodes; the cluster stopped responding to some request for more than half a minute. A complete node failure is probably the worst of what could happen since it makes all pods on the node unavailable. It turned out that K3s set timeouts too generous for our use case (for example <a href=\"https:\/\/github.com\/rancher\/k3s\/blob\/v1.17.2%2Bk3s1\/pkg\/daemons\/control\/server.go#L794\" target=\"_blank\" rel=\"noopener noreferrer\">here<\/a>).<br>Drastically reducing the timeouts as explained <a href=\"https:\/\/stackoverflow.com\/q\/53641252\" target=\"_blank\" rel=\"noopener noreferrer\">here<\/a>, <a href=\"https:\/\/fatalfailure.wordpress.com\/2016\/06\/10\/improving-kubernetes-reliability-quicker-detection-of-a-node-down\/\" target=\"_blank\" rel=\"noopener noreferrer\">here<\/a> and <a href=\"https:\/\/github.com\/Sheldonwl\/rpi-cluster-k3s\/blob\/master\/docs\/setup-k3s-server.md\" target=\"_blank\" rel=\"noopener noreferrer\">here<\/a> \u2014 in some cases from 60 seconds (!) to only 4 \u2014 was enough to reduce the request stall in case of a complete node failure; Requests would no longer hang for more than 30 seconds but only a mere few which was tolerable.<\/p>\n<h3>Crashing all pod instances of a service<\/h3>\n<p>Crashing entire <em>nodes<\/em> was easy so what would happen if <em>all instances<\/em> of a pod crashed? We&#8217;ve alluded to this possibility in <a href=\"\/index.php\/2020\/02\/28\/image-editor-on-kubernetes-with-kompose-minikube-k3s-k3sup-and-helm-part-1\/\" target=\"_blank\" rel=\"noopener noreferrer\">part 1<\/a> already. Following the idea of domain driven design, we argued that account dependent capabilities should be independent from freely available ones. Either should be unaffected if the other becomes unavailable. During presenting this idea in class, there arose the question <code class=\"\" data-line=\"\">Well, do you _actually_ test for that? What if there are dependencies you didn&#039;t think about?<\/code> So let&#8217;s find out if our idea holds up to reality!<\/p>\n<p>Testing for this is quite fun: Simply <code class=\"\" data-line=\"\">kubectl delete<\/code> all deployments and <code class=\"\" data-line=\"\">ReplicaSet<\/code>s of a given domain. This will automatically terminate all corresponding pods. Then check if everything else still works as intended. As it turns out, freely available services \u2014 namely image conversions \u2014 are indeed independent from account dependent services. The same is true vice versa.<br>Account management is itself independent from the gallery functions but the gallery can&#8217;t be accessed without the former. Finally, without the ingress component, nothing can be accessed (<em>who would&#8217;ve thought?<\/em>). In short: Yep, it works just as advertised!<\/p>\n<p><a href=\"\/wp-content\/uploads\/2020\/02\/DDD_dependencies-1024x119.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img decoding=\"async\" src=\"\/wp-content\/uploads\/2020\/02\/DDD_dependencies-1024x119.png\"><\/a><\/p>\n<p>Still, our conclusion stands: Better not let it get so far that every single instance of a service is down.<\/p>\n<h3>chaoskube: Entering the world of automatic fault injection<\/h3>\n<p>What would happen if not a node, not a whole service, but only single pods running on a node crashed?<br><code class=\"\" data-line=\"\">chaoskube<\/code>, as per its description, periodically kills random pods in your Kubernetes cluster. Installed with<\/p>\n<pre style=\"font-size: 13px\"><code class=\"\" data-line=\"\">helm install chaoskube stable\/chaoskube --set rbac.create=true --set rbac.serviceAccountName=chaoskube --set dryRun=false --set interval=30s<\/code><\/pre>\n<p>, <code class=\"\" data-line=\"\">chaoskube<\/code> kills one pod every 30 seconds. More pods are killed by providing an additional <code class=\"\" data-line=\"\">replicas<\/code> configuration, e.g. <code class=\"\" data-line=\"\">--set replicas=5<\/code> to kill 5 pods on every run.<\/p>\n<p>While <code class=\"\" data-line=\"\">chaoskube<\/code> was randomly destroying pods in our cluster, we ran <a href=\"https:\/\/httpd.apache.org\/docs\/current\/programs\/ab.html\" target=\"_blank\" rel=\"noopener noreferrer\"><code class=\"\" data-line=\"\">ab<\/code><\/a>, the Apache HTTP server benchmarking tool, to automatically test various functionality of our application (e.g. the conversion of images and the login) over a longer period of time under moderate load (with <code class=\"\" data-line=\"\">-c 100<\/code> for 100 concurrent requests and <code class=\"\" data-line=\"\">-n 1000000<\/code> to issue a total of 1 million requests).<br>The application continued to work as expected as long as not all instances of a service were terminated by <code class=\"\" data-line=\"\">chaoskube<\/code> and not a pod which was currently processing a request was killed. In those two cases, a <code class=\"\" data-line=\"\">502 Bad Gateway<\/code> response was returned to the client (which is fine since the outtake couldn&#8217;t be handle in any other way except by busy-waiting for a pod-instance to become available again before responding to the request). <a href=\"https:\/\/www.martinfowler.com\/ieeeSoftware\/failFast.pdf\" target=\"_blank\" rel=\"noopener noreferrer\">Fail-fast<\/a> was the corresponding design pattern we wanted to adhere to.<\/p>\n<h1>Take-Aways<\/h1>\n<ul>\n<li><a href=\"https:\/\/github.com\/kubernetes\/kompose\" target=\"_blank\" rel=\"noopener noreferrer\">Kompose<\/a>: \ud83d\udc4d, super helpful for Kubernetes beginners, saves quite some time when converting lots of files<\/li>\n<li><a href=\"https:\/\/kubernetes.io\/docs\/tasks\/tools\/install-minikube\/\" target=\"_blank\" rel=\"noopener noreferrer\">Minikube<\/a>: \ud83d\udc4d, one-command-bootstrap of development cluster, nothing more to say except that it&#8217;s near crucial for development<\/li>\n<li><a href=\"https:\/\/k3s.io\/\" target=\"_blank\" rel=\"noopener noreferrer\">k3s<\/a> \/ <a href=\"https:\/\/github.com\/alexellis\/k3sup\" target=\"_blank\" rel=\"noopener noreferrer\">k3sup<\/a>: \ud83d\udc4d, one-command-bootstrap of &#8220;production-ready&#8221; cluster, more tests need to be made to validate this statement<\/li>\n<li><a href=\"https:\/\/helm.sh\/\" target=\"_blank\" rel=\"noopener noreferrer\">Helm<\/a>: \ud83d\udc4d, installing highly-available components to a Kubernetes cluster has never been easier. It really is <em>the<\/em> package manager for Kubernetes.<\/li>\n<\/ul>\n<h1>Outlook<\/h1>\n<p>As much as we\u2019ve learned \u2014 so far we have only seen the tip of the eisberg. There are many more things to try and questions to ask:&nbsp;<\/p>\n<p>In order to run load tests that provide meaningful results, one would need to invest in stronger hardware. Also, <code class=\"\" data-line=\"\">ab<\/code> (the HTTP benchmarking utility we used) can hardly represent real-world loads caused by <em>actual<\/em> users. This entire area could well be worth another blog post in the future.<\/p>\n<p>We considered using a service mesh for our project. Service meshes like <a href=\"https:\/\/istio.io\/\">Istio<\/a>, <a href=\"https:\/\/linkerd.io\/\">Linkerd<\/a> and <a href=\"https:\/\/www.consul.io\/\">Consul<\/a> can provide many useful capabilities that could help us improve our application immensely.<br>For example, we could use Istio to improve security (use encrypted data transfer between the pods), control traffic flow to our services, manage versions, monitor the entire network using the provided <a href=\"https:\/\/grafana.com\/\">Grafana<\/a> integration and more \u2014 all of this without coding it into the services themselves. It even offers fault injection capabilities. Unfortunately, with our limited hardware resources we never managed to get Istio running; Istio has a substantial resource footprint on its own. It thus remains on our bucket list.<\/p>\n<p>The last few years have seen the creation of many new tools and concepts that apply established software engineering methods to machine learning applications. Projects like <a href=\"https:\/\/www.tensorflow.org\/tfx\/guide\/serving\">TensorFlow Serving<\/a> (<a href=\"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2019\/03\/12\/large-scale-deployment-for-deep-learning-models-with-tensorflow-serving\/\">written before<\/a> about on this blog), <a href=\"https:\/\/www.cortex.dev\/\">Cortex<\/a> or <a href=\"https:\/\/www.pachyderm.com\/\">Pachyderm<\/a> (<a href=\"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2019\/02\/26\/reproducibility-in-ml\/\">as discussed in this post<\/a>) promise to offer improved ways to tackle version management, data provenance, reproducibility, monitoring and more. We did not use any of them \u2014 that is not to say we didn\u2019t consider it, it was simply out of scope. In fact, we very much treat the converters as <em>black boxes<\/em>.<br>Now that our app has a general working frame it would be great to revisit this aspect and to use those tools to add many more awesome image converters!<\/p>\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>This is part two of our series on how we designed and implemented a scalable, highly-available and fault-tolerant microservice-based Image Editor. This part depicts how we went from a basic Docker Compose setup to running our application on our own \u00bbbare-metal\u00ab Kubernetes cluster.<\/p>\n","protected":false},"author":961,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1,21,651,2],"tags":[49,346,210,225,51,3,342,341,339,347,338,337,336,154,348,91,340,345,343,48],"ppma_author":[808],"class_list":["post-9655","post","type-post","status-publish","format-standard","hentry","category-allgemein","category-system-architecture","category-system-designs","category-system-engineering","tag-architecture","tag-beginner","tag-cluster","tag-container","tag-design","tag-docker","tag-docker-compose","tag-ha","tag-helm","tag-intro","tag-k3s","tag-k3sup","tag-kompose","tag-kubernetes","tag-microservice","tag-microservices","tag-minikube","tag-rancher","tag-scalable","tag-scaling"],"aioseo_notices":[],"jetpack_featured_media_url":"","jetpack-related-posts":[{"id":5175,"url":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2019\/02\/24\/benefiting-kubernetes-part-2-deploy-with-kubectl\/","url_meta":{"origin":9655,"position":0},"title":"Migrating to Kubernetes Part 2 &#8211; Deploy with kubectl","author":"Can Kattwinkel","date":"24. February 2019","format":false,"excerpt":"Written by: Pirmin Gersbacher, Can Kattwinkel, Mario Sallat Migrating from Bare Metal to Kubernetes The interest in software containers is a relatively new trend in the developers world. Classic VMs have not lost their right to exist within a world full of monoliths yet, but the trend is clearly towards\u2026","rel":"","context":"In &quot;Allgemein&quot;","block_context":{"text":"Allgemein","link":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/category\/allgemein\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2019\/02\/pexels-photo-379964.jpeg?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2019\/02\/pexels-photo-379964.jpeg?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2019\/02\/pexels-photo-379964.jpeg?resize=525%2C300&ssl=1 1.5x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2019\/02\/pexels-photo-379964.jpeg?resize=700%2C400&ssl=1 2x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2019\/02\/pexels-photo-379964.jpeg?resize=1050%2C600&ssl=1 3x"},"classes":[]},{"id":9973,"url":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2020\/02\/29\/image-editor-on-kubernetes-with-kompose-minikube-k3s-k3sup-and-helm-part-1\/","url_meta":{"origin":9655,"position":1},"title":"Kubernetes: from Zero to Hero with Kompose, Minikube, k3sup and Helm \u2014  Part 1: Design","author":"Florian Wintel","date":"29. February 2020","format":false,"excerpt":"This is part one of our series on how we designed and implemented a scalable, highly-available and fault-tolerant microservice-based Image Editor. The series covers the various design choices we made and the difficulties we faced during design and development of our system. It shows how we set up the scaling\u2026","rel":"","context":"In &quot;Allgemein&quot;","block_context":{"text":"Allgemein","link":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/category\/allgemein\/"},"img":{"alt_text":"","src":"\/wp-content\/uploads\/2020\/02\/hdm.jpg","width":350,"height":200,"srcset":"\/wp-content\/uploads\/2020\/02\/hdm.jpg 1x, \/wp-content\/uploads\/2020\/02\/hdm.jpg 1.5x, \/wp-content\/uploads\/2020\/02\/hdm.jpg 2x"},"classes":[]},{"id":10190,"url":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2020\/03\/01\/autoscaling-of-docker-containers-in-google-kubernetes-engine\/","url_meta":{"origin":9655,"position":2},"title":"Autoscaling of Docker Containers  in Google Kubernetes Engine","author":"de032","date":"1. March 2020","format":false,"excerpt":"In this blog post we are taking a look at scaling possibilities within Kubernetes in a cloud environment. We are going to present and discuss various options that all have the same target: increase the availability of a service.","rel":"","context":"In &quot;Allgemein&quot;","block_context":{"text":"Allgemein","link":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/category\/allgemein\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2020\/03\/1052ebad-d01f-4803-bde6-e943c4598ef9.jpeg?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2020\/03\/1052ebad-d01f-4803-bde6-e943c4598ef9.jpeg?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2020\/03\/1052ebad-d01f-4803-bde6-e943c4598ef9.jpeg?resize=525%2C300&ssl=1 1.5x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2020\/03\/1052ebad-d01f-4803-bde6-e943c4598ef9.jpeg?resize=700%2C400&ssl=1 2x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2020\/03\/1052ebad-d01f-4803-bde6-e943c4598ef9.jpeg?resize=1050%2C600&ssl=1 3x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2020\/03\/1052ebad-d01f-4803-bde6-e943c4598ef9.jpeg?resize=1400%2C800&ssl=1 4x"},"classes":[]},{"id":5179,"url":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2019\/02\/24\/migrating-to-kubernetes-part-4-create-environments-via-gitlab\/","url_meta":{"origin":9655,"position":3},"title":"Migrating to Kubernetes Part 4 &#8211; Create Environments via Gitlab","author":"Can Kattwinkel","date":"24. February 2019","format":false,"excerpt":"Written by: Pirmin Gersbacher, Can Kattwinkel, Mario Sallat Connect Gitlab with Kubernetes With the Review Apps Gitlab offers an excellent improvement of the Developer Experience. More or less Gitlab enables the management of environments. For each environment, there is a CI task to each set-up and tear down. It is\u2026","rel":"","context":"In &quot;Allgemein&quot;","block_context":{"text":"Allgemein","link":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/category\/allgemein\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2019\/02\/pexels-photo-379964.jpeg?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2019\/02\/pexels-photo-379964.jpeg?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2019\/02\/pexels-photo-379964.jpeg?resize=525%2C300&ssl=1 1.5x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2019\/02\/pexels-photo-379964.jpeg?resize=700%2C400&ssl=1 2x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2019\/02\/pexels-photo-379964.jpeg?resize=1050%2C600&ssl=1 3x"},"classes":[]},{"id":5177,"url":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2019\/02\/24\/migrating-to-kubernetes-part-3-creating-environments-with-helm\/","url_meta":{"origin":9655,"position":4},"title":"Migrating to Kubernetes Part 3 &#8211; Creating Environments with Helm","author":"Can Kattwinkel","date":"24. February 2019","format":false,"excerpt":"Written by: Pirmin Gersbacher, Can Kattwinkel, Mario Sallat Creating Environments on the Fly The last step has been the deployment of a classic 3 tier application onto a Kubernetes Cluster powered by Minikube. In the next stage it gets a little complicated, since there are two things to do that\u2026","rel":"","context":"In &quot;Allgemein&quot;","block_context":{"text":"Allgemein","link":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/category\/allgemein\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2019\/02\/pexels-photo-379964.jpeg?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2019\/02\/pexels-photo-379964.jpeg?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2019\/02\/pexels-photo-379964.jpeg?resize=525%2C300&ssl=1 1.5x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2019\/02\/pexels-photo-379964.jpeg?resize=700%2C400&ssl=1 2x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2019\/02\/pexels-photo-379964.jpeg?resize=1050%2C600&ssl=1 3x"},"classes":[]},{"id":28823,"url":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/02\/28\/morehuehner-ein-moorhuhn-remake-als-cloud-native-multiplayer-browsergame\/","url_meta":{"origin":9655,"position":5},"title":"Morehuehner: Ein Moorhuhn-Remake als Cloud-Native Multiplayer-Browsergame","author":"Michael Dick","date":"28. February 2026","format":false,"excerpt":"1. Einleitung \u201cMoorhuhn\u201d, wer erinnert sich nicht? Damals auf Windows XP, in der Mittagspause oder nach der Schule, mit dem Fadenkreuz \u00fcber den Bildschirm und auf pixelige H\u00fchner geballert. F\u00fcr uns war Moorhuhn eines dieser Spiele, das man eigentlich nie alleine spielen wollte. Man sa\u00df vor dem Rechner, jemand schaute\u2026","rel":"","context":"In &quot;Allgemein&quot;","block_context":{"text":"Allgemein","link":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/category\/allgemein\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/redis.png?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/redis.png?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/redis.png?resize=525%2C300&ssl=1 1.5x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/redis.png?resize=700%2C400&ssl=1 2x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/redis.png?resize=1050%2C600&ssl=1 3x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/02\/redis.png?resize=1400%2C800&ssl=1 4x"},"classes":[]}],"jetpack_sharing_enabled":true,"authors":[{"term_id":808,"user_id":961,"is_guest":0,"slug":"lk163","display_name":"Leon Klingele","avatar_url":"https:\/\/secure.gravatar.com\/avatar\/1f0b9e6e47bd4b8d164510c4e7cdcdd346a8dc16f447bac78cbc44ce876d4d72?s=96&d=mm&r=g","0":null,"1":"","2":"","3":"","4":"","5":"","6":"","7":"","8":""}],"_links":{"self":[{"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/posts\/9655","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/users\/961"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/comments?post=9655"}],"version-history":[{"count":294,"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/posts\/9655\/revisions"}],"predecessor-version":[{"id":10335,"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/posts\/9655\/revisions\/10335"}],"wp:attachment":[{"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/media?parent=9655"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/categories?post=9655"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/tags?post=9655"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/ppma_author?post=9655"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}