先前參加SRE Conference時,認識了CNTUG(Cloud Native Taiwan User Group)這個開源社群,除了推廣雲端原生的相關技術以外,也提供Lab讓大家能夠申請、在上面做一些很難在自己本機上面的實驗(例如kubernetes cluster的建立),很幸運地前陣子遞交的Lab申請通過了,就也打算來寫一篇文章記錄整個實驗的架設與心得。

環境準備

參考了CNTUG網站與kubernetes官方網站上面關於VM硬體條件的文件,這次在openstack上架了四台VM,一台bastion host、一台control plane(m0)、兩台worker node(n0, n1)。 四台VM分別的硬體條件如下:

IP Address Server Name Role CPU Ram OS
192.168.200.100 bastion-host Bastion Host 2 2G Ubuntu 22.04
192.168.200.101 k8s-m0 Master Node - 0 4 4G Ubuntu 22.04
192.168.200.102 k8s-n0 Worker Node - 0 4 4G Ubuntu 22.04
192.168.200.103 k8s-n1 Worker Node - 1 4 4G Ubuntu 22.04

網路設定

準備兩張網卡:public與private。本實驗環境會將kubernetes cluster都放在內網,僅讓bastion host做對外的連線。

在拿到openstack的帳號時,public網卡已經先幫我們建立好了,接下來要自己手動新增內網,並且能讓內網去連接到外網。在設定內網時,記得勾選「啟用DHCP」讓每個加進這個網路的VM都會被自動分配到唯一的ip位址。

  • Public Network:
    • Public IPv4: 103.122.XXX.0/23
  • Private Network:
    • 子網路名稱:private-net
    • 網路位址:192.168.200.0/24
    • IP版本:IPv4
    • 閘道IP(Gateway IP):192.168.200.1
    • 靜態路由(內網所有的設備要訪問任何不在內網的位址時,都把封包送到下一跳點的位址):
      • 目標CIDR:0.0.0.0/0
      • 下一跳點:103.122.117.XXX (Public gateway IP)

安全性群組設定

方才我們透過網路來設定各個網路設備的連接,那麼接下來進一步地透過安全性群組來管理端口與每台VM的流量。

除了kubernetes本身需要開啟的端口以外,我會希望可以透過bastion host來ssh到cluster裡面的每個node,在內網的所有intance也需要互相連結。除此之外,bastion host也要開一個對外的22 port讓我可以從本地進行操作。

Control Plane和Worker Node的port全部都參考官網建議去設置:kubernetes: Ports and Protocols

最後,會有四個安全性群組:(所有的outbound都設置為 0.0.0.0/0,故以下僅列出inbound rule)

Security Group Ether Type IP protocol Port Range Remote IP Prefix Remote Security Group Description
Public SSH IPv4 TCP 22 (SSH) (自己的電腦IP) NULL NULL
Private SSH IPv4 TCP 22 (SSH) 192.168.200.0/24 NULL NULL
Master node IPv4 TCP 2379 - 2380 192.168.200.0/24 etcd server client API
IPv4 TCP 6443 192.168.200.0/24 Kubernetes API server
IPv4 TCP 10250 NULL Master node Kubelet API
IPv4 TCP 10257 NULL Master node kube-controller-manager
IPv4 TCP 10259 NULL Master node kube-schedule
Worker node IPv4 TCP 10250 NULL Worker node Kubelet API Self
Worker node IPv4 TCP 10256 NULL Worker node kube-proxy
IPv4 TCP 30000 - 32767 0.0.0.0/0 Worker node NodePort Services

Master Node中的etcd元件是一個分散式的key-value資料庫,會保存kubernetes cluster裡面的所有資料,cluster裡面的所有狀態——例如Pod, Volume, Service的當前狀態——都會保存在這個資料庫中,由於在Worker Node的狀態會被回傳到etcd元件,因此開放讓內網裡所有的IP都能連線。

