Deploy a control plane and 1/2/3/... workers.
One of the worker will run a Plex server with hardware transcoding on an Intel ARC GPU.
- Ubuntu 22.04.3 LTS with 6.5 kernel (HWE)
- Intel ARC A380 GPU for hardware transcoding
- Fast local storage for Plex metadata
- Simple home networking
This is useful for any node, even outside kubernetes:
sudo apt-get install -y zsh zsh-syntax-highlighting
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
In the new ZSH shell:
git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions
echo "source /usr/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" >> ~/.zshrc
sed -i 's/^ZSH_THEME="[^"]*"/ZSH_THEME="lukerandall"/' ~/.zshrc
sed -i '/^plugins=(git)/c\plugins=(\n git\n zsh-autosuggestions\n helm\n kubectl\n)' ~/.zshrc
sudo swapoff -a
To make it permanent, edit the /etc/fstab
if needed (comment line with swap img)
sudo nano /etc/fstab
Since we are using Ubuntu 22, we need to load br_netfilter and make sure it persists across reboots.
sudo tee /etc/modules-load.d/containerd.conf <<EOF
br_netfilter
EOF
sudo modprobe br_netfilter
# sysctl params required by setup, params persist across reboots
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
EOF
# Apply sysctl params without reboot
sudo sysctl --system
This parameter enables the bridge network driver to pass IPv6 traffic to ip6tables for processing. In simpler terms, it allows ip6tables to filter traffic that is bridged (i.e., passed from one network interface to another without being processed by the host's IP stack).
Similarly, this parameter enables the bridge network driver to pass IPv4 traffic to iptables for processing. This allows iptables to filter bridged IPv4 traffic.
The net.ipv4.ip_forward parameter controls whether the Linux kernel forwards IPv4 packets. By default, a Linux machine acts as a host, not a router, and does not forward IP packets from one network interface to another. This parameter must be enabled if you want your Linux machine to act as a router, forwarding packets between different network interfaces.
In a Kubernetes cluster, suppose you have a service that exposes port 6379 (commonly used by Redis) and allows pods to communicate with this service.
- Service Definition:
You define a Kubernetes service that maps port 6379 to one or more backend pods. Kubernetes creates iptables rules to handle this mapping, ensuring that traffic destined for the service IP on port 6379 is correctly forwarded to the appropriate pods.
- Bridged Traffic:
Pods communicate over the network using virtual network interfaces (veth pairs) and bridges. For example, a pod on Node A might want to communicate with the Redis service, which is mapped to port 6379.
What Happens When This Parameter Is Set (net.bridge.bridge-nf-call-iptables = 1):
- Packet Handling:
- When a packet destined for port 6379 arrives at the network bridge interface on the node, the Linux kernel passes this packet to iptables for processing.
- Iptables applies the rules defined by Kubernetes, such as NAT and port forwarding rules, to ensure the packet is routed correctly to the backend pod running Redis.
What Happens When This Parameter Is Not Set (net.bridge.bridge-nf-call-iptables = 0):
- Packet Handling:
- When a packet destined for port 6379 arrives at the network bridge interface, it bypasses iptables because the kernel is not configured to pass bridged traffic to iptables.
- The packet does not get processed by the iptables rules, meaning the necessary NAT and port forwarding rules are not applied.
- As a result, the packet may not reach the intended pod running Redis, leading to communication failures and service disruptions.
wget -q --show-progress https://github.com/opencontainers/runc/releases/download/v1.1.12/runc.amd64
sudo install -m 755 runc.amd64 /usr/local/sbin/runc
wget -q --show-progress https://github.com/containernetworking/plugins/releases/download/v1.4.1/cni-plugins-linux-amd64-v1.4.1.tgz
sudo mkdir -p /opt/cni/bin
sudo tar Cxzvf /opt/cni/bin cni-plugins-linux-amd64-v1.4.1.tgz
wget -q --show-progress \
https://github.com/containerd/containerd/releases/download/v1.7.16/containerd-1.7.16-linux-amd64.tar.gz \
https://raw.githubusercontent.com/containerd/containerd/v1.7.16/containerd.service
sudo tar Cxzvf /usr/local containerd-1.7.16-linux-amd64.tar.gz
sudo mkdir /etc/containerd
/usr/local/bin/containerd config default | sudo tee /etc/containerd/config.toml
sudo mkdir -p /usr/local/lib/systemd/system
sudo mv containerd.service /usr/local/lib/systemd/system/
As per the kubernetes doc, if the cgroup is provided by systemd
, one setting must be changed in config.toml:
sudo nano /etc/containerd/config.toml
You must look for the runc.options
portion:
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
...
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true
And now, enable the service:
sudo systemctl daemon-reload
sudo systemctl enable --now containerd
Reboot before installing Kubernetes:
sudo reboot
These instructions are for Kubernetes v1.30.
- Update the apt package index and install packages needed to use the Kubernetes apt repository:
sudo apt-get update
# apt-transport-https may be a dummy package; if so, you can skip that package
sudo apt-get install -y apt-transport-https ca-certificates curl gpg nfs-common
- Download the public signing key for the Kubernetes package repositories. The same signing key is used for all repositories so you can disregard the version in the URL:
# If the directory `/etc/apt/keyrings` does not exist, it should be created before the curl command, read the note below.
# sudo mkdir -p -m 755 /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
Note: In releases older than Debian 12 and Ubuntu 22.04, directory /etc/apt/keyrings does not exist by default, and it should be created before the curl command.
- Add the appropriate Kubernetes apt repository. Please note that this repository have packages only for Kubernetes 1.30; for other Kubernetes minor versions, you need to change the Kubernetes minor version in the URL to match your desired minor version (you should also check that you are reading the documentation for the version of Kubernetes that you plan to install).
# This overwrites any existing configuration in /etc/apt/sources.list.d/kubernetes.list
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list
- Update the apt package index, install kubelet, kubeadm and kubectl, and pin their version:
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl
- (Optional) Enable the kubelet service before running kubeadm:
sudo systemctl enable --now kubelet
IMPORTANT
The installation ends here for worker nodes !!!
Please usekubeadm join
instead ofkubeadm init
You can change the pods CIDR and service CIDR to match your needs. More information here.
sudo kubeadm init --pod-network-cidr 10.244.0.0/16
IMPORTANT
Please write down thekubeadm join
command for later use by the worker nodes !!!
If the command is successful, you get instructions at the end, to help you setup the admin config. Set it up on the user that will access the cluster (probably your current admin user):
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
(optional) By default, your cluster will not schedule Pods on the control plane nodes for security reasons. If you want to be able to schedule Pods on the control plane nodes, for example for a single machine Kubernetes cluster, run:
kubectl taint nodes --all node-role.kubernetes.io/control-plane-
The instructions can be viewed here.
- Download
wget https://get.helm.sh/helm-v3.15.0-linux-amd64.tar.gz
- Install
tar -zxvf helm-v3.15.0-linux-amd64.tar.gz
sudo mv linux-amd64/helm /usr/local/bin/helm
- Check
helm help
We will later install MetalLB to provide the load balancing to our cluster. Check this page for the latest compatibility between MetalLB and network addons.
I have chosen Flannel. Instructions can be found here.
Install Flannel:
# Needs manual creation of namespace to avoid helm error
kubectl create ns kube-flannel
kubectl label --overwrite ns kube-flannel pod-security.kubernetes.io/enforce=privileged
helm repo add flannel https://flannel-io.github.io/flannel/
helm install flannel --set podCidr="10.244.0.0/16" --namespace kube-flannel flannel/flannel
From now on, the coreDNS pods should start working, since the network addon is active:
kubectl get pods -A -o wide
You should see the coredns pods in "running" state:
kube-system coredns-XXX-YY 1/1 Running 1
kube-system coredns-XXX-YY 1/1 Running 1
The instructions can be viewed here.
- First, we must change the strictARP setting:
kubectl edit configmap -n kube-system kube-proxy
Change strictARP:
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "ipvs"
ipvs:
strictARP: true
- Deploy MetalLB:
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.5/config/manifests/metallb-native.yaml
kubectl get pods -n metallb-system
- Set the IP pool:
metallb-ippool.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: lan-pool
namespace: metallb-system
spec:
addresses:
- 10.0.1.2-10.0.1.254
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: lan
namespace: metallb-system
spec:
ipAddressPools:
- lan-pool
kubectl apply -f metallb-ippool.yaml
You can find the installation steps for ingress-nginx in the ingress folder.
TLDR
# ingress-nginx install
helm upgrade --set controller.service.externalTrafficPolicy=Local \
--install ingress-nginx ingress-nginx \
--repo https://kubernetes.github.io/ingress-nginx \
--namespace ingress-nginx --create-namespace
# cert-manager repo
helm repo add jetstack https://charts.jetstack.io --force-update
# cert-manager install
helm install \
cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--version v1.15.1 \
--set crds.enabled=true
Then, define a ClusterIssuer or some Ingress resources, based on your needs. My setup uses an AWS Route53 ClusterIssuer.
NFS subdir external provisioner is an automatic provisioner that use your existing and already configured NFS server to support dynamic provisioning of Kubernetes Persistent Volumes via Persistent Volume Claims. Persistent volumes are provisioned as ${namespace}-${pvcName}-${pvName}.
TLDR
helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
helm install nfs-subdir-external-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner --set nfs.server=10.0.0.2 --set nfs.path=/mnt/user/k8s-data
Please check the k8s-nfs-storage folder for more information on the possible configurable parameters.
Check your BIOS/UEFI settings to see what can be enabled. In my setup, I have set up the following on a B550m from Asrock:
- Main graphics adapter: External (if the CPU has a GPU, avoids having two GPUs in the server, could cause issues)
- SR-IOV: enabled (more details here
- Resizable BAR : enabled (free performance for the GPU)
MetalLB will assign an IP from the pool 10.0.1.2 - 10.0.1.254 to your Plex service. The network needs a way to know where to find 10.0.1.X.
There are various ways to deal with that, but in a simple homelab context, the easiest way it to configure your router with a static route that says:
"Any traffic to 10.0.1.0/24 network needs to go through the plex host machine"
In my case, my kubernetes node has the IP 10.0.0.8, so I must create a static route from 10.0.1.0/24 to 10.0.0.8. Then, our network stack on the kubernetes host knows how to deal with traffic for 10.0.1.X.
Since homelab networks use NAT (one single public IP), don't forget to forward 32400 to the appropriate IP (10.0.1.2 in my case, the first in the load balancer pool).