
คู่มือแบบละเอียดสำหรับการติดตั้ง join และดูแล k3s cluster แบบ high-availability ที่แต่ละโหนดรันคนละ distro พร้อมตัวอย่างการ deploy ใช้งานจริง
k3s เป็น Kubernetes distribution แบบเบาที่ได้รับการรับรอง CNCF พัฒนาโดย Rancher (ปัจจุบันอยู่ใต้ SUSE) ต่างจาก Docker Swarm cluster ที่ทุกโหนดต้องรัน Docker Engine binary เดียวกันเป๊ะ ๆ k3s cluster ต้องการแค่ให้แต่ละโหนดรัน k3s เวอร์ชัน เดียวกันเท่านั้น — ระบบปฏิบัติการที่อยู่ข้างใต้ไม่มีผลต่อ control plane เลย ทำให้เหมาะกับ home lab หรือ edge fleet ที่มีฮาร์ดแวร์และ distro หลากหลายสะสมมาตามกาลเวลา
คู่มือนี้จะสร้าง 3-server, embedded-etcd, highly-available cluster โดยใช้ 3 distro ที่ต่างกัน — หนึ่งโหนดต่อหนึ่ง distro:
| โหนด | Hostname | Distro | บทบาท |
|---|---|---|---|
| โหนด 1 | k3s-node1 |
openSUSE Tumbleweed | Server (cluster-init) |
| โหนด 2 | k3s-node2 |
Ubuntu (22.04/24.04) | Server (join) |
| โหนด 3 | k3s-node3 |
Arch Linux | Server (join) |
เนื่องจากทั้ง 3 โหนดรัน server role พร้อม embedded etcd ถ้าโหนดใดโหนดหนึ่งล่ม cluster ก็ยังคง serve API และ schedule workload ต่อได้ตามปกติ — resilience ระดับเดียวกับที่คุณคาดหวังจาก Docker Swarm แบบ 3-manager หรือ Nomad/Consul quorum แบบ 3-server
(3-1)/2 = 1) นี่คือเหตุผลที่จำนวนโหนดควรเป็นเลขคี่ (1, 3, 5) เสมอ — ข้อจำกัดเดียวกับที่ server pool ของ HashiCorp Consul/Nomad ต้องปฏิบัติตามlocal-path-provisioner ถูก bundle มาเป็น default StorageClass รองรับ PersistentVolumeClaim ด้วย directory บนตัวโหนดเองตัวอย่างชุด IP ที่ใช้ตลอดคู่มือนี้ — เปลี่ยนเป็นของคุณเองได้:
| โหนด | IP Address |
|---|---|
| k3s-node1 (openSUSE) | 192.168.1.11 |
| k3s-node2 (Ubuntu) | 192.168.1.12 |
| k3s-node3 (Arch) | 192.168.1.13 |
สิ่งที่ต้องมีในทุกโหนด:
chronyd หรือ systemd-timesyncd บนทุกโหนดก่อนเริ่มkubeadm แบบ vanilla, k3s รองรับการเปิด swap ได้ตามปกติรันคำสั่งต่อไปนี้บน ทั้งสามโหนด ไม่ว่าจะเป็น distro ไหนก็ตาม
ตั้ง hostname ที่ไม่ซ้ำกัน:
sudo hostnamectl set-hostname k3s-node1 # เปลี่ยนตามแต่ละโหนด
ทำให้ทุกโหนด resolve หากันได้ ตั้ง local DNS หรือเพิ่ม entry ใน /etc/hosts บนทั้งสามโหนด:
sudo tee -a /etc/hosts <<'EOF'
192.168.1.11 k3s-node1
192.168.1.12 k3s-node2
192.168.1.13 k3s-node3
EOF
Port ที่ firewall ต้องเปิดระหว่างโหนด (คำสั่งเฉพาะแต่ละ distro อยู่ในหัวข้อถัดไป):
| Port | Protocol | หน้าที่ |
|---|---|---|
| 6443 | TCP | Kubernetes API server |
| 2379-2380 | TCP | etcd client/peer traffic (เฉพาะระหว่าง server ด้วยกัน) |
| 8472 | UDP | Flannel VXLAN overlay (traffic ระหว่าง pod) |
| 10250 | TCP | Kubelet metrics/logs API |
| 51820, 51821 | UDP | Flannel Wireguard backend (เฉพาะถ้าเปลี่ยน CNI backend เป็น Wireguard) |
เตรียม package และ firewall:
sudo zypper refresh
sudo zypper install -y curl open-iscsi
# openSUSE มี firewalld มาให้เป็น default
sudo firewall-cmd --permanent --add-port=6443/tcp
sudo firewall-cmd --permanent --add-port=2379-2380/tcp
sudo firewall-cmd --permanent --add-port=8472/udp
sudo firewall-cmd --permanent --add-port=10250/tcp
sudo firewall-cmd --reload
ติดตั้ง k3s และเริ่มต้น cluster:
curl -sfL https://get.k3s.io | sh -s - server \
--cluster-init \
--node-name k3s-node1 \
--write-kubeconfig-mode 644
--cluster-init บอกให้ k3s สร้าง embedded-etcd cluster ใหม่ตั้งแต่ต้นบนโหนดนี้ แทนที่จะ join cluster ที่มีอยู่แล้ว
ดึง token สำหรับ join — ต้องใช้กับโหนดที่ 2 และ 3:
sudo cat /var/lib/rancher/k3s/server/node-token
ยืนยันว่า service ทำงานปกติ:
sudo systemctl status k3s
sudo k3s kubectl get nodes
เตรียม package และ firewall:
sudo apt update
sudo apt install -y curl
sudo ufw allow 6443/tcp
sudo ufw allow 2379:2380/tcp
sudo ufw allow 8472/udp
sudo ufw allow 10250/tcp
sudo ufw reload
ข้อควรระวังเรื่อง Ubuntu + systemd-resolved: DNS stub resolver default ของ Ubuntu (
127.0.0.53) อาจทำให้ CoreDNS ในคลัสเตอร์สับสนได้ ถ้าภายหลังพบว่า DNS resolution จาก pod มีปัญหา ให้ติดตั้ง k3s ใหม่โดยเพิ่ม--resolv-conf /run/systemd/resolve/resolv.confเพื่อข้าม stub ของ systemd-resolved
Join เป็น server เพิ่ม (แทนที่ <TOKEN> ด้วยค่าจากโหนด 1):
curl -sfL https://get.k3s.io | sh -s - server \
--server https://192.168.1.11:6443 \
--token <TOKEN> \
--node-name k3s-node2 \
--write-kubeconfig-mode 644
ข้อมูลอ้างอิง — ถ้าต้องการ agent-only node แทน (ไม่ได้ใช้ใน topology 3-server นี้ แต่มีประโยชน์ถ้าภายหลังเพิ่มเครื่องที่ 4 ไว้รัน workload อย่างเดียว):
curl -sfL https://get.k3s.io | K3S_URL=https://192.168.1.11:6443 K3S_TOKEN=<TOKEN> sh -
Arch ไม่มี package k3s อย่างเป็นทางการใน repo หลัก แต่ install script จากต้นทางใช้งานได้เหมือนกับ distro อื่นที่ใช้ systemd ทุกประการ นอกจากนี้ยังมี AUR package (k3s-bin) ให้เลือกใช้ถ้าอยากติดตั้งผ่าน pacman
เตรียม package และ firewall. Arch ไม่มี firewall มาให้เป็น default — ติดตั้ง firewalld ให้สอดคล้องกับอีกสองโหนด หรือจะใช้ iptables/nftables ตรง ๆ ก็ได้:
sudo pacman -Syu --noconfirm curl firewalld
sudo systemctl enable --now firewalld
sudo firewall-cmd --permanent --add-port=6443/tcp
sudo firewall-cmd --permanent --add-port=2379-2380/tcp
sudo firewall-cmd --permanent --add-port=8472/udp
sudo firewall-cmd --permanent --add-port=10250/tcp
sudo firewall-cmd --reload
Join เป็น server ตัวที่สาม:
curl -sfL https://get.k3s.io | sh -s - server \
--server https://192.168.1.11:6443 \
--token <TOKEN> \
--node-name k3s-node3 \
--write-kubeconfig-mode 644
อีกทางเลือก: AUR package ถ้าอยากจัดการ k3s ผ่าน pacman แทน:
yay -S k3s-bin
sudo systemctl enable --now k3s
(ยังต้องตั้งค่า K3S_URL/K3S_TOKEN หรือ flag เทียบเท่าใน /etc/systemd/system/k3s.service.env ก่อน start service)
รันจากโหนด server ตัวไหนก็ได้ในสามตัว:
sudo k3s kubectl get nodes -o wide
ผลลัพธ์ที่ควรได้ — ทั้งสามโหนดสถานะ Ready แต่ละตัวแสดง OS image ของตัวเอง ในคอลัมน์ OS-IMAGE (พิสูจน์ว่านี่คือ mixed-distro cluster จริง ๆ):
NAME STATUS ROLES AGE VERSION OS-IMAGE
k3s-node1 Ready control-plane,etcd,master 10m v1.34.6+k3s1 openSUSE Tumbleweed
k3s-node2 Ready control-plane,etcd,master 7m v1.34.6+k3s1 Ubuntu 24.04.2 LTS
k3s-node3 Ready control-plane,etcd,master 5m v1.34.6+k3s1 Arch Linux
ตรวจสอบว่า system pod ขึ้นมาเรียบร้อยดี:
sudo k3s kubectl get pods -A
โดย default k3s จะเขียน kubeconfig ไว้ที่ /etc/rancher/k3s/k3s.yaml ชี้ไปที่ 127.0.0.1 ถ้าจะใช้จาก laptop/workstation ให้ copy มาแล้วแก้ server address:
scp k3s-node1:/etc/rancher/k3s/k3s.yaml ~/.kube/k3s-lab.yaml
sed -i 's/127.0.0.1/192.168.1.11/' ~/.kube/k3s-lab.yaml
export KUBECONFIG=~/.kube/k3s-lab.yaml
kubectl get nodes
เพื่อความทนทานเผื่อโหนด 1 ล่ม สามารถชี้ kubectl ไปที่ IP ของ server ตัวไหนก็ได้ในสามตัวบน port 6443 หรือจะวาง VIP/round-robin DNS ครอบทั้งสาม IP ไว้ก็ได้
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-web
spec:
replicas: 3
selector:
matchLabels:
app: hello-web
template:
metadata:
labels:
app: hello-web
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: hello-web
spec:
selector:
app: hello-web
ports:
- port: 80
targetPort: 80
kubectl apply -f hello-web.yaml
kubectl get pods -o wide # สังเกตว่า replica กระจายไปทั้ง 3 โหนด
มีประโยชน์เมื่ออยากเข้าถึง service ตรง ๆ ผ่าน IP ของโหนดโดยไม่ต้องตั้ง Ingress:
apiVersion: v1
kind: Service
metadata:
name: hello-web-nodeport
spec:
type: NodePort
selector:
app: hello-web
ports:
- port: 80
targetPort: 80
nodePort: 30080
ตอนนี้ http://192.168.1.11:30080, http://192.168.1.12:30080 หรือ http://192.168.1.13:30080 ทุกที่จะเข้าถึง service เดียวกันได้หมด ไม่ว่า pod จะไปอยู่โหนดไหนจริง ๆ ก็ตาม
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hello-web-ingress
spec:
rules:
- host: hello.homelab.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: hello-web
port:
number: 80
ชี้ hello.homelab.local ไปที่ IP โหนดใดก็ได้ (ผ่าน /etc/hosts บนเครื่อง client หรือใช้ DNS จริง) แล้ว:
curl -H "Host: hello.homelab.local" http://192.168.1.11
ตัวอย่างการใช้ local-path StorageClass ที่ bundle มาให้ ผ่าน PVC template — ในที่นี้ใช้กับ instance PostgreSQL ขนาดเล็ก โดยดึง credential มาจาก Secret (ดู 10.5):
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16-alpine
envFrom:
- secretRef:
name: postgres-credentials
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: local-path
resources:
requests:
storage: 2Gi
kubectl create secret generic postgres-credentials \
--from-literal=POSTGRES_USER=appuser \
--from-literal=POSTGRES_PASSWORD='change-me'
kubectl create configmap app-config \
--from-literal=APP_ENV=production \
--from-literal=LOG_LEVEL=info
การอ้างอิงทั้งสองใน pod spec:
spec:
containers:
- name: app
image: your-app:latest
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: postgres-credentials
# Scale ออก
kubectl scale deployment hello-web --replicas=6
# Roll ไปเวอร์ชัน image ใหม่
kubectl set image deployment/hello-web nginx=nginx:1.27-alpine
kubectl rollout status deployment/hello-web
# Rollback ถ้ามีอะไรพัง
kubectl rollout undo deployment/hello-web
k3s ใช้ containerd เป็น default แต่สามารถชี้ไปที่ CRI-compliant runtime ตัวไหนก็ได้ — รวมถึง CRI-O ที่มีสายตระกูลเดียวกับ Podman ติดตั้งและเปิดใช้ CRI-O บนโหนดก่อน แล้วติดตั้ง k3s ด้วย:
curl -sfL https://get.k3s.io | sh -s - server \
--container-runtime-endpoint unix:///var/run/crio/crio.sock \
--cluster-init
ใช้ flag เดียวกันตอน join โหนด server/agent เพิ่มเติม คุ้มค่าที่จะทำถ้าอยากให้ทุก layer ของ stack — ตั้งแต่ build image, รัน container เดี่ยว ๆ ไปจนถึง cluster node — อยู่ในสาย Podman/CRI-O tooling เดียวกันหมด แทนที่จะผสม containerd เข้ามา
รัน install script ใหม่โดยระบุเวอร์ชันที่ต้องการ ทำทีละโหนด เริ่มจาก server ก่อน:
curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=v1.34.7+k3s1 sh -s - server --cluster-init
สำหรับ fleet ระดับ production system-upgrade-controller ช่วย automate การ rollout แบบทยอยทำทั่วทุกโหนดได้ แทนที่จะต้อง SSH เข้าไปทำเองทีละเครื่อง
k3s สำรอง etcd snapshot อัตโนมัติบน server node อยู่แล้ว แต่สามารถสั่งเองได้ด้วย:
sudo k3s etcd-snapshot save --name pre-upgrade-backup
sudo ls /var/lib/rancher/k3s/server/db/snapshots/
การกู้คืน (รันบนโหนดเดียว ต้องหยุด cluster ก่อน):
sudo systemctl stop k3s
sudo k3s server --cluster-reset \
--cluster-reset-restore-path=/var/lib/rancher/k3s/server/db/snapshots/pre-upgrade-backup
sudo systemctl start k3s
# รันจาก server node ที่ยังปกติดี
kubectl drain k3s-node3 --ignore-daemonsets --delete-emptydir-data
kubectl delete node k3s-node3
จากนั้นบนโหนดที่กำลังจะถอดออก:
sudo /usr/local/bin/k3s-uninstall.sh # ถ้าเป็น server
sudo /usr/local/bin/k3s-agent-uninstall.sh # ถ้าเป็น agent
timedatectl status ว่านาฬิกา sync กันทั้งสามโหนดContainerCreating — ตรวจสอบว่า Flannel VXLAN (UDP 8472) ไม่ถูก firewall บล็อกระหว่างโหนด--resolv-conf /run/systemd/resolve/resolv.conf เพื่อข้าม stub ของ systemd-resolvedsudo journalctl -u k3s -f
/var/lib/rancher/k3s/server/node-token บน server ตัวไหนก็ได้ที่มีอยู่แล้ว ระวังเรื่อง whitespace ตอน copy3-server, embedded-etcd k3s cluster ให้ resilience แบบเดียวกับ Swarm — ทนต่อการเสียโหนดได้ 1 ตัวโดยไม่ downtime — พร้อมกับได้ Kubernetes API และ ecosystem เต็มรูปแบบข้างใต้ เพราะ k3s สนใจแค่เวอร์ชันตรงกัน ไม่สนใจว่า distro จะตรงกันหรือไม่ จึงเป็นวิธีที่สะดวกจริง ๆ ในการรวม home lab หรือ edge fleet ที่มี openSUSE, Ubuntu, และ Arch สะสมมาตามธรรมชาติ ให้ทำงานร่วมกันได้โดยไม่ต้องบังคับ standardize OS ก่อน