<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Victor Brinkhorst on Qstars IT Blog - Cloud, Kubernetes &amp; Software Engineering</title>
        <link>https://blog.qstars.nl/authors/victor-brinkhorst/</link>
        <description>Recent content in Victor Brinkhorst on Qstars IT Blog - Cloud, Kubernetes &amp; Software Engineering</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>en-us</language>
        <lastBuildDate>Sun, 23 Aug 2026 09:00:00 +0200</lastBuildDate><atom:link href="https://blog.qstars.nl/authors/victor-brinkhorst/index.xml" rel="self" type="application/rss+xml" /><item>
        <title>Cheap Self-Hosted Kubernetes on Hetzner Cloud</title>
        <link>https://blog.qstars.nl/posts/cheap-self-hosted-kubernetes-on-hetzner-cloud/</link>
        <pubDate>Tue, 30 Dec 2025 09:21:00 +0200</pubDate>
        
        <guid>https://blog.qstars.nl/posts/cheap-self-hosted-kubernetes-on-hetzner-cloud/</guid>
        <description>&lt;img src="https://blog.qstars.nl/posts/cheap-self-hosted-kubernetes-on-hetzner-cloud/cover.webp" alt="Featured image of post Cheap Self-Hosted Kubernetes on Hetzner Cloud" /&gt;&lt;blockquote&gt;
&lt;p&gt;This blog&amp;rsquo;s starter project can be found at &lt;a class=&#34;link&#34; href=&#34;https://github.com/qstarsit/hcloud-kube-hetzner&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;github.com/qstarsit/hcloud-kube-hetzner&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Kubernetes is a brilliant workload API, the community has built a highly extensible standard which abstracts the underlying machines and infrastructure from our application deployments. However, the &lt;em&gt;cloud-native&lt;/em&gt; ecosystem can come with a sense of vendor lock-in. Functionality like managed load balancing, seamless node autoscaling, extensive storage options, integrated networking, and polished operational tooling are what you pay a premium for.&lt;/p&gt;
&lt;p&gt;Running and operating Kubernetes yourself, &lt;em&gt;&amp;ldquo;The Hard Way&amp;rdquo;&lt;/em&gt;, can be overwhelming, and time intensive. That’s why in this post, we’re taking a look at &lt;a class=&#34;link&#34; href=&#34;https://github.com/mysticaltech/terraform-hcloud-kube-hetzner&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;&lt;strong&gt;terraform-hcloud-kube-hetzner&lt;/strong&gt;&lt;/a&gt;. An open-source Terraform project that simplifies deploying a production-ready K3s cluster on Hetzner Cloud, striking a balance between ease of use, reliability, and cost efficiency. All while keeping true to our European origin 🇪🇺.&lt;/p&gt;
&lt;h2 id=&#34;hetzner&#34;&gt;Hetzner
&lt;/h2&gt;&lt;p&gt;Hetzner is a German infrastructure provider that has built a strong reputation for offering high-performance hardware at reasonable and predictable prices. With less features than hyperscalers they focus on doing few things very well. This has made them a popular hosting provider among indie hackers and startups alike.&lt;/p&gt;
&lt;p&gt;Their Cloud offering has matured a lot in recent years and features a solid API to manage resources. It features private networking, load balancers, network attached storage and virtual machines. This makes it a solid alternative for teams looking for proper automation on a more cost-effective platform.&lt;/p&gt;
&lt;p&gt;Hetzner operates modern data centers primarily in Europe, while also offering locations in the USA and Asia. In light of the recent focus on sovereign data centers, the fact that Hetzner is a German company adds reassurance around data sovereignty.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://blog.qstars.nl/posts/cheap-self-hosted-kubernetes-on-hetzner-cloud/hetzner-park-fsn.webp&#34;
	width=&#34;1200&#34;
	height=&#34;800&#34;
	srcset=&#34;https://blog.qstars.nl/posts/cheap-self-hosted-kubernetes-on-hetzner-cloud/hetzner-park-fsn_hu_abd329684b19130a.webp 480w, https://blog.qstars.nl/posts/cheap-self-hosted-kubernetes-on-hetzner-cloud/hetzner-park-fsn_hu_cba6a16591782cd.webp 1024w&#34;
	loading=&#34;lazy&#34;
	
		alt=&#34;Hetzner Falkenstein datacenter park&#34;
	
	
		class=&#34;gallery-image&#34; 
		data-flex-grow=&#34;150&#34;
		data-flex-basis=&#34;360px&#34;
	
&gt;&lt;/p&gt;
&lt;h2 id=&#34;kube-hetzner&#34;&gt;Kube-Hetzner
&lt;/h2&gt;&lt;p&gt;&lt;a class=&#34;link&#34; href=&#34;https://github.com/mysticaltech/terraform-hcloud-kube-hetzner&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Kube-Hetzner&lt;/a&gt; is an optimized, easy-to-use terraform module to set up a (highly available) auto-upgraded cluster. It codifies many best practices required to run K3s reliably on Hetzner Cloud.&lt;/p&gt;
&lt;p&gt;The project handles much of the heavy lifting involved in bootstrapping and maintaining a cluster: provisioning compute resources, configuring private networking, setting up load balancers, managing node pools, and wiring it all together. Upgrades are automated and rolling by default, reducing manual intervention and downtime during maintenance.&lt;/p&gt;
&lt;p&gt;It integrates Hetzner’s native tooling directly into your cluster, including the &lt;a class=&#34;link&#34; href=&#34;https://github.com/hetznercloud/csi-driver&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;hcloud CSI&lt;/a&gt; driver and the &lt;a class=&#34;link&#34; href=&#34;https://github.com/hetznercloud/hcloud-cloud-controller-manager&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;hcloud Cloud Controller Manager (CCM)&lt;/a&gt;. This enables Kubernetes to automatically provision and manage resources like load balancers and block storage via Kubernetes.&lt;/p&gt;
&lt;h3 id=&#34;microos&#34;&gt;MicroOS
&lt;/h3&gt;&lt;p&gt;In the world of containers, less is more. The same applies for the operating system that functions as the base for our workloads. Traditional server distributions like &lt;a class=&#34;link&#34; href=&#34;https://ubuntu.com/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Ubuntu&lt;/a&gt;, &lt;a class=&#34;link&#34; href=&#34;https://www.debian.org/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Debian&lt;/a&gt; or &lt;a class=&#34;link&#34; href=&#34;https://www.redhat.com/en/technologies/linux-platforms/enterprise-linux&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;RedHat Enterprise Linux&lt;/a&gt; include a wide range of packages and services for general-purpose use, many of which are unnecessary for container workloads. This has led to the development of container-centric operating systems; &lt;a class=&#34;link&#34; href=&#34;https://www.talos.dev/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Talos&lt;/a&gt;, &lt;a class=&#34;link&#34; href=&#34;https://www.fedoraproject.org/coreos/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;CoreOS&lt;/a&gt;, &lt;a class=&#34;link&#34; href=&#34;https://github.com/bottlerocket-os/bottlerocket&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Bottlerocket&lt;/a&gt; and &lt;a class=&#34;link&#34; href=&#34;https://www.flatcar.org/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;flatcar&lt;/a&gt; just to name a few.&lt;/p&gt;
&lt;p&gt;Kube-Hetzner utilises openSUSE&amp;rsquo;s &lt;a class=&#34;link&#34; href=&#34;https://microos.opensuse.org/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;MicroOS&lt;/a&gt; for all of its nodes. MicroOS is a stable operating system with a small footprint specialized in running container workloads. It sports rolling releases, atomic &amp;amp; transactional updates and secure updates by default. The OS is designed to be predictable, resilient and to behave the same on every boot.&lt;/p&gt;
&lt;p&gt;MicroOS is immutable and cannot be modified during runtime. Updates are applied transactionally using &lt;a class=&#34;link&#34; href=&#34;https://en.wikipedia.org/wiki/Btrfs&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Btrfs&lt;/a&gt; snapshots, allowing the system to fully succeed or roll back to a clean state. In the event of a faulty update a previous snapshot is automatically booted with no manual interaction required.&lt;/p&gt;
&lt;p&gt;A read-only root filesystem also reduces our attack surface. After a container is compromised, modifying host binaries or operating system configuration is not possible. Even if an attacker gains host-level access, any changes are discarded on reboot.&lt;/p&gt;
&lt;p&gt;Our application workloads are managed independently from the host operating system and are not installed on the root filesystem. This prevents configuration drift and ensures all of our nodes remain interchangeable and can soundly be rolled out, replaced or recovered repeatedly.&lt;/p&gt;
&lt;h3 id=&#34;k3s&#34;&gt;K3s
&lt;/h3&gt;&lt;p&gt;The heart of our cluster consists of &lt;a class=&#34;link&#34; href=&#34;https://k3s.io/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;K3s&lt;/a&gt;, Rancher&amp;rsquo;s lightweight Kubernetes distribution. Designed specifically for compute-constrained environments like edge or IoT devices. K3s packages components like the Kubernetes API server, kube proxy and kubelet into a single binary to reduce dependencies and steps required to bootstrap a cluster.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a fully conformant distribution that simplifies our operations. Despite its minimal design, K3s is production ready and well suited for small to medium-sized clusters. It compacts and abstracts a lot of the overhead that comes with running a standard Kubernetes installation.&lt;/p&gt;
&lt;p&gt;Furthermore, K3s and Kube-Hetzner come with sensible defaults. You are free and able to change advanced configurations in your cluster such as the IP ranges used for the pod and service network or change the internal cluster hostname. More on these settings later as we&amp;rsquo;ll configure them with Terraform.&lt;/p&gt;
&lt;p&gt;&lt;a class=&#34;link&#34; href=&#34;https://docs.k3s.io/cli/server&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;K3s server configuration&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;planning-our-cluster&#34;&gt;Planning our cluster
&lt;/h2&gt;&lt;p&gt;For this blog we&amp;rsquo;ll deploy a standard highly-available cluster. Three control plane nodes and three worker nodes spread out across Falkenstein, Nuremberg and Helsinki.&lt;/p&gt;
&lt;p&gt;For cost efficiency, we’ll enable the Rancher &lt;a class=&#34;link&#34; href=&#34;https://github.com/rancher/local-path-provisioner&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;local-path&lt;/a&gt; provisioner to make use of the disks attached to our VPS nodes. This lets us choose between regional block storage or disks attached to our nodes using local-path, depending on the workload.&lt;/p&gt;
&lt;p&gt;I’ll opt out of using &lt;a class=&#34;link&#34; href=&#34;https://longhorn.io/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Longhorn&lt;/a&gt; as the local-path provisioner combined with the block storage is sufficient for my workloads. If you require storage that is replicated across regions and independent of pod scheduling decisions, you can enable Longhorn. Do beware of performance bottlenecks when using Longhorn and &lt;a class=&#34;link&#34; href=&#34;https://dennislee22.github.io/docs/longhorn/benchmarking/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;benchmark it&lt;/a&gt; for your use case.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 Alternatively, you could opt for a dual-region failover setup across the two German sites. While the Helsinki region generally has good server availability and acceptable latency for production workloads, its latency is still higher than the site-to-site connection between Falkenstein and Nuremberg.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Select your desired &lt;a class=&#34;link&#34; href=&#34;https://www.hetzner.com/cloud&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;machine type&lt;/a&gt; for the control plane and worker nodes. I’ll opt for &lt;code&gt;CX23&lt;/code&gt; control planes (2 virtual cores / 4GB ram / 40GB disk @ &lt;strong&gt;€5,49&lt;/strong&gt;/mo) and &lt;code&gt;CX33&lt;/code&gt; (4 virtual cores / 8GB ram / 80GB disk @ &lt;strong&gt;€8,49&lt;/strong&gt;/mo) for our workers. These instances are part of Hetzner’s shared cloud offering, meaning resources are shared with other customers on the same physical host. In my experience, I haven’t observed significant CPU steal time and do not believe that Hetzner aggressively oversubscribes their servers. That said, you should evaluate this choice carefully based on your specific workload and business requirements.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 At the time of this update, &lt;code&gt;CX23&lt;/code&gt; and &lt;code&gt;CX33&lt;/code&gt; are showing as temporarily sold out in most EU locations due to high demand on Hetzner’s shared vCPU lines. This has historically been intermittent — check the &lt;a class=&#34;link&#34; href=&#34;https://console.hetzner.cloud/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Hetzner Cloud console&lt;/a&gt; or try a different location (Falkenstein, Nuremberg, Helsinki) before reaching for a pricier dedicated-vCPU type.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That puts us up to &lt;strong&gt;€41,94&lt;/strong&gt; a month on our six machines so far. Now we need to decide on a network configuration for our cluster.&lt;/p&gt;
&lt;p&gt;For this post, I&amp;rsquo;ll keep it simple. We&amp;rsquo;ll opt to give each node a public IP address and whitelist our IP address to connect on port 22 (SSH). The API Server will be closed for external traffic as we&amp;rsquo;ll put a load balancer infront of our control planes.&lt;/p&gt;
&lt;p&gt;Additionally, our traefik ingress class gets its own load balancer. This gives us a highly available, seperate entrypoint for cluster management and application traffic. These two load balancers (&lt;code&gt;lb11&lt;/code&gt;) cost &lt;strong&gt;€7,49&lt;/strong&gt;/mo each, adding &lt;strong&gt;€14,98&lt;/strong&gt;/mo to our total.&lt;/p&gt;
&lt;p&gt;A public IPv4 address now costs &lt;strong&gt;€0,50&lt;/strong&gt;/mo which adds an additional &lt;strong&gt;€3,00&lt;/strong&gt; making our total &lt;strong&gt;€59,92&lt;/strong&gt;/mo pre-VAT and &lt;strong&gt;€72,50&lt;/strong&gt;/mo with VAT.&lt;/p&gt;
&lt;p&gt;To me, this remains a reasonable cost for our cluster, though the margin has narrowed since Hetzner’s &lt;a class=&#34;link&#34; href=&#34;https://docs.hetzner.com/general/infrastructure-and-availability/price-adjustment/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;2026 price adjustments&lt;/a&gt;. Barring promotional discounts and free tiers, a typical managed cluster starts at &lt;strong&gt;$70&lt;/strong&gt;-&lt;strong&gt;$75&lt;/strong&gt;/mo on &lt;a class=&#34;link&#34; href=&#34;https://cloud.google.com/kubernetes-engine/pricing?hl=en&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;GCP GKE&lt;/a&gt; or &lt;a class=&#34;link&#34; href=&#34;https://aws.amazon.com/eks/pricing/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;AWS EKS&lt;/a&gt; — comparing against our pre-VAT total (VAT-registered businesses reclaim it), we’re still meaningfully cheaper, but not by the wide margin we used to be. That is just the cost of running your cluster, not the workloads you are going to schedule on it.&lt;/p&gt;
&lt;p&gt;However, there are some options for those looking to push down the costs further:&lt;/p&gt;
&lt;h3 id=&#34;1-use-a-single-load-balancer-or-metallb&#34;&gt;1. Use a single load balancer or MetalLB
&lt;/h3&gt;&lt;p&gt;Instead of using separate load balancers for the API server and ingress traffic, you could consolidate to a single load balancer that handles both. This saves &lt;strong&gt;€7,49&lt;/strong&gt;/mo pre-VAT, bringing the total down to &lt;strong&gt;€63,44&lt;/strong&gt;/mo with VAT.&lt;/p&gt;
&lt;p&gt;Alternatively, skip Hetzner’s load balancers entirely and use &lt;a class=&#34;link&#34; href=&#34;https://metallb.universe.tf/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;MetalLB&lt;/a&gt; as a software load balancer running inside your cluster.&lt;/p&gt;
&lt;h3 id=&#34;2-remove-public-ip-addresses-and-use-private-networking&#34;&gt;2. Remove public IP addresses and use private networking
&lt;/h3&gt;&lt;p&gt;Each public IPv4 address now costs &lt;strong&gt;€0,50&lt;/strong&gt;/mo, totaling &lt;strong&gt;€3,00&lt;/strong&gt;/mo for six nodes — a much smaller saving than it used to be, now that Hetzner has cut IPv4 pricing. By removing public IPs and connecting to your cluster through a bastion host, NAT gateway, or VPN solution, you still improve your security posture, even if the direct cost savings are now marginal.&lt;/p&gt;
&lt;p&gt;Options include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bastion host&lt;/strong&gt;: A single small node with a public IP that you SSH through to access cluster nodes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WireGuard VPN&lt;/strong&gt;: Set up a WireGuard server on one node or a separate instance to provide secure private network access&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tailscale/Nebula&lt;/strong&gt;: Use a mesh VPN solution for seamless private connectivity&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This approach removes &lt;strong&gt;€3,00&lt;/strong&gt;/mo in IP costs, though the main benefit today is the reduced attack surface rather than the savings.&lt;/p&gt;
&lt;h2 id=&#34;rolling-out-our-cluster&#34;&gt;Rolling out our cluster
&lt;/h2&gt;&lt;p&gt;It&amp;rsquo;s time to actually deploy the cluster. Head over and clone our &lt;a class=&#34;link&#34; href=&#34;https://github.com/qstarsit/hcloud-kube-hetzner&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;starter project&lt;/a&gt; to get started. Take note of the &lt;strong&gt;Important configurations&lt;/strong&gt; section in the Readme file and change the values to your liking.&lt;/p&gt;
&lt;p&gt;Now we assume that you have &lt;a class=&#34;link&#34; href=&#34;https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Terraform&lt;/a&gt;, &lt;a class=&#34;link&#34; href=&#34;https://kubernetes.io/docs/tasks/tools/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Kubectl&lt;/a&gt; and the &lt;a class=&#34;link&#34; href=&#34;https://github.com/hetznercloud/cli&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;hcloud CLI&lt;/a&gt; installed. If you do not have these, install them now.&lt;/p&gt;
&lt;p&gt;In the project directory, start by installing the terraform dependencies by running &lt;code&gt;terraform init -upgrade&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://blog.qstars.nl/posts/cheap-self-hosted-kubernetes-on-hetzner-cloud/tf-init.webp&#34;
	width=&#34;4508&#34;
	height=&#34;4227&#34;
	srcset=&#34;https://blog.qstars.nl/posts/cheap-self-hosted-kubernetes-on-hetzner-cloud/tf-init_hu_bcb908cad308a86f.webp 480w, https://blog.qstars.nl/posts/cheap-self-hosted-kubernetes-on-hetzner-cloud/tf-init_hu_194d0d3f6f04a2b6.webp 1024w&#34;
	loading=&#34;lazy&#34;
	
		alt=&#34;Terraform command output&#34;
	
	
		class=&#34;gallery-image&#34; 
		data-flex-grow=&#34;106&#34;
		data-flex-basis=&#34;255px&#34;
	
