Kubernetes 號稱云原生操作系統,可想而知其復雜程度也是非常大的,由許多組件組成,我們很難去追蹤到所有的組件信息。
上圖中至少列出了七八個組件,我們這里會忽略其中大部分組件,要運行一個最小級別的 Kubernetes 至少要包括如下三個基本組件:
- kubelet:在集群中每個節點上運行的代理,負責容器真正運行的核心組件
- kube-apiserver:Kubernetes 控制平面的組件,提供資源操作的唯一入口
- 容器運行時(Docker)
這里我們來嘗試配置一個最小級別的 Kubernetes,這對于我們加速對集群的理解也是非常有幫助的。
安裝
首先需要在節點上安裝 Docker 容器運行時,我們這里使用的是操作系統為 centos7 版本,在 root 用戶下面執行相關操作。執行如下所示命令直接安裝即可:
$ yum install -y yum-utils
$ yum-config-manager
--add-repo
https://download.docker.com/linux/centos/docker-ce.repo
$ yum install -y docker-ce docker-ce-cli containerd.io
$ systemctl enable docker
$ systemctl daemon-reload
$ systemctl start docker
接下來,我們需要獲取 Kubernetes 二進制文件。實際上,我們只需要使用 kubelet 組件來引導我們的“集群”,因為我們可以使用 kubelet 來運行其他組件,一旦集群啟動了,我們就可以使用 kubectl 來進行操作了。
$ curl -L https://dl.k8s.io/v1.18.5/kubernetes-server-linux-amd64.tar.gz > server.tar.gz
$ tar xzvf server.tar.gz
$ cp kubernetes/server/bin/kubelet .
$ cp kubernetes/server/bin/kubectl .
$ ./kubelet --version
Kubernetes v1.18.5
由于 kubelet 配置太多,但是這里我們只需要幾個設置參數即可:
$ ./kubelet -h
<far too much output to copy here>
$ ./kubelet -h | wc -l
284
我們這里需要使用的是 --pod-manifest-path 這個參數,該參數用于指定要運行的靜態 Pod 文件的目錄,靜態 Pod 不受 Kubernetes API 管理,雖然平時我們在使用 Kubernetes 的時候比較少使用靜態 Pod,但是對于引導集群卻是非常有用,對 Kubeadm 熟悉的應該知道,該方案就是利用靜態 Pod 將 Kubernetes 控制面板容器化的。下面我們來嘗試下是否可以使用 kubelet 來運行 Pod。
首先我們創建一個靜態 Pod 目錄來運行 kubelet:
$ mkdir pods
$ ./kubelet --pod-manifest-path=pods
然后重新打開一個終端,創建如下所示的 Pod 資源清單文件:
$ cat <<EOF > pods/hello.yaml
apiVersion: v1
kind: Pod
metadata:
name: hello
spec:
containers:
- image: busybox
name: hello
command: ["echo", "hello world!"]
EOF
上面資源清單出現在 pods 目錄后,就可以在 kubelet 日志中看到如下所示的錯誤信息:
......
E0707 10:25:58.489839 21311 pod_workers.go:191] Error syncing pod ab61ef0307c6e0dee2ab05dc1ff94812 ("hello-iz2ze8x2keg0a301rpa7cvz_default(ab61ef0307c6e0dee2ab05dc1ff94812)"), skipping: failed to "CreatePodSandbox" for "hello-iz2ze8x2keg0a301rpa7cvz_default(ab61ef0307c6e0dee2ab05dc1ff94812)" with CreatePodSandboxError: "CreatePodSandbox for pod "hello-iz2ze8x2keg0a301rpa7cvz_default(ab61ef0307c6e0dee2ab05dc1ff94812)" failed: rpc error: code = Unknown desc = failed pulling image "k8s.gcr.io/pause:3.2": Error response from daemon: Get https://k8s.gcr.io/v2/: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)"
......
這是因為 Kubernetes 的 Pod 默認情況下會優先啟動一個 k8s.gcr.io/pause:3.2的 pause 鏡像,而該鏡像由于某些原因獲取不到,我們可以 --pod-infra-container-image 參數重新指定一個可以訪問到的鏡像:
$ ./kubelet --pod-manifest-path=pods --pod-infra-container-image=registry.aliyuncs.com/google_containers/pause:3.2
現在我們檢查下 Docker 容器是否有新的容器啟動:
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d6e822dbcebd busybox "echo 'hello world!'" 27 seconds ago Exited (0) 26 seconds ago k8s_hello_hello-iz2ze8x2keg0a301rpa7cvz_default_ab61ef0307c6e0dee2ab05dc1ff94812_3
102b999be2dd registry.aliyuncs.com/google_containers/pause:3.2 "/pause" 2 minutes ago Up 2 minutes k8s_POD_hello-iz2ze8x2keg0a301rpa7cvz_default_ab61ef0307c6e0dee2ab05dc1ff94812_0
$ docker logs k8s_hello_hello-iz2ze8x2keg0a301rpa7cvz_default_ab61ef0307c6e0dee2ab05dc1ff94812_3
hello world!
kubelet 通過我們指定的靜態 Pod 目錄,讀取其中的 YAML 文件來創建 Pod。由于我們這里執行的就是 echo 命令,所以會不斷的重啟,驗證完成后刪除該 YAML 文件即可。
當然這還不夠,我們還需要運行 APIServer,要做到這一點,我們需要首先運行 etcd,同樣的我們也可以使用靜態 Pod 來運行 etcd,創建如下所示的 etcd 資源清單文件:
$ cat <<EOF > pods/etcd.yaml
apiVersion: v1
kind: Pod
metadata:
name: etcd
namespace: kube-system
spec:
containers:
- name: etcd
command:
- etcd
- --data-dir=/var/lib/etcd
image: registry.aliyuncs.com/google_containers/etcd:3.4.3-0
volumeMounts:
- mountPath: /var/lib/etcd
name: etcd-data
hostNetwork: true
volumes:
- hostPath:
path: /var/lib/etcd
type: DirectoryOrCreate
name: etcd-data
EOF
這就是一個非常普通的 Pod 資源清單文件,大家應該都非常熟悉,不過還是需要注意兩件事:
- 我們將宿主機的 /var/lib/etcd 目錄掛載到 Pod 容器中,這樣可以保證 etcd 在重新啟動以后數據依然存在。
- 另外我們設置了 hostNetwork=true,這樣可以使容器和宿主機共享網絡命名空間,可以讓 APIServer 更容易和 etcd 通信。
我們可以使用如下所示的命令來檢查 etcd 是否啟動成功:
$ curl localhost:2379/version
{"etcdserver":"3.4.3","etcdcluster":"3.4.0"}
$ tree /var/lib/etcd/
/var/lib/etcd/
└── member
├── snap
│ └── db
└── wal
├── 0000000000000000-0000000000000000.wal
└── 0.tmp
3 directories, 3 files
現在 etcd 啟動成功了,就可以來啟動 APIServer 了,我們這里只需要通過參數 --etcd-servers 傳遞 etcd 地址即可,同樣在靜態 pods 目錄下面創建如下所示的資源清單:
$ cat <<EOF > pods/apiserver.yaml
apiVersion: v1
kind: Pod
metadata:
name: kube-apiserver
namespace: kube-system
spec:
containers:
- name: kube-apiserver
command:
- kube-apiserver
- --etcd-servers=http://127.0.0.1:2379
image: cnych/kube-apiserver:v1.18.5 # 阿里云鏡像未同步
hostNetwork: true
EOF
創建完成后正常 APIServer 就會正常啟動,可以通過如下所示的命令來驗證:
$ curl localhost:8080/healthz
ok
$ curl localhost:8080/api/v1/pods
{
"kind": "PodList",
"apiVersion": "v1",
"metadata": {
"selfLink": "/api/v1/pods",
"resourceVersion": "59"
},
"items": []
}
而且 kubectl 也不需要額外的配置就可以直接使用了:
$ ./kubectl version
Client Version: version.Info{Major:"1", Minor:"18", GitVersion:"v1.18.5", GitCommit:"e6503f8d8f769ace2f338794c914a96fc335df0f", GitTreeState:"clean", BuildDate:"2020-06-26T03:47:41Z", GoVersion:"go1.13.9", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"18", GitVersion:"v1.18.5", GitCommit:"e6503f8d8f769ace2f338794c914a96fc335df0f", GitTreeState:"clean", BuildDate:"2020-06-26T03:39:24Z", GoVersion:"go1.13.9", Compiler:"gc", Platform:"linux/amd64"}
$ ./kubectl get pod
No resources found in default namespace.
這是因為 kubectl 默認是通過 localhost:8080 和 APIServer 進行通信的。
配置
但是當我們去獲取剛剛創建的靜態 Pod 的時候卻發現沒有對應的記錄:
$ ./kubectl get pod -n kube-system
No resources found in kube-system namespace.
而且運行 kubelet 的節點也根本沒有顯示:
$ ./kubectl get nodes
No resources found in default namespace.
這其實是因為 kubelet 不知道如何與 APIServer 進行通信并更新狀態造成的,我們可以通過 kubelet 的 --kubeconfig 參數來指定 KUBECONFIG 文件的路徑,可以通過該文件來指定如何連接到 APIServer。由于我們這里就是啟動一個最新的 Kubernetes,沒有身份驗證或者證書之類的麻煩事情,所以非常簡單,創建名為 kubeconfig.yaml 的如下所示文件:
apiVersion: v1
kind: Config
clusters:
- cluster:
server: http://127.0.0.1:8080
name: mink8s
contexts:
- context:
cluster: mink8s
name: mink8s
current-context: mink8s
然后殺掉 kubelet 進程,添加上 --kubeconfig 參數重新運行:
$ ./kubelet --pod-manifest-path=pods --pod-infra-container-image=registry.aliyuncs.com/google_containers/pause:3.2 --kubeconfig=kubeconfig.yaml
隔一會兒后我們再次使用 kubectl 來查看上面我們運行的靜態 Pod 就正常了:
$ ./kubectl get pods -A
NAMESPACE NAME READY STATUS RESTARTS AGE
default hello-mink8s 0/1 CrashLoopBackOff 261 21h
kube-system etcd-mink8s 1/1 Running 0 21h
kube-system kube-apiserver-mink8s 1/1 Running 0 21h
$ ./kubectl get nodes -owide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
mink8s Ready <none> 21h v1.18.5 10.70.10.228 <none> CentOS Linux 7 (Core) 4.15.0-109-generic docker://19.3.6
這樣我們就運行了一個最小功能集的 Kubernetes 集群了。下面我們來嘗試運行一個普通的 Pod 看能否正常運行。
同樣嘗試來創建一個 Nginx 的 Pod:
$ cat <<EOF > nginx.yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- image: nginx
name: nginx
EOF
然后使用 kubectl 來創建上面的資源對象:
$ ./kubectl Apply -f nginx.yaml
Error from server (Forbidden): error when creating "nginx.yaml": pods "nginx" is
forbidden: error looking up service account default/default: serviceaccount
"default" not found
$ ./kubectl get serviceaccounts
No resources found in default namespace.
可以看到有錯誤信息,這是因為我們上面部署的最小級別的 Kubernetes 環境完整性還是不夠,沒有自動生成默認的 default 這個 ServiceAccount,我們來手動創建再來驗證一次:
$ cat <<EOF | ./kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: default
EOF
serviceaccount/default created
$ ./kubectl apply -f nginx.yaml
Error from server (ServerTimeout): error when creating "nginx.yaml": No API
token found for service account "default", retry after the token is
automatically created and added to the service account
我們手動創建了 ServiceAccount,但是卻并沒有創建對應的身份驗證的 Token,我們可以看到以前很多自動完成的操作現在都沒有了。
不過我們可以通過 automountServiceAccountToken 參數在 ServiceAccount 上來規避這個特定問題,因為實際上我們這里并不需要使用 ServiceAccount:
$ cat <<EOF | ./kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: default
automountServiceAccountToken: false
EOF
serviceaccount/default configured
$ ./kubectl apply -f nginx.yaml
pod/nginx created
$ ./kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx 0/1 Pending 0 13m
現在我們可以看到 Pod 出現了,但是處于 pending 狀態,這是因為我們并沒有部署 kube-scheduler 這個負責調度的組件,自然是不能被調度的,當然我們也可以不需要調度程序,直接使用 nodeName 屬性將 Pod 手動固定到節點上即可:
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- image: nginx
name: nginx
nodeName: mink8s
現在將之前部署的 Pod 刪除重新來部署,正常就可以運行了:
$ ./kubectl delete pod nginx
pod "nginx" deleted
$ ./kubectl apply -f nginx.yaml
pod/nginx created
$ ./kubectl get pods -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx 1/1 Running 0 30s 172.17.0.2 mink8s <none> <none>
$ curl -s 172.17.0.2 | head -4
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
為了驗證 Pod 與 Pod 之間是可以正常通信的,我們可以使用如下的 Pod 來驗證:
$ cat <<EOF | ./kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: curl
spec:
containers:
- image: curlimages/curl
name: curl
command: ["curl", "172.17.0.2"]
nodeName: mink8s
EOF
pod/curl created
$ ./kubectl logs curl | head -6
% Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
可以看到可以正常通信。這樣我們就完成了一個最小的 Kubernetes 集群部署。當然這也僅僅是為了簡化我們對 Kubernetes 的理解而已,在實際的生產環境是絕對不能這樣去部署使用的。
參考
- https://eevans.co/blog/minimum-viable-kubernetes/
- https://commons.wikimedia.org/w/index.php?curid=53571935
- https://kubernetes.io/docs/concepts/overview/components/
- https://kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file/