常用對象操作:(4)


常用對象操作:(4)

1. Replication Controller與Replica Set

1.1 使用Replication Controller、Replica Set 管理Pod

前面我們的課程中學習了Pod的一些基本使用方法,而且前面我們都是直接來操作的Pod,假如我們現在有一個Pod正在提供線上的服務,我們來想想一下我們可能會遇到的一些場景:

  • 某次運營活動非常成功,網站訪問量突然暴增
  • 運行當前Pod的節點發生故障了,Pod不能正常提供服務了

第一種情況,可能比較好應對,一般活動之前我們會大概計算下會有多大的訪問量,提前多啟動幾個Pod,活動結束后再把多余的Pod殺掉,雖然有點麻煩,但是應該還是能夠應對這種情況的。

第二種情況,可能某天夜里收到大量報警說服務掛了,然后起來打開電腦在另外的節點上重新啟動一個新的Pod,問題也很好的解決了。

如果我們都人工的去解決遇到的這些問題,似乎又回到了以前刀耕火種的時代了是吧,如果有一種工具能夠來幫助我們管理Pod就好了,Pod不夠了自動幫我新增一個,Pod掛了自動幫我在合適的節點上重新啟動一個Pod,這樣是不是遇到上面的問題我們都不需要手動去解決了。

幸運的是,Kubernetes就為我們提供了這樣的資源對象:

  • Replication Controller:用來部署、升級Pod
  • Replica Set:下一代的Replication Controller
  • Deployment:可以更加方便的管理Pod和Replica Set

1.2 Replication Controller(RC)

Replication Controller簡稱RCRCKubernetes系統中的核心概念之一,簡單來說,RC可以保證在任意時間運行Pod的副本數量,能夠保證Pod總是可用的。如果實際Pod數量比指定的多那就結束掉多余的,如果實際數量比指定的少就新啟動一些Pod,當Pod失敗、被刪除或者掛掉后,RC都會去自動創建新的Pod來保證副本數量,所以即使只有一個Pod,我們也應該使用RC來管理我們的Pod

我們想想如果現在我們遇到上面的問題的話,可能除了第一個不能做到完全自動化,其余的我們是不是都不用擔心了,運行Pod的節點掛了,RC檢測到Pod失敗了,就會去合適的節點重新啟動一個Pod就行,不需要我們手動去新建一個Pod了。如果是第一種情況的話在活動開始之前我們給Pod指定10個副本,結束后將副本數量改成2,這樣是不是也遠比我們手動去啟動、手動去關閉要好得多,而且我們后面還會給大家介紹另外一種資源對象HPA可以根據資源的使用情況來進行自動擴縮容,這樣以后遇到這種情況,我們就真的可以安心的去睡覺了。

現在我們來使用RC來管理我們前面使用的NginxPodYAML文件如下:

[root@node01 ~]# vim rc-demo.yaml
[root@node01 ~]# cat rc-demo.yaml 
apiVersion: v1
kind: ReplicationController
metadata:
  name: rc-demo
  labels:
    name: rc
spec:
  replicas: 3
  selector:
    name: rc
  template:
    metadata:
     labels:
       name: rc
    spec:
     containers:
     - name: nginx-demo
       image: nginx
       ports:
       - containerPort: 80

上面的YAML文件相對於我們之前的Pod的格式:

  • kind:ReplicationController
  • spec.replicas: 指定Pod副本數量,默認為1
  • spec.selector: RC通過該屬性來篩選要控制的Pod
  • spec.template: 這里就是我們之前的Pod的定義的模塊,但是不需要apiVersionkind
  • spec.template.metadata.labels: 注意這里的Podlabels要和spec.selector相同,這樣RC就可以來控制當前這個Pod了。

這個YAML文件中的意思就是定義了一個RC資源對象,它的名字叫rc-demo,保證一直會有3個Pod運行,Pod的鏡像是nginx鏡像。

注意spec.selectorspec.template.metadata.labels這兩個字段必須相同,否則會創建失敗的,當然我們也可以不寫spec.selector,這樣就默認與Pod
模板中的metadata.labels相同了。所以為了避免不必要的錯誤的話,不寫為好。

然后我們來創建上面的RC對象(保存為 rc-demo.yaml):

[root@node01 ~]# kubectl create -f rc-demo.yaml 
replicationcontroller "rc-demo" created

查看RC:

[root@node01 ~]# kubectl get rc
NAME      DESIRED   CURRENT   READY     AGE
rc-demo   3         3         0         17s

查看具體信息:

[root@node01 ~]# kubectl describe rc rc-demo
Name:         rc-demo
Namespace:    default
Selector:     name=rc
Labels:       name=rc
Annotations:  <none>
Replicas:     3 current / 3 desired
Pods Status:  0 Running / 3 Waiting / 0 Succeeded / 0 Failed
Pod Template:
  Labels:  name=rc
  Containers:
   nginx-demo:
    Image:        nginx
    Port:         80/TCP
    Host Port:    0/TCP
    Environment:  <none>
    Mounts:       <none>
  Volumes:        <none>
Events:
  Type    Reason            Age   From                    Message
  ----    ------            ----  ----                    -------
  Normal  SuccessfulCreate  1m    replication-controller  Created pod: rc-demo-d2kw8
  Normal  SuccessfulCreate  1m    replication-controller  Created pod: rc-demo-9cdm8
  Normal  SuccessfulCreate  1m    replication-controller  Created pod: rc-demo-rqb6b

然后我們通過RC來修改下Pod的副本數量為2:

[root@node01 ~]# kubectl apply -f rc-demo.yaml
Warning: kubectl apply should be used on resource created by either kubectl create --save-config or kubectl apply
replicationcontroller "rc-demo" configured

或者

[root@node01 ~]# kubectl edit rc rc-demo

而且我們還可以用RC來進行滾動升級,比如我們將鏡像地址更改為nginx:1.7.9:

$ kubectl rolling-update rc-demo --image=nginx:1.7.9

但是如果我們的Pod中多個容器的話,就需要通過修改YAML文件來進行修改了:

$ kubectl rolling-update rc-demo -f rc-demo.yaml

如果升級完成后出現了新的問題,想要一鍵回滾到上一個版本的話,使用RC只能用同樣的方法把鏡像地址替換成之前的,然后重新滾動升級。

1.3 Replication Set(RS)

Replication Set簡稱RS,隨着Kubernetes的高速發展,官方已經推薦我們使用RSDeployment來代替RC了,實際上RSRC的功能基本一致,目前唯一的一個區別就是RC只支持基於等式的selector(env=dev或environment!=qa),但RS還支持基於集合的selector(version in (v1.0, v2.0)),這對復雜的運維管理就非常方便了。

kubectl命令行工具中關於RC的大部分命令同樣適用於我們的RS資源對象。不過我們也很少會去單獨使用RS,它主要被Deployment這個更加高層的資源對象使用,除非用戶需要自定義升級功能或根本不需要升級Pod,在一般情況下,我們推薦使用Deployment而不直接使用Replica Set

