cloud

使用 kubeadm 建立 Kubernetes 叢集

以 kubeadm 建立私有 Kubernetes 叢集的實作流程。

English繁中
使用 kubeadm 建立 Kubernetes 叢集

這是我在 2019 年建立的 lab:Kubernetes 1.15.3、Docker 19.3.1、Ubuntu 18.04 與 CentOS 7。下列指令保留當時環境;新建叢集請參考目前的 kubeadm 安裝文件,套件來源與 Docker 整合方式已有變更。

Kubernetes dashboard

硬體與節點配置

我以一台實體主機執行整個多節點環境,配置是 Intel Core i7-8700(6 cores/12 threads)、32 GB RAM 與 512 GB SSD,另有供 GPU workloads 使用的 NVIDIA RTX 2070 O8G。這是當時的實機配置,不是 Kubernetes 最低需求。

Ubuntu host 擔任 control-plane node,四台 CentOS Vagrant VMs 擔任 workers。也可以把相同拓撲分布到多台實體機器。

Host 安裝 VirtualBox 與 Vagrant,並在 BIOS 啟用硬體虛擬化。

設定每台 VM

各 worker 的 Vagrantfile 如下,需分別調整 IP、bridge、MAC、hostname 與 VM 名稱:

# save this file as Vagrantfile
Vagrant.configure(2) do |config|
  config.vm.box = "centos/7"
  config.vm.synced_folder '.', '/vagrant', disabled: true
  # change IP to your local IP address
  config.vm.network "public_network", ip: "192.168.2.150",bridge: "eth1", :mac => "8CA3ABCE4001"
  # change hostname to k8s-node1, k8s-node2...
  config.vm.hostname = "k8s-node1"

  config.vm.provision "shell",
    inline: "sudo iptables -P FORWARD ACCEPT"
  # load prepare_k8s.sh script
  config.vm.provision "shell", path: "./../prepare_k8s.sh", keep_color: true
  config.vm.provider :virtualbox do |virtualbox, override|
    # change name, memory, cpu
    virtualbox.name = "node1"
    virtualbox.memory = 5000
    virtualbox.cpus = 4
  end
end

Memory 與 CPU 依主機能力調整。共同的 prepare_k8s.sh 安裝 Docker 與 Kubernetes 元件,設定 DNS、SELinux、bridge sysctl,並停用 swap:

#! /bin/sh

# give nodes a DNS server; otherwise DNS can fail during setup.
sudo cat <<EOF > /etc/resolv.conf
nameserver 8.8.8.8
EOF

# update and install Docker.
sudo yum update -y
sudo yum install -y vim yum-utils device-mapper-persistent-data lvm2 net-tools
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo -y
sudo yum install -y docker-ce docker-ce-cli containerd.io
sudo systemctl start docker.service
sudo systemctl enable docker.service


# install kubernetes
sudo cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
exclude=kube*
EOF

# set SELINUX enforcing -> permissive
sudo setenforce 0
sudo sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config
sudo sed -i 's/^GRUB_DEFAULT=saved$/GRUB_DEFAULT=0/' /etc/default/grub

sudo yum install -y kubelet kubeadm kubectl --disableexcludes=kubernetes

sudo systemctl enable --now kubelet

sudo cat <<EOF >  /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
EOF
sudo sysctl --system

# important: turn off swap.
sudo swapoff -a

目錄配置如下:

kubernetes/node1/Vagrantfile
kubernetes/node2/Vagrantfile
kubernetes/node3/Vagrantfile
kubernetes/node4/Vagrantfile
kubernetes/prepare_k8s.sh

啟動並檢查 workers

進入各 node 目錄啟動 VM,再登入確認 swap 設定:

# cd node1, node2, node3, node4
$ sudo vagrant up
......some boot message......

# ssh login to node1
$ sudo vagrant ssh

# ensure swap is disabled.
[vagrant@k8s-node1 ~]$ cat /etc/fstab
#
# /etc/fstab
UUID=8ac075e3-1124-4bb6-bef7-xxxxxx /                       xfs     defaults        0 0
#/swapfile none swap defaults 0 0