&gt;&lt;/p&gt;
&lt;p&gt;Next we plan our infrastructure changes with &lt;code&gt;terraform plan&lt;/code&gt;. Take a moment to look over the plan, it shows you exactly what terraform is going to create for you in Hetzner. If you&amp;rsquo;re satisfied with the planned changes apply them with &lt;code&gt;terraform apply -auto-approve&lt;/code&gt;. The applying process can take up to 10 minutes.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 Getting stuck waiting for MicroOS to become active in the &lt;code&gt;local-exec&lt;/code&gt; step during the apply? Make sure your machine has connectivity to the nodes and you&amp;rsquo;ve whitelisted your IP address for SSH connections.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;After a successful apply we can get our kubeconfig with &lt;code&gt;terraform output -raw kubeconfig &amp;gt; config&lt;/code&gt;. Move the config file to the default &lt;code&gt;.kube&lt;/code&gt; directory &lt;code&gt;~/.kube/config&lt;/code&gt; for Linux &amp;amp; MacOS or &lt;code&gt;%USERPROFILE%\.kube\config&lt;/code&gt; for Windows.&lt;/p&gt;
&lt;p&gt;Now we can test connectivity with our cluster by running &lt;code&gt;kubectl get nodes&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://blog.qstars.nl/posts/cheap-self-hosted-kubernetes-on-hetzner-cloud/nodes.webp&#34;
	width=&#34;2455&#34;
	height=&#34;828&#34;
	srcset=&#34;https://blog.qstars.nl/posts/cheap-self-hosted-kubernetes-on-hetzner-cloud/nodes_hu_67b3052465af6f03.webp 480w, https://blog.qstars.nl/posts/cheap-self-hosted-kubernetes-on-hetzner-cloud/nodes_hu_3ae2ca2f70b55d42.webp 1024w&#34;
	loading=&#34;lazy&#34;
	
		alt=&#34;Cluster node list&#34;
	
	
		class=&#34;gallery-image&#34; 
		data-flex-grow=&#34;296&#34;
		data-flex-basis=&#34;711px&#34;
	
&gt;&lt;/p&gt;
&lt;p&gt;Success! My 6 node cluster has successfully rolled out with my nodes being spread over the three Hetzner zones. Checking my &lt;code&gt;~/.kube/config&lt;/code&gt; file I can see I am connecting to &lt;code&gt;https://142.132.242.235:6443&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://blog.qstars.nl/posts/cheap-self-hosted-kubernetes-on-hetzner-cloud/kconfig.webp&#34;
	width=&#34;4652&#34;
	height=&#34;2499&#34;
	srcset=&#34;https://blog.qstars.nl/posts/cheap-self-hosted-kubernetes-on-hetzner-cloud/kconfig_hu_a6e0c4ed0655fdf5.webp 480w, https://blog.qstars.nl/posts/cheap-self-hosted-kubernetes-on-hetzner-cloud/kconfig_hu_cef2463b346553ab.webp 1024w&#34;
	loading=&#34;lazy&#34;
	
		alt=&#34;Kube config file outputs&#34;
	
	
		class=&#34;gallery-image&#34; 
		data-flex-grow=&#34;186&#34;
		data-flex-basis=&#34;446px&#34;
	
&gt;&lt;/p&gt;
&lt;p&gt;This should be the IP address of the controlplane load balancer, let&amp;rsquo;s verify it in the Hetzner UI.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://blog.qstars.nl/posts/cheap-self-hosted-kubernetes-on-hetzner-cloud/loadbalancer-hetzner.webp&#34;
	width=&#34;2584&#34;
	height=&#34;720&#34;
	srcset=&#34;https://blog.qstars.nl/posts/cheap-self-hosted-kubernetes-on-hetzner-cloud/loadbalancer-hetzner_hu_c9551a3a87029c81.webp 480w, https://blog.qstars.nl/posts/cheap-self-hosted-kubernetes-on-hetzner-cloud/loadbalancer-hetzner_hu_313681cd5f0d7976.webp 1024w&#34;
	loading=&#34;lazy&#34;
	
		alt=&#34;Hetzner Cloud load balancer UI&#34;
	
	
		class=&#34;gallery-image&#34; 
		data-flex-grow=&#34;358&#34;
		data-flex-basis=&#34;861px&#34;
	
&gt;&lt;/p&gt;
&lt;p&gt;Perfect! All resources have been created successfully. Our cluster is now up and running with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;6 nodes spread across 3 availability zones&lt;/li&gt;
&lt;li&gt;2 load balancers for API server and ingress traffic&lt;/li&gt;
&lt;li&gt;Private, encrypted networking between all nodes&lt;/li&gt;
&lt;li&gt;Automated K3s installation and configuration&lt;/li&gt;
&lt;li&gt;Ready to deploy workloads&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;From here, you can start deploying your applications using &lt;code&gt;kubectl apply&lt;/code&gt; or install additional tooling like monitoring, logging, or CI/CD integrations. The cluster is production-ready and will automatically handle upgrades and maintenance through the built-in controllers we discussed earlier.&lt;/p&gt;
&lt;h2 id=&#34;backups&#34;&gt;Backups
&lt;/h2&gt;&lt;p&gt;Kube-Hetzner makes use of K3s&amp;rsquo; built-in etcd snapshot capability. This allows us to backup our cluster state to an external S3 compatible storage provider. K3s automatically creates etcd snapshots on the nodes themselves, relying solely on local snapshots leaves us vulnerable to a catastrophic node failure.&lt;/p&gt;
&lt;p&gt;Node backups are facilitated through Hetzner&amp;rsquo;s built-in backupping mechanism.&lt;/p&gt;
&lt;h2 id=&#34;upgrades&#34;&gt;Upgrades
&lt;/h2&gt;&lt;p&gt;KH uses Rancher’s &lt;a class=&#34;link&#34; href=&#34;https://github.com/rancher/system-upgrade-controller&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;&lt;strong&gt;System Upgrade Controller (SUC)&lt;/strong&gt;&lt;/a&gt; to manage Kubernetes and node-level upgrades in a safe, fully automated way. SUC enables upgrades to be defined declaratively and executed natively within Kubernetes.&lt;/p&gt;
&lt;p&gt;The controller continuously watches for &lt;code&gt;Plan&lt;/code&gt; resources. When an upgrade is required, it selects the appropriate nodes and coordinates the rollout according to the defined strategy. This includes handling upgrade order, limiting concurrency, and applying cordon and drain rules. Each &lt;code&gt;Plan&lt;/code&gt; clearly defines what is upgraded, how the upgrade is performed, and which nodes are affected.&lt;/p&gt;
&lt;p&gt;Complementing the SUC, KH also deploys &lt;a class=&#34;link&#34; href=&#34;https://kured.dev&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;kured&lt;/a&gt; the Kubernetes Reboot Daemon. Kured handles node restarts when the OS signals that a reboot is required. Kured monitors the reboot signal and coordinates reboots across nodes managing concurrency and cordoning the nodes. You can tell kured to only reboot your nodes within set timeframes and on specified days only.&lt;/p&gt;
&lt;aside class=&#34;qstars-cta qstars-cta--kubernetes&#34; lang=&#34;en&#34;&gt;
    &lt;p class=&#34;qstars-cta__kicker&#34;&gt;From the team behind this blog&lt;/p&gt;
    &lt;h3 class=&#34;qstars-cta__title&#34;&gt;Want this running in production?&lt;/h3&gt;
    &lt;div class=&#34;qstars-cta__body&#34;&gt;Setting up a cluster is the easy part. Keeping it healthy at 3am is the rest
of it. We design, build and run hybrid cloud platforms for Dutch
organisations, in your own data centre or on Azure and AWS.&lt;/div&gt;
    &lt;a class=&#34;qstars-cta__button&#34; href=&#34;https://www.qstars.nl/wat-we-doen?utm_source=blog&amp;amp;utm_medium=cta&amp;amp;utm_campaign=kubernetes&#34; rel=&#34;noopener&#34;&gt;
        &lt;span&gt;See what we do&lt;/span&gt;
        &lt;svg class=&#34;qstars-cta__arrow&#34; viewBox=&#34;0 0 24 24&#34; width=&#34;18&#34; height=&#34;18&#34;
             fill=&#34;none&#34; stroke=&#34;currentColor&#34; stroke-width=&#34;2&#34;
             stroke-linecap=&#34;round&#34; stroke-linejoin=&#34;round&#34; aria-hidden=&#34;true&#34;&gt;
            &lt;line x1=&#34;5&#34; y1=&#34;12&#34; x2=&#34;19&#34; y2=&#34;12&#34;&gt;&lt;/line&gt;
            &lt;polyline points=&#34;12 5 19 12 12 19&#34;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
    &lt;/a&gt;
&lt;/aside&gt;

</description>
        </item>
        <item>
        <title>Recap EuroPython 2025</title>
        <link>https://blog.qstars.nl/posts/europython-2025/</link>
        <pubDate>Mon, 08 Sep 2025 10:00:00 +0200</pubDate>
        
        <guid>https://blog.qstars.nl/posts/europython-2025/</guid>
        <description>&lt;img src="https://blog.qstars.nl/posts/europython-2025/europython-2025.jpeg" alt="Featured image of post Recap EuroPython 2025" /&gt;&lt;p&gt;EuroPython is a full week dedicated to all things Python. This year the conference ran from 14–20 July in historic Prague.&lt;/p&gt;
&lt;h2 id=&#34;my-first-europython&#34;&gt;My first EuroPython
&lt;/h2&gt;&lt;p&gt;This was my very first EuroPython. Right after picking up the badge, you&amp;rsquo;re welcomed by a table full of stickers to customize it — interests, working groups, and even a “first EuroPython” tag. It was fun to see such a mix of long-time regulars and newcomers like myself.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://blog.qstars.nl/posts/europython-2025/europython-core.jpeg&#34;
	width=&#34;800&#34;
	height=&#34;510&#34;
	srcset=&#34;https://blog.qstars.nl/posts/europython-2025/europython-core_hu_907b685fc9cf7b51.jpeg 480w, https://blog.qstars.nl/posts/europython-2025/europython-core_hu_1679e14ea3cbe9ec.jpeg 1024w&#34;
	loading=&#34;lazy&#34;
	
		alt=&#34;Presentation about CPython core development&#34;
	
	
		class=&#34;gallery-image&#34; 
		data-flex-grow=&#34;156&#34;
		data-flex-basis=&#34;376px&#34;
	