最后我們總結下關於RC/RS的一些特性和作用吧:

  • 大部分情況下,我們可以通過定義一個RC實現的Pod的創建和副本數量的控制
  • RC中包含一個完整的Pod定義模塊(不包含apiversionkind
  • RC是通過label selector機制來實現對Pod副本的控制的
  • 通過改變RC里面的Pod副本數量,可以實現Pod的擴縮容功能
  • 通過改變RC里面的Pod模板中鏡像版本,可以實現Pod的滾動升級功能(但是不支持一鍵回滾,需要用相同的方法去修改鏡像地址)

2. Deployment

2.1 Deployment的使用

前面的課程中我們學習了Replication ControllerReplica Set兩種資源對象,RCRS的功能基本上是差不多的,唯一的區別就是RS支持集合的selector。我們也學習到了用RC/RS來控制Pod副本的數量,也實現了滾動升級Pod的功能。現在看上去似乎一切都比較完美的運行着,但是我們上節課最后也提到了現在我們推薦使用Deployment這種控制器了,而不是我們之前的RC或者RS,這是為什么呢?

沒有對比就沒有傷害對吧,我們來對比下二者之間的功能吧,首先RCKubernetes的一個核心概念,當我們把應用部署到集群之后,需要保證應用能夠持續穩定的運行,RC就是這個保證的關鍵,主要功能如下:

  • 確保Pod數量:它會確保Kubernetes中有指定數量的Pod在運行,如果少於指定數量的PodRC就會創建新的,反之這會刪除多余的,保證Pod的副本數量不變。
  • 確保Pod健康:當Pod不健康,比如運行出錯了,總之無法提供正常服務時,RC也會殺死不健康的Pod,重新創建新的。
  • 彈性伸縮:在業務高峰或者低峰的時候,可以用過RC來動態的調整Pod數量來提供資源的利用率,當然我們也提到過如果使用HPA這種資源對象的話可以做到自動伸縮。
  • 滾動升級:滾動升級是一種平滑的升級方式,通過逐步替換的策略,保證整體系統的穩定性。

Deployment同樣也是Kubernetes系統的一個核心概念,主要職責和RC一樣的都是保證Pod的數量和健康,二者大部分功能都是完全一致的,我們可以看成是一個升級版的RC控制器,那Deployment又具備那些新特性呢?

  • RC的全部功能:Deployment具備上面描述的RC的全部功能
  • 事件和狀態查看:可以查看Deployment的升級詳細進度和狀態
  • 回滾:當升級Pod的時候如果出現問題,可以使用回滾操作回滾到之前的任一版本
  • 版本記錄:每一次對Deployment的操作,都能夠保存下來,這也是保證可以回滾到任一版本的基礎
  • 暫停和啟動:對於每一次升級都能夠隨時暫停和啟動

作為對比,我們知道Deployment作為新一代的RC,不僅在功能上更為豐富了,同時我們也說過現在官方也都是推薦使用Deployment來管理Pod的,比如一些官方組件kube-dnskube-proxy也都是使用的Deployment來管理的,所以當大家在使用的使用也最好使用Deployment來管理Pod

2.2 創建

image_1euqbe9k7aidh6n1c3iliq1ppm9.png-25.9kB

可以看出一個Deployment擁有多個Replica Set,而一個Replica Set擁有一個或多個Pod。一個Deployment控制多個rs主要是為了支持回滾機制,每當Deployment操作時,Kubernetes會重新生成一個Replica Set並保留,以后有需要的話就可以回滾至之前的狀態。

下面創建一個Deployment,它創建了一個Replica Set來啟動3個nginx pod,yaml文件如下:

[root@node01 ~]# vim nginx-deployment.yaml
[root@node01 ~]# cat nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deploy
  labels:
    k8s-app: nginx-demo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

將上面內容保存為: nginx-deployment.yaml,執行命令:

[root@node01 ~]# kubectl create -f nginx-deployment.yaml 
deployment.apps "nginx-deploy" created

然后執行一下命令查看剛剛創建的Deployment:

[root@node01 ~]# kubectl get deployments
NAME           DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
nginx-deploy   3         3         3            0           6s

隔一會再次執行上面命令:

[root@node01 ~]# kubectl get deployments
NAME           DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
nginx-deploy   3         3         3            3           1m

我們可以看到Deployment已經創建了1個Replica Set了,執行下面的命令查看rs和pod:

[root@node01 ~]# kubectl get rs
NAME                      DESIRED   CURRENT   READY     AGE
nginx-deploy-75675f5897   3         3         3         1m

[root@node01 ~]# kubectl get pod --show-labels
NAME                            READY     STATUS    RESTARTS   AGE       LABELS
nginx-deploy-75675f5897-jrx57   1/1       Running   0          2m        app=nginx,pod-template-hash=3123191453
nginx-deploy-75675f5897-r4j2j   1/1       Running   0          2m        app=nginx,pod-template-hash=3123191453
nginx-deploy-75675f5897-rpp4m   1/1       Running   0          2m        app=nginx,pod-template-hash=3123191453

上面的Deployment的yaml文件中的replicas:3將會保證我們始終有3個POD在運行。

由於DeploymentRC的功能大部分都一樣的,我們上節課已經和大家演示了大部分功能了,我們這里重點給大家演示下Deployment的滾動升級和回滾功能。

2.3 滾動升級

現在我們將剛剛保存的yaml文件中的nginx鏡像修改為nginx:1.13.3,然后在spec下面添加滾動升級策略:

[root@node01 ~]# tail -7 nginx-deployment.yaml 
minReadySeconds: 5
strategy:
  # indicate which strategy we want for rolling update
  type: RollingUpdate
  rollingUpdate:
    maxSurge: 1
    maxUnavailable: 1
  • minReadySeconds:
    • Kubernetes在等待設置的時間后才進行升級
    • 如果沒有設置該值,Kubernetes會假設該容器啟動起來后就提供服務了
    • 如果沒有設置該值,在某些極端情況下可能會造成服務不正常運行
  • maxSurge:
    • 升級過程中最多可以比原先設置多出的POD數量
    • 例如:maxSurage=1,replicas=5,則表示Kubernetes會先啟動1一個新的Pod后才刪掉一個舊的POD,整個升級過程中最多會有5+1個POD。
  • maxUnavaible:
    • 升級過程中最多有多少個POD處於無法提供服務的狀態
    • maxSurge不為0時,該值也不能為0
    • 例如:maxUnavaible=1,則表示Kubernetes整個升級過程中最多會有1個POD處於無法服務的狀態。

然后執行命令:

[root@node01 ~]# kubectl apply -f nginx-deployment.yaml --validate=false
Warning: kubectl apply should be used on resource created by either kubectl create --save-config or kubectl apply
deployment.apps "nginx-deploy" configured

然后我們可以使用rollout命令:

查看狀態:

[root@node01 ~]# kubectl rollout status deployment/nginx-deploy
Waiting for rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for rollout to finish: 1 old replicas are pending termination...
Waiting for rollout to finish: 1 old replicas are pending termination...
deployment "nginx-deploy" successfully rolled out

暫停升級

$ kubectl rollout pause deployment <deployment>

繼續升級

$ kubectl rollout resume deployment <deployment>

升級結束后,繼續查看rs的狀態:

[root@node01 ~]# kubectl get rs
NAME                      DESIRED   CURRENT   READY     AGE
nginx-deploy-594b456c98   3         3         3         1m
nginx-deploy-75675f5897   0         0         0         10m

根據AGE我們可以看到離我們最近的當前狀態是:3,和我們的yaml文件是一致的,證明升級成功了。用describe命令可以查看升級的全部信息:

[root@node01 ~]# kubectl describe rs nginx-deploy
Name:           nginx-deploy-594b456c98
Namespace:      default
Selector:       app=nginx,pod-template-hash=1506012754
Labels:         app=nginx
                pod-template-hash=1506012754
Annotations:    deployment.kubernetes.io/desired-replicas=3
                deployment.kubernetes.io/max-replicas=4
                deployment.kubernetes.io/revision=2
Controlled By:  Deployment/nginx-deploy
Replicas:       3 current / 3 desired
Pods Status:    3 Running / 0 Waiting / 0 Succeeded / 0 Failed
Pod Template:
  Labels:  app=nginx
           pod-template-hash=1506012754
  Containers:
   nginx:
    Image:        nginx:1.13.3
    Port:         80/TCP
    Host Port:    0/TCP
    Environment:  <none>
    Mounts:       <none>
  Volumes:        <none>
Events:
  Type    Reason            Age   From                   Message
  ----    ------            ----  ----                   -------
  Normal  SuccessfulCreate  15m   replicaset-controller  Created pod: nginx-deploy-594b456c98-jrkgx
  Normal  SuccessfulCreate  15m   replicaset-controller  Created pod: nginx-deploy-594b456c98-gz5zv
  Normal  SuccessfulCreate  14m   replicaset-controller  Created pod: nginx-deploy-594b456c98-w6kcc

Name:           nginx-deploy-75675f5897
Namespace:      default
Selector:       app=nginx,pod-template-hash=3123191453
Labels:         app=nginx
                pod-template-hash=3123191453
Annotations:    deployment.kubernetes.io/desired-replicas=3
                deployment.kubernetes.io/max-replicas=4
                deployment.kubernetes.io/revision=1
Controlled By:  Deployment/nginx-deploy
Replicas:       0 current / 0 desired
Pods Status:    0 Running / 0 Waiting / 0 Succeeded / 0 Failed
Pod Template:
  Labels:  app=nginx
           pod-template-hash=3123191453
  Containers:
   nginx:
    Image:        nginx:1.7.9
    Port:         80/TCP
    Host Port:    0/TCP
    Environment:  <none>
    Mounts:       <none>
  Volumes:        <none>
Events:
  Type    Reason            Age   From                   Message
  ----    ------            ----  ----                   -------
  Normal  SuccessfulCreate  24m   replicaset-controller  Created pod: nginx-deploy-75675f5897-jrx57
  Normal  SuccessfulCreate  24m   replicaset-controller  Created pod: nginx-deploy-75675f5897-rpp4m
  Normal  SuccessfulCreate  24m   replicaset-controller  Created pod: nginx-deploy-75675f5897-r4j2j
  Normal  SuccessfulDelete  15m   replicaset-controller  Deleted pod: nginx-deploy-75675f5897-jrx57
  Normal  SuccessfulDelete  14m   replicaset-controller  Deleted pod: nginx-deploy-75675f5897-rpp4m
  Normal  SuccessfulDelete  14m   replicaset-controller  Deleted pod: nginx-deploy-75675f5897-r4j2j

2.4 回滾Deployment

我們已經能夠滾動平滑的升級我們的Deployment了,但是如果升級后的POD出了問題該怎么辦?我們能夠想到的最好最快的方式當然是回退到上一次能夠提供正常工作的版本,Deployment就為我們提供了回滾機制。

首先,查看Deployment的升級歷史:

[root@node01 ~]# kubectl rollout history deployment nginx-deploy
deployments "nginx-deploy"
REVISION  CHANGE-CAUSE
1         <none>
2         <none>

從上面的結果可以看出在執行Deployment升級的時候最好帶上record參數,便於我們查看歷史版本信息。

默認情況下,所有通過kubectl xxxx --record都會被kubernetes記錄到etcd進行持久化,這無疑會占用資源,最重要的是,時間久了,當你kubectl get rs時,會有成百上千的垃圾RS返回給你,那時你可能就眼花繚亂了。

上生產時,我們最好通過設置Deployment的.spec.revisionHistoryLimit來限制最大保留的revision number,比如15個版本,回滾的時候一般只會回滾到最近的幾個版本就足夠了。其實rollout history中記錄的revision都和ReplicaSets一一對應。如果手動delete某個ReplicaSet,對應的rollout history就會被刪除,也就是還說你無法回滾到這個revison了。

rollout historyReplicaSet的對應關系,可以在kubectl describe rs $RSNAME返回的revision字段中得到,這里的revision就對應着rollout history返回的revison

同樣我們可以使用下面的命令查看單個revison的信息:

[root@node01 ~]# kubectl rollout history deployment nginx-deploy --revision=2
deployments "nginx-deploy" with revision #2
Pod Template:
  Labels:	app=nginx
	pod-template-hash=1506012754
  Containers:
   nginx:
    Image:	nginx:1.13.3
    Port:	80/TCP
    Host Port:	0/TCP
    Environment:	<none>
    Mounts:	<none>
  Volumes:	<none>

假如現在要直接回退到當前版本的前一個版本:

$ kubectl rollout undo deployment nginx-deploy
deployment "nginx-deploy" rolled back

當然也可以用revision回退到指定的版本:

$ kubectl rollout undo deployment nginx-deploy --to-revision=2
deployment "nginx-deploy" rolled back

現在可以用命令查看Deployment現在的狀態了。

3. HPA

Pod 自動擴縮容

在前面的課程中,我們提到過通過手工執行kubectl scale命令和在Dashboard上操作可以實現Pod的擴縮容,但是這樣畢竟需要每次去手工操作一次,而且指不定什么時候業務請求量就很大了,所以如果不能做到自動化的去擴縮容的話,這也是一個很麻煩的事情。如果Kubernetes系統能夠根據Pod當前的負載的變化情況來自動的進行擴縮容就好了,因為這個過程本來就是不固定的,頻繁發生的,所以純手工的方式不是很現實。

幸運的是Kubernetes為我們提供了這樣一個資源對象:Horizontal Pod Autoscaling(Pod水平自動伸縮),簡稱HPAHAP通過監控分析RC或者Deployment控制的所有Pod的負載變化情況來確定是否需要調整Pod的副本數量,這是HPA最基本的原理。

image_1euqc2gj64pbnk713a7vng163b1g.png-74.8kB

HPAkubernetes集群中被設計成一個controller,我們可以簡單的通過kubectl autoscale命令來創建一個HPA資源對象,HPA Controller默認30s輪詢一次(可通過kube-controller-manager的標志--horizontal-pod-autoscaler-sync-period進行設置),查詢指定的資源(RC或者Deployment)中Pod的資源使用率,並且與創建時設定的值和指標做對比,從而實現自動伸縮的功能。

當你創建了HPA后,HPA會從Heapster或者用戶自定義的RESTClient端獲取每一個一個Pod利用率或原始值的平均值,然后和HPA中定義的指標進行對比,同時計算出需要伸縮的具體值並進行相應的操作。目前,HPA可以從兩個地方獲取數據:

  • Heapster:僅支持CPU使用率
  • 自定義監控:我們到后面的監控的課程中再給大家講解這部分的使用方法

我們這節課來給大家介紹從Heapster獲取監控數據來進行自動擴縮容的方法,所以首先我們得安裝Heapster,前面我們在kubeadm搭建集群的課程中,實際上我們已經默認把Heapster相關的鏡像都已經拉取到節點上了,所以接下來我們只需要部署即可,我們這里使用的是Heapster 1.4.2 版本的,前往Heapstergithub頁面:

https://github.com/kubernetes/heapster

我們將該目錄下面的yaml文件保存到我們的集群上,然后使用kubectl命令行工具創建即可,另外創建完成后,如果需要在Dashboard當中看到監控圖表,我們還需要在Dashboard中配置上我們的heapster-host。

同樣的,我們來創建一個Deployment管理的Nginx Pod,然后利用HPA來進行自動擴縮容。定義DeploymentYAML文件如下:(hpa-deploy-demo.yaml)

[root@node01 ~]# vim hpa-deploy-demo.yaml
[root@node01 ~]# cat hpa-deploy-demo.yaml 
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hpa-nginx-deploy
  labels:
    app: nginx-demo
spec:
  revisionHistoryLimit: 15
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80

然后創建Deployment:

[root@node01 ~]# kubectl create -f hpa-deploy-demo.yaml
deployment.apps "hpa-nginx-deploy" created

現在我們來創建一個HPA,可以使用kubectl autoscale命令來創建:

[root@node01 ~]# kubectl autoscale deployment hpa-nginx-deploy --cpu-percent=10 --min=1 --max=10
deployment.apps "hpa-nginx-deploy" autoscaled


[root@node01 ~]# kubectl get hpa
NAME               REFERENCE                     TARGETS         MINPODS   MAXPODS   REPLICAS   AGE
hpa-nginx-deploy   Deployment/hpa-nginx-deploy   <unknown>/10%   1         10        1          32s

此命令創建了一個關聯資源 hpa-nginx-deploy 的HPA,最小的 pod 副本數為1,最大為10。HPA會根據設定的 cpu使用率(10%)動態的增加或者減少pod數量。

當然出來使用kubectl autoscale命令來創建外,我們依然可以通過創建YAML文件的形式來創建HPA資源對象。如果我們不知道怎么編寫的話,可以查看上面命令行創建的HPAYAML文件:

[root@node01 ~]# kubectl get hpa hpa-nginx-deploy -o yaml
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  annotations:
    autoscaling.alpha.kubernetes.io/conditions: '[{"type":"AbleToScale","status":"True","lastTransitionTime":"2021-07-04T14:51:10Z","reason":"SucceededGetScale","message":"the
      HPA controller was able to get the target''s current scale"},{"type":"ScalingActive","status":"False","lastTransitionTime":"2021-07-04T14:51:10Z","reason":"FailedGetResourceMetric","message":"the
      HPA was unable to compute the replica count: unable to get metrics for resource
      cpu: unable to fetch metrics from resource metrics API: the server could not
      find the requested resource (get pods.metrics.k8s.io)"}]'
  creationTimestamp: 2021-07-04T14:50:40Z
  name: hpa-nginx-deploy
  namespace: default
  resourceVersion: "30703"
  selfLink: /apis/autoscaling/v1/namespaces/default/horizontalpodautoscalers/hpa-nginx-deploy
  uid: 337d5704-dcd7-11eb-8fc3-000c2927597c
spec:
  maxReplicas: 10   #資源最大副本數
  minReplicas: 1    #資源最小副本數
  scaleTargetRef:
    apiVersion: extensions/v1beta1
    kind: Deployment           #需要伸縮的資源類型
    name: hpa-nginx-deploy     #需要伸縮的資源名稱
  targetCPUUtilizationPercentage: 10    #觸發伸縮的cpu使用率
status:
  currentReplicas: 1     #當前的副本數
  desiredReplicas: 0     #期望的副本數

好,現在我們根據上面的YAML文件就可以自己來創建一個基於YAMLHPA描述文件了。

現在我們來增大負載進行測試,我們來創建一個busybox,並且循環訪問上面創建的服務。

$ kubectl run -i --tty load-generator --image=busybox /bin/sh
If you don't see a command prompt, try pressing enter.
/ # while true; do wget -q -O- http://172.16.255.60:4000; done

下圖可以看到,HPA已經開始工作。

$ kubectl get hpa
NAME        REFERENCE              TARGET    CURRENT   MINPODS   MAXPODS   AGE
hpa-nginx-deploy   Deployment/hpa-nginx-deploy   10%       29%        1         10        27m

同時我們查看相關資源hpa-nginx-deploy的副本數量,副本數量已經從原來的1變成了3。

$ kubectl get deployment hpa-nginx-deploy
NAME        DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
hpa-nginx-deploy   3         3         3            3           4d

同時再次查看HPA,由於副本數量的增加,使用率也保持在了10%左右。

$ kubectl get hpa
NAME        REFERENCE              TARGET    CURRENT   MINPODS   MAXPODS   AGE
hpa-nginx-deploy   Deployment/hpa-nginx-deploy   10%       9%        1         10        35m

同樣的這個時候我們來關掉busybox來減少負載,然后等待一段時間觀察下HPADeployment對象

$ kubectl get hpa     
NAME        REFERENCE              TARGET    CURRENT   MINPODS   MAXPODS   AGE
hpa-nginx-deploy   Deployment/hpa-nginx-deploy   10%       0%        1         10        48m
$ kubectl get deployment hpa-nginx-deploy
NAME        DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
hpa-nginx-deploy   1         1         1            1           4d

可以看到副本數量已經由3變為1。

不過當前的HPA只有CPU使用率這一個指標,還不是很靈活的,在后面的課程中我們來根據我們自定義的監控來自動對Pod進行擴縮容。

4. Job/Cronjob

4.1 Job 和 Cronjob 的使用

上節課我們學習了Pod自動伸縮的方法,我們使用到了HPA這個資源對象,我們在后面的課程中還會和大家接觸到HPA的。今天我們來給大家介紹另外一類資源對象:Job,我們在日常的工作中經常都會遇到一些需要進行批量數據處理和分析的需求,當然也會有按時間來進行調度的工作,在我們的Kubernetes集群中為我們提供了JobCronJob兩種資源對象來應對我們的這種需求。

Job負責處理任務,即僅執行一次的任務,它保證批處理任務的一個或多個Pod成功結束。而CronJob則就是在Job上加上了時間調度。

4.2 Job

我們用Job這個資源對象來創建一個任務,我們定一個Job來執行一個倒計時的任務,定義YAML文件:

[root@node01 ~]# vim job.yaml
[root@node01 ~]# cat job.yaml 
apiVersion: batch/v1
kind: Job
metadata:
  name: job-demo
spec:
  template:
    metadata:
      name: job-demo
    spec:
      restartPolicy: Never
      containers:
      - name: counter
        image: busybox
        command:
        - "bin/sh"
        - "-c"
        - "for i in 9 8 7 6 5 4 3 2 1; do echo $i; done"

注意JobRestartPolicy僅支持NeverOnFailure兩種,不支持Always,我們知道Job就相當於來執行一個批處理任務,執行完就結束了,如果支持Always的話是不是就陷入了死循環了?

然后來創建該Job,保存為job-demo.yaml

[root@node01 ~]# kubectl create -f ./job.yaml 
job.batch "job-demo" created

然后我們可以查看當前的Job資源對象:

[root@node01 ~]# kubectl get jobs
NAME       DESIRED   SUCCESSFUL   AGE
job-demo   1         1            9m

注意查看我們的Pod的狀態,同樣我們可以通過kubectl logs來查看當前任務的執行結果。

4.3 CronJob

CronJob其實就是在Job的基礎上加上了時間調度,我們可以:在給定的時間點運行一個任務,也可以周期性地在給定時間點運行。這個實際上和我們Linux中的crontab就非常類似了。

一個CronJob對象其實就對應中crontab文件中的一行,它根據配置的時間格式周期性地運行一個Job,格式和crontab也是一樣的。

crontab的格式如下:

分 時 日 月 星期 要運行的命令 第1列分鍾0~59 第2列小時0~23) 第3列日1~31 第4列月1~12 第5列星期0~7(0和7表示星期天) 第6列要運行的命令