在Master Node中另一個會需要開放給內網所有instance的原件還有Kubernetes API server,它讓使用者可以直接與cluster進行互動,包含查詢狀態、建立、更新、刪除cluster內部的資源等。同時由於我們先前已讓bastion host來處理所有來自外部的連線,因此在這邊的IP也是設定為內網IP即可。

VM設置

網路與安全性群組都設定好後,cluster裡面的VM全都插入private網卡,再根據master/worker node分別分派各自的安全性群組。bastion host則插入public與private兩張網卡,以便與外部和內部網路進行連接。

openstack的網路設定有個小坑:bastion-host加入private網卡後,依然沒有辦法ping到內網的任何一台VM。經由ip addr確認後證實雖然openstack的UI介面顯示已經加入網卡了,但實際上private網卡並沒有設定到。

這時候可以手動修改設定檔:

sudo vim /etc/netplan/50-cloud-init.yaml

在設定檔中手動加入網卡:

network:
    ethernets:
        enp3s0:
            dhcp4: true
            match:
                macaddress: fa:16:3e:4f:f7:6c
            set-name: enp3s0
        
        enp3s1:  # 新增的配置
            dhcp4: true
            match:
                macaddress: fa:16:3e:1f:77:9f
            set-name: enp3s1  # 指定新的網卡名稱

    version: 2

