Overview #
I’m using the following VMs based on Ubuntu 24.04 servers:
192.168.30.10 ubuntu1 # HAproxy & Keepalived Node 1
192.168.30.11 ubuntu2 # HAproxy & Keepalived Node 2
192.168.30.12 ubuntu3 # Controller Node 1
192.168.30.13 ubuntu4 # Controller Node 2
192.168.30.14 ubuntu5 # Controller Node 3
192.168.30.15 ubuntu6 # Etcd Node 1
192.168.30.16 ubuntu7 # Etcd Node 2
192.168.30.17 ubuntu8 # Etcd Node 3
192.168.30.18 ubuntu9 # Worker Node 1
192.168.30.19 ubuntu10 # Worker Node 2
192.168.30.9 # Floating IP for HA
In this tutorial I’m using a script to setup the Kubernetes nodes. For more details about the required setup and the deployment of MetalLB and the Nginx Ingress controller, please refer to my previous post:
Load Balancer #
Overview #
Keepalived: Provides a virtual IP managed by a configurable health check.
HAproxy: Load balancer for the Controller nodes.
Install Packages #
# Install Keepalived, HAproxy & psmisc
sudo apt install keepalived haproxy psmisc -y
# Verify the installation / check version
haproxy -v
# Verify haproxy user and group exist
getent passwd haproxy
getent group haproxy
The psmisc package contains utilities that are used for managing processes.
HAproxy #
Create Configuration #
Use the same configuration for both HAproxy & Keepalived nodes:
# Edit the configuration
sudo vi /etc/haproxy/haproxy.cfg
global
log /dev/log local0 warning
chroot /var/lib/haproxy
pidfile /var/run/haproxy.pid
maxconn 4000
user haproxy
group haproxy
daemon
stats socket /run/haproxy/admin.sock mode 660 level admin
defaults
log global
option httplog
option dontlognull
timeout connect 5000
timeout client 50000
timeout server 50000
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http
frontend kube-apiserver
bind *:6443
mode tcp
option tcplog
default_backend kube-apiserver
backend kube-apiserver
mode tcp
option tcp-check
balance roundrobin
default-server inter 10s downinter 5s rise 2 fall 2 slowstart 60s maxconn 250 maxqueue 256 weight 100
server kube-apiserver-1 192.168.30.12:6443 check # Controller Node 1
server kube-apiserver-2 192.168.30.13:6443 check # Controller Node 2
server kube-apiserver-3 192.168.30.14:6443 check # Controller Node 3
Test the Configuration #
# Validate configuration
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
Restart HAproxy #
# Restart HAproxy
sudo systemctl restart haproxy
# Enable HAproxy after boot (Should be enabled per default)
sudo systemctl enable haproxy
Keepalived #
Service Configuration #
# Edit the configuration
sudo vi /etc/keepalived/keepalived.conf
HAproxy & Keepalived Node 1:
global_defs {
notification_email {
}
router_id LVS_DEVEL
vrrp_skip_check_adv_addr
vrrp_garp_interval 0
vrrp_gna_interval 0
script_security 1
max_auto_priority 1000
}
vrrp_script chk_haproxy {
script "/usr/bin/killall -0 haproxy" # Full path specified
interval 2
weight 2
}
vrrp_instance haproxy-vip {
state BACKUP
priority 100
interface ens33 # Define the network interface
virtual_router_id 60
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
unicast_src_ip 192.168.30.10 # Current Keepalived Node
unicast_peer {
192.168.30.11 # Peer Keepalived Node
}
virtual_ipaddress {
192.168.30.9/24 # Floating IP
}
track_script {
chk_haproxy
}
}
HAproxy & Keepalived Node 2:
global_defs {
notification_email {
}
router_id LVS_DEVEL
vrrp_skip_check_adv_addr
vrrp_garp_interval 0
vrrp_gna_interval 0
script_security 1
max_auto_priority 1000
}
vrrp_script chk_haproxy {
script "/usr/bin/killall -0 haproxy" # Full path specified
interval 2
weight 2
}
vrrp_instance haproxy-vip {
state BACKUP
priority 100
interface ens33 # Define the network interface
virtual_router_id 60
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
unicast_src_ip 192.168.30.11 # Current Keepalived Node
unicast_peer {
192.168.30.10 # Peer Keepalived Node
}
virtual_ipaddress {
192.168.30.9/24 # Floating IP
}
track_script {
chk_haproxy
}
}
Restart Serivce #
# Restart Keepalived
sudo systemctl restart keepalived
# Enable Keepalived after boot (Should be enabled per default)
sudo systemctl enable keepalived
# Check the status
systemctl status keepalived
Kubernetes Nodes #
Prerequisites #
This scripts set up the requirements for the Kubernetes nodes, installes the Containerd runtime, as well as Kubeadm, Kubelet and Kubectl. Kubectl is optional for the etcd nodes, and not necessary for the Worker nodes.
Install Kubeadm, Kubelet & Kubectl
# Create a file for the script
vi kubernetes-setup.sh
### Prerequisites ###
# Install dependencies
sudo apt update && sudo apt upgrade -y
sudo apt install apt-transport-https ca-certificates curl -y
# Enable IPv4 forwarding between network interfaces
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 settings
sudo sysctl --system
# Disable Swap
sudo sed -i '/[ \t]swap[ \t]/ s/^\(.*\)$/#\1/g' /etc/fstab
sudo swapoff -a
# Load the kernel modules at boot
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
# Load the kernel modules
sudo modprobe overlay && sudo modprobe br_netfilter
### Containerd Runtime ###
# Download the Docker GPG Key / save to file
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o docker.gpg
# Add the Key to the Trusted Keyring
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/docker-archive-keyring.gpg --import docker.gpg
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/docker-archive-keyring.gpg --export --output /etc/apt/trusted.gpg.d/docker-archive-keyring.gpg
# Set up the stable Docker repository
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/trusted.gpg.d/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install the Containerd package
sudo apt-get update && sudo apt-get install -y containerd.io
# Create configuration directory
sudo mkdir -p /etc/containerd
# Generates and save default configuration
containerd config default | sudo tee /etc/containerd/config.toml
# Set "SystemdCgroup" to "true"
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
# Restart Containerd service
sudo systemctl restart containerd
# Enable containerd after boot (Should be enabled per default)
sudo systemctl enable containerd
### PKubeadm & Kubelet ###
# Download the Kubernetes GPG Key / save to file
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.26/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
# Add the latest stable Kubernetes repository (Version 1.26.15)
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.26/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list
# Install kubelet & kubeadm
sudo apt update &&
sudo apt install -y kubelet kubeadm kubectl
# Stop automatic upgrades for the packages
sudo apt-mark hold kubelet kubeadm kubectl
# Start & enable kubelet
sudo systemctl enable --now kubelet
# Run the script
chmod +x kubernetes-setup.sh &&
./kubernetes-setup.sh
Install Kubeadm & Kubelet
# Create a file for the script
vi kubernetes-setup.sh
### Prerequisites ###
# Install dependencies
sudo apt update && sudo apt upgrade -y
sudo apt install apt-transport-https ca-certificates curl -y
# Enable IPv4 forwarding between network interfaces
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 settings
sudo sysctl --system
# Disable Swap
sudo sed -i '/[ \t]swap[ \t]/ s/^\(.*\)$/#\1/g' /etc/fstab
sudo swapoff -a
# Load the kernel modules at boot
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
# Load the kernel modules
sudo modprobe overlay && sudo modprobe br_netfilter
### Containerd Runtime ###
# Download the Docker GPG Key / save to file
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o docker.gpg
# Add the Key to the Trusted Keyring
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/docker-archive-keyring.gpg --import docker.gpg
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/docker-archive-keyring.gpg --export --output /etc/apt/trusted.gpg.d/docker-archive-keyring.gpg
# Set up the stable Docker repository
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/trusted.gpg.d/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install the Containerd package
sudo apt-get update && sudo apt-get install -y containerd.io
# Create configuration directory
sudo mkdir -p /etc/containerd
# Generates and save default configuration
containerd config default | sudo tee /etc/containerd/config.toml
# Set "SystemdCgroup" to "true"
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
# Restart Containerd service
sudo systemctl restart containerd
# Enable containerd after boot (Should be enabled per default)
sudo systemctl enable containerd
### PKubeadm & Kubelet ###
# Download the Kubernetes GPG Key / save to file
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.26/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
# Add the latest stable Kubernetes repository (Version 1.26.15)
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.26/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list
# Install kubelet & kubeadm
sudo apt update &&
sudo apt install -y kubelet kubeadm
# Stop automatic upgrades for the packages
sudo apt-mark hold kubelet kubeadm
# Start & enable kubelet
sudo systemctl enable --now kubelet
# Run the script
chmod +x kubernetes-setup.sh &&
./kubernetes-setup.sh
Etcd Nodes #
Kubelet Configuration #
Create Configuration #
Note: The path in the official documentation /etc/systemd/system/kubelet.service.d/20-etcd-service-manager.conf
is deprecated. This is the correct path: /usr/lib/systemd/system/kubelet.service.d/20-etcd-service-manager.conf
Create a new systemd service unit file that has higher precedence than the kubeadm-provided kubelet unit file:
cat << EOF | sudo tee /usr/lib/systemd/system/kubelet.service.d/20-etcd-service-manager.conf > /dev/null
[Service]
ExecStart=
ExecStart=/usr/bin/kubelet --address=127.0.0.1 --pod-manifest-path=/etc/kubernetes/manifests --cgroup-driver=systemd --runtime-request-timeout=15m --container-runtime-endpoint=unix:///var/run/containerd/containerd.sock
Restart=always
EOF
Restart Systemd Service Unit #
# Reload service unit configurations & restart the Kubelet service
sudo systemctl daemon-reload &&
sudo systemctl restart kubelet
Check Kubelet Status & Logs #
# Check the Kubelet status
sudo systemctl status kubelet
# Check the Kubelet logs
journalctl -u kubelet
Kubeadm Configuration Files #
This script creates a kubeadm configuration file for every etcd node:
# Create a file for the Kubeadm configuration script
vi kubeadm-config.sh
# Update HOST0, HOST1 and HOST2 with the IPs of your hosts
export HOST0=192.168.30.15
export HOST1=192.168.30.16
export HOST2=192.168.30.17
# Update NAME0, NAME1 and NAME2 with the hostnames of your hosts
export NAME0="ubuntu6"
export NAME1="ubuntu7"
export NAME2="ubuntu8"
# Create temp directories to store files that will end up on other hosts
mkdir -p /tmp/${HOST0}/ /tmp/${HOST1}/ /tmp/${HOST2}/
HOSTS=(${HOST0} ${HOST1} ${HOST2})
NAMES=(${NAME0} ${NAME1} ${NAME2})
for i in "${!HOSTS[@]}"; do
HOST=${HOSTS[$i]}
NAME=${NAMES[$i]}
cat << EOF > /tmp/${HOST}/kubeadmcfg.yaml
---
apiVersion: "kubeadm.k8s.io/v1beta3"
kind: InitConfiguration
nodeRegistration:
name: ${NAME}
localAPIEndpoint:
advertiseAddress: ${HOST}
---
apiVersion: "kubeadm.k8s.io/v1beta3"
kind: ClusterConfiguration
etcd:
local:
serverCertSANs:
- "${HOST}"
peerCertSANs:
- "${HOST}"
extraArgs:
initial-cluster: ${NAMES[0]}=https://${HOSTS[0]}:2380,${NAMES[1]}=https://${HOSTS[1]}:2380,${NAMES[2]}=https://${HOSTS[2]}:2380
initial-cluster-state: new
name: ${NAME}
listen-peer-urls: https://${HOST}:2380
listen-client-urls: https://${HOST}:2379
advertise-client-urls: https://${HOST}:2379
initial-advertise-peer-urls: https://${HOST}:2380
EOF
done
# Run the script
chmod +x kubeadm-config.sh &&
./kubeadm-config.sh
Generate the Certificate Authority #
Create CA Certs #
# Create CA certs
sudo kubeadm init phase certs etcd-ca
# Shell output:
I0627 13:35:49.570866 9642 version.go:256] remote version is much newer: v1.30.2; falling back to: stable-1.26
[certs] Generating "etcd/ca" certificate and key
Create Certs for other Etcd Nodes #
Create certificates for each etcd member:
# Create a file for the cert script
vi kubeadm-certs.sh
# Update HOST0, HOST1 and HOST2 with the IPs of your hosts
export HOST0=192.168.30.15
export HOST1=192.168.30.16
export HOST2=192.168.30.17
# Update NAME0, NAME1 and NAME2 with the hostnames of your hosts
export NAME0="ubuntu6"
export NAME1="ubuntu7"
export NAME2="ubuntu8"
kubeadm init phase certs etcd-server --config=/tmp/${HOST2}/kubeadmcfg.yaml
kubeadm init phase certs etcd-peer --config=/tmp/${HOST2}/kubeadmcfg.yaml
kubeadm init phase certs etcd-healthcheck-client --config=/tmp/${HOST2}/kubeadmcfg.yaml
kubeadm init phase certs apiserver-etcd-client --config=/tmp/${HOST2}/kubeadmcfg.yaml
cp -R /etc/kubernetes/pki /tmp/${HOST2}/
# cleanup non-reusable certificates
find /etc/kubernetes/pki -not -name ca.crt -not -name ca.key -type f -delete
kubeadm init phase certs etcd-server --config=/tmp/${HOST1}/kubeadmcfg.yaml
kubeadm init phase certs etcd-peer --config=/tmp/${HOST1}/kubeadmcfg.yaml
kubeadm init phase certs etcd-healthcheck-client --config=/tmp/${HOST1}/kubeadmcfg.yaml
kubeadm init phase certs apiserver-etcd-client --config=/tmp/${HOST1}/kubeadmcfg.yaml
cp -R /etc/kubernetes/pki /tmp/${HOST1}/
find /etc/kubernetes/pki -not -name ca.crt -not -name ca.key -type f -delete
kubeadm init phase certs etcd-server --config=/tmp/${HOST0}/kubeadmcfg.yaml
kubeadm init phase certs etcd-peer --config=/tmp/${HOST0}/kubeadmcfg.yaml
kubeadm init phase certs etcd-healthcheck-client --config=/tmp/${HOST0}/kubeadmcfg.yaml
kubeadm init phase certs apiserver-etcd-client --config=/tmp/${HOST0}/kubeadmcfg.yaml
# No need to move the certs because they are for HOST0
# clean up certs that should not be copied off this host
find /tmp/${HOST2} -name ca.key -type f -delete
find /tmp/${HOST1} -name ca.key -type f -delete
# Run the script
chmod +x kubeadm-certs.sh &&
sudo ./kubeadm-certs.sh
# Shell output:
I0627 13:42:35.499902 12242 version.go:256] remote version is much newer: v1.30.2; falling back to: stable-1.26
[certs] Generating "etcd/server" certificate and key
[certs] etcd/server serving cert is signed for DNS names [localhost ubuntu8] and IPs [192.168.30.17 127.0.0.1 ::1]
I0627 13:42:36.343628 12255 version.go:256] remote version is much newer: v1.30.2; falling back to: stable-1.26
[certs] Generating "etcd/peer" certificate and key
[certs] etcd/peer serving cert is signed for DNS names [localhost ubuntu8] and IPs [192.168.30.17 127.0.0.1 ::1]
I0627 13:42:37.208700 12269 version.go:256] remote version is much newer: v1.30.2; falling back to: stable-1.26
[certs] Generating "etcd/healthcheck-client" certificate and key
I0627 13:42:38.159396 12282 version.go:256] remote version is much newer: v1.30.2; falling back to: stable-1.26
[certs] Generating "apiserver-etcd-client" certificate and key
I0627 13:42:39.009913 12297 version.go:256] remote version is much newer: v1.30.2; falling back to: stable-1.26
[certs] Generating "etcd/server" certificate and key
[certs] etcd/server serving cert is signed for DNS names [localhost ubuntu7] and IPs [192.168.30.16 127.0.0.1 ::1]
I0627 13:42:39.868768 12310 version.go:256] remote version is much newer: v1.30.2; falling back to: stable-1.26
[certs] Generating "etcd/peer" certificate and key
[certs] etcd/peer serving cert is signed for DNS names [localhost ubuntu7] and IPs [192.168.30.16 127.0.0.1 ::1]
I0627 13:42:40.917507 12324 version.go:256] remote version is much newer: v1.30.2; falling back to: stable-1.26
[certs] Generating "etcd/healthcheck-client" certificate and key
I0627 13:42:41.805712 12337 version.go:256] remote version is much newer: v1.30.2; falling back to: stable-1.26
[certs] Generating "apiserver-etcd-client" certificate and key
I0627 13:42:42.618189 12352 version.go:256] remote version is much newer: v1.30.2; falling back to: stable-1.26
[certs] Generating "etcd/server" certificate and key
[certs] etcd/server serving cert is signed for DNS names [localhost ubuntu6] and IPs [192.168.30.15 127.0.0.1 ::1]
I0627 13:42:43.405003 12359 version.go:256] remote version is much newer: v1.30.2; falling back to: stable-1.26
[certs] Generating "etcd/peer" certificate and key
[certs] etcd/peer serving cert is signed for DNS names [localhost ubuntu6] and IPs [192.168.30.15 127.0.0.1 ::1]
I0627 13:42:44.356683 12373 version.go:256] remote version is much newer: v1.30.2; falling back to: stable-1.26
[certs] Generating "etcd/healthcheck-client" certificate and key
I0627 13:42:45.160194 12386 version.go:256] remote version is much newer: v1.30.2; falling back to: stable-1.26
[certs] Generating "apiserver-etcd-client" certificate and key
Create SSH Key #
Create a SSH key pair on the first etcd node and copy the public key to the other two etcd nodes and the first Controller node:
# Create a SSH key pair
ssh-keygen -t rsa -b 4096 -C "etcd-node-1"
# Copy the public key to the other two etcd nodes
ssh-copy-id ubuntu@192.168.30.16
ssh-copy-id ubuntu@192.168.30.17
# Copy the public key to the first controller node
ssh-copy-id ubuntu@192.168.30.12
Copy Certs and Kubeadm Configs #
Copy the Kubeadmin files and certificates that were created via the scripts on the first etcd node, to the other two etcd nodes.
Etcd Node 1:
# Copy the filles from the tmp directory into the home directory
sudo cp /tmp/192.168.30.15/* ~/
Etcd Node 2:
# Copy all files from /tmp/192.168.30.15/ to the remote host
sudo rsync -avz -e "ssh -i /home/ubuntu/.ssh/id_rsa" /tmp/192.168.30.16/* ubuntu@192.168.30.16:
# SSH into the host
ssh ubuntu@192.168.30.16
# Switch to sudo
sudo su
# Change ownership of the pki directory
chown -R root:root pki
# Move the pki directory to the proper location
mv pki /etc/kubernetes/
Etcd Node 3:
# Copy all files from /tmp/192.168.30.15/ to the remote host
sudo rsync -avz -e "ssh -i /home/ubuntu/.ssh/id_rsa" /tmp/192.168.30.17/* ubuntu@192.168.30.17:
# SSH into the host
ssh ubuntu@192.168.30.17
# Switch to sudo
sudo su
# Change ownership of the pki directory
chown -R root:root pki
# Move the pki directory to the proper location
mv pki /etc/kubernetes/
Verify the files and folders on all three etcd notes:
# List file and folder structure
tree /etc/kubernetes/pki
# Shell output:
/etc/kubernetes/pki
├── apiserver-etcd-client.crt
├── apiserver-etcd-client.key
└── etcd
├── ca.crt
├── healthcheck-client.crt
├── healthcheck-client.key
├── peer.crt
├── peer.key
├── server.crt
└── server.key
Create Static Pod Manifests #
Run the following command on all three etcd nodes:
# Generate a static pod manifest for etcd
sudo kubeadm init phase etcd local --config=/home/ubuntu/kubeadmcfg.yaml
# Shell output: Etcd node 1
I0627 14:06:32.898612 20735 version.go:256] remote version is much newer: v1.30.2; falling back to: stable-1.26
[etcd] Creating static Pod manifest for local etcd in "/etc/kubernetes/manifests"
# Shell output: Etcd node 2
I0627 14:06:47.806495 10250 version.go:256] remote version is much newer: v1.30.2; falling back to: stable-1.26
[etcd] Creating static Pod manifest for local etcd in "/etc/kubernetes/manifests"
# Shell output: Etcd node 3
I0627 14:06:55.302481 10549 version.go:256] remote version is much newer: v1.30.2; falling back to: stable-1.26
[etcd] Creating static Pod manifest for local etcd in "/etc/kubernetes/manifests"
Copy Certs to first Controller Node #
# Copy the certificates from the first etcd node to the first Controller node
sudo rsync -avz -e "ssh -i /home/ubuntu/.ssh/id_rsa" /etc/kubernetes/pki/etcd/ca.crt ubuntu@192.168.30.12: &&
sudo rsync -avz -e "ssh -i /home/ubuntu/.ssh/id_rsa" /etc/kubernetes/pki/apiserver-etcd-client.crt ubuntu@192.168.30.12: &&
sudo rsync -avz -e "ssh -i /home/ubuntu/.ssh/id_rsa" /etc/kubernetes/pki/apiserver-etcd-client.key ubuntu@192.168.30.12:
SSH into the Controller node and move the files into the right directory:
sudo mkdir -p /etc/kubernetes/pki/etcd/
sudo mv ~/ca.crt /etc/kubernetes/pki/etcd/
sudo mv ~/apiserver-etcd-client.crt /etc/kubernetes/pki/
sudo mv ~/apiserver-etcd-client.key /etc/kubernetes/pki/
# Set owner and permissions
sudo chown root:root /etc/kubernetes/pki/etcd/* &&
sudo chmod 600 /etc/kubernetes/pki/etcd/{apiserver-etcd-client.crt,apiserver-etcd-client.key} &&
sudo chmod 644 /etc/kubernetes/pki/etcd/ca.crt
Initialize the Cluster #
Create Kubeadm Configuration #
Create a Kubeadm configuration on the first Controller node:
# Create a configuration file for the cluster initialization
vi kubeadm-config.yaml
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: "stable"
controlPlaneEndpoint: "192.168.30.9:6443"
networking:
podSubnet: "10.0.0.0/16"
etcd:
external:
endpoints:
- https://192.168.30.15:2379 # Define etc nodes
- https://192.168.30.16:2379 # Define etc nodes
- https://192.168.30.17:2379 # Define etc nodes
caFile: /etc/kubernetes/pki/etcd/ca.crt
certFile: /etc/kubernetes/pki/apiserver-etcd-client.crt
keyFile: /etc/kubernetes/pki/apiserver-etcd-client.key
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd
Initialize First Controller Node #
# Pull the images
sudo kubeadm config images pull
# Initialize the cluster with the first Controller node
sudo kubeadm init --config kubeadm-config.yaml --upload-certs
# Shell output:
Your Kubernetes control-plane has initialized successfully!
To start using your cluster, you need to run the following as a regular user:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
Alternatively, if you are the root user, you can run:
export KUBECONFIG=/etc/kubernetes/admin.conf
You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
https://kubernetes.io/docs/concepts/cluster-administration/addons/
You can now join any number of the control-plane node running the following command on each as root:
kubeadm join 192.168.30.9:6443 --token k7y6gf.q1v4twk9l5d31rxc \
--discovery-token-ca-cert-hash sha256:ed7f440f5638284ec5f70063a93127f80e19d8d605fcc3d6028e036486d12c2a \
--control-plane --certificate-key 91d02730de815d933ea08f1ce9aaa1653a6009d959e1dee3ae7f26fc9b72aa93
Please note that the certificate-key gives access to cluster sensitive data, keep it secret!
As a safeguard, uploaded-certs will be deleted in two hours; If necessary, you can use
"kubeadm init phase upload-certs --upload-certs" to reload certs afterward.
Then you can join any number of worker nodes by running the following on each as root:
kubeadm join 192.168.30.9:6443 --token k7y6gf.q1v4twk9l5d31rxc \
--discovery-token-ca-cert-hash sha256:ed7f440f5638284ec5f70063a93127f80e19d8d605fcc3d6028e036486d12c2a
Kubectl Configuration #
Root user: Permanent
# Add kubeconfig path environment variable
echo 'export KUBECONFIG=/etc/kubernetes/admin.conf' >> ~/.bashrc
# Apply changes
source ~/.bashrc
Non root user: Permanent
# Create directory and copy kubeconfig file
mkdir -p $HOME/.kube &&
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config &&
sudo chown $(id -u):$(id -g) $HOME/.kube/config
# Add kubeconfig path environment variable & apply changes
echo 'export KUBECONFIG=$HOME/.kube/config' >> ~/.bashrc &&
source ~/.bashrc
Install Pod Network Add-On #
# Switch to root user
sudo su
Download the binaries:
# Download the Cilium binaries
curl -LO https://github.com/cilium/cilium-cli/releases/latest/download/cilium-linux-amd64.tar.gz
# Extract the binarie into the "/usr/local/bin" directory
sudo tar xzvfC cilium-linux-amd64.tar.gz /usr/local/bin
# Remove the archive
rm cilium-linux-amd64.tar.gz
Install Cilium:
# Install Cilium
cilium install
# Shell output:
ℹ️ Using Cilium version 1.15.5
🔮 Auto-detected cluster name: kubernetes
🔮 Auto-detected kube-proxy has been installed
Check the Status:
# Verify status
cilium status
# Shell output:
/¯¯\
/¯¯\__/¯¯\ Cilium: OK
\__/¯¯\__/ Operator: OK
/¯¯\__/¯¯\ Envoy DaemonSet: disabled (using embedded mode)
\__/¯¯\__/ Hubble Relay: disabled
\__/ ClusterMesh: disabled
Deployment cilium-operator Desired: 1, Ready: 1/1, Available: 1/1
DaemonSet cilium Desired: 1, Ready: 1/1, Available: 1/1
Containers: cilium Running: 1
cilium-operator Running: 1
Cluster Pods: 2/2 managed by Cilium
Helm chart version:
Image versions cilium quay.io/cilium/cilium:v1.15.5@sha256:4ce1666a73815101ec9a4d360af6c5b7f1193ab00d89b7124f8505dee147ca40: 1
cilium-operator quay.io/cilium/operator-generic:v1.15.5@sha256:f5d3d19754074ca052be6aac5d1ffb1de1eb5f2d947222b5f10f6d97ad4383e8: 1
Verify the Kubelet #
Verify the pods are up and running:
# List the Kubelet pods
kubectl get pods -n kube-system
# Shell output:
NAME READY STATUS RESTARTS AGE
cilium-445h5 1/1 Running 0 75s
cilium-operator-fdf6bc9f4-959rv 1/1 Running 0 75s
coredns-787d4945fb-6mxwg 1/1 Running 0 3m31s
coredns-787d4945fb-kcwdz 1/1 Running 0 3m31s
kube-apiserver-ubuntu3 1/1 Running 0 3m36s
kube-controller-manager-ubuntu3 1/1 Running 0 3m36s
kube-proxy-5zlqx 1/1 Running 0 3m31s
kube-scheduler-ubuntu3 1/1 Running 0 3m36s
# Check the Kubelet status
sudo systemctl status kubelet
# List Kubelet logs
sudo journalctl -u kubelet
Verify the Cluster #
# List the nodes
kubectl get nodes -o wide
# Shell output:
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
ubuntu3 Ready control-plane 3m54s v1.26.15 192.168.30.12 <none> Ubuntu 24.04 LTS 6.8.0-35-generic containerd://1.7.18
# List cluster info
kubectl cluster-info
# Shell output:
Kubernetes control plane is running at https://192.168.30.9:6443
CoreDNS is running at https://192.168.30.9:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
Verify Etcd Health #
Verify the etcd health / status from one of the Controller nodes:
Environment Variables #
echo ’export KUBECONFIG=/etc/kubernetes/admin.conf’ » ~/.bashrc
cat <<EOF >> ~/.bashrc
export ETCDCTL_API=3
export ETCDCTL_CACERT="/etc/kubernetes/pki/etcd/ca.crt"
export ETCDCTL_CERT="/etc/kubernetes/pki/apiserver-etcd-client.crt"
export ETCDCTL_KEY="/etc/kubernetes/pki/apiserver-etcd-client.key"
export ETCDCTL_ENDPOINTS="https://192.168.30.15:2379,https://192.168.30.16:2379,https://192.168.30.17:2379"
EOF
# Apply
source ~/.bashrc
Install Etcd Client #
# Install the etcd client
sudo apt update && sudo apt install etcd-client
Check the Cluster Health #
# Check the Etcd cluster health
etcdctl endpoint health
# Shell output:
https://192.168.30.15:2379 is healthy: successfully committed proposal: took = 7.039241ms
https://192.168.30.16:2379 is healthy: successfully committed proposal: took = 6.95793ms
https://192.168.30.17:2379 is healthy: successfully committed proposal: took = 8.35439ms
# Check the Etcd cluster health: More details
etcdctl endpoint status --write-out=table
# Shell output:
+----------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| ENDPOINT | ID | VERSION | DB SIZE | IS LEADER | IS LEARNER | RAFT TERM | RAFT INDEX | RAFT APPLIED INDEX | ERRORS |
+----------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| https://192.168.30.15:2379 | 1e18502efc8cdc6c | 3.5.10 | 5.4 MB | true | false | 2 | 5242 | 5242 | |
| https://192.168.30.16:2379 | 83b8fa383a8312d2 | 3.5.10 | 5.4 MB | false | false | 2 | 5242 | 5242 | |
| https://192.168.30.17:2379 | 340916df7770c511 | 3.5.10 | 5.3 MB | false | false | 2 | 5242 | 5242 | |
+----------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
Add Controller Nodes #
Add the other Controller Nodes #
Kubelet automatically sets up a stacked etcd cluster where etcd runs as a set of pods, one on each Controller node.
# Add the other two Controller nodes
This node has joined the cluster and a new control plane instance was created:
* Certificate signing request was sent to apiserver and approval was received.
* The Kubelet was informed of the new secure connection details.
* Control plane label and taint were applied to the new node.
* The Kubernetes control plane instances scaled up.
To start administering your cluster from this node, you need to run the following as a regular user:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
Run 'kubectl get nodes' to see this node join the cluster.
Verify the Cluster #
# List the nodes
kubectl get nodes -o wide
# Shell output:
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
ubuntu3 Ready control-plane 53m v1.26.15 192.168.30.12 <none> Ubuntu 24.04 LTS 6.8.0-35-generic containerd://1.7.18
ubuntu4 Ready control-plane 3m15s v1.26.15 192.168.30.13 <none> Ubuntu 24.04 LTS 6.8.0-35-generic containerd://1.7.18
ubuntu5 Ready control-plane 44s v1.26.15 192.168.30.14 <none> Ubuntu 24.04 LTS 6.8.0-35-generic containerd://1.7.18
br>
Add Worker Nodes #
Join the Worker Nodes #
# Join the Worker nodes
sudo kubeadm join 192.168.30.9:6443 --token k7y6gf.q1v4twk9l5d31rxc \
--discovery-token-ca-cert-hash sha256:ed7f440f5638284ec5f70063a93127f80e19d8d605fcc3d6028e036486d12c2a
Label Worker Nodes #
# Label the worker nodes
kubectl label nodes ubuntu9 kubernetes.io/role=worker &&
kubectl label nodes ubuntu10 kubernetes.io/role=worker
Verify the Cluster #
# List the nodes
kubectl get nodes -o wide
# Shell output:
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
ubuntu10 Ready worker 73s v1.26.15 192.168.30.19 <none> Ubuntu 24.04 LTS 6.8.0-35-generic containerd://1.7.18
ubuntu3 Ready control-plane 63m v1.26.15 192.168.30.12 <none> Ubuntu 24.04 LTS 6.8.0-35-generic containerd://1.7.18
ubuntu4 Ready control-plane 12m v1.26.15 192.168.30.13 <none> Ubuntu 24.04 LTS 6.8.0-35-generic containerd://1.7.18
ubuntu5 Ready control-plane 10m v1.26.15 192.168.30.14 <none> Ubuntu 24.04 LTS 6.8.0-35-generic containerd://1.7.18
ubuntu9 Ready worker 91s v1.26.15 192.168.30.18 <none> Ubuntu 24.04 LTS 6.8.0-35-generic containerd://1.7.18
Links #
# Kubeadm for external etcd nodes
https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/setup-ha-etcd-with-kubeadm/
# Cluster initialization with external etcd nodes
https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/high-availability/#manual-certs
# Load Balancing
https://github.com/kubernetes/kubeadm/blob/main/docs/ha-considerations.md#options-for-software-load-balancing
# Kubeadm High Availability
https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/high-availability/
# Network Add-ons
https://kubernetes.io/docs/concepts/cluster-administration/addons/
# Configure cgroup driver
https://v1-26.docs.kubernetes.io/docs/tasks/administer-cluster/kubeadm/configure-cgroup-driver/