#Kubernetes 默認沒有 enable CronJob 功能,需要在 kube-apiserver 中加入這個功能
[root@master ~]# vim /etc/kubernetes/manifests/kube-apiserver.yaml 
[root@master ~]# sed -n "42p" /etc/kubernetes/manifests/kube-apiserver.yaml
    - --runtime-config=batch/v2alpha1=true
[root@master ~]# systemctl restart kubelet

現在,我們用CronJob來管理我們上面的Job任務,

[root@node01 ~]# vim cronjob-demo.yaml
[root@node01 ~]# cat cronjob-demo.yaml 
apiVersion: batch/v2alpha1
kind: CronJob
metadata:
  name: cronjob-demo
spec:
  schedule: "*/1 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
          - name: hello
            image: busybox
            args:
            - "bin/sh"
            - "-c"
            - "for i in 9 8 7 6 5 4 3 2 1; do echo $i; done"

我們這里的Kind是CronJob了,要注意的是.spec.schedule字段是必須填寫的,用來指定任務運行的周期,格式就和crontab一樣,另外一個字段是.spec.jobTemplate, 用來指定需要運行的任務,格式當然和Job是一致的。還有一些值得我們關注的字段.spec.successfulJobsHistoryLimit.spec.failedJobsHistoryLimit,表示歷史限制,是可選的字段。它們指定了可以保留多少完成和失敗的Job,默認沒有限制,所有成功和失敗的Job都會被保留。然而,當運行一個Cron Job時,Job可以很快就堆積很多,所以一般推薦設置這兩個字段的值。如果設置限制的值為 0,那么相關類型的Job完成后將不會被保留。

接下來我們來創建這個cronjob

[root@node01 ~]# kubectl create -f cronjob-demo.yaml 
cronjob.batch "cronjob-demo" created

當然,也可以用kubectl run來創建一個CronJob

kubectl run hello --schedule="*/1 * * * *" --restart=OnFailure --image=busybox -- /bin/sh -c "date; echo Hello from the Kubernetes cluster"
[root@node01 ~]# kubectl get cronjob
NAME           SCHEDULE      SUSPEND   ACTIVE    LAST SCHEDULE   AGE
cronjob-demo   */1 * * * *   False     0         <none>          17s
[root@node01 ~]# kubectl get jobs
NAME                      DESIRED   SUCCESSFUL   AGE
cronjob-demo-1625990160   1         1            44s
job-demo                  1         1            23m

[root@node01 ~]# pods=$(kubectl get pods --selector=job-name=cronjob-demo-1625990160 --output=jsonpath={.items..metadata.name} -a)
Flag --show-all has been deprecated, will be removed in an upcoming release

一旦不再需要 Cron Job,簡單地可以使用 kubectl 命令刪除它:

[root@node01 ~]# kubectl delete cronjob cronjob-demo
cronjob.batch "cronjob-demo" deleted

一旦 Job 被刪除,由 Job 創建的 Pod 也會被刪除。注意,所有由名稱為 “hello” 的 Cron Job 創建的 Job 會以前綴字符串 “hello-” 進行命名。如果想要刪除當前 Namespace 中的所有 Job,可以通過命令 kubectl delete jobs --all 立刻刪除它們。

5. Service

5.1 Service

我們前面的課程中學習了Pod的基本用法,我們也了解到Pod的生命是有限的,死亡過后不會復活了。我們后面學習到的RCDeployment可以用來動態的創建和銷毀Pod。盡管每個Pod都有自己的IP地址,但是如果Pod重新啟動了的話那么他的IP很有可能也就變化了。這就會帶來一個問題:比如我們有一些后端的Pod的集合為集群中的其他前端的Pod集合提供API服務,如果我們在前端的Pod中把所有的這些后端的Pod的地址都寫死,然后去某種方式去訪問其中一個Pod的服務,這樣看上去是可以工作的,對吧?但是如果這個Pod掛掉了,然后重新啟動起來了,是不是IP地址非常有可能就變了,這個時候前端就極大可能訪問不到后端的服務了。

遇到這樣的問題該怎么解決呢?在沒有使用Kubernetes之前,我相信可能很多同學都遇到過這樣的問題,不一定是IP變化的問題,比如我們在部署一個WEB服務的時候,前端一般部署一個Nginx作為服務的入口,然后Nginx后面肯定就是掛載的這個服務的大量后端,很早以前我們可能是去手動更改Nginx配置中的upstream選項,來動態改變提供服務的數量,到后面出現了一些服務發現的工具,比如ConsulZooKeeper還有我們熟悉的etcd等工具,有了這些工具過后我們就可以只需要把我們的服務注冊到這些服務發現中心去就可以,然后讓這些工具動態的去更新Nginx的配置就可以了,我們完全不用去手工的操作了,是不是非常方便。

圖片.png-18.6kB

同樣的,要解決我們上面遇到的問題是不是實現一個服務發現的工具也可以解決啊?沒錯的,當我們Pod被銷毀或者新建過后,我們可以把這個Pod的地址注冊到這個服務發現中心去就可以,但是這樣的話我們的前端的Pod結合就不能直接去連接后台的Pod集合了是吧,應該連接到一個能夠做服務發現的中間件上面,對吧?

沒錯,Kubernetes集群就為我們提供了這樣的一個對象 - ServiceService是一種抽象的對象,它定義了一組Pod的邏輯集合和一個用於訪問它們的策略,其實這個概念和微服務非常類似。一個Serivce下面包含的Pod集合一般是由Label Selector來決定的。

比如我們上面的例子,假如我們后端運行了3個副本,這些副本都是可以替代的,因為前端並不關心它們使用的是哪一個后端服務。盡管由於各種原因后端的Pod集合會發送變化,但是前端卻不需要知道這些變化,也不需要自己用一個列表來記錄這些后端的服務,Service的這種抽象就可以幫我們達到這種解耦的目的。

5.2 三種IP

在繼續往下學習Service之前,我們需要先弄明白Kubernetes系統中的三種IP這個問題,因為經常有同學混亂。

  • Node IP:Node節點的IP地址
  • Pod IP: PodIP地址
  • Cluster IP: ServiceIP地址

首先,Node IPKubernetes集群中節點的物理網卡IP地址(一般為內網),所有屬於這個網絡的服務器之間都可以直接通信,所以Kubernetes集群外要想訪問Kubernetes集群內部的某個節點或者服務,肯定得通過Node IP進行通信(這個時候一般是通過外網IP了)

然后Pod IP是每個PodIP地址,它是Docker Engine根據docker0網橋的IP地址段進行分配的(我們這里使用的是flannel這種網絡插件保證所有節點的Pod IP不會沖突)

最后Cluster IP是一個虛擬的IP,僅僅作用於Kubernetes Service這個對象,由Kubernetes自己來進行管理和分配地址,當然我們也無法ping這個地址,他沒有一個真正的實體對象來響應,他只能結合Service Port來組成一個可以通信的服務。

5.3 定義Service

定義Service的方式和我們前面定義的各種資源對象的方式類型,例如,假定我們有一組Pod服務,它們對外暴露了 8080 端口,同時都被打上了app=myapp這樣的標簽,那么我們就可以像下面這樣來定義一個Service對象:

apiVersion: v1
kind: Service
metadata:
  name: myservice
spec:
  selector:
    app: myapp
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
    name: myapp-http

然后通過的使用kubectl create -f myservice.yaml就可以創建一個名為myserviceService對象,它會將請求代理到使用 TCP 端口為 8080,具有標簽app=myappPod上,這個Service會被系統分配一個我們上面說的Cluster IP,該Service還會持續的監聽selector下面的Pod,會把這些Pod信息更新到一個名為myserviceEndpoints對象上去,這個對象就類似於我們上面說的Pod集合了。

需要注意的是,Service能夠將一個接收端口映射到任意的targetPort。 默認情況下,targetPort將被設置為與port字段相同的值。 可能更有趣的是,targetPort 可以是一個字符串,引用了 backend Pod 的一個端口的名稱。 因實際指派給該端口名稱的端口號,在每個 backend Pod 中可能並不相同,所以對於部署和設計 Service ,這種方式會提供更大的靈活性。

另外Service能夠支持 TCPUDP 協議,默認是 TCP 協議。

5.4 kube-proxy

前面我們講到過,在Kubernetes集群中,每個Node會運行一個kube-proxy進程, 負責為Service實現一種 VIP(虛擬 IP,就是我們上面說的clusterIP)的代理形式,現在的Kubernetes中默認是使用的iptables這種模式來代理。這種模式,kube-proxy會監視Kubernetes master對 Service 對象和 Endpoints 對象的添加和移除。 對每個 Service,它會添加上 iptables 規則,從而捕獲到達該 Service 的 clusterIP(虛擬 IP)和端口的請求,進而將請求重定向到 Service 的一組 backend 中的某一個個上面。 對於每個 Endpoints 對象,它也會安裝 iptables 規則,這個規則會選擇一個 backend Pod。

默認的策略是,隨機選擇一個 backend。 我們也可以實現基於客戶端 IP 的會話親和性,可以將 service.spec.sessionAffinity 的值設置為 "ClientIP" (默認值為 "None")。

另外需要了解的是如果最開始選擇的 Pod 沒有響應,iptables 代理能夠自動地重試另一個 Pod,所以它需要依賴 readiness probes。

圖片.png-124.8kB

5.5 Service 類型

我們在定義Service的時候可以指定一個自己需要的類型的Service,如果不指定的話默認是ClusterIP類型。

我們可以使用的服務類型如下:

  • ClusterIP:通過集群的內部 IP 暴露服務,選擇該值,服務只能夠在集群內部可以訪問,這也是默認的ServiceType。
  • NodePort:通過每個 Node 節點上的 IP 和靜態端口(NodePort)暴露服務。NodePort 服務會路由到 ClusterIP 服務,這個 ClusterIP 服務會自動創建。通過請求 ,可以從集群的外部訪問一個 NodePort 服務。
  • LoadBalancer:使用雲提供商的負載局衡器,可以向外部暴露服務。外部的負載均衡器可以路由到 NodePort 服務和 ClusterIP 服務,這個需要結合具體的雲廠商進行操作。
  • ExternalName:通過返回 CNAME 和它的值,可以將服務映射到 externalName 字段的內容(例如, foo.bar.example.com)。沒有任何類型代理被創建,這只有 Kubernetes 1.7 或更高版本的 kube-dns 才支持。

5.6 NodePort 類型

如果設置 type 的值為 "NodePort",Kubernetes master 將從給定的配置范圍內(默認:30000-32767)分配端口,每個 Node 將從該端口(每個 Node 上的同一端口)代理到 Service。該端口將通過 Service 的 spec.ports[*].nodePort 字段被指定,如果不指定的話會自動生成一個端口。

需要注意的是,Service 將能夠通過 :spec.ports[].nodePort 和 spec.clusterIp:spec.ports[].port 而對外可見。

接下來我們來給大家創建一個NodePort的服務來訪問我們前面的Nginx服務:(保存為service-demo.yaml)

[root@node01 ~]# vim service-demo.yaml
[root@node01 ~]# cat service-demo.yaml 
apiVersion: v1
kind: Service
metadata:
  name: myservice
spec:
  selector:
    app: myapp
  type: NodePort
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
    name: myapp-http

創建該Service:

[root@node01 ~]# kubectl create -f service-demo.yaml
service "myservice" created

然后我們可以查看Service對象信息:

[root@node01 ~]# kubectl get svc
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP        32d
myservice    NodePort    10.100.13.170   <none>        80:31941/TCP   3s

我們可以看到myservice的 TYPE 類型已經變成了NodePort ,后面的PORT(S)部分也多了一個 31941 的映射端口。

5.7 ExternalName

ExternalName 是 Service 的特例,它沒有 selector,也沒有定義任何的端口和 Endpoint。 對於運行在集群外部的服務,它通過返回該外部服務的別名這種方式來提供服務。

kind: Service
apiVersion: v1
metadata:
  name: my-service
  namespace: prod
spec:
  type: ExternalName
  externalName: my.database.example.com

當查詢主機 my-service.prod.svc.cluster.local (后面服務發現的時候我們會再深入講解)時,集群的 DNS 服務將返回一個值為 my.database.example.com 的 CNAME 記錄。 訪問這個服務的工作方式與其它的相同,唯一不同的是重定向發生在 DNS 層,而且不會進行代理或轉發。 如果后續決定要將數據庫遷移到 Kubernetes 集群中,可以啟動對應的 Pod,增加合適的 Selector 或 Endpoint,修改 Service 的 type,完全不需要修改調用的代碼,這樣就完全解耦了。

6. ConfigMap

6.1 ConfigMap

前面的課程中我們學習了Servie的使用,ServiceKubernetes系統中非常重要的一個核心概念,我們還會在后面 的課程中繼續學習Service的一些使用方法的。這節課我們來學習另外一個非常重要的資源對象:ConfigMap,我們知道許多應用經常會有從配置文件、命令行參數或者環境變量中讀取一些配置信息,這些配置信息我們肯定不會直接寫死到應用程序中去的,比如你一個應用連接一個redis服務,下一次想更換一個了的,還得重新去修改代碼,重新制作一個鏡像,這肯定是不可取的,而ConfigMap就給我們提供了向容器中注入配置信息的能力,不僅可以用來保存單個屬性,也可以用來保存整個配置文件,比如我們可以用來配置一個redis服務的訪問地址,也可以用來保存整個redis的配置文件。