若 swap 仍開啟,執行 sudo swapoff -a,並修改 /etc/fstab,避免重開機後恢復。各 worker 與 control-plane node 都要確認自己的基礎設定。

查詢 worker 的網路介面:

# ssh to each node
$ ifconfig eth1

這個 lab 的節點位於同一個 subnet:

master: 192.168.2.110
node1:  192.168.2.150
node2:  192.168.2.151
node3:  192.168.2.152
node4:  192.168.2.153

Host 上執行 top 可以看到四個 VBoxHeadless processes。

Host 上執行的 VM processes

準備 control-plane node

Ubuntu host 使用當時的 APT 套件來源安裝 Kubernetes:

# Ubuntu, Debian
apt-get update && apt-get install -y apt-transport-https curl
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
cat <<EOF >/etc/apt/sources.list.d/kubernetes.list
deb https://apt.kubernetes.io/ kubernetes-xenial main
EOF
apt-get update
apt-get install -y kubelet kubeadm kubectl
apt-mark hold kubelet kubeadm kubectl

若 control-plane 使用 CentOS,當時對應的安裝方式是:

# CentOS
cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
EOF

# Set SELinux in permissive mode (effectively disabling it)
setenforce 0
sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config

yum install -y kubelet kubeadm kubectl --disableexcludes=kubernetes

systemctl enable --now kubelet

Workers 已由 prepare_k8s.sh 安裝 kubeadm 與 kubelet,不必重複安裝。這些舊版套件來源不能用來推定今日可安裝的版本。

初始化 control plane

使用預定的 Pod CIDR 執行 kubeadm init

$ sudo kubeadm init --pod-network-cidr=10.244.0.0/16

[init] Using Kubernetes version: vX.Y.Z
[preflight] Running pre-flight checks
[preflight] Pulling images required for setting up a Kubernetes cluster
[preflight] This might take a minute or two, depending on the speed of your internet connection
[preflight] You can also perform this action in beforehand using 'kubeadm config images pull'
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Activating the kubelet service
.........
kubeadm join --token <token> <master-ip>:<master-port> --discovery-token-ca-cert-hash sha256:<hash>
# you should get this kubeadm join information.

完成後設定 kubectl:

$ mkdir -p $HOME/.kube
$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ sudo chown $(id -u):$(id -g) $HOME/.kube/config

此時叢集只有 control-plane node。保留 init 輸出的 join 指令,供 worker 加入。

加入 workers 並指定正確的 node IP

登入每個 worker,使用 init 輸出的 server、token 與 CA hash:

# use `sudo vagrant ssh` to log in to each node.
$ sudo kubeadm join 192.168.2.xxxx:6443 --token xxxxxxxxx --discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxx

多網卡 VM 要確認 kubelet 使用同一個 LAN 上的 node IP。以下以 node1 為例,在 kubeadm-flags.env 設定 --node-ip,再重啟 kubelet:

$ sudo vim /var/lib/kubelet/kubeadm-flags.env
KUBELET_KUBEADM_ARGS="--cgroup-driver=cgroupfs --network-plugin=cni --pod-infra-container-image=k8s.gcr.io/pause:3.1 --node-ip=192.168.2.150"

# restart kubelet
$ sudo systemctl restart kubelet

其餘 nodes 使用各自的位址。

安裝 Flannel

Pod network 不可與 host network 重疊。kubeadm init --pod-network-cidr 與 Flannel 的 Network 必須一致。本例預設是 10.244.0.0/16,bridge 的 net.bridge.bridge-nf-call-iptables 需為 1;前面的 script 已設定。

當時使用的 Flannel manifest:

# apply flannel to your cluster.
kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/62e44c867a2846fefb68bd5f178daf4da3095ccb/Documentation/kube-flannel.yml

其中的網段設定是:

# at kube-flannel.yml we can see this network config.
net-conf.json: |
    {
      "Network": "10.244.0.0/16",
      "Backend": {
        "Type": "vxlan"
      }
    }