&gt;&lt;/p&gt;
&lt;h2 id=&#34;talks&#34;&gt;Talks
&lt;/h2&gt;&lt;p&gt;When I first opened the schedule, I was a little overwhelmed. With more than 180 speakers and around 100 talks, it wasn’t easy to choose where to start. One thing was clear: boredom wasn’t a part of the programme.&lt;/p&gt;
&lt;p&gt;The conference offered variety on every level — speakers, topics, and presentation styles. From catching Ukrainian snacks while discussing football analytics (thanks, Ruslan), to learning about Polish strawberry pasta recipe during a deep dive into concurrency controls. Each speaker, whether a first-timer or a veteran, brought a piece of themselves to the stage, making every talk all the more engaging.&lt;/p&gt;
&lt;p&gt;Beyond the sheer number of talks, the range of topics was amazing. One moment you&amp;rsquo;re diving into the internals of FastAPI and Django, and the next you&amp;rsquo;re building your own ASGI-compliant protocol. The programme spanned everything from DevOps, Kubernetes and geospatial engineering to GPU-accelerated computing, microcontrollers, and of course, AI. Whatever your area of expertise, if you work with Python, chances are you’ll find someone tackling similar challenges at EuroPython.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://blog.qstars.nl/posts/europython-2025/europython-contribute.jpg&#34;
	width=&#34;1080&#34;
	height=&#34;1350&#34;
	srcset=&#34;https://blog.qstars.nl/posts/europython-2025/europython-contribute_hu_66bc17348b8398ee.jpg 480w, https://blog.qstars.nl/posts/europython-2025/europython-contribute_hu_82ff2982119e7c7f.jpg 1024w&#34;
	loading=&#34;lazy&#34;
	
		alt=&#34;Presentation about contributing without code&#34;
	
	
		class=&#34;gallery-image&#34; 
		data-flex-grow=&#34;80&#34;
		data-flex-basis=&#34;192px&#34;
	
&gt;&lt;/p&gt;
&lt;p&gt;Below is a selection of talks I&amp;rsquo;d like to highlight.&lt;/p&gt;
&lt;h3 id=&#34;myths-and-fairy-tales-around-python-performance---antonio-cuni&#34;&gt;Myths and fairy tales around Python performance - Antonio Cuni
&lt;/h3&gt;&lt;p&gt;Antonio Cuni, principal software engineer at Anaconda and author of &lt;a class=&#34;link&#34; href=&#34;https://github.com/spylang/spy&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;SPylang&lt;/a&gt;. SPy is subset of Python designed to be statically compiled. The project aims to balance the ease-of-use and productivity of a dynamically typed language, while stripping the language of some features to make it faster.&lt;/p&gt;
&lt;p&gt;In his talk, Antonio covers some of the most common misconceptions around Python performance. He began with a comparison of the different layers of memory, from the very fast L1 cache to the much slower main RAM, to illustrate why optimizing for CPU efficiency usually matters more than chasing small micro-optimizations. He then showed how Python features translate into CPU instructions, explaining why compile-time inlining is not possible given the language’s semantics and why a simple attribute call like &lt;code&gt;foo.x()&lt;/code&gt; is more expensive than it might seem.&lt;/p&gt;
&lt;h3 id=&#34;intuition-vs-reality-surprising-truths-in-python-performance---adrien-cacciaguerra--arthur-pastel&#34;&gt;Intuition vs. Reality: Surprising Truths in Python Performance - Adrien Cacciaguerra &amp;amp; Arthur Pastel
&lt;/h3&gt;&lt;p&gt;Adrien and Arthurs are both co-founders of CodSpeed, a &lt;em&gt;Continuous Performance Analysis&lt;/em&gt; solution. Their talk focusses on performance analysis and shows us that our intuitions about code performance can actually make our programs perform slower.&lt;/p&gt;
&lt;p&gt;For example, in Python both lists and generators let you iterate over sequences, but they work very differently. A list is &lt;strong&gt;eager&lt;/strong&gt;: when you run &lt;code&gt;[x for x in range(10_000_000)]&lt;/code&gt;, all ten million elements are created and stored in memory immediately. In contrast, a generator is &lt;strong&gt;lazy&lt;/strong&gt;: it produces values one at a time, yielding them on demand while only keeping a small amount of state in memory. This makes generators far more memory-efficient than lists.&lt;/p&gt;
&lt;p&gt;However, memory efficiency doesn’t always translate to raw speed. Each time a generator yields, the interpreter must resume its frame and execute Python bytecode, which adds per-item overhead compared with iterating over a list stored contiguously in memory. That said, generators avoid allocating and populating a full intermediate list, so in single-pass operations (for example &lt;code&gt;sum(x for x in range(...))&lt;/code&gt;), they can actually be faster overall.&lt;/p&gt;
&lt;p&gt;In addition to the insights, they showcased their LLM-based performance analysis platform &lt;a class=&#34;link&#34; href=&#34;https://p99.chat/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;P99 Chat&lt;/a&gt;. In P99 you can upload a snippet of code and have the AI write a summary of how well it&amp;rsquo;s performing or where it can be tweaked.&lt;/p&gt;
&lt;h3 id=&#34;breaking-http-boundaries-asgi-to-build-a-fleet-management-system-in-python---kirill-druzhinin&#34;&gt;Breaking HTTP Boundaries: ASGI to build a fleet management system in Python - Kirill Druzhinin
&lt;/h3&gt;&lt;p&gt;The &lt;a class=&#34;link&#34; href=&#34;https://en.wikipedia.org/wiki/Asynchronous_Server_Gateway_Interface&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;ASGI&lt;/a&gt; (Asynchronous Server Gateway Interface) protocol is the successor of &lt;a class=&#34;link&#34; href=&#34;https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;WSGI&lt;/a&gt;. ASGI provides a standard interface between async-capable Python network protocol servers, frameworks and applications. It&amp;rsquo;s most commonly known for it&amp;rsquo;s use with web applications, but you can do way more with the protocol.&lt;/p&gt;
&lt;p&gt;Kirill’s demo went beyond the usual HTTP boundaries, showing how ASGI can be used for custom protocols and real-time communication. Unlike HTTP, which is typically request-response and half-duplex, ASGI supports full-duplex communication—meaning data can flow in both directions simultaneously. This opens up possibilities for multiplexed protocols, streaming, and more advanced use cases (websockets or custom IoT protocols).&lt;/p&gt;
&lt;p&gt;He explained how his protocol server processes incoming frames, checking if they arrive sequentially and handling them according to the protocol’s rules. The server acts as a bridge between the network layer and the Python application, ensuring that each frame is correctly routed and that the client and app stay in sync. Features like &lt;code&gt;more_body=True&lt;/code&gt; in ASGI allow for streaming data, making it possible to handle large payloads or continuous updates efficiently.&lt;/p&gt;
&lt;h2 id=&#34;the-community&#34;&gt;The community
&lt;/h2&gt;&lt;p&gt;What stood out most was the community (&lt;em&gt;how clichë&lt;/em&gt;). Python’s culture is just as approachable as the language itself: people are eager to help at every level, whether you’re learning Django for the first time or digging into the CPython JIT.&lt;/p&gt;
&lt;p&gt;This was refreshing compared to other programming communities where beginners can get dismissed as or feel like they&amp;rsquo;re “not good enough”. I started teaching myself programming when I was about 12, there were plenty of moments when I felt like giving up for a plethora of reasons. It means the world to me to see how patient and supportive people at EuroPython are.&lt;/p&gt;
&lt;p&gt;It felt like an environment where learning and socializing come naturally — where your intelligence or worth isn’t measured by your seniority in a particular field.&lt;/p&gt;
&lt;p&gt;I say all of this to give Python and it&amp;rsquo;s community the flowers it deserves for fostering an amazing community of positive and like-minded people.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://blog.qstars.nl/posts/europython-2025/europython-outside.jpg&#34;
	width=&#34;1439&#34;
	height=&#34;971&#34;
	srcset=&#34;https://blog.qstars.nl/posts/europython-2025/europython-outside_hu_853ffccda1f3defa.jpg 480w, https://blog.qstars.nl/posts/europython-2025/europython-outside_hu_7964999dc7237392.jpg 1024w&#34;
	loading=&#34;lazy&#34;
	
		alt=&#34;Community meetup at Střelecký Island&#34;
	
	
		class=&#34;gallery-image&#34; 
		data-flex-grow=&#34;148&#34;
		data-flex-basis=&#34;355px&#34;
	
&gt;&lt;/p&gt;
&lt;h2 id=&#34;the-origin-story&#34;&gt;The origin story
&lt;/h2&gt;&lt;p&gt;At the closing ceremony, we were treated to a twenty–minute preview of the newly released &lt;a class=&#34;link&#34; href=&#34;https://www.youtube.com/watch?v=GfH4QL4VqJ0&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Python documentary: An origin story&lt;/a&gt;. The film traces Python back to its beginnings, with Guido van Rossum himself narrating how the language emerged during his time at CWI — the Dutch national research institute for mathematics and computer science.&lt;/p&gt;
&lt;p&gt;During the doc, notable figures like Brett Cannon and Benjamin Peterson share their perspectives on contributing to the ecosystem and how much they gained from the commuity. What struck me was how the documentary highlights not just the technical milestones, but also the people who shaped Python’s growth.&lt;/p&gt;
&lt;p&gt;It’s a fascinating look at how a personal project evolved into one of the most ubiquitous programming languages in the world, growing in both maturity and popularity while fostering it&amp;rsquo;s community-driven spirit.&lt;/p&gt;
&lt;h2 id=&#34;closing-words&#34;&gt;Closing words
&lt;/h2&gt;&lt;p&gt;Overall, EuroPython 2025 was a fantastic experience. The event was well organized, the catering was excellent, and the staff were very friendly. Behind the scenes, the conference depends on a dedicated team of volunteers who make everything run seamlessly. A heartfelt thank-you to all of them for giving up their own time to ensure talks and operations went off without a hitch.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://blog.qstars.nl/posts/europython-2025/europython-sprints.jpeg&#34;
	width=&#34;1280&#34;
	height=&#34;2276&#34;
	srcset=&#34;https://blog.qstars.nl/posts/europython-2025/europython-sprints_hu_38d078c5e17d6fe5.jpeg 480w, https://blog.qstars.nl/posts/europython-2025/europython-sprints_hu_3b073f48322c110.jpeg 1024w&#34;
	loading=&#34;lazy&#34;
	
		alt=&#34;Open-source sprint weekend&#34;
	
	
		class=&#34;gallery-image&#34; 
		data-flex-grow=&#34;56&#34;
		data-flex-basis=&#34;134px&#34;
	
&gt;&lt;/p&gt;
</description>
        </item>
        <item>
        <title>Local Kubernetes on Mac: a Multi-Node Cluster with UTM</title>
        <link>https://blog.qstars.nl/posts/macos-local-kubernetes-cluster-with-utm/</link>
        <pubDate>Fri, 11 Jul 2025 09:38:00 +0200</pubDate>
        
        <guid>https://blog.qstars.nl/posts/macos-local-kubernetes-cluster-with-utm/</guid>
        <description>&lt;img src="https://blog.qstars.nl/posts/macos-local-kubernetes-cluster-with-utm/macos-k8s.jpg" alt="Featured image of post Local Kubernetes on Mac: a Multi-Node Cluster with UTM" /&gt;&lt;blockquote&gt;
&lt;p&gt;Today we&amp;rsquo;re going to configure a Kubernetes cluster using virtual machines on our MacOS host. We are going to use UTM which utilises QEMU, a type-2 hypervisor to actually virtualise the new machines. I will be using a 2024 Macbook Pro with the Apple Silicon M4 chip, this guide can also be followed on older M-chips or Intel CPU&amp;rsquo;s.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;virtual-machines-vs-dockerized-kubernetes&#34;&gt;Virtual Machines vs Dockerized Kubernetes
&lt;/h2&gt;&lt;p&gt;There is an abundance of easy to use tools out there to quickly spin up a local Kubernetes cluster. You may have heard of &lt;a class=&#34;link&#34; href=&#34;https://minikube.sigs.k8s.io/docs/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Minikube&lt;/a&gt;, &lt;a class=&#34;link&#34; href=&#34;https://kind.sigs.k8s.io/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;KinD&lt;/a&gt; (Kubernetes-in-Docker) or &lt;a class=&#34;link&#34; href=&#34;https://k3s.io/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;K3S&lt;/a&gt;. Running Kubernetes inside actual VMs simulates a more realistic production environment. Each VM has their own kernel and networking stack, this doesn&amp;rsquo;t just offer a higher level of protection to your host machine it also allows us tighter controls over the OS &amp;amp; Kernel.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re going to be installing the Kubernetes (&lt;code&gt;v1.36&lt;/code&gt;*) cluster using &lt;a class=&#34;link&#34; href=&#34;https://Kubernetes.io/docs/reference/setup-tools/kubeadm/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;kubeadm&lt;/a&gt;, meaning that all of our control plane components are going to be running as &lt;a class=&#34;link&#34; href=&#34;https://kubernetes.io/docs/concepts/workloads/pods/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Pods&lt;/a&gt; on the cluster (in the &lt;code&gt;kube-system&lt;/code&gt; namespace, more about that later).&lt;/p&gt;
&lt;p&gt;&lt;em&gt;*At the time of writing (August 2026), &lt;code&gt;v1.36&lt;/code&gt; is the latest minor release; &lt;code&gt;v1.32&lt;/code&gt;, used when this guide was first written, has since reached &lt;a class=&#34;link&#34; href=&#34;https://kubernetes.io/releases/patch-releases/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;end of life&lt;/a&gt;. Other minor versions can be installed following this guide, you just need to swap out the version numbers where specified. &lt;a class=&#34;link&#34; href=&#34;https://Kubernetes.io/releases/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Learn more&lt;/a&gt; about Kubernetes versioning.&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 This guide&amp;rsquo;s screenshots were captured against Kubernetes v1.32, Alpine 3.21 and Ubuntu Server 24.04. The commands below have since been updated to currently supported versions, so the exact version strings you see on screen may differ slightly from the screenshots — the installation flow itself hasn&amp;rsquo;t changed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;prerequisites&#34;&gt;Prerequisites
&lt;/h2&gt;&lt;p&gt;Don&amp;rsquo;t know your CPU architecture? Run &lt;code&gt;uname -m&lt;/code&gt; in a terminal of your choice.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 Got an Apple Silicon M-chip (&lt;code&gt;arm64&lt;/code&gt;)? Use &lt;code&gt;aarch64&lt;/code&gt; where there is no specific &lt;code&gt;arm64&lt;/code&gt; download available.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;A MacOS machine (Intel or Apple Silicon) running macOS 13 (Ventura) or later — required by current UTM releases&lt;/li&gt;
&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://mac.getutm.app/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;UTM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://ubuntu.com/download/server&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Ubuntu Server&lt;/a&gt; (24.04 LTS or newer — 26.04 LTS is the current release) &amp;amp; &lt;a class=&#34;link&#34; href=&#34;https://alpinelinux.org/downloads/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Alpine Linux Standard&lt;/a&gt; (3.24+) ISO files matching your cpu architecture&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;installation&#34;&gt;Installation
&lt;/h2&gt;&lt;p&gt;This section will cover the installation of three virtual machine types. The &lt;strong&gt;Router&lt;/strong&gt; will provide internet &amp;amp; inter-node connectivity to our Kubernetes cluster, using a router like this will allow us to connect to any internet device without the inter-node network being disrupted (i.e by changing IP addresses or firewall rules).&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;control plane&lt;/strong&gt; and &lt;strong&gt;worker&lt;/strong&gt; node(s) are standard Kubernetes components, in this guide we&amp;rsquo;ll install one worker and one dedicated control plane node. Repeat the installation steps to add more &lt;strong&gt;control plane&lt;/strong&gt; or &lt;strong&gt;worker&lt;/strong&gt; nodes to your cluster for &lt;a class=&#34;link&#34; href=&#34;https://Kubernetes.io/docs/setup/production-environment/tools/kubeadm/high-availability/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;high-avalability&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;❗The amount of RAM (memory) is expressed in &lt;a class=&#34;link&#34; href=&#34;https://nl.wikipedia.org/wiki/Mebibyte&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Mebibytes&lt;/a&gt;. The amount of processor cores refer to &lt;strong&gt;&lt;em&gt;virtual&lt;/em&gt;&lt;/strong&gt; CPU cores assigned to the VM. On Apple Silicon, these are distributed across &lt;a class=&#34;link&#34; href=&#34;https://eclecticlight.co/2024/02/19/apple-silicon-1-cores-clusters-and-performance&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;efficiency and performance cores&lt;/a&gt;, while on Intel Macs, they may correspond to physical or logical &lt;a class=&#34;link&#34; href=&#34;https://en.wikipedia.org/wiki/Hyper-threading&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;hyperthreaded&lt;/a&gt; cores.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;router&#34;&gt;Router
&lt;/h3&gt;&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;&lt;strong&gt;Resource&lt;/strong&gt;&lt;/th&gt;
          &lt;th&gt;Value&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Operating system&lt;/td&gt;
          &lt;td&gt;Alpine Standard 3.24+&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Memory&lt;/td&gt;
          &lt;td&gt;512 Mebibyte&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Processor cores&lt;/td&gt;
          &lt;td&gt;1&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Disk size&lt;/td&gt;
          &lt;td&gt;10 Gigabytes&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Let&amp;rsquo;s get this party started by setting up our Router VM. Press the + icon in UTM and select &lt;strong&gt;Virtualize&lt;/strong&gt;. On the next screen, select &lt;strong&gt;Linux&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.imgur.com/7Il6VE4.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Getting started&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;On the Linux virtualisation page we are going to leave &lt;code&gt;Apple Virtualization&lt;/code&gt; &amp;amp; &lt;code&gt;Boot from kernal image&lt;/code&gt; off. Press &lt;code&gt;Browse...&lt;/code&gt; and select the Alpine ISO we have just downloaded.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.imgur.com/fIUCIsh.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Linux virtualization engine&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;Fill in the memory, CPU core requirements &amp;amp; storage as per the values above. Leave OpenGL acceleration turned off. We are going to skip the &lt;strong&gt;Shared Directory&lt;/strong&gt; page.&lt;/p&gt;