6.2 創建

ConfigMap 資源對象使用key-value形式的鍵值對來配置數據,這些數據可以在Pod里面使用,ConfigMap和我們后面要講到的Secrets比較類似,一個比較大的區別是ConfigMap可以比較方便的處理一些非敏感的數據,比如密碼之類的還是需要使用Secrets來進行管理。我們來舉個例子說明下ConfigMap的使用方法:

kind: ConfigMap
apiVersion: v1
metadata:
  name: cm-demo
  namespace: default
data:
  data.1: hello
  data.2: world
  config: |
    property.1=value-1
    property.2=value-2
    property.3=value-3

其中配置數據在data屬性下面進行配置,前兩個被用來保存單個屬性,后面一個被用來保存一個配置文件。

當然同樣的我們可以使用kubectl create -f xx.yaml來創建上面的ConfigMap對象,但是如果我們不知道怎么創建ConfigMap的話,不要忘記kubectl是我們最好的老師,可以使用kubectl create configmap -h來查看關於創建ConfigMap的幫助信息,

Examples:
  # Create a new configmap named my-config based on folder bar
  kubectl create configmap my-config --from-file=path/to/bar

  # Create a new configmap named my-config with specified keys instead of file basenames on disk
  kubectl create configmap my-config --from-file=key1=/path/to/bar/file1.txt --from-file=key2=/path/to/bar/file2.txt

  # Create a new configmap named my-config with key1=config1 and key2=config2
  kubectl create configmap my-config --from-literal=key1=config1 --from-literal=key2=config2

我們可以看到可以從一個給定的目錄來創建一個ConfigMap對象,比如我們有一個testcm的目錄,該目錄下面包含一些配置文件,redismysql的連接信息,如下:

[root@node01 ~]# mkdir testcm

[root@node01 ~]# vim testcm/redis.conf
[root@node01 ~]# cat testcm/redis.conf 
host=127.0.0.1
port=6379

[root@node01 ~]# vim testcm/mysql.conf
[root@node01 ~]# cat testcm/mysql.conf 
host=127.0.0.1
port=3306

然后我們可以使用from-file關鍵字來創建包含這個目錄下面所以配置文件的ConfigMap

[root@node01 ~]# kubectl create configmap cm-demo1 --from-file=testcm
configmap "cm-demo1" created

其中from-file參數指定在該目錄下面的所有文件都會被用在ConfigMap里面創建一個鍵值對,鍵的名字就是文件名,值就是文件的內容。

創建完成后,同樣我們可以使用如下命令來查看ConfigMap列表:

[root@node01 ~]# kubectl get configmap
NAME       DATA      AGE
cm-demo1   2         5s

可以看到已經創建了一個cm-demo1ConfigMap對象,然后可以使用describe命令查看詳細信息:

[root@node01 ~]# kubectl describe configmap cm-demo1
Name:         cm-demo1
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
mysql.conf:
----
host=127.0.0.1
port=3306

redis.conf:
----
host=127.0.0.1
port=6379

Events:  <none>

我們可以看到兩個keytestcm目錄下面的文件名稱,對應的value值的話就是文件內容,這里值得注意的是如果文件里面的配置信息很大的話,describe的時候可能不會顯示對應的值,要查看鍵值的話,可以使用如下命令:

[root@node01 ~]# kubectl get configmap cm-demo1 -o yaml
apiVersion: v1
data:
  mysql.conf: |
    host=127.0.0.1
    port=3306
  redis.conf: |
    host=127.0.0.1
    port=6379
kind: ConfigMap
metadata:
  creationTimestamp: 2021-07-18T12:36:38Z
  name: cm-demo1
  namespace: default
  resourceVersion: "9281"
  selfLink: /api/v1/namespaces/default/configmaps/cm-demo1
  uid: cbf961bc-e7c4-11eb-8fc3-000c2927597c

除了通過文件目錄進行創建,我們也可以使用指定的文件進行創建ConfigMap,同樣的,以上面的配置文件為例,我們創建一個redis的配置的一個單獨ConfigMap對象:

[root@node01 ~]# kubectl create configmap cm-demo2 --from-file=testcm/redis.conf
configmap "cm-demo2" created

[root@node01 ~]# kubectl get configmap cm-demo2 -o yaml
apiVersion: v1
data:
  redis.conf: |
    host=127.0.0.1
    port=6379
kind: ConfigMap
metadata:
  creationTimestamp: 2021-07-18T12:40:41Z
  name: cm-demo2
  namespace: default
  resourceVersion: "9575"
  selfLink: /api/v1/namespaces/default/configmaps/cm-demo2
  uid: 5cd4cd56-e7c5-11eb-8fc3-000c2927597c

我們可以看到一個關聯redis.conf文件配置信息的ConfigMap對象創建成功了,另外值得注意的是--from-file這個參數可以使用多次,比如我們這里使用兩次分別指定redis.confmysql.conf文件,就和直接指定整個目錄是一樣的效果了。

另外,通過幫助文檔我們可以看到我們還可以直接使用字符串進行創建,通過--from-literal參數傳遞配置信息,同樣的,這個參數可以使用多次,格式如下:

[root@node01 ~]# kubectl create configmap cm-demo3 --from-literal=db.host=localhost --from-literal=db.port=3306
configmap "cm-demo3" created

[root@node01 ~]# kubectl get configmap cm-demo3 -o yaml
apiVersion: v1
data:
  db.host: localhost
  db.port: "3306"
kind: ConfigMap
metadata:
  creationTimestamp: 2021-07-18T12:41:44Z
  name: cm-demo3
  namespace: default
  resourceVersion: "9651"
  selfLink: /api/v1/namespaces/default/configmaps/cm-demo3
  uid: 8226fcbd-e7c5-11eb-8fc3-000c2927597c

6.3 使用

ConfigMap創建成功了,那么我們應該怎么在Pod中來使用呢?我們說ConfigMap這些配置數據可以通過很多種方式在Pod里使用,主要有以下幾種方式:

  • 設置環境變量的值
  • 在容器里設置命令行參數
  • 在數據卷里面創建config文件

首先,我們使用ConfigMap來填充我們的環境變量:

[root@node01 ~]# vim testcm1.yaml
[root@node01 ~]# cat testcm1.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: testcm1-pod
spec:
  containers:
    - name: testcm1
      image: busybox
      command: [ "/bin/sh", "-c", "env" ]
      env:
        - name: DB_HOST
          valueFrom:
            configMapKeyRef:
              name: cm-demo3
              key: db.host
        - name: DB_PORT
          valueFrom:
            configMapKeyRef:
              name: cm-demo3
              key: db.port
      envFrom:
        - configMapRef:
            name: cm-demo1

創建這個Pod

[root@node01 ~]# kubectl create -f ./testcm1.yaml 
pod "testcm1-pod" created

這個Pod運行后會輸出如下幾行:

[root@node01 ~]# kubectl logs testcm1-pod
KUBERNETES_SERVICE_PORT=443
KUBERNETES_PORT=tcp://10.96.0.1:443
HOSTNAME=testcm1-pod
DB_PORT=3306
SHLVL=1
HOME=/root
mysql.conf=host=127.0.0.1
port=3306

redis.conf=host=127.0.0.1
port=6379

KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
KUBERNETES_SERVICE_HOST=10.96.0.1
PWD=/
DB_HOST=localhost
......

我們可以看到DB_HOSTDB_PORT都已經正常輸出了,另外的環境變量是因為我們這里直接把cm-demo1給注入進來了,所以把他們的整個鍵值給輸出出來了,這也是符合預期的。

另外我們可以使用ConfigMap來設置命令行參數,ConfigMap也可以被用來設置容器中的命令或者參數值,如下Pod:

[root@node01 ~]# vim testcm2.yaml 
[root@node01 ~]# cat testcm2.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: testcm2-pod
spec:
  containers:
    - name: testcm2
      image: busybox
      command: [ "/bin/sh", "-c", "echo $(DB_HOST) $(DB_PORT)" ]
      env:
        - name: DB_HOST
          valueFrom:
            configMapKeyRef:
              name: cm-demo3
              key: db.host
        - name: DB_PORT
          valueFrom:
            configMapKeyRef:
              name: cm-demo3
              key: db.port

創建這個Pod

[root@node01 ~]# kubectl create -f ./testcm2.yaml 
pod "testcm2-pod" created

運行這個Pod后會輸出如下信息:

[root@node01 ~]# kubectl logs testcm2-pod
localhost 3306

另外一種是非常常見的使用ConfigMap的方式:通過數據卷使用,在數據卷里面使用ConfigMap,就是將文件填入數據卷,在這個文件中,鍵就是文件名,鍵值就是文件內容:

[root@node01 ~]# vim testcm3.yaml
[root@node01 ~]# cat testcm3.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: testcm3-pod
spec:
  containers:
    - name: testcm3
      image: busybox
      command: [ "/bin/sh", "-c", "cat /etc/config/redis.conf" ]
      volumeMounts:
      - name: config-volume
        mountPath: /etc/config
  volumes:
    - name: config-volume
      configMap:
        name: cm-demo2

創建這個Pod

[root@node01 ~]# kubectl create -f ./testcm3.yaml
pod "testcm3-pod" created

運行這個Pod的,查看日志:

[root@node01 ~]# kubectl logs testcm3-pod
host=127.0.0.1
port=6379

當然我們也可以在ConfigMap值被映射的數據卷里去控制路徑,如下Pod定義:

[root@node01 ~]# vim testcm4.yaml
[root@node01 ~]# cat testcm4.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: testcm4-pod
spec:
  containers:
    - name: testcm4
      image: busybox
      command: [ "/bin/sh","-c","cat /etc/config/path/to/msyql.conf" ]
      volumeMounts:
      - name: config-volume
        mountPath: /etc/config
  volumes:
    - name: config-volume
      configMap:
        name: cm-demo1
        items:
        - key: mysql.conf
          path: path/to/msyql.conf

創建這個Pod

[root@node01 ~]# kubectl create -f ./testcm4.yaml 
pod "testcm4-pod" created

運行這個Pod的,查看日志:

[root@node01 ~]# kubectl logs testcm4-pod
host=127.0.0.1
port=3306

另外需要注意的是,當ConfigMap以數據卷的形式掛載進Pod的時,這時更新ConfigMap(或刪掉重建ConfigMap),Pod內掛載的配置信息會熱更新。這時可以增加一些監測配置文件變更的腳本,然后reload對應服務。

7. Secret

7.1 Secret

上節課我們學習了ConfigMap的時候,我們說ConfigMap這個資源對象是Kubernetes當中非常重要的一個對象,一般情況下ConfigMap是用來存儲一些非安全的配置信息,如果涉及到一些安全相關的數據的話用ConfigMap就非常不妥了,因為ConfigMap是名為存儲的,我們說這個時候我們就需要用到另外一個資源對象了:SecretSecret用來保存敏感信息,例如密碼、OAuth 令牌和 ssh key等等,將這些信息放在Secret中比放在Pod的定義中或者docker鏡像中來說更加安全和靈活。

Secret有三種類型:

  • Opaque:base64 編碼格式的 Secret,用來存儲密碼、密鑰等;但數據也可以通過base64 –decode解碼得到原始數據,所有加密性很弱。
  • kubernetes.io/dockerconfigjson:用來存儲私有docker registry的認證信息。
  • kubernetes.io/service-account-token:用於被serviceaccount引用,serviceaccout 創建時Kubernetes會默認創建對應的secret。Pod如果使用了serviceaccount,對應的secret會自動掛載到Pod目錄/run/secrets/kubernetes.io/serviceaccount中。

7.2 Opaque Secret

Opaque 類型的數據是一個 map 類型,要求value是base64編碼格式,比如我們來創建一個用戶名為 admin,密碼為 admin321 的 Secret 對象,首先我們先把這用戶名和密碼做 base64 編碼,

[root@node01 ~]# echo -n "admin" | base64
YWRtaW4=
[root@node01 ~]# echo -n "admin321" | base64
YWRtaW4zMjE=

然后我們就可以利用上面編碼過后的數據來編寫一個YAML文件:(secret-demo.yaml)

[root@node01 ~]# vim secret-demo.yaml
[root@node01 ~]# cat secret-demo.yaml 
apiVersion: v1
kind: Secret
metadata:
  name: mysecret
type: Opaque
data:
  username: YWRtaW4=
  password: YWRtaW4zMjE=

然后同樣的我們就可以使用kubectl命令來創建了:

[root@node01 ~]# kubectl create -f secret-demo.yaml
secret "mysecret" created

利用get secret命令查看:

[root@node01 ~]# kubectl get secret
NAME                  TYPE                                  DATA      AGE
default-token-ztfk2   kubernetes.io/service-account-token   3         47d
mysecret              Opaque                                2         7s

其中default-token-cty7pdefault-token-n9w2d為創建集群時默認創建的 secret,被serviceacount/default 引用。

使用describe命令,查看詳情:

[root@node01 ~]# kubectl describe secret mysecret
Name:         mysecret
Namespace:    default
Labels:       <none>
Annotations:  <none>

Type:  Opaque

Data
====
username:  5 bytes
password:  8 bytes

我們可以看到利用describe命令查看到的Data沒有直接顯示出來,如果想看到Data里面的詳細信息,同樣我們可以輸出成YAML文件進行查看:

[root@node01 ~]# kubectl get secret mysecret -o yaml
apiVersion: v1
data:
  password: YWRtaW4zMjE=
  username: YWRtaW4=
kind: Secret
metadata:
  creationTimestamp: 2021-08-01T08:52:14Z
  name: mysecret
  namespace: default
  resourceVersion: "117383"
  selfLink: /api/v1/namespaces/default/secrets/mysecret
  uid: c4a0ccbd-f2a5-11eb-8fc3-000c2927597c
type: Opaque

創建好Secret對象后,有兩種方式來使用它:

  • 以環境變量的形式
  • 以Volume的形式掛載

7.3 環境變量

首先我們來測試下環境變量的方式,同樣的,我們來使用一個簡單的busybox鏡像來測試下:(secret1-pod.yaml)