如果改用 10.32.0.0/16,初始化時的 CIDR 與 Flannel YAML 都要同步修改。套用後,worker 上會出現 flannel.1cni0;使用該 CIDR 時,可看到 10.32.x.x 位址:

# I use CIDR 10.32.0.0/16. ifconfig will display 10.32.x.x

[vagrant@k8s-node1 ~]$ ifconfig flannel.1
flannel.1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1450
        inet 10.32.4.0  netmask 255.255.255.255  broadcast 0.0.0.0
        inet6 fe80::24ae:5eff:xxxx:xxxx  prefixlen 64  scopeid 0x20<link>
        ether 26:ae:5e:29:57:d8  txqueuelen 0  (Ethernet)
        RX packets 994894  bytes 1042168344 (993.8 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 361101  bytes 400393922 (381.8 MiB)
        TX errors 0  dropped 8 overruns 0  carrier 0  collisions 0

[vagrant@k8s-node1 ~]$ ifconfig cni0
cni0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1450
        inet 10.32.4.1  netmask 255.255.255.0  broadcast 0.0.0.0
        inet6 fe80::1876:f4ff:xxxx:xxxx  prefixlen 64  scopeid 0x20<link>
        ether 1a:76:f4:0d:6c:5a  txqueuelen 1000  (Ethernet)
        RX packets 460962  bytes 353293465 (336.9 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 478311  bytes 1050286009 (1001.6 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

另一個 node 的介面範例:

[vagrant@k8s-node2 ~]$ ifconfig cni0
cni0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1450
        inet 10.32.3.1  netmask 255.255.255.0  broadcast 0.0.0.0
        inet6 fe80::c468:e0ff:xxxx:xxxx  prefixlen 64  scopeid 0x20<link>
        ether c6:68:e0:40:12:31  txqueuelen 1000  (Ethernet)
        RX packets 592707  bytes 157355303 (150.0 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 522413  bytes 655781024 (625.4 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

[vagrant@k8s-node2 ~]$ ifconfig flannel.1
flannel.1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1450
        inet 10.32.3.0  netmask 255.255.255.255  broadcast 0.0.0.0
        inet6 fe80::c003:63ff:xxxx:xxxx  prefixlen 64  scopeid 0x20<link>
        ether c2:03:63:59:12:d1  txqueuelen 0  (Ethernet)
        RX packets 291008  bytes 166468118 (158.7 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 300398  bytes 140955741 (134.4 MiB)
        TX errors 0  dropped 8 overruns 0  carrier 0  collisions 0

確認 cluster 就緒

# at master node
$ kubectl get node -o wide
NAME           STATUS   ROLES    AGE   VERSION   INTERNAL-IP     EXTERNAL-IP   OS-IMAGE                KERNEL-VERSION               CONTAINER-RUNTIME
k8s-node1      Ready    <none>   18h   v1.15.3   192.168.2.150   <none>        CentOS Linux 7 (Core)   3.10.0-957.27.2.el7.x86_64   docker://19.3.1
k8s-node2      Ready    <none>   23h   v1.15.3   192.168.2.151   <none>        CentOS Linux 7 (Core)   3.10.0-957.27.2.el7.x86_64   docker://19.3.1
k8s-node3      Ready    <none>   23h   v1.15.3   192.168.2.152   <none>        CentOS Linux 7 (Core)   3.10.0-957.27.2.el7.x86_64   docker://19.3.1
k8s-node4      Ready    <none>   23h   v1.15.3   192.168.2.153   <none>        CentOS Linux 7 (Core)   3.10.0-957.27.2.el7.x86_64   docker://19.3.1
master         Ready    master   23h   v1.15.3   192.168.2.110   <none>        Ubuntu 18.04.3 LTS      5.0.0-25-generic             docker://19.3.1

所有 nodes 應顯示 Ready。需要跨 Pod 重建保留資料時,可以接著閱讀 NFS StorageClass

Node 或 Pod 網路失敗時,先檢查介面、Pod CIDR、CNI 與防火牆規則。不要把清空所有 iptables chains 當成通用修法,因為它也會移除 Kubernetes 與 host firewall 使用的其他規則。