&lt;p&gt;Now you should be greeted with the &lt;strong&gt;Summary&lt;/strong&gt; page, here you can review what we&amp;rsquo;ve just configured before spinning up the vm. Give it a name of your liking and press &lt;strong&gt;Save&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.imgur.com/0xpEuBn.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Summary Page&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;Before we can continue installing Alpine on the machine we have to add a &lt;strong&gt;Serial&lt;/strong&gt; (terminal) output to the vm. Right click on the machine in the lefthand overview and press &lt;strong&gt;Edit&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Head over to the &lt;strong&gt;Devices&lt;/strong&gt; tab, we are going to remove the primary Display that&amp;rsquo;s configured for the machine and instead add a &lt;strong&gt;Serial&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.imgur.com/6HcLux6.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Router devices configuration&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re ready to install Alpine Linux! Hit &lt;strong&gt;Save&lt;/strong&gt; &amp;amp; press the play button to spin up our virtual machine. After waiting for some seconds you&amp;rsquo;ll be greeted with a login screen, login with the &lt;code&gt;root&lt;/code&gt; username and no password.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.imgur.com/M7SVK4U.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Alpine Linux login screen&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;We can begin the installation process by running the &lt;code&gt;setup-alpine&lt;/code&gt; command in the terminal. We&amp;rsquo;ll be prompted by the setup wizard to configure a hostname, use a name that you&amp;rsquo;ll be able to identify the machine with, I choose &lt;code&gt;router&lt;/code&gt;. For the &lt;strong&gt;Interface&lt;/strong&gt; (networking) configuration, you can simply press enter. We want to configure the default interface to be assigned an IP address from our internet modem using &lt;a class=&#34;link&#34; href=&#34;https://en.wikipedia.org/wiki/Dynamic_Host_Configuration_Protocol&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;DHCP&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.imgur.com/SDF7mct.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Alpine initial installation configuration&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;Next, enter a memorable password for the &lt;code&gt;root&lt;/code&gt; user. This is the most privileged user on the machine and it can perform any action, make it secure! After filling in your password you&amp;rsquo;ll be asked what timezone you&amp;rsquo;re from. From this part onwards, you can safely use the default values for sections &lt;strong&gt;Proxy&lt;/strong&gt;, &lt;strong&gt;Network Time Protocol&lt;/strong&gt;, &lt;strong&gt;APK Mirror&lt;/strong&gt; and &lt;strong&gt;User&lt;/strong&gt;. This will leave us to configure the system disk.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.imgur.com/C6jFkZX.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Alpine Linux disk configuration&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;Wait for the installer to finish and then exit out of the vm. We are going to go back into the settings for the vm by right clicking on it in our overview and pressing &lt;strong&gt;Edit&lt;/strong&gt; (if the vm isn&amp;rsquo;t stopped yet, press &lt;strong&gt;Stop&lt;/strong&gt; first to make sure it&amp;rsquo;s turned off!). Now, under the &lt;strong&gt;Drives&lt;/strong&gt; section, select the &lt;strong&gt;USB Drive&lt;/strong&gt; and delete it, this will make sure the installation image is no longer mounted when starting the machine.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.imgur.com/sDEtVi9.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;USB Drive configuration&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;Make sure to save your changes! You&amp;rsquo;ve now installed Alpine Linux on a virtual machine. Turn on the vm and login with your previously configured password for the &lt;code&gt;root&lt;/code&gt; user. We&amp;rsquo;re going to update our virtual machine, but before we can do that we have to make sure we have the right &lt;a class=&#34;link&#34; href=&#34;https://wiki.alpinelinux.org/wiki/Repositories&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;repositories&lt;/a&gt; set.&lt;/p&gt;
&lt;p&gt;In the terminal, run the following command: &lt;code&gt;cat /etc/apk/repositories&lt;/code&gt;
&lt;img src=&#34;https://i.imgur.com/hiD1Mma.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Repositories list&#34;
	
	
&gt;
It&amp;rsquo;s important to set the official Alpine &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;community&lt;/code&gt; repositories as seen above. If you do &lt;strong&gt;not&lt;/strong&gt; see these two repositories, you can add them with the command below.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; -e &lt;span class=&#34;s1&#34;&gt;&amp;#39;http://dl-cdn.alpinelinux.org/alpine/v3.24/main\nhttp://dl-cdn.alpinelinux.org/alpine/v3.24/community&amp;#39;&lt;/span&gt; &amp;gt; /etc/apk/repositories
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Now, we&amp;rsquo;re ready to update our system. Run the &lt;code&gt;apk update &amp;amp;&amp;amp; apk upgrade -y&lt;/code&gt; command and wait for it to complete. We&amp;rsquo;re going to have to completely turn off the vm one last time to edit the network configurations. Our goal is to have our own isolated inter-node Kubernetes network that also has a gateway to the internet.&lt;/p&gt;
&lt;p&gt;To achieve this, we&amp;rsquo;ll have to configure two network devices for our Router. A Kubernetes &lt;strong&gt;Host-only&lt;/strong&gt; network device and one that&amp;rsquo;s set to the &lt;strong&gt;Bridge&lt;/strong&gt; mode. This will allow our Kubernetes nodes to direct their internet requests via the router. Let&amp;rsquo;s dig into the machine configurations first.&lt;/p&gt;
&lt;p&gt;Under &lt;strong&gt;Devices&lt;/strong&gt; you&amp;rsquo;ll see a &lt;strong&gt;Network&lt;/strong&gt; device. Select it and simply change the &lt;strong&gt;Network Mode&lt;/strong&gt; from &lt;em&gt;Shared Network&lt;/em&gt; to &lt;em&gt;Bridged (Advanced)&lt;/em&gt;. Select &lt;em&gt;en0&lt;/em&gt; as the &lt;strong&gt;Bridged Interface&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.imgur.com/WGvysOR.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Bridged network device configuration&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;Add a new Network device and set it&amp;rsquo;s &lt;strong&gt;Network Mode&lt;/strong&gt; to &lt;strong&gt;Host-Only&lt;/strong&gt;. Save your configurations and spin up the machine. After you&amp;rsquo;ve authenticated in the vm, run the &lt;code&gt;ip a&lt;/code&gt; command to list the network devices.
&lt;img src=&#34;https://i.imgur.com/DimX52U.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Network device output&#34;
	
	
&gt;
We can ignore the &lt;code&gt;lo&lt;/code&gt; device, it&amp;rsquo;s irrelevant to us. The &lt;code&gt;eth0&lt;/code&gt; network device already has ip address &lt;code&gt;192.168.0.171&lt;/code&gt; assigned, this is an ip address in my local home network. It&amp;rsquo;s time for us to install some required networking related packages to turn this machine into a router.&lt;/p&gt;
&lt;p&gt;Run the following command to start installing and configuring the necessary packages, and wait for the machine to reboot.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;4
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;5
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;apk add iptables &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;/etc/init.d/iptables save &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;rc-update add iptables &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;net.ipv4.ip_forward = 1&amp;#39;&lt;/span&gt; &amp;gt; /etc/sysctl.d/router.conf &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;reboot
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;We&amp;rsquo;re almost done with the router now. All we need to do is configure the host-only device and give it a static IP in our network. I&amp;rsquo;m going to go with the &lt;code&gt;192.168.100.0/24&lt;/code&gt; subnet, where &lt;code&gt;192.168.100.1&lt;/code&gt; will be our router, and we will increment per Kubernetes node.&lt;/p&gt;
&lt;p&gt;First, let&amp;rsquo;s check in on our current network interfaces configuration. Run the &lt;code&gt;cat /etc/network/interfaces&lt;/code&gt; command.
&lt;img src=&#34;https://i.imgur.com/Y8RfQIq.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Network interfaces&#34;
	
	
&gt;
As you can see, two of our network devices have default configurations to request IP addresses. The &lt;code&gt;lo&lt;/code&gt;opback interface lets the system communicate with itself using IP address &lt;code&gt;127.0.0.1&lt;/code&gt;, it allows the system to talk to itself without needing a physical network device.&lt;/p&gt;
&lt;p&gt;In &lt;strong&gt;Bridged mode&lt;/strong&gt;, the &lt;code&gt;eth0&lt;/code&gt; interface acts like a physical device directly connected to your home network. This means it gets an IP address from your home router’s &lt;strong&gt;DHCP server&lt;/strong&gt;, just like any other device on your network (e.g., your laptop or phone).&lt;/p&gt;
&lt;p&gt;As you can see, there is no entry for the &lt;code&gt;eth1&lt;/code&gt; interface yet. This is the also the reason why we didn&amp;rsquo;t see an IP address assigned to the &lt;code&gt;eth1&lt;/code&gt; interface previously. Let&amp;rsquo;s set it up!&lt;/p&gt;
&lt;p&gt;Run the following command in your terminal to set default configurations for &lt;code&gt;eth1&lt;/code&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;printf&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;auto eth1\niface eth1 inet static\n\taddress 192.168.100.1\n\tnetmask 255.255.255.0\n&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; /etc/network/interfaces
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Verify that our &lt;code&gt;eth1&lt;/code&gt; entry is now added to &lt;code&gt;/etc/network/interfaces&lt;/code&gt;
&lt;img src=&#34;https://i.imgur.com/mPCTHEm.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Interfaces file with eth1 setup&#34;
	
	
&gt;
Apply the configuration by running &lt;code&gt;rc-service networking restart&lt;/code&gt;.
&lt;img src=&#34;https://i.imgur.com/wfJNF8h.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;rc-service network restart&#34;
	
	
&gt;
Voilà! You now have an Alpine Linux router virtual machine. Everything has been set up as it should, to verify our &lt;code&gt;eth1&lt;/code&gt; interface now has a static ip address of &lt;code&gt;192.168.100.1&lt;/code&gt;&lt;em&gt;/24&lt;/em&gt;, run the &lt;code&gt;ip a&lt;/code&gt; command one last time.
&lt;img src=&#34;https://i.imgur.com/RLJpcbc.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Ip address overview&#34;
	
	
&gt;
To quickly recap: We&amp;rsquo;ve just installed Alpine Linux on a virtual machine, configured the box to allow the forwarding of network packets from our &lt;strong&gt;Host-Only&lt;/strong&gt; network to our &lt;strong&gt;Bridged&lt;/strong&gt; network and ultimately the internet.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;ℹ️ Make sure you always have the router running alongside your Kubernetes control plane nodes. The machines that make up your Kubernetes cluster live inside the &lt;strong&gt;Host-Only&lt;/strong&gt; network on your Mac, allowing them to talk to eachother but not your home router for internet access. If you notice an issue with internet connectivity, restarting the router is a great troubleshooting starting point.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It&amp;rsquo;s time for us to move on to the first Kubernetes node.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;🛜 Switching Wi-Fi networks (e.g Office to Home)? Either run &lt;code&gt;rc-service networking restart&lt;/code&gt; or reboot the router to make sure it&amp;rsquo;s assigned a new IP and connected to the new router.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;control-plane&#34;&gt;Control plane
&lt;/h3&gt;&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;&lt;strong&gt;Resource&lt;/strong&gt;&lt;/th&gt;
          &lt;th&gt;Value&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Operating system&lt;/td&gt;
          &lt;td&gt;Ubuntu Server 24.04+&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Memory&lt;/td&gt;
          &lt;td&gt;2048 Mebibyte&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Processor cores&lt;/td&gt;
          &lt;td&gt;2&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Disk size&lt;/td&gt;
          &lt;td&gt;30 Gigabytes&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;In Kubernetes, a node refers to a machine running a Kubernetes distribution. You have control plane and worker nodes, we&amp;rsquo;re going to start off with the control plane as it&amp;rsquo;s the brains of the operation and requires the most installation efforts of the two.&lt;/p&gt;