[root@node01 ~]# vim secret1-pod.yaml
[root@node01 ~]# cat secret1-pod.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: secret1-pod
spec:
  containers:
  - name: secret1
    image: busybox
    command: [ "/bin/sh", "-c", "env" ]
    env:
    - name: USERNAME
      valueFrom:
        secretKeyRef:
          name: mysecret
          key: username
    - name: PASSWORD
      valueFrom:
        secretKeyRef:
          name: mysecret
          key: password

主要上面環境變量中定義的secretKeyRef關鍵字,和我們上節課的configMapKeyRef是不是比較類似,一個是從Secret對象中獲取,一個是從ConfigMap對象中獲取,創建上面的Pod

[root@node01 ~]# kubectl create -f secret1-pod.yaml
pod "secret1-pod" created

然后我們查看Pod的日志輸出:

[root@node01 ~]# kubectl logs secret1-pod
...
USERNAME=admin
...
PASSWORD=admin321

可以看到有 USERNAME 和 PASSWORD 兩個環境變量輸出出來。

7.4 Volume 掛載

同樣的我們用一個Pod來驗證下Volume掛載,創建一個Pod文件:(secret2-pod.yaml)

[root@node01 ~]# vim secret2-pod.yaml
[root@node01 ~]# cat secret2-pod.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: secret2-pod
spec:
  containers:
  - name: secret2
    image: busybox
    command: ["/bin/sh", "-c", "ls /etc/secrets"]
    volumeMounts:
    - name: secrets
      mountPath: /etc/secrets
  volumes:
  - name: secrets
    secret:
     secretName: mysecret

創建Pod:

[root@node01 ~]# kubectl create -f secret2-pod.yaml
pod "secret2-pod" created

然后我們查看輸出日志:

[root@node01 ~]# kubectl logs secret2-pod
password
username

可以看到secret把兩個key掛載成了兩個對應的文件。當然如果想要掛載到指定的文件上面,是不是也可以使用上一節課的方法:在secretName下面添加items指定 key 和 path,這個大家可以參考上節課ConfigMap中的方法去測試下。

7.5 kubernetes.io/dockerconfigjson

除了上面的Opaque這種類型外,我們還可以來創建用戶docker registry認證的Secret,直接使用kubectl create命令創建即可,如下:

[root@node01 ~]# kubectl create secret docker-registry myregistry --docker-server=DOCKER_SERVER --docker-username=DOCKER_USER --docker-password=DOCKER_PASSWORD --docker-email=DOCKER_EMAIL
secret "myregistry" created

然后查看Secret列表:

[root@node01 ~]# kubectl get secret
NAME                  TYPE                                  DATA      AGE
default-token-ztfk2   kubernetes.io/service-account-token   3         47d
myregistry            kubernetes.io/dockerconfigjson        1         3s
mysecret              Opaque                                2         23m

注意看上面的TYPE類型,myregistry是不是對應的kubernetes.io/dockerconfigjson,同樣的可以使用describe命令來查看詳細信息:

[root@node01 ~]# kubectl describe secret myregistry
Name:         myregistry
Namespace:    default
Labels:       <none>
Annotations:  <none>

Type:  kubernetes.io/dockerconfigjson

Data
====
.dockerconfigjson:  152 bytes

同樣的可以看到Data區域沒有直接展示出來,如果想查看的話可以使用-o yaml來輸出展示出來:

[root@node01 ~]# kubectl get secret myregistry -o yaml
apiVersion: v1
data:
  .dockerconfigjson: eyJhdXRocyI6eyJET0NLRVJfU0VSVkVSIjp7InVzZXJuYW1lIjoiRE9DS0VSX1VTRVIiLCJwYXNzd29yZCI6IkRPQ0tFUl9QQVNTV09SRCIsImVtYWlsIjoiRE9DS0VSX0VNQUlMIiwiYXV0aCI6IlJFOURTMFZTWDFWVFJWSTZSRTlEUzBWU1gxQkJVMU5YVDFKRSJ9fX0=
kind: Secret
metadata:
  creationTimestamp: 2021-08-01T10:41:11Z
  name: myregistry
  namespace: default
  resourceVersion: "5552"
  selfLink: /api/v1/namespaces/default/secrets/myregistry
  uid: fcd87d58-f2b4-11eb-8fc3-000c2927597c
type: kubernetes.io/dockerconfigjson

可以把上面的data.dockerconfigjson下面的數據做一個base64解碼,看看里面的數據是怎樣的呢?

[root@node01 ~]# echo eyJhdXRocyI6eyJET0NLRVJfU0VSVkVSIjp7InVzZXJuYW1lIjoiRE9DS0VSX1VTRVIiLCJwYXNzd29yZCI6IkRPQ0tFUl9QQVNTV09SRCIsImVtYWlsIjoiRE9DS0VSX0VNQUlMIiwiYXV0aCI6IlJFOURTMFZTWDFWVFJWSTZSRTlEUzBWU1gxQkJVMU5YVDFKRSJ9fX0= | base64 -d
{"auths":{"DOCKER_SERVER":{"username":"DOCKER_USER","password":"DOCKER_PASSWORD","email":"DOCKER_EMAIL","auth":"RE9DS0VSX1VTRVI6RE9DS0VSX1BBU1NXT1JE"}}}

如果我們需要拉取私有倉庫中的docker鏡像的話就需要使用到上面的myregistry這個Secret

apiVersion: v1
kind: Pod
metadata:
  name: foo
spec:
  containers:
  - name: foo
    image: 192.168.1.100:5000/test:v1
  imagePullSecrets:
  - name: myregistrykey

我們需要拉取私有倉庫鏡像192.168.1.100:5000/test:v1,我們就需要針對該私有倉庫來創建一個如上的Secret,然后在PodYAML 文件中指定imagePullSecrets,我們會在后面的私有倉庫搭建的課程中跟大家詳細說明的。

7.6 kubernetes.io/service-account-token

另外一種Secret類型就是kubernetes.io/service-account-token,用於被serviceaccount引用。serviceaccout 創建時 Kubernetes 會默認創建對應的 secret。Pod 如果使用了 serviceaccount,對應的secret會自動掛載到Pod的/run/secrets/kubernetes.io/serviceaccount目錄中。

這里我們使用一個nginx鏡像來驗證一下,大家想一想為什么不是呀busybox鏡像來驗證?當然也是可以的,但是我們就不能在command里面來驗證了,因為token是需要Pod運行起來過后才會被掛載上去的,直接在command命令中去查看肯定是還沒有 token 文件的。

[root@node01 ~]# kubectl run secret-pod3 --image nginx:1.7.9
deployment.apps "secret-pod3" created

[root@node01 ~]# kubectl get pods
NAME                           READY     STATUS             RESTARTS   AGE
secret-pod3-78c8c76db8-d2kw8   1/1       Running            0          14s
[root@node01 ~]# kubectl exec secret-pod3-78c8c76db8-d2kw8 ls /run/secrets/kubernetes.io/serviceaccount
ca.crt
namespace
token

[root@node01 ~]# kubectl exec secret-pod3-78c8c76db8-d2kw8 cat /run/secrets/kubernetes.io/serviceaccount/token
eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImRlZmF1bHQtdG9rZW4tenRmazIiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGVmYXVsdCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjA3YTVmYzY3LWNkM2UtMTFlYi04ZmMzLTAwMGMyOTI3NTk3YyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlZmF1bHQifQ.GGz7ZsyDQUnYKJyoOddCoMXq4NROD9eG0AFANJgosk8_5H6za2x3TL1UPyWXRZ_tpDpS2Kr8mU3qEr85H03hCQGeS_NSXxXtnTeTcC5vJo3SYDZhb-CNhuARr0_PhjfV4HvB_guOB2rckTfLVFINl-xz6LYpQGgs6CK6KFq1WaD1YHiRjCHSAhBfgyTOqsHXVE3mT73xjtT5x4E3w2mdbzeBMsOq9EXpquVWrsacUjaPGb_Xf-SW6ZCHvyxI75efoY5dJX7Djvq0-9XUa0WIXmlfpi7t02u3XCja4tGqCr7VCCH4d_9O6BwxIzSt1Za59v3FeMl4GWI712_81xA1Ow

7.7 Secret 與 ConfigMap 對比

最后我們來對比下SecretConfigMap這兩種資源對象的異同點:

相同點:

  • key/value的形式
  • 屬於某個特定的namespace
  • 可以導出到環境變量
  • 可以通過目錄/文件形式掛載
  • 通過 volume 掛載的配置信息均可熱更新

不同點:

  • Secret 可以被 ServerAccount 關聯
  • Secret 可以存儲 docker register 的鑒權信息,用在 ImagePullSecret 參數中,用於拉取私有倉庫的鏡像
  • Secret 支持 Base64 加密
  • Secret 分為 kubernetes.io/service-account-token、kubernetes.io/dockerconfigjson、Opaque 三種類型,而 Configmap 不區分類型

8. RBAC

8.1 RBAC

前面兩節課我們學習了Kubernetes中的兩個用於配置信息的重要資源對象:ConfigMapSecret,其實到這里我們基本上學習的內容已經覆蓋到Kubernetes中一些重要的資源對象了,來部署一個應用程序是完全沒有問題的了。在我們演示一個完整的示例之前,我們還需要給大家講解一個重要的概念:RBAC - 基於角色的訪問控制。

RBAC使用rbac.authorization.k8s.io API Group 來實現授權決策,允許管理員通過 Kubernetes API 動態配置策略,要啟用RBAC,需要在 apiserver 中添加參數--authorization-mode=RBAC,如果使用的kubeadm安裝的集群,1.6 版本以上的都默認開啟了RBAC,可以通過查看 Master 節點上 apiserver 的靜態Pod定義文件:

[root@master ~]# sed -n '37p' /etc/kubernetes/manifests/kube-apiserver.yaml
    - --authorization-mode=Node,RBAC

如果是二進制的方式搭建的集群,添加這個參數過后,記得要重啟 apiserver 服務。

8.2 RBAC API 對象

Kubernetes有一個很基本的特性就是它的所有資源對象都是模型化的 API 對象,允許執行 CRUD(Create、Read、Update、Delete)操作(也就是我們常說的增、刪、改、查操作),比如下面的這下資源:

  • Pods
  • ConfigMaps
  • Deployments
  • Nodes
  • Secrets
  • Namespaces

上面這些資源對象的可能存在的操作有:

  • create
  • get
  • delete
  • list
  • update
  • edit
  • watch
  • exec

在更上層,這些資源和 API Group 進行關聯,比如Pods屬於 Core API Group,而Deployements屬於 apps API Group,要在Kubernetes中進行RBAC的管理,除了上面的這些資源和操作以外,我們還需要另外的一些對象:

  • Rule:規則,規則是一組屬於不同 API Group 資源上的一組操作的集合
  • Role 和 ClusterRole:角色和集群角色,這兩個對象都包含上面的 Rules 元素,二者的區別在於,在 Role 中,定義的規則只適用於單個命名空間,也就是和 namespace 關聯的,而 ClusterRole 是集群范圍內的,因此定義的規則不受命名空間的約束。另外 Role 和 ClusterRole 在Kubernetes中都被定義為集群內部的 API 資源,和我們前面學習過的 Pod、ConfigMap 這些類似,都是我們集群的資源對象,所以同樣的可以使用我們前面的kubectl相關的命令來進行操作
  • Subject:主題,對應在集群中嘗試操作的對象,集群中定義了3種類型的主題資源:
  • Service Account:服務帳號,通過Kubernetes
  • RoleBinding 和 ClusterRoleBinding:角色綁定和集群角色綁定,簡單來說就是把聲明的 Subject 和我們的 Role 進行綁定的過程(給某個用戶綁定上操作的權限),二者的區別也是作用范圍的區別:RoleBinding 只會影響到當前 namespace 下面的資源操作權限,而 ClusterRoleBinding 會影響到所有的 namespace。

接下來我們來通過幾個示例來演示下RBAC的配置方法。

8.3 創建一個只能訪問某個 namespace 的用戶

我們來創建一個 User Account,只能訪問 kube-system 這個命名空間:

  • username: haimaxy
  • group: youdianzhishi

8.3.1 第1步:創建用戶憑證

我們前面已經提到過,Kubernetes沒有 User Account 的 API 對象,不過要創建一個用戶帳號的話也是挺簡單的,利用管理員分配給你的一個私鑰就可以創建了,這個我們可以參考官方文檔中的方法,這里我們來使用OpenSSL證書來創建一個 User,當然我們也可以使用更簡單的cfssl工具來創建:

(1)給用戶 haimaxy 創建一個私鑰,命名成:haimaxy.key:

[root@node01 ~]# openssl genrsa -out haimaxy.key 2048
Generating RSA private key, 2048 bit long modulus
....................................+++
..............................+++
e is 65537 (0x10001)

(2)使用我們剛剛創建的私鑰創建一個證書簽名請求文件:haimaxy.csr,要注意需要確保在-subj參數中指定用戶名和組(CN表示用戶名,O表示組):

[root@node01 ~]# openssl req -new -key haimaxy.key -out haimaxy.csr -subj "/CN=haimaxy/O=youdianzhis"

(3)然后找到我們的Kubernetes集群的CA,我們使用的是kubeadm安裝的集群,CA相關證書位於/etc/kubernetes/pki/目錄下面,如果你是二進制方式搭建的,你應該在最開始搭建集群的時候就已經指定好了CA的目錄,我們會利用該目錄下面的ca.crtca.key兩個文件來批准上面的證書請求
(4)生成最終的證書文件,我們這里設置證書的有效期為500天:

[root@node01 ~]# openssl x509 -req -in haimaxy.csr -CA /etc/kubernetes/pki/ca.crt -CAkey /etc/kubernetes/pki/ca.key -CAcreateserial -out haimaxy.crt -days 500
Signature ok
subject=/CN=haimaxy/O=youdianzhis
Getting CA Private Key

(5)現在查看我們當前文件夾下面是否生成了一個證書文件:

[root@node01 ~]# ls haimaxy.*
haimaxy.crt  haimaxy.csr  haimaxy.key

(6) 現在我們可以使用剛剛創建的證書文件和私鑰文件在集群中創建新的憑證和上下文(Context):

[root@node01 ~]# kubectl config set-credentials haimaxy --client-certificate=haimaxy.crt  --client-key=haimaxy.key
User "haimaxy" set.

我們可以看到一個用戶haimaxy創建了,然后為這個用戶設置新的 Context:

[root@node01 ~]# kubectl config set-context haimaxy-context --cluster=kubernetes --namespace=kube-system --user=haimaxy
Context "haimaxy-context" created.

到這里,我們的用戶haimaxy就已經創建成功了,現在我們使用當前的這個配置文件來操作kubectl命令的時候,應該會出現錯誤,因為我們還沒有為該用戶定義任何操作的權限呢:

[root@node01 ~]# kubectl get pods --context=haimaxy-context
Error from server (Forbidden): pods is forbidden: User "haimaxy" cannot list pods in the namespace "kube-system"

8.3.2 第2步:創建角色

用戶創建完成后,接下來就需要給該用戶添加操作權限,我們來定義一個YAML文件,創建一個允許用戶操作 Deployment、Pod、ReplicaSets 的角色,如下定義:(haimaxy-role.yaml)

[root@node01 ~]# vim haimaxy-role.yaml
[root@node01 ~]# cat haimaxy-role.yaml 
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: haimaxy-role
  namespace: kube-system
rules:
- apiGroups: ["", "extensions", "apps"]
  resources: ["deployments", "replicasets", "pods"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] #也可以使用['*']

其中Pod屬於 core 這個 API Group,在YAML中用空字符就可以,而Deployment屬於 apps 這個 API Group,ReplicaSets屬於extensions這個 API Group(我怎么知道的?點這里查文檔),所以 rules 下面的 apiGroups 就綜合了這幾個資源的 API Group:["", "extensions", "apps"],其中verbs就是我們上面提到的可以對這些資源對象執行的操作,我們這里需要所有的操作方法,所以我們也可以使用['*']來代替。

然后創建這個Role

[root@node01 ~]# kubectl create -f haimaxy-role.yaml
role.rbac.authorization.k8s.io "haimaxy-role" created

注意這里我們沒有使用上面的haimaxy-context這個上下文了,因為木有權限啦

8.3.3 第3步:創建角色權限綁定

Role 創建完成了,但是很明顯現在我們這個 Role 和我們的用戶 haimaxy 還沒有任何關系,對吧?這里我就需要創建一個RoleBinding對象,在 kube-system 這個命名空間下面將上面的 haimaxy-role 角色和用戶 haimaxy 進行綁定:(haimaxy-rolebinding.yaml)

[root@node01 ~]# vim haimaxy-rolebinding.yaml
[root@node01 ~]# cat haimaxy-rolebinding.yaml 
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: haimaxy-rolebinding
  namespace: kube-system
subjects:
- kind: User
  name: haimaxy
  apiGroup: ""
roleRef:
  kind: Role
  name: haimaxy-role
  apiGroup: ""

上面的YAML文件中我們看到了subjects關鍵字,這里就是我們上面提到的用來嘗試操作集群的對象,這里對應上面的 User 帳號 haimaxy,使用kubectl創建上面的資源對象:

[root@node01 ~]# kubectl create -f haimaxy-rolebinding.yaml
rolebinding.rbac.authorization.k8s.io "haimaxy-rolebinding" created

8.3.4 第4步. 測試

現在我們應該可以上面的haimaxy-context上下文來操作集群了:

[root@node01 ~]# kubectl get pods --context=haimaxy-context
NAME                                    READY     STATUS    RESTARTS   AGE
etcd-master                             1/1       Running   0          47d
kube-apiserver-master                   1/1       Running   0          47d
kube-controller-manager-master          1/1       Running   0          47d
kube-dns-86f4d74b45-d4xsl               3/3       Running   0          47d
kube-flannel-ds-amd64-nmr2k             1/1       Running   0          47d
kube-flannel-ds-amd64-rzvfc             1/1       Running   0          47d
kube-proxy-6zbdw                        1/1       Running   0          47d
kube-proxy-zg7v2                        1/1       Running   0          47d
kube-scheduler-master                   1/1       Running   0          47d
kubernetes-dashboard-669f9bbd46-tf6bz   1/1       Running   0          47d

我們可以看到我們使用kubectl的使用並沒有指定 namespace 了,這是因為我們已經為該用戶分配了權限了,如果我們在后面加上一個-n default試看看呢?

[root@node01 ~]# kubectl --context=haimaxy-context get pods --namespace=default
Error from server (Forbidden): pods is forbidden: User "haimaxy" cannot list pods in the namespace "default"

是符合我們預期的吧?因為該用戶並沒有 default 這個命名空間的操作權限

8.4 創建一個只能訪問某個 namespace 的ServiceAccount

上面我們創建了一個只能訪問某個命名空間下面的普通用戶,我們前面也提到過 subjects 下面還有一直類型的主題資源:ServiceAccount,現在我們來創建一個集群內部的用戶只能操作 kube-system 這個命名空間下面的 pods 和 deployments,首先來創建一個 ServiceAccount 對象:

[root@node01 ~]# kubectl create sa haimaxy-sa -n kube-system
serviceaccount "haimaxy-sa" created

當然我們也可以定義成YAML文件的形式來創建。

然后新建一個 Role 對象:(haimaxy-sa-role.yaml)

[root@node01 ~]# vim haimaxy-sa-role.yaml
[root@node01 ~]# cat haimaxy-sa-role.yaml 
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: haimaxy-sa-role
  namespace: kube-system
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "watch", "list"]
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

可以看到我們這里定義的角色沒有創建、刪除、更新 Pod 的權限,待會我們可以重點測試一下,創建該 Role 對象:

[root@node01 ~]# kubectl create -f haimaxy-sa-role.yaml
role.rbac.authorization.k8s.io "haimaxy-sa-role" created

然后創建一個 RoleBinding 對象,將上面的 haimaxy-sa 和角色 haimaxy-sa-role 進行綁定:(haimaxy-sa-rolebinding.yaml)

[root@node01 ~]# vim haimaxy-sa-rolebinding.yaml
[root@node01 ~]# cat haimaxy-sa-rolebinding.yaml 
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: haimaxy-sa-rolebinding
  namespace: kube-system
subjects:
- kind: ServiceAccount
  name: haimaxy-sa
  namespace: kube-system
roleRef:
  kind: Role
  name: haimaxy-sa-role
  apiGroup: rbac.authorization.k8s.io

添加這個資源對象:

[root@node01 ~]# kubectl create -f haimaxy-sa-rolebinding.yaml
rolebinding.rbac.authorization.k8s.io "haimaxy-sa-rolebinding" created

然后我們怎么去驗證這個 ServiceAccount 呢?我們前面的課程中是不是提到過一個 ServiceAccount 會生成一個 Secret 對象和它進行映射,這個 Secret 里面包含一個 token,我們可以利用這個 token 去登錄 Dashboard,然后我們就可以在 Dashboard 中來驗證我們的功能是否符合預期了:

[root@node01 ~]# kubectl get secret -n kube-system |grep haimaxy-sa
haimaxy-sa-token-8vnpr                           kubernetes.io/service-account-token   3         8m

[root@node01 ~]# kubectl get secret haimaxy-sa-token-8vnpr -o jsonpath={.data.token} -n kube-system |base64 -d
eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJoYWltYXh5LXNhLXRva2VuLTh2bnByIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImhhaW1heHktc2EiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiIxMWQ0NjM4NS1mMmJhLTExZWItOGZjMy0wMDBjMjkyNzU5N2MiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZS1zeXN0ZW06aGFpbWF4eS1zYSJ9.RIi5fOebs08JqJ1WFApYIuYMjLu_y-9a0k66aJ9jgCrEn6iWL45d-a857NOaPHeu1wkBrE39xblapNIAscQJ0HONgHANgY2TGYHCky5y5ee2VNamp45ft_rcufgMVhEWKmSjBF0sn70OJj2zEnrRTbR3kdKXPfkbs9wEQcYxAf3JmiQBhu_py6995hudzJ-SQtXiFplpLOBWLc_hgT4a037n4wbd4awRsE6SIFcZ48nHIQXC2nu3JF9X25SJOzTYkWSTA7fzAI1kfbqfDh11Mp6yRyY5IFjYZTJbCfoaSMUTEMcTILqkjkNDe7eqkdwPOVfvQgVjK78tLfGPlDnVRQ

使用這里的 token 去 Dashboard 頁面進行登錄: https://192.168.200.10:30422/

圖片.png-129.4kB

圖片.png-247.6kB

我們可以看到上面的提示信息,這是因為我們登錄進來后默認跳轉到 default 命名空間,我們切換到 kube-system 命名空間下面就可以了:

圖片.png-237.8kB

我們可以看到可以訪問pod列表了,但是也會有一些其他額外的提示:events is forbidden: User “system:serviceaccount:kube-system:haimaxy-sa” cannot list events in the namespace “kube-system”,這是因為當前登錄用只被授權了訪問 pod 和 deployment 的權限,同樣的,訪問下deployment看看可以了嗎?

同樣的,你可以根據自己的需求來對訪問用戶的權限進行限制,可以自己通過 Role 定義更加細粒度的權限,也可以使用系統內置的一些權限……

8.5 創建一個可以訪問所有 namespace 的ServiceAccount

剛剛我們創建的haimaxy-sa這個 ServiceAccount 和一個 Role 角色進行綁定的,如果我們現在創建一個新的 ServiceAccount,需要他操作的權限作用於所有的 namespace,這個時候我們就需要使用到 ClusterRole 和 ClusterRoleBinding 這兩種資源對象了。同樣,首先新建一個 ServiceAcount 對象:(haimaxy-sa2.yaml)

[root@node01 ~]# vim haimaxy-sa2.yaml
[root@node01 ~]# cat haimaxy-sa2.yaml 
apiVersion: v1
kind: ServiceAccount
metadata:
  name: haimaxy-sa2
  namespace: kube-system

創建:

[root@node01 ~]# kubectl create -f haimaxy-sa2.yaml
serviceaccount "haimaxy-sa2" created

然后創建一個 ClusterRoleBinding 對象(haimaxy-clusterolebinding.yaml):

[root@node01 ~]# vim haimaxy-clusterolebinding.yaml
[root@node01 ~]# cat haimaxy-clusterolebinding.yaml 
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: haimaxy-sa2-clusterrolebinding
subjects:
- kind: ServiceAccount
  name: haimaxy-sa2
  namespace: kube-system
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io

從上面我們可以看到我們沒有為這個資源對象聲明 namespace,因為這是一個 ClusterRoleBinding 資源對象,是作用於整個集群的,我們也沒有單獨新建一個 ClusterRole 對象,而是使用的 cluster-admin 這個對象,這是Kubernetes集群內置的 ClusterRole 對象,我們可以使用kubectl get clusterrolekubectl get clusterrolebinding查看系統內置的一些集群角色和集群角色綁定,這里我們使用的 cluster-admin 這個集群角色是擁有最高權限的集群角色,所以一般需要謹慎使用該集群角色。

創建上面集群角色綁定資源對象,創建完成后同樣使用 ServiceAccount 對應的 token 去登錄 Dashboard 驗證下:

[root@node01 ~]# kubectl create -f haimaxy-clusterolebinding.yaml
clusterrolebinding.rbac.authorization.k8s.io "haimaxy-sa2-clusterrolebinding" created
[root@node01 ~]# kubectl get secret -n kube-system |grep haimaxy-sa2
haimaxy-sa2-token-n5qwz                          kubernetes.io/service-account-token   3         2m

[root@node01 ~]# kubectl get secret haimaxy-sa2-token-n5qwz -o jsonpath={.data.token} -n kube-system |base64 -d
eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJoYWltYXh5LXNhMi10b2tlbi1uNXF3eiIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJoYWltYXh5LXNhMiIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6ImRjZjc2ZmIxLWYyYmMtMTFlYi04ZmMzLTAwMGMyOTI3NTk3YyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDprdWJlLXN5c3RlbTpoYWltYXh5LXNhMiJ9.PLFRB2KX92eG-RP5KFzKSiH9b3uVFtvGWicGzehY7BR2Gz-mBy1-4Z0rp8VMTWe0nl6FzyNyIx8iTLfUH3_eJUXQOZq_THlvk7blA_DnrAgXNKVEBWRKQlRuc458Rg2kvmylB_d8gT2_8_Nggw0lNxeOjPPws3pkHTrRTKrt64mvNMsgOczXLWSclplsqDM0TYtC5JflgKN2hXq0IHMlfFk9-wIOf9BWvGZKopBOBmvT8sBsOndrN0RjhuvFEnsp3OxzklKGSiUh_vV_6hS1jhmMmezlHjuC6dkKq342ulC4k-PXhPfZ1CDmFE2zpp2MUzj9Wu3TK1chgSWc0677LA

圖片.png-217.2kB

我們在最開始接觸到RBAC認證的時候,可能不太熟悉,特別是不知道應該怎么去編寫rules規則,大家可以去分析系統自帶的 clusterrole、clusterrolebinding 這些資源對象的編寫方法,怎么分析?還是利用 kubectl 的 get、describe、 -o yaml 這些操作,所以kubectl最基本的用戶一定要掌握好。

RBAC只是Kubernetes中安全認證的一種方式,當然也是現在最重要的一種方式,后面我們再和大家一起聊一聊Kubernetes中安全設計。

9. 部署 Wordpress 示例

9.1 部署 Wordpress 示例

前面的課程中我們基本上了解了Kubernetes當中常見的一些對象資源,這節課我們就來利用前面學習的知識點來部署一個實際的應用 - 將Wordpress應用部署到我們的集群當中,我們前面是不是已經用docker-compose的方式部署過了,我們可以了解到要部署一個Wordpress應用主要涉及到兩個鏡像:wordpressmysqlwordpress是應用的核心程序,mysql是用於數據存儲的。

現在我們來看看如何來部署我們的這個wordpress應用

9.2 一個Pod

我們知道一個Pod中可以包含多個容器,那么很明顯我們這里是不是就可以將wordpress部署成一個獨立的Pod?我們將我們的應用都部署到blog這個命名空間下面,所以先創建一個命名空間:

[root@node01 ~]# kubectl create namespace blog
namespace "blog" created

然后來編寫YAML文件:(wordpress-pod.yaml)

[root@node01 ~]# vim wordpress-pod.yaml
[root@node01 ~]# cat wordpress-pod.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: wordpress
  namespace: blog