(以上做法是看tico大大的部落格解決的:https://ithelp.ithome.com.tw/articles/10293768)

Key generation

我們希望可以從本地電腦連線到bastion-host,也希望bastion-host可以連線到cluster裡面的任一個node。

  • 先在本地產一個key,把public key上傳到openstack的密鑰對。接著在設置VM時都加入這個密鑰對
  • 把本地的private key用ssh的方式傳到bastion host
ssh -i <PRIVATE_KEY_PATH> ubuntu@<PUBLIC_IP>

到這邊,確定四台VM的網路設置沒問題後,才真正可以來架設kubernetes cluster了。

kubespray

kubespray是一個開源專案,用來進行kubernetes cluster的創建。自version 2.3開始,kubespray的內部採用與kubeadm相同的生命週期管理,再透過Ansible Playbook來調用kubeadm來做叢集的建置。 kubeadm則是由官方開發維護,用來建立原生kubernetes環境的工具。因此,使用kubespray不僅兼顧了部署的方便性,也兼顧了kubernetes生命週期管理的穩定性。

因此在安裝kubespray之前,要先在機器上面安裝Ansible以及Ansible所需要的Python環境。相關安裝步驟我是參考kubespray官方文件:Installing Ansible

版本資訊

  • Python3 v3.10.4
  • Ansible v9.5.1
  • kubespray v2.25.0

安裝步驟

以下的安裝步驟,若無特別標註,就都是在bastion host來做執行。

由於從openstack拿到的VM裡面已有內建Python,在這邊可以先裝Python的虛擬環境。

ubuntu@bastion-host:~$ sudo apt update
ubuntu@bastion-host:~$ sudo apt install python3-virtualenv
ubuntu@bastion-host:~$ virtualenv --version
virtualenv 20.13.0+ds from /usr/lib/python3/dist-packages/virtualenv/__init__.py

接下來就可以把kubespray專案clone下來,這邊我裝的版本是撰寫當下的最新版本2.25.0。

git clone --depth 1 --branch v2.25.0 https://github.com/kubernetes-sigs/kubespray.git

安裝Ansible

VENVDIR=kubespray-venv #指定virtual env位置
KUBESPRAYDIR=kubespray # kubespray資料夾位置
python3 -m venv $VENVDIR
source $VENVDIR/bin/activate
cd $KUBESPRAYDIR
pip install -U -r requirements.txt # 安裝Ansible所需套件

如果順利的話,到這邊就可以看到(kubespray-env)已經被啟動了!

在這邊我們會先把cluster的配置文件拷貝出來,創建一個自己的叢集目錄,方便配置與管理。

cp -rfp inventory/sample inventory/mycluster

接著,進入到mycluster目錄中,編輯inventory.ini。裡面的設定檔就是我們server相關資訊的設定,其餘的參數代表意義可以參考kubespray的官方文件。

# ## Configure 'ip' variable to bind kubernetes services on a
# ## different ip than the default iface
# ## We should set etcd_member_name for etcd cluster. The node that is not a etcd member do not need to set the value, or can set the empty string value.
[all]
# 根據文件範例設定,server_name ansiblehost=<IP> ansible_user=<USERNAME>
k8s-m0 ansible_host=192.168.200.101 ansible_user=ubuntu
k8s-n0 ansible_host=192.168.200.102 ansible_user=ubuntu
k8s-n1 ansible_host=192.168.200.103 ansible_user=ubuntu

[kube_control_plane]
k8s-m0

[etcd]
# etcd的位置,在這邊就是與master node是在同一個位置
k8s-m0

[kube_node]
k8s-n0
k8s-n1

[calico_rr]

[k8s_cluster:children]
kube_control_plane
kube_node
calico_rr

目前搭建的實驗環境資源有限,因此也將etcd放在master node上。master node目前也沒有做到HA的設計,是以單節點的方式存在。

也由於master node以單節點的方式存在,不需要Load Balancer API server,我們進到inventory/mycluster/group_vars/all/all.yml把Line 20的loadbalancer_apiserver_localhost修改為False

接著,再進到inventory/mycluster/group_vars/k8s_cluster/k8s-cluster.yml把Line 160 的cluster_name修改成自己的cluster name。

最後啟動ansible進行部署。

# ansible-playbook -i <INVENTORY_FILE> --private-key=<PRIVATE_KEY> --become --become-user=root cluster.yml
ansible-playbook -i inventory/mycluster/inventory.ini --private-key=~/.ssh/private.key --become --become-user=root cluster.yml

這邊的private key就是在前面文章中,我們要從bastion host連線到cluster node的那把private key。

在bastion host完成叢集的創建後,日後想要存取叢集需要拿取master node上面的token才行,因此我們先從bastion host SSH到master node。

ssh -i ~/.ssh/private.key ubuntu@192.168.200.101

先前提到kubespray是透過ansible做自動化部署,但底層仍舊遵循kubeadm的lifecycle。/etc/kubernetes/admin.conf在kubeadm初始化叢集時便會創建,可以算是使用者與Kubernetes API Server進行溝通的token。

我們在操作叢集資源時一般都是使用kubectl來做到,為了要讓kubectl可以使用這個token來做資源操作,我們要修改一下權限,並且把token丟回bastion host,這樣在bastion host也可以使用kubectl 操作資源。

ubuntu@k8s-m0:~$ sudo cp /etc/kubernetes/admin.conf ~/
ubuntu@k8s-m0:~$ sudo chown ubuntu:ubuntu ~/admin.conf # 將user/group修改成當前用戶,這樣不需要sudo 也能操作token
ubuntu@k8s-m0:~$ mkdir -p .kube #kubectl 會從這個目錄裡面來讀取相關的config文件
ubuntu@k8s-m0:~$ mv ~/admin.conf ~/.kube/config #將原本kubeadm創建的token移到能被kubectl 讀取的地方

確定能讀取到cluster的訊息後,再把~/.kube/config scp到bastion host的相同目錄下,讓kubectl可以讀取。

(kubespray-venv) ubuntu@bastion-host:~$ mkdir -p ~/.kube
(kubespray-venv) ubuntu@bastion-host:~$ scp -i ~/private.key ubuntu@192.168.200.101:~/.kube/config ~/.kube/config

另外要記得的是,config檔的line 5記錄著API Server的位置,因為我們是直接從master node將文件複製到bastion host,在這邊要記得把127.0.0.1改成192.168.200.101也就是master node的內網IP。

安裝kubectl

這邊要注意的是kubectl的版本要和cluster的版本相同(可以差到一個minor version)。 查看完cluster版本後就直接按普通方式安裝即可。

# 從kubectl get node就能查看cluster版本為v1.29.5
kubectl get node
NAME     STATUS   ROLES           AGE   VERSION
k8s-m0   Ready    control-plane   24h   v1.29.5
k8s-n0   Ready    <none>          24h   v1.29.5
k8s-n1   Ready    <none>          24h   v1.29.5

# download a specific version of kubectl
curl -LO "https://dl.k8s.io/release/v1.29.5/bin/linux/amd64/kubectl"

# Download the kubectl checksum file:
curl -LO "https://dl.k8s.io/v1.29.5/bin/linux/amd64/kubectl.sha256"

# validate the binary file
echo "$(cat kubectl.sha256)  kubectl" | sha256sum --check

# check ok後就可以正式安裝
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl

# 安裝後,不需要的檔案可以刪除
rm kubectl*

在bastion host下kubectl get node指令,大功告成! kubectl get node

踩坑紀錄

前面才剛提供一個private網卡踩坑的解決辦法,結果隔天上bastion host就突然沒辦法讀取cluster的資源了。一查看發現是private那張網卡的設定又跑掉了。

ubuntu@bastion-host:~$ ip addr show enp3s1
3: enp3s1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1442 qdisc fq_codel state UP group default qlen 1000
    link/ether fa:16:3e:1f:77:9f brd ff:ff:ff:ff:ff:ff
    inet6 fe80::f816:3eff:fe1f:779f/64 scope link
       valid_lft forever preferred_lft forever

由上面資訊可以發現它的IPv4配置不見了。因此我又手動加了回去。

ubuntu@bastion-host:~$ sudo ip addr add 192.168.200.100/24 dev enp3s1
ubuntu@bastion-host:~$ ip addr show enp3s1
3: enp3s1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1442 qdisc fq_codel state UP group default qlen 1000
    link/ether fa:16:3e:1f:77:9f brd ff:ff:ff:ff:ff:ff
    inet 192.168.200.100/24 scope global enp3s1
       valid_lft forever preferred_lft forever
    inet6 fe80::f816:3eff:fe1f:779f/64 scope link
       valid_lft forever preferred_lft forever 

成功!但不確定是不是openstack的網路設置本就有一些問題,先把這個坑紀錄,之後看看會不會再發生。

ubuntu@bastion-host:~$ ping 192.168.200.101
PING 192.168.200.101 (192.168.200.101) 56(84) bytes of data.
64 bytes from 192.168.200.101: icmp_seq=1 ttl=64 time=5.44 ms
64 bytes from 192.168.200.101: icmp_seq=2 ttl=64 time=1.41 ms
64 bytes from 192.168.200.101: icmp_seq=3 ttl=64 time=1.06 ms
64 bytes from 192.168.200.101: icmp_seq=4 ttl=64 time=0.696 ms

Reference

Installing kubeadm https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/#before-you-begin

關於我怎麼把一年內學到的新手 IT/SRE 濃縮到 30 天筆記這檔事 https://ithelp.ithome.com.tw/users/20112934/ironman/5640

Comparison - Kubespray vs Kubeadm https://github.com/kubernetes-sigs/kubespray/blob/master/docs/getting_started/comparisons.md

Installing Ansible https://github.com/kubernetes-sigs/kubespray/blob/v2.19.1/docs/ansible.md#inventory

Install and Set Up kubectl on Linux https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/

CNTUG Infra Labs 說明文件 https://docs.cloudnative.tw/docs/category/%E5%9F%BA%E7%A4%8E%E6%95%99%E5%AD%B8

OpenStack 上利用 kubeadm 搭建 K8S Cluster https://docs.cloudnative.tw/docs/self-paced-labs/kubeadm/