&lt;p&gt;Start by repeating the three first steps to setting up a new virtual machine. Give it a name, select the operating system installation image (be sure to select Ubuntu!) and set the CPU, memory and disk specifications as outlined above.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.imgur.com/CqymX29.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Ubuntu control plane configuration overview&#34;
	
	
&gt;
Toggle the &lt;strong&gt;Open VM Settings&lt;/strong&gt; checkbox and hit &lt;strong&gt;Save&lt;/strong&gt;. In the machine settings, swap out the &lt;strong&gt;Display&lt;/strong&gt; for a &lt;strong&gt;Serial&lt;/strong&gt; again, and set the &lt;strong&gt;Network&lt;/strong&gt; device to &lt;strong&gt;Host-Only&lt;/strong&gt; mode. Save your settings and spin up the control plane.&lt;/p&gt;
&lt;p&gt;At first you will be greeted with some fairly standard installation options, continue the installation in &lt;strong&gt;rich mode&lt;/strong&gt; before selecting your language and geographical location.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.imgur.com/Sxq0EIn.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Ubuntu installer&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;When asked what base you&amp;rsquo;d like to use, keep the default &lt;strong&gt;Ubuntu Server&lt;/strong&gt; base.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.imgur.com/EgEqyIX.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Ubuntu base installation selection&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;The next screen is important, it&amp;rsquo;s the network configuration for our control plane. Select the network device, it should only be one as we configured the default device to sit in the &lt;strong&gt;Host-Only&lt;/strong&gt; network. In the new dropdown box select &lt;strong&gt;Edit IPv4&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.imgur.com/kPy1RyA.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Ubuntu network configuration&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;Set the &lt;strong&gt;IPv4 Method&lt;/strong&gt; to &lt;strong&gt;Manual&lt;/strong&gt; and copy the values you see below. This will assign the control plane IP address &lt;code&gt;192.168.100.2&lt;/code&gt; and use our router &lt;code&gt;192.168.100.1&lt;/code&gt; as it&amp;rsquo;s &lt;a class=&#34;link&#34; href=&#34;https://en.wikipedia.org/wiki/Gateway_address&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;gateway address&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.imgur.com/YYfbVXZ.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Ubuntu ipv4 configuration&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;Before hitting &lt;strong&gt;Save&lt;/strong&gt;, in the &lt;code&gt;Nameservers&lt;/code&gt; tab, fill in &lt;code&gt;1.1.1.1&lt;/code&gt;. Now you can save the network configuration! For the next three steps up until &lt;strong&gt;Profile configuration&lt;/strong&gt; you can safely use the default values.&lt;/p&gt;
&lt;p&gt;In the profile configuration, set the machine&amp;rsquo;s name to &lt;strong&gt;controlplane&lt;/strong&gt;. Fill in your own username for the user account along with a secure password.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.imgur.com/IZH5bG7.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Ubuntu profile configuration&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;The next pages can safely be skipped using the default configurations. Wait for the system installer to complete before &lt;strong&gt;fully turning off the machine&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.imgur.com/vptXlnm.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Ubuntu system installer progress&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;Once the vm is fully turned off, edit it and remove the &lt;strong&gt;USB Drive&lt;/strong&gt; from it&amp;rsquo;s attached &lt;strong&gt;Devices&lt;/strong&gt; again. Afterwards turn on the control plane again and login using your previously chosen username and password.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re going to need access to the entire system while installing Kubeadm. To get privileged access to the machine, switch to the &lt;code&gt;root&lt;/code&gt; user by running &lt;code&gt;sudo su -&lt;/code&gt; and typing in your password.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s test our internet connectivity by saying hello to google.com using a &lt;code&gt;ping&lt;/code&gt;.
&lt;img src=&#34;https://i.imgur.com/Y0ZcHVf.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Google ping test&#34;
	
	
&gt;
Beautiful! Our control plane machine was able to find &lt;code&gt;google.com&lt;/code&gt; on the internet when the machine itself doesn&amp;rsquo;t have an internet connection. The control plane uses the router to access the internet. To test this, fully shutdown the router and run the same &lt;code&gt;ping&lt;/code&gt; command again.
&lt;img src=&#34;https://i.imgur.com/DYWMW2F.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Ping command without internet&#34;
	
	
&gt;
As you can see, the machine cannot even resolve an ip address for &lt;code&gt;google.com&lt;/code&gt;, this is because the nameserver we have configured is Cloudflare&amp;rsquo;s &lt;code&gt;1.1.1.1&lt;/code&gt;, in order for us to resolve a new domain name we need an internet connection in order to do so.&lt;/p&gt;
&lt;p&gt;With the network sorted we can pivot our attention to setting up the node for Kubernetes. Run the following command to set up the required system settings &amp;amp; kernel modules.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;4
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;5
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;6
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;7
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo swapoff -a &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo sed -i &lt;span class=&#34;s1&#34;&gt;&amp;#39;/\/swap.img/ s/^/#/&amp;#39;&lt;/span&gt; /etc/fstab &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo systemctl mask swap.img.swap &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo sysctl -w net.ipv4.ip_forward&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;1&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;net.ipv4.ip_forward=1&amp;#39;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; sudo tee -a /etc/sysctl.conf &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo sysctl --system &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo modprobe overlay &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo modprobe br_netfilter
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;We&amp;rsquo;ve enabled &lt;strong&gt;IPv4 Forwarding&lt;/strong&gt;, the &lt;strong&gt;overlay&lt;/strong&gt; &amp;amp; &lt;strong&gt;br_netfilter&lt;/strong&gt; kernel modules and disabled memory &lt;a class=&#34;link&#34; href=&#34;https://en.wikipedia.org/wiki/Memory_paging&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;swapping&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s not all though, we need to install a &lt;a class=&#34;link&#34; href=&#34;https://Kubernetes.io/docs/concepts/architecture/cri/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Container Runtime Interface&lt;/a&gt; before we can bootstrap a Kubernetes cluster on the machine. We&amp;rsquo;ll use &lt;a class=&#34;link&#34; href=&#34;https://containerd.io/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;containerd&lt;/a&gt;, you can install containerd from source or using a package repository. We&amp;rsquo;re going to use the latter.&lt;/p&gt;
&lt;p&gt;The containerd team does not publish their updates to package repositories directly, this is done by the Docker team. Docker uses containerd internally and they also publish the apt packages. This means that we&amp;rsquo;re going to have to add the Docker apt repository as a source for our machine to find packages from.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt; 1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 3
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 4
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 5
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 6
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 7
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 8
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 9
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo apt-get update &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo apt-get install -y apt-transport-https ca-certificates curl gpg &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo install -m &lt;span class=&#34;m&#34;&gt;0755&lt;/span&gt; -d /etc/apt/keyrings &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo chmod a+r /etc/apt/keyrings/docker.asc &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;s2&#34;&gt;&amp;#34;deb [arch=&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;dpkg --print-architecture&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt; signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s2&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;. /etc/os-release &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;UBUNTU_CODENAME&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;:-&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$VERSION_CODENAME&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt; stable&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo apt-get update
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;That was a long command! It pulled Docker&amp;rsquo;s GPG key that&amp;rsquo;ll be used to check if packages we download &lt;em&gt;really&lt;/em&gt; come from Docker, afterwards we update the available package list with an &lt;code&gt;apt-get update&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Now the &lt;code&gt;containerd.io&lt;/code&gt; package should be available for us to be installed, run &lt;code&gt;apt install containerd.io&lt;/code&gt; to install it!&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.imgur.com/qlHh5IX.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Containerd apt installation&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;Verify that the installation went successful by running.&lt;code&gt;systemctl status containerd&lt;/code&gt;, you should see the service is in an &lt;strong&gt;active (running)&lt;/strong&gt; state.
&lt;img src=&#34;https://i.imgur.com/cBZGmGS.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Containerd system service status&#34;
	
	
&gt;
&lt;em&gt;Do you see our previously installed kernel driver being executed? &lt;code&gt;ExecStartPre=/sbin/modprobe&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Run the following commands to generate and tweak the default containerd configuration.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;containerd config default &amp;gt; /etc/containerd/config.toml &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo sed -i &lt;span class=&#34;s1&#34;&gt;&amp;#39;s/SystemdCgroup = false/SystemdCgroup = true/&amp;#39;&lt;/span&gt; /etc/containerd/config.toml &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo systemctl restart containerd
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;With our CRI installed we&amp;rsquo;ve finally completed the necessary prerequisites to transform this vm into a Kubernetes control plane, so let&amp;rsquo;s do that! Just like with the Docker repository previously, we have to download the official Kubernetes apt repository GPG key to verify we are installing the legitimate packages. Run the following command to do so;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.36/deb/Release.key &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; sudo gpg --dearmor -o /etc/apt/keyrings/Kubernetes-apt-keyring.gpg &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;deb [signed-by=/etc/apt/keyrings/Kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.36/deb/ /&amp;#39;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; sudo tee /etc/apt/sources.list.d/Kubernetes.list &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo apt-get update
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;We&amp;rsquo;ve now updated apt to also look for packages inside the Kubernetes repository. To check what packages and their versions are available to us, run &lt;code&gt;apt-cache madison &amp;lt;package&amp;gt;&lt;/code&gt;. Where &lt;code&gt;&amp;lt;package&amp;gt;&lt;/code&gt; can either be &lt;code&gt;kubeadm&lt;/code&gt;, &lt;code&gt;kubelet&lt;/code&gt; or &lt;code&gt;kubectl&lt;/code&gt; (we&amp;rsquo;re going to be installing all three!).
&lt;img src=&#34;https://i.imgur.com/mrojCco.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;All available kubeadm versions&#34;
	
	
&gt;
&lt;img src=&#34;https://i.imgur.com/kuJHjg2.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;All available kubelet versions&#34;
	
	
&gt;
These packages follow the &lt;a class=&#34;link&#34; href=&#34;https://semver.org/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Semantic Versioning&lt;/a&gt; standard. We have three packages with major release &lt;code&gt;1&lt;/code&gt;, minor release &lt;code&gt;36&lt;/code&gt; with a handful of varying patch versions. We aren&amp;rsquo;t too worried about the specific patches, let&amp;rsquo;s just go with the latest one available (&lt;code&gt;1.36.2&lt;/code&gt; at the time of writing).&lt;/p&gt;
&lt;p&gt;Run the &lt;code&gt;sudo apt install kubelet kubeadm kubectl -y&lt;/code&gt; command to install the required Kubernetes binaries. Afterwards, pin their versions to make sure you don&amp;rsquo;t accidentally upgrade any of them using &lt;code&gt;sudo apt-mark hold kubelet kubeadm kubectl&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;(Optional) Let&amp;rsquo;s quickly verify the versions of the installed components directly.
&lt;img src=&#34;https://i.imgur.com/wRrZdVc.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Kubeadm version command&#34;
	
	
&gt;&lt;img src=&#34;https://i.imgur.com/JQGSlLJ.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Kubelet version command&#34;
	
	
&gt;
&lt;img src=&#34;https://i.imgur.com/DM0kjr5.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Kubectl version command&#34;
	
	
&gt;
Hm&amp;hellip; the &lt;strong&gt;kubectl&lt;/strong&gt; command shows us two versions and a connection error. Thankfully we see that the &lt;strong&gt;Client Version&lt;/strong&gt; is set to our target version. The client version refers to the version of &lt;strong&gt;kubectl&lt;/strong&gt; we have installed, which as per our apt command earlier is &lt;code&gt;1.36.2&lt;/code&gt;. &lt;a class=&#34;link&#34; href=&#34;https://kustomize.io/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Kustomize&lt;/a&gt; is a Kubernetes configuration manager which allows you to update manifest files &lt;strong&gt;&lt;em&gt;without&lt;/em&gt;&lt;/strong&gt; a templating language like &lt;a class=&#34;link&#34; href=&#34;https://helm.sh/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Helm&lt;/a&gt;&amp;rsquo;s chart templates. This isn&amp;rsquo;t relevant to our installation however, so we can safely ignore the Kustomize version.&lt;/p&gt;
&lt;p&gt;That leaves us with a connection refused. &lt;strong&gt;Kubectl&lt;/strong&gt; automatically tries to connect to the &lt;a class=&#34;link&#34; href=&#34;https://Kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;kube-apiserver&lt;/a&gt; on &lt;code&gt;localhost:8080&lt;/code&gt; if no alternative destination is configured. In our case this fails because we haven&amp;rsquo;t got the &lt;a class=&#34;link&#34; href=&#34;https://Kubernetes.io/docs/concepts/overview/components/#control-plane-components&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;control plane components&lt;/a&gt; running yet.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s finally time for us to bootstrap a Kubernetes cluster using kubeadm. Let me introduce you to two very important initialization flags. The &lt;code&gt;--service-cidr&lt;/code&gt; flag lets you set a subnet that Kubernetes will use for all of its services. The &lt;code&gt;--pod-network-cidr&lt;/code&gt; lets you specify a subnet that will be used to assign IP addresses to pods.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s important that both of these subnets are not being used on your network! Thankfully, our Kubernetes cluster lives inside our Mac&amp;rsquo;s &lt;strong&gt;Host-Only&lt;/strong&gt; network and does not automatically have access to a wider network, limiting the chance for a collision, however it&amp;rsquo;s still a good idea to decide on a dedicated subnet. I am going to use the default service cidr, and I&amp;rsquo;m going to specify &lt;code&gt;192.168.0.0/16&lt;/code&gt; as the pod network cidr.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;🛜 Be careful and don&amp;rsquo;t randomly assign a subnet to your cluster! It&amp;rsquo;s best to use a private subnet as your pod cidr, as per &lt;a class=&#34;link&#34; href=&#34;https://en.wikipedia.org/wiki/Private_network&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;RFC 1918&lt;/a&gt; &lt;code&gt;192.168.0.0/16&lt;/code&gt; is a private subnet. &lt;code&gt;10.96.0.0/12&lt;/code&gt; (default service cidr) is &lt;em&gt;technically&lt;/em&gt; not private, but practically it is so we&amp;rsquo;re fine with keeping the default.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That leaves us to run the &lt;code&gt;kubeadm init --pod-network-cidr=192.168.0.0/16&lt;/code&gt; command to get our cluster started.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ Having issues getting &lt;code&gt;kubeadm init&lt;/code&gt; to work? Check if &lt;strong&gt;cri&lt;/strong&gt; isn&amp;rsquo;t a disabled plugin in containerd, run &lt;code&gt;cat /etc/containerd/config.toml | grep disabled_plugins&lt;/code&gt;. If you see &lt;code&gt;disabled_plugins = [&amp;quot;cri&amp;quot;]&lt;/code&gt;, scroll down to the &lt;strong&gt;Troubleshooting &amp;gt; Containerd CRI Disabled Plugin&lt;/strong&gt; section of this post before retrying the init command and continuing.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If all is well, your terminal output should look something like this;
&lt;img src=&#34;https://i.imgur.com/4uarqT5.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Kubeadm init command stdout&#34;
	
	
&gt;
That&amp;rsquo;s a lot of information, let&amp;rsquo;s laser in on what we actually need.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.imgur.com/RZ6Zpn8.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Kubeadm init thankyou output&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;Our main area of concern here is the long &lt;code&gt;kubeadm join&lt;/code&gt; command at the very bottom, save this in a note. As you can see, it points to our control plane machine &lt;code&gt;192.168.100.2&lt;/code&gt; at port &lt;code&gt;6443&lt;/code&gt;. We haven&amp;rsquo;t explicitly opened a port, or a ran an application that binds to that port ourselves&amp;hellip; Run the &lt;code&gt;export KUBECONFIG=/etc/kubernetes/admin.conf&lt;/code&gt; command followed by &lt;code&gt;kubectl get pods -A&lt;/code&gt;.
&lt;img src=&#34;https://i.imgur.com/ku6CkP0.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Kubernetes system pods&#34;
	
	
&gt;
We just ran the command to list all pods in every namespace. As we&amp;rsquo;ve just bootstrapped this cluster using kubeadm, our control plane components are ran as pods inside the &lt;code&gt;kube-system&lt;/code&gt; namespace. As you can see, the &lt;code&gt;coredns&lt;/code&gt; pods are in a &lt;strong&gt;Pending&lt;/strong&gt; state. Let&amp;rsquo;s have a look at how our control plane node is doing.
&lt;img src=&#34;https://i.imgur.com/WwR1sMk.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Kubernetes node list&#34;
	
	
&gt;
Our control plane node has a status of &lt;strong&gt;Not Ready&lt;/strong&gt;, this is because CoreDNS is not running. Our CoreDNS pods will stay in this pending state until we have installed a &lt;a class=&#34;link&#34; href=&#34;https://github.com/containernetworking/cni&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Container Networking Interface&lt;/a&gt;. There&amp;rsquo;s a couple of options to choose from such as; &lt;a class=&#34;link&#34; href=&#34;https://github.com/flannel-io/flannel&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Flannel&lt;/a&gt;, &lt;a class=&#34;link&#34; href=&#34;https://github.com/projectcalico/calico&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Calico&lt;/a&gt; and &lt;a class=&#34;link&#34; href=&#34;https://github.com/cilium/cilium&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Cilium&lt;/a&gt;. Pick whichever one you prefer, I&amp;rsquo;m going to pick Cilium.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;ℹ️ The Flannel CNI is only a layer 3 networking solution for Kubernetes. Many Kubernetes features such as network policies are not supported and do not take effect when using Flannel.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Though we can install Cilium using Kubernetes manifest files, they have made it easy for us to get everything set-up with their own installer. Let&amp;rsquo;s install the Cilium CLI.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 Preparing for the CKS exam? Cilium is the CNI used on the exam, by installing Cilium in your local cluster you can fully utilise it&amp;rsquo;s features such as the &lt;a class=&#34;link&#34; href=&#34;https://cilium.io/blog/2020/11/10/ebpf-future-of-networking/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;eBPF&lt;/a&gt; based network stack, &lt;a class=&#34;link&#34; href=&#34;https://docs.cilium.io/en/latest/security/policy/index.html&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Cilium Network Policies&lt;/a&gt; (layer 3, 4 &amp;amp; 7) and &lt;a class=&#34;link&#34; href=&#34;https://docs.cilium.io/en/latest/network/servicemesh/mutual-authentication/mutual-authentication/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Mutual Authentication&lt;/a&gt; (mTLS). Keep your eyes on the &lt;a class=&#34;link&#34; href=&#34;https://github.com/cncf/curriculum/tree/master&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;exam curriculum&lt;/a&gt; to stay up to date.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;4
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;5
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;6
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;CILIUM_CLI_VERSION&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;curl -s https://raw.githubusercontent.com/cilium/cilium-cli/main/stable.txt&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;CLI_ARCH&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;amd64
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;[&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;uname -m&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;aarch64&amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;then&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;CLI_ARCH&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;arm64&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;curl -L --fail --remote-name-all https://github.com/cilium/cilium-cli/releases/download/&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;CILIUM_CLI_VERSION&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;/cilium-linux-&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;CLI_ARCH&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;.tar.gz&lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;,.sha256sum&lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sha256sum --check cilium-linux-&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;CLI_ARCH&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;.tar.gz.sha256sum
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo tar xzvfC cilium-linux-&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;CLI_ARCH&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;.tar.gz /usr/local/bin
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;rm cilium-linux-&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;CLI_ARCH&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;.tar.gz&lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;,.sha256sum&lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;We now have access to the command &lt;code&gt;cilium&lt;/code&gt;. We can install Cilium onto our cluster using the &lt;code&gt;cilium install&lt;/code&gt; command. I am going to install it with the SPIRE server enabled which is required for mTLS.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;cilium install &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;--set authentication.mutual.spire.enabled&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;true&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;--set authentication.mutual.spire.install.enabled&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;img src=&#34;https://i.imgur.com/SP7yDFH.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Cilium install command&#34;
	
	
&gt;
So far so good, let&amp;rsquo;s check on our Cilium installation with the &lt;code&gt;cilium status&lt;/code&gt; command.
&lt;img src=&#34;https://i.imgur.com/QBQ5oc6.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Cilium status&#34;
	
	
&gt;
Everything works! We can verify this one more time by running &lt;code&gt;kubectl get pods -A&lt;/code&gt; again.
&lt;img src=&#34;https://i.imgur.com/NmRLB7r.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Kubernetes pod list with cilium&#34;
	
	
&gt;
Look at that! Cilium is installed and is running it&amp;rsquo;s pods, our CoreDNS pods are now also in a &lt;strong&gt;Running&lt;/strong&gt; state. Let&amp;rsquo;s check on our node again to see if it&amp;rsquo;s changed it&amp;rsquo;s status using &lt;code&gt;kubectl get nodes&lt;/code&gt;.
&lt;img src=&#34;https://i.imgur.com/haxLc3w.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Control plane ready status&#34;
	
	
&gt;
Perfect! Our cluster is up and running! Remember that &lt;code&gt;kubectl version&lt;/code&gt; command that was giving us the connection error from before?
&lt;img src=&#34;https://i.imgur.com/mC3sRmP.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Kubectl version command working as intended&#34;
	
	
&gt;
No more connection issues, we now have a working kube-apiserver to handle the request. However, in it&amp;rsquo;s current state our cluster isn&amp;rsquo;t going to run any of our applications just yet.
&lt;img src=&#34;https://i.imgur.com/2itQIQ3.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Kubectl create nginx pod&#34;
	
	
&gt;
&lt;img src=&#34;https://i.imgur.com/TKwPtdX.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Kubectl nginx pod pending&#34;
	
	
&gt;
Our pod is stuck in a &lt;strong&gt;Pending&lt;/strong&gt; state. This is because our control plane machine has a &lt;a class=&#34;link&#34; href=&#34;https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;taint&lt;/a&gt; on it that prevents us to accidentally schedule a workload on the control plane. It&amp;rsquo;s a best practice to keep your control plane isolated from any of your other applications, both for cluster security and to ensure your control plane has enough resources to function. Control planes come with the &lt;code&gt;node-role.kubernetes.io/control-plane&lt;/code&gt; taint which has a &lt;code&gt;NoSchedule&lt;/code&gt; effect.
&lt;img src=&#34;https://i.imgur.com/vtfQ9Dq.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Node taint&#34;
	
	
&gt;
If we really wanted to, we can remove this taint and all of our applications are able to be scheduled freely on the control plane. You can also choose to control what applications can run on the control plane by giving it a toleration.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re going to follow the best practice and leave the control plane as is, it&amp;rsquo;s time to set up a worker machine.&lt;/p&gt;
&lt;h3 id=&#34;workers&#34;&gt;Worker(s)
&lt;/h3&gt;&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;&lt;strong&gt;Resource&lt;/strong&gt;&lt;/th&gt;
          &lt;th&gt;Value&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Operating system&lt;/td&gt;
          &lt;td&gt;Ubuntu Server 24.04+&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Memory&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;1024&lt;/strong&gt; - 4096 Mebibyte&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Processor cores&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;1&lt;/strong&gt; - 4&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Disk size&lt;/td&gt;
          &lt;td&gt;30+ Gigabytes&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Our worker nodes are going to be the ones actually running our applications. Usually your worker nodes are bigger and more powerful than your control plane node, as in my case I am not going to be hosting any large applications on my cluster I have equipped my worker node with the minimal needed system resources. If you need a more powerful test environment, consider allocating more system resources to your workers.&lt;/p&gt;
&lt;p&gt;In UTM, create a new virtual machine with the same Ubuntu base image as the control plane node. Remove the &lt;strong&gt;Display&lt;/strong&gt; from the &lt;strong&gt;Devices&lt;/strong&gt; section and don&amp;rsquo;t forget to change the &lt;strong&gt;Network&lt;/strong&gt; to &lt;strong&gt;Host-Only&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.imgur.com/r6cWndM.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Worker machine configuration overview&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;Spin up the machine and follow the same installation steps as the control plane. Don&amp;rsquo;t forget to change the assigned IP &lt;strong&gt;Address&lt;/strong&gt; in the network tab to &lt;code&gt;192.168.100.3&lt;/code&gt;. Set the same Cloudflare &lt;code&gt;1.1.1.1&lt;/code&gt; nameserver.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.imgur.com/Q3UmSOm.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Network configuration&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;You can safely use the defaults for all the installation steps, just fill in the device &lt;strong&gt;Profile&lt;/strong&gt; section with your username and password again.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.imgur.com/BtWtRJ3.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Ubuntu profile configuration&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;After completing the installation wizard, fully turn off the VM and remove the &lt;strong&gt;USB Drive&lt;/strong&gt; from it&amp;rsquo;s &lt;strong&gt;Devices&lt;/strong&gt;, remember? Start up the machine again and switch to the root user &lt;code&gt;sudo su -&lt;/code&gt;. Update the system packages &lt;code&gt;apt update &amp;amp;&amp;amp; apt upgrade -y&lt;/code&gt;.
&lt;img src=&#34;https://i.imgur.com/ZA8q1Nh.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Apt update command&#34;
	
	
&gt;
To get our worker to join the cluster, we need to perform the same tweaks and installations as the control plane node. Copy and paste the following command to enable kernel modules, turn off swap, allow for ip forwarding and install the Kubernetes distribution binaries &lt;em&gt;(I&amp;rsquo;ve chained all of our previous commands into one!)&lt;/em&gt;.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt; 1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 3
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 4
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 5
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 6
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 7
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 8
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 9
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;10
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;11
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;12
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;13
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;14
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;15
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;16
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;17
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;18
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;19
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;20
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;21
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;22
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;23
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo swapoff -a &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo sed -i &lt;span class=&#34;s1&#34;&gt;&amp;#39;/ swap / s/^/#/&amp;#39;&lt;/span&gt; /etc/fstab &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo sysctl -w net.ipv4.ip_forward&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;1&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;net.ipv4.ip_forward=1&amp;#39;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; sudo tee -a /etc/sysctl.conf &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo sysctl --system &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo modprobe overlay &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo modprobe br_netfilter &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo apt-get update &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo apt-get install -y apt-transport-https ca-certificates curl gpg &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo install -m &lt;span class=&#34;m&#34;&gt;0755&lt;/span&gt; -d /etc/apt/keyrings &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo chmod a+r /etc/apt/keyrings/docker.asc &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;s2&#34;&gt;&amp;#34;deb [arch=&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;dpkg --print-architecture&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt; signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s2&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;. /etc/os-release &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;UBUNTU_CODENAME&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;:-&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$VERSION_CODENAME&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt; stable&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.36/deb/Release.key &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; sudo gpg --dearmor -o /etc/apt/keyrings/Kubernetes-apt-keyring.gpg &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;deb [signed-by=/etc/apt/keyrings/Kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.36/deb/ /&amp;#39;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; sudo tee /etc/apt/sources.list.d/Kubernetes.list &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo apt-get update &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo apt install containerd.io kubeadm kubelet kubectl -y &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;containerd config default &amp;gt; /etc/containerd/config.toml &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo sed -i &lt;span class=&#34;s1&#34;&gt;&amp;#39;s/SystemdCgroup = false/SystemdCgroup = true/&amp;#39;&lt;/span&gt; /etc/containerd/config.toml &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo systemctl restart containerd kubelet
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Remember that &lt;code&gt;kubeadm join&lt;/code&gt; command you saved from before? We&amp;rsquo;ll run it on our worker node to join the cluster (you can omit the &lt;code&gt;--v=5&lt;/code&gt; flag).
&lt;img src=&#34;https://i.imgur.com/iWsHIP6.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Kubeadm join output&#34;
	
	
&gt;
Success! Our worker node has joined the cluster according to kubeadm, on our control plane we can run &lt;code&gt;kubectl get nodes&lt;/code&gt; to have a look at the worker status.
&lt;img src=&#34;https://i.imgur.com/4d5PyZE.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Kubectl node list&#34;
	
	
&gt;
We can see our worker node has joined the cluster and is ready! By default, when joining a cluster, the &lt;code&gt;kubeadm join&lt;/code&gt; command does not assign a role label to nodes. If you would like to explicitly label your node with the worker role run &lt;code&gt;kubectl label node worker node-role.kubernetes.io/worker=&lt;/code&gt;. Functionally this doesn&amp;rsquo;t make a difference for us, but it&amp;rsquo;s nice to have.
&lt;img src=&#34;https://i.imgur.com/ZD31ojS.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Kubernetes node list with worker labelled&#34;
	
	
&gt;
You can substitute the role of your node with anything you like. This allows you to dedicate nodes per role in your landscape. Instead of categorising your nodes as control plane &amp;amp; worker nodes, you can separate workers based on their workloads (i.e database, backend, etc.)
&lt;img src=&#34;https://i.imgur.com/hD8vnMm.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Kubernetes node list with a custom worker label&#34;
	
	
&gt;
Running &lt;code&gt;kubectl get pods -A -o custom-columns=&amp;quot;NAME:.metadata.name,NODE:.spec.nodeName,STATUS:.status.phase&lt;/code&gt; will show us all of the pods running on the system and on which node they have been scheduled.
&lt;img src=&#34;https://i.imgur.com/MU0sMKF.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Kubernetes pod list&#34;
	
	
&gt;
We can see that the necessary &lt;strong&gt;Cilium&lt;/strong&gt; and &lt;strong&gt;kube-proxy&lt;/strong&gt; pods are running on the new worker node. And look at that, our &lt;strong&gt;nginx&lt;/strong&gt; image is now out of it&amp;rsquo;s limbo state and is running on the worker node.&lt;/p&gt;
&lt;p&gt;🎉 Congratulations! You now have a fully functioning, host-isolated Kubernetes cluster based on virtual machines. Adding more workers or control planes for &lt;a class=&#34;link&#34; href=&#34;https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/ha-topology/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;high availability&lt;/a&gt; is as easy as just repeating the steps we&amp;rsquo;ve taken previously. Just make sure you add the &lt;code&gt;--control-plane&lt;/code&gt; flag on the &lt;code&gt;kubeadm join&lt;/code&gt; command for additional control planes.&lt;/p&gt;
&lt;h2 id=&#34;challenge-connect-your-mac-to-the-isolated-kubernetes-cluster&#34;&gt;Challenge: Connect Your Mac to the Isolated Kubernetes Cluster
&lt;/h2&gt;&lt;p&gt;Your cluster is now up and running inside its own host-only network—completely isolated from your Mac and the rest of your home network. But can you bridge the gap and manage your cluster directly from macOS?&lt;/p&gt;
&lt;p&gt;Right now, &lt;code&gt;kubectl&lt;/code&gt; only works inside the control plane VM. Your mission: configure secure connectivity so you can run &lt;code&gt;kubectl&lt;/code&gt; from your Mac, just like you would with any remote cluster.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hints to get started:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You&amp;rsquo;ll need to copy the &lt;code&gt;admin.conf&lt;/code&gt; kubeconfig file from your control plane VM to your Mac.&lt;/li&gt;
&lt;li&gt;The Kubernetes API server (&lt;code&gt;kube-apiserver&lt;/code&gt;) is only accessible from inside the host-only network. Consider using SSH port forwarding, a VPN tunnel or setting up the required internal networking routes to expose it to your Mac.&lt;/li&gt;
&lt;li&gt;Make sure your Mac can reach the control plane node&amp;rsquo;s IP (e.g., &lt;code&gt;192.168.100.2:6443&lt;/code&gt;), but don&amp;rsquo;t expose it to your entire home network for security reasons.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Can you set up seamless, secure access? Give it a try and unlock full control of your cluster from your Mac!&lt;/p&gt;
&lt;h2 id=&#34;troubleshooting&#34;&gt;Troubleshooting
&lt;/h2&gt;&lt;h3 id=&#34;node-stuck-on-notready-after-reboot--persistent-swap-issues&#34;&gt;Node stuck on NotReady after reboot / Persistent swap issues
&lt;/h3&gt;&lt;p&gt;Restarted your worker node and it&amp;rsquo;s status stays stuck on &lt;strong&gt;NotReady&lt;/strong&gt;? It could be that you still have swap enabled on your machine(s). If you followed our steps before, you probably have swap disabled in the usual locations but it doesn&amp;rsquo;t hurt to check!&lt;/p&gt;
&lt;p&gt;Have a look into at the Kubelet logs with &lt;code&gt;journalctl -n 10 -u kubelet --no-pager&lt;/code&gt;.
&lt;img src=&#34;https://i.imgur.com/W5srfT9.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Kubelet logs&#34;
	
	
&gt;At the bottom we see the error &lt;strong&gt;running with swap on is not supported, please disable swap or set &amp;ndash;fail-swap-on flag to false&lt;/strong&gt;. This means we still have swapping enabled on our system. First and foremost, temporarily disable swap with &lt;code&gt;sudo swapoff -a&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We need to make sure all of the &lt;strong&gt;swap&lt;/strong&gt; entries in &lt;code&gt;/etc/fstab&lt;/code&gt; are commented out.
&lt;img src=&#34;https://i.imgur.com/St6Ip7u.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;fstab file&#34;
	
	
&gt;
At the very bottom, there is a line with &lt;code&gt;/swap.img&lt;/code&gt;, we need to make sure this line is commented out. Either manually edit the line to &lt;code&gt;#/swap.img&lt;/code&gt; or run&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo sed -i &lt;span class=&#34;s1&#34;&gt;&amp;#39;/\/swap.img/ s/^/#/&amp;#39;&lt;/span&gt; /etc/fstab
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Let&amp;rsquo;s check if there&amp;rsquo;s any &lt;strong&gt;systemd&lt;/strong&gt; units left that enable swapping &lt;code&gt;systemctl list-units --type swap&lt;/code&gt;.
&lt;img src=&#34;https://i.imgur.com/qqSBJo0.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Systemctl unit list&#34;
	
	
&gt;
This machine has a systemd unit that enables swapping, meaning that after a reboot swapping will be enabled again even after disabling it in &lt;code&gt;/etc/fstab&lt;/code&gt;. Copy the name of the &lt;strong&gt;UNIT&lt;/strong&gt; and run the following command, changing &lt;code&gt;swap.img.swap&lt;/code&gt; if it differentiates from your unit name.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;systemctl mask swap.img.swap
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Restart your machine, repeat the &lt;code&gt;journalctl&lt;/code&gt; command for the kubelet.
&lt;img src=&#34;https://i.imgur.com/DonwbFv.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Kubelet logs after swap disable&#34;
	
	
&gt;
The swap errors have disappeared! Our kubelet is running healthy again!&lt;/p&gt;
&lt;h3 id=&#34;containerd-cri-plugin-disabled&#34;&gt;Containerd CRI Plugin Disabled
&lt;/h3&gt;&lt;p&gt;The CRI plugin is necessary for the kubelet to interact with containerd. During the installation of containerd it&amp;rsquo;s possible that the CRI plugin get&amp;rsquo;s added to the &lt;code&gt;disabled_plugins&lt;/code&gt; list in containerd, let&amp;rsquo;s check.
&lt;img src=&#34;https://i.imgur.com/SPVHnzD.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;Containerd config file disabled plugin list&#34;
	
	
&gt;
As we can see, &lt;strong&gt;cri&lt;/strong&gt; is in my disabled_plugins list. Run the following command to remove CRI from the disabled plugin list and restart containerd.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sed -i &lt;span class=&#34;s1&#34;&gt;&amp;#39;/disabled_plugins/s/\[&amp;#34;cri&amp;#34;\]/[]/&amp;#39;&lt;/span&gt; /etc/containerd/config.toml &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;systemctl restart containerd
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
        </item>
        <item>
        <title>Advanced User Data Collection: Performance, Precision, and Persistence</title>
        <link>https://blog.qstars.nl/posts/advanced-marketing-optimalisatie/</link>
        <pubDate>Tue, 06 May 2025 12:28:22 +0100</pubDate>
        
        <guid>https://blog.qstars.nl/posts/advanced-marketing-optimalisatie/</guid>
        <description>&lt;img src="https://blog.qstars.nl/posts/advanced-marketing-optimalisatie/cover.png" alt="Featured image of post Advanced User Data Collection: Performance, Precision, and Persistence" /&gt;&lt;p&gt;We&amp;rsquo;ve arrived at the third and final iteration of the three part marketing blog series. In the &lt;a class=&#34;link&#34; href=&#34;https://blog.qstars.nl/posts/understanding-digital-advertising/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;first post&lt;/a&gt; we looked at how data is collected and how users are being tracked on the web. The &lt;a class=&#34;link&#34; href=&#34;https://blog.qstars.nl/posts/optimizing-marketing-efforts-with-user-data/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;second instalment&lt;/a&gt; focussed on common practices to gather deeper insights from your data. Now, in this final part, we&amp;rsquo;ll build on those practices and explore advanced strategies; optimizing data collection, enhancing tracking accuracy, and leveraging server-side techniques to maximize marketing performance.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Some of the techniques outlined in this post are in an &lt;a class=&#34;link&#34; href=&#34;https://bima.co.uk/the-ethics-of-digital-marketing-navigating-grey-areas/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;ethical grey area&lt;/a&gt;. Precisely, server side tagging and cname cloaking can get around ad-blocking filters your users may use. These practices draw sharp criticism from privacy enthousiasts and activists alike, however, this does not stop businesses from implementing them. At Qstars IT we do not endorse these techniques, this post is for educational purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;server-side-tagging-sst&#34;&gt;Server Side Tagging (SST)
&lt;/h2&gt;&lt;p&gt;You may have heard this before; &amp;ldquo;Amazon found that &lt;a class=&#34;link&#34; href=&#34;https://www.gigaspaces.com/blog/amazon-found-every-100ms-of-latency-cost-them-1-in-sales&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;every 100 milliseconds of added latency costs them 1% in sales&lt;/a&gt;&amp;rdquo;. Amazon&amp;rsquo;s claim is widely cited, but it&amp;rsquo;s not the only proof that site speed has a real economic impact. &lt;a class=&#34;link&#34; href=&#34;https://www.thinkwithgoogle.com/marketing-strategies/app-and-mobile/mobile-page-speed-new-industry-benchmarks-load-time-vs-bounce/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Google&lt;/a&gt;, &lt;a class=&#34;link&#34; href=&#34;https://www.akamai.com/newsroom/press-release/akamai-releases-spring-2017-state-of-online-retail-performance-report&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Akamai&lt;/a&gt; and &lt;a class=&#34;link&#34; href=&#34;https://portent.com/blog/design-dev/ultimate-site-speed-guide-why-site-speed-matters.htm&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Portent&lt;/a&gt; have all. released studies showing that marginal gains in site speed can increase conversion rates, user traffic and customer satisfaction.&lt;/p&gt;
&lt;p&gt;In our last post, we looked at adding tracking code via tag managers. As we learned, these tags are individual snippets of code that get executed on the user&amp;rsquo;s browser which measure a variety of statistics about the user&amp;rsquo;s site visit. In a traditional setup, every action is executed on the visitors device. The computation required to gather the statistics, format and/or manipulate data and even the reporting to analytics services (such as Google Analytics) is done by the user.&lt;/p&gt;
&lt;p&gt;This is not ideal, as you can imagine there are many downsides to the traditional approach. What if your end user(s) have filters that deny their browser from talking to Google Analytics? That&amp;rsquo;d mean you aren&amp;rsquo;t actually receiving the metrics that your tracking installation has collected. Or your visitors are on an older device? A heap of tracking snippets can gradually start slowing down the browsing experience for your users, especially folks on older hardware will get hit hardest by these performance penalties. And is it even a good idea for such important business logic to depend on the varying and unstable environments that are your customer&amp;rsquo;s browsers?&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://blog.qstars.nl/posts/advanced-marketing-optimalisatie/sst.jpg&#34;
	width=&#34;1024&#34;
	height=&#34;1024&#34;
	srcset=&#34;https://blog.qstars.nl/posts/advanced-marketing-optimalisatie/sst_hu_44076b747deb0771.jpg 480w, https://blog.qstars.nl/posts/advanced-marketing-optimalisatie/sst_hu_dc7d5558ec815058.jpg 1024w&#34;
	loading=&#34;lazy&#34;
	
		alt=&#34;SST Infographic&#34;
	
	
		class=&#34;gallery-image&#34; 
		data-flex-grow=&#34;100&#34;
		data-flex-basis=&#34;240px&#34;
	
&gt;&lt;/p&gt;
&lt;h3 id=&#34;technical-improvements&#34;&gt;Technical Improvements
&lt;/h3&gt;&lt;p&gt;This is where Server-Side Tagging (SST) comes in. As the name suggests, SST is used to decouple the business logic from our data collection. By deploying SST, you ensure that your end users&amp;rsquo; devices are only responsible for collecting data, while any transformations, validation, or reporting is handled by your central machine.&lt;/p&gt;
&lt;p&gt;In addition to increased reliability of the base components (e.g., data collection through JavaScript execution), there are also fewer network requests, which can significantly improve performance—especially for visitors with a slower internet connection.&lt;/p&gt;
&lt;p&gt;SST also implements smaller optimizations, such as more efficient batch sending of raw event data to your backend.&lt;/p&gt;
&lt;p&gt;Furthermore, SST enhances security by leveraging &lt;strong&gt;HttpOnly&lt;/strong&gt; cookies, reducing exposure to client-side attacks. HttpOnly cookies are cookies that can’t be accessed by JavaScript, reducing the risk of &lt;a class=&#34;link&#34; href=&#34;https://owasp.org/www-community/attacks/xss/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;XSS attacks&lt;/a&gt;. They are sent with HTTP requests, protecting sensitive data like session IDs from theft.&lt;/p&gt;
&lt;h3 id=&#34;analytics-improvements&#34;&gt;Analytics Improvements
&lt;/h3&gt;&lt;p&gt;You also gain greater control over data handling, improving accuracy, regulatory compliance and third party integrations.&lt;/p&gt;
&lt;h4 id=&#34;improved-data-accuracy&#34;&gt;Improved data accuracy
&lt;/h4&gt;&lt;p&gt;As SST processes events on dedicated server than your user&amp;rsquo;s device, it reduces inconsistencies caused by your visitor&amp;rsquo;s environment. Network issues, browser restrictions (adblockers) and client-side execution failures can lead you to incomplete event tracking.&lt;/p&gt;
&lt;h3 id=&#34;regulatory-compliance-and-governance&#34;&gt;Regulatory compliance and governance
&lt;/h3&gt;&lt;p&gt;Privacy regulations like &lt;strong&gt;GDPR&lt;/strong&gt;, &lt;strong&gt;CCPA&lt;/strong&gt;, and &lt;strong&gt;ePrivacy Directive&lt;/strong&gt; impose strict guidelines on data collection and user consent. With SST, companies can better manage data anonymization, user consent enforcement, and data retention policies before forwarding data to external analytics providers. By processing data in a controlled environment, organizations can ensure compliance without relying entirely on third-party scripts.&lt;/p&gt;
&lt;h3 id=&#34;third-party-integrations&#34;&gt;Third party integrations
&lt;/h3&gt;&lt;p&gt;Many marketing platforms and analytics tools require event data in a specific format. With SST, businesses can preprocess and normalize data before sending it to Google Analytics, Facebook Pixel, or other third-party services. This leads to cleaner, more structured reporting and ensures that the data is optimized to each platform’s requirements.&lt;/p&gt;
&lt;h3 id=&#34;cross-device-tracking&#34;&gt;Cross-device tracking
&lt;/h3&gt;&lt;p&gt;SST allows for &lt;strong&gt;server-managed user identifiers&lt;/strong&gt;, which improves attribution models by connecting data points across different sessions and devices, leading to a more comprehensive view of the customer journey.&lt;/p&gt;
&lt;h2 id=&#34;cname-cloaking&#34;&gt;CNAME Cloaking
&lt;/h2&gt;&lt;p&gt;CNAME cloaking is a technique used to bypass content and ad-blockers by masking third-party tracking scripts under a first-party domain. Essentially, it makes third-party requests appear as if they&amp;rsquo;re coming from your own domain, allowing them to slip past filters that would otherwise block or restrict them.&lt;/p&gt;
&lt;p&gt;Let’s break it down: a &lt;strong&gt;CNAME record&lt;/strong&gt; is a type of DNS record that allows one domain to alias another. In this case, you’d configure something like &lt;code&gt;track.yoursite.com&lt;/code&gt; to point to a third-party tracking service (e.g., a CDN or analytics platform). From the browser’s point of view, everything appears to originate from your domain—even though the underlying infrastructure is external.&lt;/p&gt;
&lt;p&gt;This tactic is increasingly used to maintain visibility in analytics and marketing platforms, despite widespread use of privacy tools and extensions that block known trackers.&lt;/p&gt;
&lt;h3 id=&#34;why-it-works&#34;&gt;Why it works
&lt;/h3&gt;&lt;p&gt;Most ad-blockers use predefined lists (like EasyPrivacy or uBlock Origin filters) to identify and block known third-party domains. But when you serve those same scripts through your own subdomain via a CNAME, they appear &amp;ldquo;first-party&amp;rdquo; and bypass detection. Since the blocklists rely on domain name matching, this trick sidesteps their protection mechanisms.&lt;/p&gt;
&lt;p&gt;This gives businesses continued access to event data and user behavior metrics that would otherwise be lost, which can significantly impact attribution models and campaign optimization.&lt;/p&gt;
&lt;h3 id=&#34;controversy-and-ethics&#34;&gt;Controversy and ethics
&lt;/h3&gt;&lt;p&gt;CNAME cloaking is controversial. It effectively disguises third-party scripts, making it harder for users to detect tracking and potentially circumventing their expressed preferences (like Do Not Track or blocking third-party cookies). This practice has been called deceptive by privacy advocates and regulators.&lt;/p&gt;
&lt;p&gt;In fact, the French CNIL (Commission Nationale de l’Informatique et des Libertés) has explicitly warned against the misuse of CNAME cloaking, stating that it can mislead users and obscure data flows—an issue particularly sensitive under GDPR.&lt;/p&gt;
&lt;p&gt;Some browsers and privacy-focused organizations have started taking action. Firefox, for instance, has implemented mechanisms that detect and block cloaked CNAME trackers.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ At Qstars IT, we acknowledge the utility of this technique in maintaining analytics integrity, but we also stress the importance of ethical marketing practices and compliance with evolving privacy regulations.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;wrapping-it-up&#34;&gt;Wrapping it up
&lt;/h2&gt;&lt;p&gt;Over the course of this three-part series, we’ve taken a deep dive into the mechanics of modern digital advertising. From understanding how user data is collected and processed to refining marketing strategies through smarter tracking and advanced technical solutions. As the landscape continues to evolve, businesses face an ongoing balancing act between optimizing ad campaign performance and respecting user privacy. By staying informed, making conscious decisions, and choosing the right tools for the job, marketers can unlock the full potential of their data while building trust with their audience.&lt;/p&gt;
</description>
        </item>
        <item>
        <title>Optimizing marketing efforts with user data</title>
        <link>https://blog.qstars.nl/posts/optimizing-marketing-efforts-with-user-data/</link>
        <pubDate>Thu, 06 Feb 2025 09:18:00 +0200</pubDate>
        
        <guid>https://blog.qstars.nl/posts/optimizing-marketing-efforts-with-user-data/</guid>
        <description>&lt;img src="https://blog.qstars.nl/posts/optimizing-marketing-efforts-with-user-data/optimizing-user.jpg" alt="Featured image of post Optimizing marketing efforts with user data" /&gt;&lt;p&gt;&lt;em&gt;Note: As developers of QStars, we are interested in this technology from a technical and research perspective. However, we do not provide any services related to martech or digital advertising. Our focus remains on understanding and exploring these technologies rather than offering them as a product or service.&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This blog post is the second installment of a three part series. Today we delve deeper into more technical details of digital advertising and cover the methods to collect data from your users.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;why-collect-user-data&#34;&gt;Why collect user data?
&lt;/h2&gt;&lt;p&gt;User data is invaluable for making informed business decisions. By analyzing user interactions on your website or application you are able to identify trends, optimize conversion rates and personalize marketing efforts. Say there is a disproportionate amount of people viewing a product but not purchasing it; you can experiment with different messaging strategies or add social proofs and measure how these changes impact your conversion rates.&lt;/p&gt;
&lt;h2 id=&#34;tag-management-systems&#34;&gt;Tag Management Systems
&lt;/h2&gt;&lt;p&gt;As we touched on briefly in the &lt;a class=&#34;link&#34; href=&#34;https://blog.qstars.nl/posts/understanding-digital-advertising/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;previous blog post&lt;/a&gt;, tag managers play a critical role by allowing you to quickly create, update and deploy bundles of code on your website. These systems simplify data collection by having one-click presets and integrations in addition to allowing you to ship your own custom code.&lt;/p&gt;
&lt;p&gt;Tag managers usually employ a Data Layer on your website. This javascript object is hidden from end users but acts as a middleman between your website and various analytic tools (such as Google Analytics). Think of the data layer as a centralized repository where all the data your site collects about a user gets stored, making the collection structured and accessible.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an example of a data layer for tracking an e-commerce purchase.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt; 1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 3
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 4
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 5
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 6
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 7
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 8
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 9
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;10
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;11
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;12
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;13
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;14
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;15
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;16
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;17
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;18
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;19
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;20
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;21
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;22
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;window&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;dataLayer&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nx&#34;&gt;event&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;purchase&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nx&#34;&gt;ecommerce&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;transaction_id&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;ORD12345&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;value&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;150.0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;currency&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;USD&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;items&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nx&#34;&gt;item_id&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;prod_001&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nx&#34;&gt;item_name&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Wireless Mouse&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nx&#34;&gt;price&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;50.0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nx&#34;&gt;quantity&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nx&#34;&gt;item_id&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;prod_002&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nx&#34;&gt;item_name&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Mechanical Keyboard&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nx&#34;&gt;price&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;100.0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nx&#34;&gt;quantity&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;With a data layer you are standardizing user behaviour data, making it much easier for your marketing teams to gain insights from it.&lt;/p&gt;
&lt;h2 id=&#34;cookie-consent&#34;&gt;Cookie Consent
&lt;/h2&gt;&lt;p&gt;Privacy regulation like the GDPR and CCPA have significantly altered how a business is allowed to collect and store data about its users. The biggest requirement is having to obtain explicit consent before deploying any tracking cookies or scripts.&lt;/p&gt;
&lt;p&gt;Without this consent businesses must only rely on anonymized data, limiting their ability to uniquely identify users and serve them personalized ads. This can also weaken segmentation efforts, making it harder to understand user behavior and preferences.&lt;/p&gt;
&lt;p&gt;To address this challenge, many businesses implement &lt;strong&gt;cookie consent management platforms (CMPs)&lt;/strong&gt;. These platforms provide users with options to control how much they are tracked while ensuring businesses remain compliant with local regulations. Some CMPs also enable partial tracking capabilities, where anonymized data is collected if full consent is not granted.&lt;/p&gt;
&lt;h2 id=&#34;ab-testing&#34;&gt;A/B Testing
&lt;/h2&gt;&lt;p&gt;A/B testing is the building block of data-driven working and optimizing your digital marketing efforts. Also known as split testing, it is a method used to compare two or more variations of a webpage, advertisement, or email campaign to determine which one performs better.&lt;/p&gt;
&lt;p&gt;By leveraging data collected through tag management systems and analytics tools, businesses can autonomously test elements such as headlines, call-to-action buttons, images, and pricing strategies. This process involves showing different versions of a page or ad to different segments of users and analyzing their interactions to identify the most effective design, messaging, or layout.&lt;/p&gt;
&lt;p&gt;The key benefits of A/B testing are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Data-driven decision making&lt;/strong&gt; eliminates the guesswork and validating user preferences with real data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Improving conversion rates&lt;/strong&gt; by identifying the most effective ad copy, design and product pricing variations.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tag managers allow you to quickly implement A/B tests on your site with the added benefit of it being stored in your data layer. This way businesses can seamlessly analyze results in their analytics platform without any change in your reporting tools.&lt;/p&gt;
&lt;h2 id=&#34;predictive-analytics--ai&#34;&gt;Predictive Analytics &amp;amp; AI
&lt;/h2&gt;&lt;p&gt;Beyond A/B tests, businesses can leverage the power of predictive analytics and machine learning to anticipate user behaviour.&lt;/p&gt;
&lt;p&gt;Although setting up custom predictive analytics models requires advanced technical expertise, these models can help with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;User behaviour forecasting&lt;/strong&gt;: By analyzing historical data, AI models try to predict what users are most likely to convert&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dynamic creative optimization&lt;/strong&gt;: Dynamically adjusts website content, product recommendations, and ad creatives based on user patterns&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fraud detection &amp;amp; risk analysis&lt;/strong&gt;: Identify patterns that indicate fraudulent activity or purchasing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Platforms like Google- &amp;amp; Meta Ads heavily rely on machine learning algorithms to enhance their ad delivery targeting, making it crucial that you are sending them data directly collected from your user&amp;rsquo;s behaviour. Providing high-quality data to these advertising managers significantly increases return on ad spend (ROAS), cost-per-acquisition (CPA) and optimize ad bidding in real-time.&lt;/p&gt;
&lt;h2 id=&#34;up-next&#34;&gt;Up next
&lt;/h2&gt;&lt;p&gt;In today’s post, we explored how businesses can enhance their digital advertising efforts by integrating user data. We covered the importance of tag managers, cookie consent, split testing, and the power of AI.&lt;/p&gt;
&lt;p&gt;In the final post of this series, we&amp;rsquo;ll dive into strategies for maximizing the data you collect from users. We’ll also discuss approaches to handle ad blockers and other methods to minimize the impact of tracking tools on your website&amp;rsquo;s performance.&lt;/p&gt;
</description>
        </item>
        <item>
        <title>Understanding digital data collection</title>
        <link>https://blog.qstars.nl/posts/understanding-digital-advertising/</link>
        <pubDate>Fri, 27 Dec 2024 09:38:00 +0200</pubDate>
        
        <guid>https://blog.qstars.nl/posts/understanding-digital-advertising/</guid>
        <description>&lt;img src="https://blog.qstars.nl/posts/understanding-digital-advertising/tracking-1-landscape.png" alt="Featured image of post Understanding digital data collection" /&gt;&lt;blockquote&gt;
&lt;p&gt;This blog post marks the beginning of a three-part series exploring the digital advertising landscape. In today’s post, we’ll examine the data collection mechanisms employed by ad platforms to deliver personalized advertisements. Future posts will delve into practical strategies and examples to help optimize digital advertising efforts, which are based on the concepts we&amp;rsquo;ll familiarize ourselves with today.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;the-current-landscape&#34;&gt;The current landscape
&lt;/h2&gt;&lt;p&gt;The global marketing industry is, and has been, booming. While estimates vary, analysts predict that the market could reach anywhere between &lt;strong&gt;$700 billion and $1.2 trillion by 2028.&lt;/strong&gt; To put that into perspective, the digital advertising market alone is on track to surpass the GDP of &lt;strong&gt;Belgium&lt;/strong&gt;. That’s an immense amount of purchasing power.&lt;/p&gt;
&lt;p&gt;A major driver behind this growth is the proliferation of personalized advertisements. Google leads the charge in the ad ecosystem, leveraging their dominance in search engines, browsers and platforms like YouTube to collect enormous amounts of user data. As they have been accused of deploying questionable tactics to collect this data, like &lt;a class=&#34;link&#34; href=&#34;https://www.brusselstimes.com/1004675/google-collected-data-from-people-using-incognito-mode-for-years&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;gathering user data in Incognito mode&lt;/a&gt; through their Chrome browser.&lt;/p&gt;
&lt;p&gt;So this begs the question: &lt;em&gt;where does all of this actionable data come from?&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&#34;how-data-is-collected&#34;&gt;How data is collected
&lt;/h2&gt;&lt;p&gt;The phrase &amp;ldquo;&lt;em&gt;If something is free, &lt;strong&gt;you&lt;/strong&gt; are the product&lt;/em&gt;&amp;rdquo; perfectly encapsulates the reality of digital advertising. There is a growing regulatory push around the globe to protect consumer privacy and control how companies store, track and handle personal data. While cookie acceptance banners have become inescapable, companies already employ more key tactics to monitor your digital footprint.&lt;/p&gt;
&lt;h3 id=&#34;1-cookies-the-foundation&#34;&gt;1. Cookies: The Foundation
&lt;/h3&gt;&lt;p&gt;Cookies are small text files that the browser stores on your device on behalf of the websites you visit. These cookies can be placed in your browser by different vendors, which introduces the concept of first vs third party cookies.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;First party cookies&lt;/strong&gt; are explicitly set by the website you are visiting. They are commonly used for the site to remember your shopping cart or login status. They &lt;em&gt;can&lt;/em&gt; also be used to track your behaviour across the website.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Third party cookies&lt;/strong&gt; are created by external parties. When you browse a site (and consent to their privacy policy), code is loaded onto your device from advertising networks and social media platforms. This enables them to store an identifier on your device which they can read when you visit another site that deploys code from this vendor.&lt;/p&gt;
&lt;p&gt;Google Analytics is a common third party cookie vendor that tracks user behaviour across its network of websites. Its tracking code sets cookies to collect data like page views and interactions, sending it to Google’s servers. If the user visits another site using Google Analytics, their browsing habits can be linked, helping businesses refine marketing and deliver targeted ads.&lt;/p&gt;
&lt;h3 id=&#34;2-fingerprinting&#34;&gt;2. Fingerprinting
&lt;/h3&gt;&lt;p&gt;Fingerprinting is a tracking method that&amp;rsquo;s harder to detect and block. It aims to identify and track users across websites and in different sessions.&lt;/p&gt;
&lt;p&gt;It works by collecting a variety of data points about a user&amp;rsquo;s device, browser, and geolocation. While there are virtually endless points that can be tracked, some common ones are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Browser and device:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Type&lt;/li&gt;
&lt;li&gt;Version&lt;/li&gt;
&lt;li&gt;Screen size&lt;/li&gt;
&lt;li&gt;Language settings&lt;/li&gt;
&lt;li&gt;Timezone&lt;/li&gt;
&lt;li&gt;Operating system&lt;/li&gt;
&lt;li&gt;Graphics processing unit (gpu) &amp;amp; processor details&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Your IP address and the approximate location it resolves to.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Any installed browser add-ons or tools.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;By combining all of these factors, fingerprinting creates a unique identifier that can persist even if you deny tracking or clear your cookies.&lt;/p&gt;
&lt;h3 id=&#34;3-tag-managers&#34;&gt;3. Tag Managers
&lt;/h3&gt;&lt;p&gt;Tag managers allow you to efficiently deploy small bundles of code, referred to as &amp;ldquo;tags&amp;rdquo;. These tags are triggered on your website by a specific user event, such as clicking on a product or viewing a page. They have become popular for streamlining the process of integrating user tracking on websites.&lt;/p&gt;
&lt;p&gt;One of the most popular tag management tools is Google Tag Manager, though similar solutions are offered by Adobe, Tealium, and others.&lt;/p&gt;
&lt;p&gt;Tag managers integrate seamlessly with analytics tools, data warehouses and advertising platforms. This has made it easier to get actionable data from tracked user behaviour. Within minutes you can segment audiences and identify high-value customers for retargeting.&lt;/p&gt;
&lt;h2 id=&#34;whats-next&#34;&gt;What&amp;rsquo;s next
&lt;/h2&gt;&lt;p&gt;Today we&amp;rsquo;ve discussed three primary methods of data collection: cookies, fingerprinting, and tag managers. As the digital advertising industry continues to expand, consumer privacy and government regulation is becoming a major concern. Future posts in this series will dive deeper into strategies for optimizing digital advertising efforts while staying compliant with privacy regulations.&lt;/p&gt;
</description>
        </item>
        
    </channel>
</rss>