spec:
  containers:
  - name: wordpress
    image: wordpress
    ports:
    - containerPort: 80
      name: wdport
    env:
    - name: WORDPRESS_DB_HOST
      value: localhost:3306
    - name: WORDPRESS_DB_USER
      value: wordpress
    - name: WORDPRESS_DB_PASSWORD
      value: wordpress
  - name: mysql
    image: mysql:5.7
    imagePullPolicy: IfNotPresent
    args:  # 新版本鏡像有更新,需要使用下面的認證插件環境變量配置才會生效
    - --default_authentication_plugin=mysql_native_password
    - --character-set-server=utf8mb4
    - --collation-server=utf8mb4_unicode_ci
    ports:
    - containerPort: 3306
      name: dbport
    env:
    - name: MYSQL_ROOT_PASSWORD
      value: rootPassW0rd
    - name: MYSQL_DATABASE
      value: wordpress
    - name: MYSQL_USER
      value: wordpress
    - name: MYSQL_PASSWORD
      value: wordpress
    volumeMounts:
    - name: db
      mountPath: /var/lib/mysql
  volumes:
  - name: db
    hostPath:
      path: /var/lib/mysql

要注意這里針對mysql這個容器我們做了一個數據卷的掛載,這是為了能夠將mysql的數據能夠持久化到節點上,這樣下次mysql容器重啟過后數據不至於丟失。然后創建上面的Pod:

[root@node01 ~]# kubectl create -f wordpress-pod.yaml
pod "wordpress" created

接下來就是等待拉取鏡像,啟動容器,同樣我們可以使用describe指令查看詳細信息:

[root@node01 ~]# kubectl describe pod wordpress -n blog

大家可以看看我們現在這種單一Pod的方式有什么缺點?假如我們現在需要部署3個Wordpress的副本,該怎么辦?是不是我們只需要在上面的YAML文件中加上replicas: 3這個屬性就可以了啊?但是有個什么問題呢?是不是不僅僅是Wordpress這個容器會被部署成3份,連我們的MySQL數據庫也會被部署成3份了呢?MySQL數據庫單純的部署成3份他們能聯合起來使用嗎?不能,如果真的這么簡單的話就不需要各種數據庫集群解決方案了,所以我們這里部署3個Pod實例,實際上他們互相之間是獨立的,因為數據不想通,明白吧?所以該怎么辦?拆分唄,把wordpressmysql這兩個容器部署成獨立的Pod是不是就可以了。

另外一個問題是我們的wordpress容器需要去連接mysql數據庫吧,現在我們這里放在一起能保證mysql先啟動起來嗎?貌似沒有特別的辦法,前面學習的InitContainer也是針對Pod來的,所以無論如何,我們都需要將他們進行拆分。

9.3 兩個Pod

現在來把上面的一個Pod拆分成兩個Pod,我們前面也反復強調過要使用Deployment來管理我們的Pod,上面只是為了單純給大家說明怎么來把前面的Docker環境下的wordpress轉換成Kubernetes環境下面的Pod,有了上面的Pod模板,我們現在來轉換成Deployment很容易了吧。

9.3.1 第一步,創建一個MySQLDeployment對象:(wordpress-db.yaml)

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql-deploy
  namespace: blog
  labels:
    app: mysql
spec:
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:5.7
        imagePullPolicy: IfNotPresent
        args:
        - --default_authentication_plugin=mysql_native_password
        - --character-set-server=utf8mb4
        - --collation-server=utf8mb4_unicode_ci
        ports:
        - containerPort: 3306
          name: dbport
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: rootPassW0rd
        - name: MYSQL_DATABASE
          value: wordpress
        - name: MYSQL_USER
          value: wordpress
        - name: MYSQL_PASSWORD
          value: wordpress
        volumeMounts:
        - name: db
          mountPath: /var/lib/mysql
      volumes:
      - name: db
        hostPath:
          path: /var/lib/mysql

如果我們只創建上面的Deployment這個對象,那么我們應該怎樣讓后面的Wordpress來訪問呢?貌似沒辦法是吧,之前在一個Pod里面還可以使用localhost來進行訪問,現在分開了該怎樣訪問呢?還記得前面的Service嗎?沒錯,使用Service就可以了,所以我們在上面的wordpress-db.yaml文件中添加上Service的信息:

---
apiVersion: v1
kind: Service
metadata:
  name: mysql
  namespace: blog
spec:
  selector:
    app: mysql
  ports:
  - name: mysqlport
    protocol: TCP
    port: 3306
    targetPort: dbport

然后創建上面的wordpress-db.yaml文件:

[root@node01 ~]# kubectl create -f wordpress-db.yaml
deployment.apps "mysql-deploy" created
service "mysql" created

然后我們查看Service的詳細情況:

[root@node01 ~]# kubectl describe svc mysql -n blog
Name:              mysql
Namespace:         blog
Labels:            <none>
Annotations:       <none>
Selector:          app=mysql
Type:              ClusterIP
IP:                10.100.13.170
Port:              mysqlport  3306/TCP
TargetPort:        dbport/TCP
Endpoints:         10.244.1.4:3306
Session Affinity:  None
Events:            <none>

可以看到Endpoints部分匹配到了一個Pod,生成了一個clusterIP:10.100.13.170,現在我們是不是就可以通過這個clusterIP加上定義的3306端口就可以正常訪問我們這個mysql服務了。

9.3.2 第二步. 創建Wordpress服務,將上面的wordpressPod轉換成Deployment對象:(wordpress.yaml)

[root@node01 ~]# vim wordpress.yaml
[root@node01 ~]# cat wordpress.yaml 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress-deploy
  namespace: blog
  labels:
    app: wordpress
spec:
  selector:
    matchLabels:
      app: wordpress
  template:
    metadata:
      labels:
        app: wordpress
    spec:
      containers:
      - name: wordpress
        image: wordpress
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80
          name: wdport
        env:
        - name: WORDPRESS_DB_HOST
          value: 10.100.13.170:3306
        - name: WORDPRESS_DB_USER
          value: wordpress
        - name: WORDPRESS_DB_PASSWORD
          value: wordpress

注意這里的環境變量WORDPRESS_DB_HOST的值將之前的localhost地址更改成了上面mysql服務的clusterIP地址了,然后創建上面的Deployment對象:

[root@node01 ~]# kubectl create -f wordpress.yaml
deployment.apps "wordpress-deploy" created

創建完成后,我們可以看看我們創建的Pod的狀態:

[root@node01 ~]# kubectl get pods -n blog
NAME                                READY     STATUS    RESTARTS   AGE
mysql-deploy-787bdb744b-d2kw8       1/1       Running   0          6m
wordpress-deploy-65586fc686-rqb6b   1/1       Running   0          2m

可以看到都已經是Running狀態了,然后我們需要怎么來驗證呢?是不是去訪問下我們的wordpress服務就可以了,要訪問,我們就需要建立一個能讓外網用戶訪問的Service,前面我們學到過是不是NodePort類型的Service就可以?所以在上面的wordpress.yaml文件中添加上Service的信息:

---
apiVersion: v1
kind: Service
metadata:
  name: wordpress
  namespace: blog
spec:
  type: NodePort
  selector:
    app: wordpress
  ports:
  - name: wordpressport
    protocol: TCP
    port: 80
    targetPort: wdport

注意要添加屬性type: NodePort,然后重新更新wordpress.yaml文件:

[root@node01 ~]# kubectl apply -f wordpress.yaml
Warning: kubectl apply should be used on resource created by either kubectl create --save-config or kubectl apply
deployment.apps "wordpress-deploy" configured
service "wordpress" created

創建完成后,查看下svc

[root@node01 ~]# kubectl get svc -n blog
NAME        TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
mysql       ClusterIP   10.100.13.170   <none>        3306/TCP       10m
wordpress   NodePort    10.109.79.65    <none>        80:31941/TCP   13s

可以看到wordpress服務產生了一個31941的端口,現在我們是不是就可以通過任意節點的NodeIP加上31941端口,就可以訪問我們的wordpress應用了,在瀏覽器中打開,如果看到wordpress跳轉到了安裝頁面,證明我們的嗯安裝是沒有任何問題的了,如果沒有出現預期的效果,那么就需要去查看下Pod的日志來查看問題了:

http://192.168.200.11:31941

圖片.png-184.4kB

圖片.png-211kB

然后根據頁面提示,填上對應的信息,點擊“安裝”即可,最終安裝成功后,我們就可以看到熟悉的首頁界面了:GDg^CJGkq4D1)faxDe

圖片.png-91.1kB

圖片.png-94.7kB

圖片.png-318.3kB

9.4 提高穩定性

現在wordpress應用已經部署成功了,那么就萬事大吉了嗎?如果我們的網站訪問量突然變大了怎么辦,如果我們要更新我們的鏡像該怎么辦?如果我們的mysql服務掛掉了怎么辦?

所以要保證我們的網站能夠非常穩定的提供服務,我們做得還不夠,我們可以通過做些什么事情來提高網站的穩定性呢?

第一. 增加健康檢測,我們前面說過liveness proberediness probe是提高應用穩定性非常重要的方法:

livenessProbe:
  tcpSocket:
    port: 80
  initialDelaySeconds: 3
  periodSeconds: 3
readinessProbe:
  tcpSocket:
    port: 80
  initialDelaySeconds: 5
  periodSeconds: 10

增加上面兩個探針,每10s檢測一次應用是否可讀,每3s檢測一次應用是否存活

第二. 增加 HPA,讓我們的應用能夠自動應對流量高峰期:

$ kubectl autoscale deployment wordpress-deploy --cpu-percent=10 --min=1 --max=10 -n blog
deployment "wordpress-deploy" autoscaled

我們用kubectl autoscale命令為我們的wordpress-deploy創建一個HPA對象,最小的 pod 副本數為1,最大為10,HPA會根據設定的 cpu使用率(10%)動態的增加或者減少pod數量。當然最好我們也為Pod聲明一些資源限制:

resources:
  limits:
    cpu: 200m
    memory: 200Mi
  requests:
    cpu: 100m
    memory: 100Mi

更新Deployment后,我們可以可以來測試下上面的HPA是否會生效:

$ kubectl run -i --tty load-generator --image=busybox /bin/sh
If you don't see a command prompt, try pressing enter.
/ # while true; do wget -q -O- http://10.244.1.62:80; done

觀察Deployment的副本數是否有變化

$ kubectl get deployment wordpress-deploy
NAME        DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
wordpress-deploy   3         3         3            3           4d

第三. 增加滾動更新策略,這樣可以保證我們在更新應用的時候服務不會被中斷:

replicas: 2
revisionHistoryLimit: 10
minReadySeconds: 5
strategy:
  type: RollingUpdate
  rollingUpdate:
    maxSurge: 1
    maxUnavailable: 1

第四. 我們知道如果mysql服務被重新創建了的話,它的clusterIP非常有可能就變化了,所以上面我們環境變量中的WORDPRESS_DB_HOST的值就會有問題,就會導致訪問不了數據庫服務了,這個地方我們可以直接使用Service的名稱來代替host,這樣即使clusterIP變化了,也不會有任何影響,這個我們會在后面的服務發現的章節和大家深入講解的:

env:
- name: WORDPRESS_DB_HOST
  value: mysql:3306

第五. 我們在部署wordpress服務的時候,mysql服務以前啟動起來了嗎?如果沒有啟動起來是不是我們也沒辦法連接數據庫了啊?該怎么辦,是不是在啟動wordpress應用之前應該去檢查一下mysql服務,如果服務正常的話我們就開始部署應用了,這是不是就是InitContainer的用法:

initContainers:
- name: init-db
  image: busybox
  command: ['sh', '-c', 'until nslookup mysql; do echo waiting for mysql service; sleep 2; done;']

直到mysql服務創建完成后,initContainer才結束,結束完成后我們才開始下面的部署。

最后,我們把部署的應用整合到一個YAML文件中來:(wordpress-all.yaml)

[root@node01 ~]# vim wordpress-all.yaml
[root@node01 ~]# cat wordpress-all.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql
  namespace: blog
  labels:
    app: mysql
spec:
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:5.7
        args:
        - --default_authentication_plugin=mysql_native_password
        - --character-set-server=utf8mb4
        - --collation-server=utf8mb4_unicode_ci
        ports:
        - containerPort: 3306
          name: dbport
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: rootPassW0rd
        - name: MYSQL_DATABASE
          value: wordpress
        - name: MYSQL_USER
          value: wordpress
        - name: MYSQL_PASSWORD
          value: wordpress
        volumeMounts:
        - name: db
          mountPath: /var/lib/mysql
      volumes:
      - name: db
        hostPath:
          path: /var/lib/mysql

---
apiVersion: v1
kind: Service
metadata:
  name: mysql
  namespace: blog
spec:
  selector:
    app: mysql
  ports:
  - name: mysqlport
    protocol: TCP
    port: 3306
    targetPort: dbport

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress
  namespace: blog
  labels:
    app: wordpress
spec:
  selector:
    matchLabels:
      app: wordpress
  minReadySeconds: 5
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
  template:
    metadata:
      labels:
        app: wordpress
    spec:
      initContainers:
      - name: init-db
        image: busybox
        command: ['sh', '-c', 'until nslookup mysql; do echo waiting for mysql service; sleep 2; done;']
      containers:
      - name: wordpress
        image: wordpress
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80
          name: wdport
        env:
        - name: WORDPRESS_DB_HOST
          value: mysql:3306
        - name: WORDPRESS_DB_USER
          value: wordpress
        - name: WORDPRESS_DB_PASSWORD
          value: wordpress
        readinessProbe:
          tcpSocket:
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 10
        resources:
          limits:
            cpu: 200m
            memory: 256Mi
          requests:
            cpu: 100m
            memory: 100Mi

---
apiVersion: v1
kind: Service
metadata:
  name: wordpress
  namespace: blog
spec:
  selector:
    app: wordpress
  type: NodePort
  ports:
  - name: wordpressport
    protocol: TCP
    port: 80
    nodePort: 31941
    targetPort: wdport

我們這里主要是針對的wordpress來做的提高穩定性的方法,如何對mysql提高一些穩定性呢?大家下去可以試一試,我們接下來會和大家講解mysql這類有狀態的應用在Kubernetes當中的使用方法。

最后,我們來把前面我們部署的相關服務全部刪掉,重新通過上面的YAML文件來創建:

[root@node01 ~]# kubectl create -f wordpress-all.yaml
deployment.apps "mysql" created
service "mysql" created
deployment.apps "wordpress" created
service "wordpress" created

看看最后能不能得到我們的最終成果呢?

圖片.png-227.3kB

10. DaemonSet和StatefulSet

10.1 DaemonSet 與 StatefulSet 的使用

前面我們的課程中學習了大部分資源對象的使用方法,上節課我們通過一個WordPress的示例把我們前面的內容做了一個總結。今天我們來給大家講解另外一個Pod控制器的使用方法,我們前面主要講解的是Deployment這種對象資源的使用,接下來我們要講解的是在特定場合下使用的控制器:DaemonSetStatefulSet

10.2 DaemonSet 的使用

通過該控制器的名稱我們可以看出它的用法:Daemon,就是用來部署守護進程的,DaemonSet用於在每個Kubernetes節點中將守護進程的副本作為后台進程運行,說白了就是在每個節點部署一個Pod副本,當節點加入到Kubernetes集群中,Pod會被調度到該節點上運行,當節點從集群只能夠被移除后,該節點上的這個Pod也會被移除,當然,如果我們刪除DaemonSet,所有和這個對象相關的Pods都會被刪除。

在哪種情況下我們會需要用到這種業務場景呢?其實這種場景還是比較普通的,比如:

  • 集群存儲守護程序,如glusterd、ceph要部署在每個節點上以提供持久性存儲;
  • 節點監視守護進程,如Prometheus監控集群,可以在每個節點上運行一個node-exporter進程來收集監控節點的信息;
  • 日志收集守護程序,如fluentd或logstash,在每個節點上運行以收集容器的日志

這里需要特別說明的一個就是關於DaemonSet運行的Pod的調度問題,正常情況下,Pod運行在哪個節點上是由Kubernetes的調度器策略來決定的,然而,由DaemonSet控制器創建的Pod實際上提前已經確定了在哪個節點上了(Pod創建時指定了.spec.nodeName),所以:

  • DaemonSet並不關心一個節點的unshedulable字段,這個我們會在后面的調度章節和大家講解的。
  • DaemonSet可以創建Pod,即使調度器還沒有啟動,這點非常重要。

下面我們直接使用一個示例來演示下,在每個節點上部署一個Nginx Pod:(nginx-ds.yaml)

[root@node01 ~]# vim nginx-ds.yaml
[root@node01 ~]# cat nginx-ds.yaml 
kind: DaemonSet
apiVersion: apps/v1
metadata:
  name: nginx-ds
  labels:
    k8s-app: nginx
spec:
  selector:
    matchLabels:
      k8s-app: nginx
  template:
    metadata:
      labels:
        k8s-app: nginx
    spec:
      containers:
      - image: nginx:1.7.9
        name: nginx
        ports:
        - name: http
          containerPort: 80

然后直接創建即可:

[root@node01 ~]# kubectl create -f nginx-ds.yaml
daemonset.apps "nginx-ds" created

然后我們可以觀察下Pod是否被分布到了每個節點上:

[root@node01 ~]# kubectl get pods -o wide
NAME             READY     STATUS    RESTARTS   AGE       IP           NODE
nginx-ds-d2kw8   1/1       Running   0          19s       10.244.1.3   node01

10.3 StatefulSet 的使用

在學習StatefulSet這種控制器之前,我們就得先弄明白一個概念:什么是有狀態服務?什么是無狀態服務?

  • 無狀態服務(Stateless Service):該服務運行的實例不會在本地存儲需要持久化的數據,並且多個實例對於同一個請求響應的結果是完全一致的,比如前面我們講解的WordPress實例,我們是不是可以同時啟動多個實例,但是我們訪問任意一個實例得到的結果都是一樣的吧?因為他唯一需要持久化的數據是存儲在MySQL數據庫中的,所以我們可以說WordPress這個應用是無狀態服務,但是MySQL數據庫就不是了,因為他需要把數據持久化到本地。
  • 有狀態服務(Stateful Service):就和上面的概念是對立的了,該服務運行的實例需要在本地存儲持久化數據,比如上面的MySQL數據庫,你現在運行在節點A,那么他的數據就存儲在節點A上面的,如果這個時候你把該服務遷移到節點B去的話,那么就沒有之前的數據了,因為他需要去對應的數據目錄里面恢復數據,而此時沒有任何數據。

現在大家對有狀態和無狀態有一定的認識了吧,比如我們常見的 WEB 應用,是通過session來保持用戶的登錄狀態的,如果我們將session持久化到節點上,那么該應用就是一個有狀態的服務了,因為我現在登錄進來你把我的session持久化到節點A上了,下次我登錄的時候可能會將請求路由到節點B上去了,但是節點B上根本就沒有我當前的session數據,就會被認為是未登錄狀態了,這樣就導致我前后兩次請求得到的結果不一致了。所以一般為了橫向擴展,我們都會把這類 WEB 應用改成無狀態的服務,怎么改?將session數據存入一個公共的地方,比如redis里面,是不是就可以了,對於一些客戶端請求API的情況,我們就不使用session來保持用戶狀態,改成用token也是可以的。

無狀態服務利用我們前面的Deployment或者RC都可以很好的控制,對應有狀態服務,需要考慮的細節就要多很多了,容器化應用程序最困難的任務之一,就是設計有狀態分布式組件的部署體系結構。由於無狀態組件可能沒有預定義的啟動順序、集群要求、點對點 TCP 連接、唯一的網絡標識符、正常的啟動和終止要求等,因此可以很容易地進行容器化。諸如數據庫,大數據分析系統,分布式 key/value 存儲和 message brokers 可能有復雜的分布式體系結構,都可能會用到上述功能。為此,Kubernetes引入了StatefulSet資源來支持這種復雜的需求。

StatefulSet類似於ReplicaSet,但是它可以處理Pod的啟動順序,為保留每個Pod的狀態設置唯一標識,同時具有以下功能:

  • 穩定的、唯一的網絡標識符
  • 穩定的、持久化的存儲
  • 有序的、優雅的部署和縮放
  • 有序的、優雅的刪除和終止
  • 有序的、自動滾動更新

10.4 創建StatefulSet

接下來我們來給大家演示下StatefulSet對象的使用方法,在開始之前,我們先准備兩個1G的存儲卷(PV),在后面的課程中我們也會和大家詳細講解 PV 和 PVC 的使用方法的,這里我們先不深究:(pv001.yaml)

[root@node01 ~]# vim pv001.yaml
[root@node01 ~]# cat pv001.yaml 
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv001
  labels:
    release: stable
spec:
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Recycle
  hostPath:
    path: /tmp/data

另外一個只需要把 name 改成 pv002即可

[root@node01 ~]# vim pv002.yaml
[root@node01 ~]# cat pv002.yaml 
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv002
  labels:
    release: stable
spec:
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Recycle
  hostPath:
    path: /tmp/data

然后創建:

[root@node01 ~]# kubectl create -f pv001.yaml && kubectl create -f pv002.yaml
persistentvolume "pv001" created
persistentvolume "pv002" created

[root@node01 ~]# kubectl get pv
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM     STORAGECLASS   REASON    AGE
pv001     1Gi        RWO            Recycle          Available                                      7s
pv002     1Gi        RWO            Recycle          Available                                      7s

可以看到成功創建了兩個 PV對象,狀態是:Available。

然后我們使用StatefulSet來創建一個 Nginx 的 Pod,對於這種類型的資源,我們一般是通過創建一個Headless Service類型的服務來暴露服務,將clusterIP設置為None就是一個無頭的服務:(statefulset-demo.yaml)

[root@node01 ~]# vim statefulset-demo.yaml
[root@node01 ~]# cat statefulset-demo.yaml 
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx
    role: stateful

---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  replicas: 2
  selector:
    matchLabels:
      app: nginx
      role: stateful
  template:
    metadata:
      labels:
        app: nginx
        role: stateful
    spec:
      containers:
      - name: nginx
        image: cnych/nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

注意上面的 YAML 文件中和volumeMounts進行關聯的是一個新的屬性:volumeClaimTemplates,該屬性會自動聲明一個 pvc 對象和 pv 進行管理:

然后這里我們開啟兩個終端窗口。在第一個終端中,使用 kubectl get 來查看 StatefulSet 的 Pods 的創建情況。

[root@node01 ~]# kubectl get pods -w -l role=stateful

在另一個終端中,使用 kubectl create 來創建定義在 statefulset-demo.yaml 中的 Headless Service 和 StatefulSet。

[root@node01 ~]# kubectl create -f statefulset-demo.yaml
service "nginx" created
statefulset.apps "web" created

10.5 檢查 Pod 的順序索引

對於一個擁有 N 個副本的 StatefulSet,Pod 被部署時是按照 {0..N-1}的序號順序創建的。在第一個終端中我們可以看到如下的一些信息:

[root@node01 ~]# kubectl get pods -w -l role=stateful
NAME      READY     STATUS    RESTARTS   AGE
web-0     0/1       Pending   0          0s
web-0     0/1       Pending   0         0s
web-0     0/1       Pending   0         1s
web-0     0/1       ContainerCreating   0         1s
web-0     1/1       Running   0         3s
web-1     0/1       Pending   0         0s
web-1     0/1       Pending   0         0s
web-1     0/1       Pending   0         1s
web-1     0/1       ContainerCreating   0         1s
web-1     1/1       Running   0         4s

請注意在 web-0 Pod 處於 Running 和 Ready 狀態后 web-1 Pod 才會被啟動。

如同 StatefulSets 概念中所提到的, StatefulSet 中的 Pod 擁有一個具有穩定的、獨一無二的身份標志。這個標志基於 StatefulSet 控制器分配給每個 Pod 的唯一順序索引。 Pod 的名稱的形式為<statefulset name>-<ordinal index>。web StatefulSet 擁有兩個副本,所以它創建了兩個 Pod:web-0 和 web-1。

上面的命令創建了兩個 Pod,每個都運行了一個 NGINX web 服務器。獲取 nginx Service 和 web StatefulSet 來驗證是否成功的創建了它們。

[root@node01 ~]# kubectl get service nginx
NAME      TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
nginx     ClusterIP   10.100.13.170   <none>        80/TCP    2m

[root@node01 ~]# kubectl get statefulset web
NAME      DESIRED   CURRENT   AGE
web       2         2         2m

[root@node01 ~]# kubectl get pod -l app=nginx -o wide
NAME      READY     STATUS    RESTARTS   AGE       IP           NODE
web-0     1/1       Running   0          5m        10.244.1.4   node01
web-1     1/1       Running   0          5m        10.244.1.5   node01

10.6 使用穩定的網絡身份標識

每個 Pod 都擁有一個基於其順序索引的穩定的主機名。使用 kubectl exec 在每個 Pod 中執行 hostname 。

[root@node01 ~]# for i in 0 1; do kubectl exec web-$i -- sh -c 'hostname'; done
web-0
web-1

然后我們使用 kubectl run 運行一個提供 nslookup 命令的容器。通過對 Pod 的主機名執行 nslookup,你可以檢查他們在集群內部的 DNS 地址。

$ kubectl run -i --tty --image busybox dns-test --restart=Never --rm /bin/sh 
nslookup web-0.nginx
Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-0.nginx
Address 1: 10.244.1.6

nslookup web-1.nginx
Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-1.nginx
Address 1: 10.244.2.6

headless service 的 CNAME 指向 SRV 記錄(記錄每個 Running 和 Ready 狀態的 Pod)。SRV 記錄指向一個包含 Pod IP 地址的記錄表項。

然后我們再來看下刪除 StatefulSet 下面的 Pod:

在一個終端中查看 StatefulSet 的 Pod:

$ kubectl get pod -w -l role=stateful

在另一個終端中使用 kubectl delete 刪除 StatefulSet 中所有的 Pod。

$ kubectl delete pod -l role=stateful
pod "web-0" deleted
pod "web-1" deleted

等待 StatefulSet 重啟它們,並且兩個 Pod 都變成 Running 和 Ready 狀態。

$ kubectl get pod -w -l app=nginx
NAME      READY     STATUS              RESTARTS   AGE
web-0     0/1       ContainerCreating   0          0s
web-0     1/1       Running   0          2s
web-1     0/1       Pending   0         0s
web-1     0/1       Pending   0         0s
web-1     0/1       ContainerCreating   0         0s
web-1     1/1       Running   0         34s

然后再次使用 kubectl exec 和 kubectl run 查看 Pod 的主機名和集群內部的 DNS 表項。

$ for i in 0 1; do kubectl exec web-$i -- sh -c 'hostname'; done
web-0
web-1
$ kubectl run -i --tty --image busybox dns-test --restart=Never --rm /bin/sh 
nslookup web-0.nginx
Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-0.nginx
Address 1: 10.244.1.7

nslookup web-1.nginx
Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-1.nginx
Address 1: 10.244.2.8

我們可以看到Pod 的序號、主機名、SRV 條目和記錄名稱沒有改變,但和 Pod 相關聯的 IP 地址可能會發生改變。所以說這就是為什么不要在其他應用中使用 StatefulSet 中的 Pod 的 IP 地址進行連接,這點很重要。一般情況下我們直接通過 SRV 記錄連接就行:web-0.nginx、web-1.nginx,因為他們是穩定的,並且當你的 Pod 的狀態變為 Running 和 Ready 時,你的應用就能夠發現它們的地址。

同樣我們可以查看 PV、PVC的最終綁定情況:

$ kubectl get pv
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS    CLAIM               STORAGECLASS   REASON    AGE
pv001     1Gi        RWO            Recycle          Bound     default/www-web-0                            1h
pv002     1Gi        RWO            Recycle          Bound     default/www-web-1                            1h
$ kubectl get pvc
NAME        STATUS    VOLUME    CAPACITY   ACCESS MODES   STORAGECLASS   AGE
www-web-0   Bound     pv001     1Gi        RWO                           22m
www-web-1   Bound     pv002     1Gi        RWO                           22m

當然 StatefulSet 還擁有其他特性,在實際的項目中,我們還是很少回去直接通過 StatefulSet 來部署我們的有狀態服務的,除非你自己能夠完全能夠 hold 住,對於一些特定的服務,我們可能會使用更加高級的 Operator 來部署,比如 etcd-operator、prometheus-operator 等等,這些應用都能夠很好的來管理有狀態的服務,而不是單純的使用一個 StatefulSet 來部署一個 Pod就行,因為對於有狀態的應用最重要的還是數據恢復、故障轉移等等。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM