# Kubernetes Getting Start **Repository Path**: jeffwang78/kubernetes-getting-start ## Basic Information - **Project Name**: Kubernetes Getting Start - **Description**: Kubernetes getting start - Learning notes - **Primary Language**: YAML - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-01-09 - **Last Updated**: 2023-03-09 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## Kubenertes Getting Start ## 1. Minikube ### 1.1 install See: https://minikube.sigs.k8s.io/docs/start/ To install the latest minikube stable release on x86-64 Linux using Debian package: ```console curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube_latest_amd64.deb sudo dpkg -i minikube_latest_amd64.deb ``` WSL2 需要启动 systemd 。 windows 用户目录下,建立 .wslconfig ```ini [boot] systemd=true ``` ``` > wsl --shutdown # 等待 关闭后,在启动可见 systemctl已生效。 ``` K8S 从 1.24开始删除了 dockerslim 。因此,需要替换runtime 。 或者,使用低版本 K8S(当前版本 1.26, 使用1.23即可)。 ```console $ minikube start --nodes=2 --kubernetes-version=v1.23.8 $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES d3113c438e41 kicbase/stable:v0.0.36 "/usr/local/bin/entr…" 54 seconds ago Up 53 seconds 127.0.0.1:49182->22/tcp, 127.0.0.1:49181->2376/tcp, 127.0.0.1:49180->5000/tcp, 127.0.0.1:49179->8443/tcp, 127.0.0.1:49178->32443/tcp minikube-m02 f89bc797ed5c kicbase/stable:v0.0.36 "/usr/local/bin/entr…" About a minute ago Up About a minute 127.0.0.1:49177->22/tcp, 127.0.0.1:49176->2376/tcp, 127.0.0.1:49175->5000/tcp, 127.0.0.1:49174->8443/tcp, 127.0.0.1:49173->32443/tcp minikube ``` 可见已启动两个 node。 启动 dashboard ,并在浏览器查看: ``` $ minikube dashboard 🔌 Enabling dashboard ... ▪ Using image docker.io/kubernetesui/dashboard:v2.7.0 ▪ Using image docker.io/kubernetesui/metrics-scraper:v1.0.8 💡 Some dashboard features require the metrics-server addon. To enable all features please run: minikube addons enable metrics-server 🤔 Verifying dashboard health ... 🚀 Launching proxy ... 🤔 Verifying proxy health ... 🎉 Opening http://127.0.0.1:46483/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/ in your default browser... ``` 浏览器打开该URL即可看到 dashboard 界面。查看Nodes: ``` $ kubectl get nodes NAME STATUS ROLES AGE VERSION minikube Ready control-plane,master 19m v1.23.8 minikube-m02 Ready 18m v1.23.8 ``` ## 2. 基础概念 ### 2.1 Swarm 和 K8S Swarm 的概念非常简单,是一种递进式的定义。 * Container: 基础容器。 * Service: 同一容器的多个实例集群。 * Stack: 多个服务的集群。 * Resource * Volume * Network * Config 层次关系也很清晰: ```mermaid graph LR subgraph stack[Stack: demo] subgraph s1[service: demo_app1] c1(("container: \napp1.1")) c12(("container: \napp1.2")) end subgraph s2[service: demo_app2] c2(("container: \napp2.1")) c22(("container: \napp2.2")) end end ``` 因此,Swarm 的概念是完全从 container 延伸而来,直观而容易理解。 K8S 概念则要复杂一些,这是因为它使用的声明式对象管理。 #### 2.1.1 POD K8S 最基础的工作单元是**POD**。而POD的各种管理、组织形式, 使用了Deployments, DeamonSet, StatefulSet, Job 等概念。 POD并不等于容器, 而是K8S包装后的容器(或多个容器)。这一点在刚刚看官方文档时会比较困惑,因此,在理解K8S整体之前,可以先简单的认为POD**相当于**Swarm里的容器。 #### 2.1.2 Deployment Deployments 则是Pod的部署声明。声明一个 Deployment 时,主要包括: * container:使用的镜像及参数(如环境变量)。 * replicas: 副本数量。 这很容易联想到 Swarm的 Service。但我们看到,K8S也有Service啊。。。这两个Service还有相似之处呢? Deployment 声明的 Pod,可以实现多个实例的集群运行,但它没有**对外暴露端口**。这相当于 Swarm里的Service没有publish端口,从而无法从集群外访问。 在K8S里想让一个服务对外可见时,需要再声明一个Service,在Service中暴露端口。 因此,Depolyment + Service 大约等于 Swarm 的Service 。 #### 2.1.3 RepilcaSet 和 StatefulSet 当你理解了这一点时,另两个名词又出现了:ReplicaSet 和 StatefulSet。 字面上看,一个是副本集合,另一个是有状态集合。 StatefulSet 的 Statefull 是指,每个运行副本的Pod的信息和资源都是有状态的,当Pod容器被销毁、被重启时,这些资源会得到保留,并应用在重启后的容器。 举个例子,如果你想运行一个 Nacos集群, 这个集群需要配置 参与 cluster的各个主机名,这时,就可以使用 StatefulSet,StatefulSet中每个容器的DNS名称是固定的,绑定的存储也是固定的。 这样看起来,StatefulSet 比 Swarm 的Service高级一点点。 StatefulSet + Service 相当于 Swarm 的 Service 。 ReplicaSet 呢? 官方已经讲了,使用Deployment 默认创建的就是 ReplicaSet。RepilcaSet与StatefulSet的区别在于,RS 仅保证实例数量。 #### 2.1.4 DaemonSet DeamonSet就很简单了,每个节点运行一个实例。这相当于 Swarm Global Service。 #### 2.1.5 Job 和 CronJob Job 是作业,与之对应的 CronJob 定时任务。K8S提供调度来完成作业,这在实际系统部署时是很常用的功能。 #### 2.1.6 ReplicationController 另一个是 ReplicationController ,这是用来控制副本的控制器,比如监视副本数量,重启,收缩等。 使用 Deployment + ReplicaSet 会创建。 #### 2.1.7 容器相关概念小结 综上,大致的对应关系如下: |**K8S**|**Swarm**|**说明**| |--|--|--| |Pod|container| 仅仅是约等于| |Deployment|Service| 无对外暴露端口 | |ReplicaSet| | Deployment 代替 | |StatefulSet|Service| 无对外暴露端口| |DeameonSet| Global Servive | | | Job | 无 | 一次性任务 | | CronJob| 无 | 定时任务 | |ReplicationController | | Deployment 代替 | |Service| Service | 对外暴露端口 | 稍微有点糊涂的是,K8S 的 Service 和 Deployment 是分别声明的,那么,两者之间是什么关系呢? #### 2.1.8 Service K8S 的 Service 是一个 **网络** 概念,仅仅用于暴露服务端口。 举个例子,创建一个 Nginx Deployment ,再创建一个 Apache Deployment。两者都提供Web服务,并且,两者都共享同一个NFS静态网页存储Volume。当外界需要使用WEB服务时,访问任意Nginx或Apache都能得到服务。此时,可以将这两组Depolyment的Pod实例都作为一个服务使用80端口暴露到外部。 #### 2.1.9 Ingress Docker Swarm 中的 Ignress publish ports 配置,在K8S中被升级为独立的、可声明、可管理的对象Ingress了。 #### 2.1.10 Namespace Namespace是指 名空间。 名空间用来划分不同的应用,可以按照名空间进行应用部署、资源管理、安全认证。 Kubernetes 的系统组件在 `kube-system` 名空间中。缺省的 应用名空间 是 `default`。 可以使用 `kubectl create `来创建一个名空间。kubectl 操作的是当前名空间,可以通过 `-n ` 或 `-namespace ` 来操作该名空间的对象。对于get list类命令,可以使用 `-A` 表示全部 名空间。比如:`kubectl get deploy -A` 来获取全部名空间里的 deployments。 可以通过命令设置当前名空间: ``` $ kubectl config set-context --current --namespace=kube-system Context "minikube" modified. $ kubectl config view | grep namespace namespace: kube-system ``` Swarm没提供名空间的概念,而是通过 Stack 来实现了一组具有共同前缀的服务, 类似于名空间。 ### 2.2 Pod 相关 #### 2.2.1 Pod K8S POD 是一个用于管理其他应用容器的 "容器"。K8S 使用Pause容器 来运行并管理 POD (及其中的应用容器)。一个POD可以管理一个或多个 容器。 比如,Pod中定义一个容器: nginx,启动该Pod将创建两个container: * pause: 用于POD管理,创建Pod的网络和存储等共享资源。 * nginx:应用容器。 在节点上使用 docker ps 可以看到这两个container。 虽然可以直接声明一个POD,但通常是通过更高层的Deployment、StatefulSet(这些称之为 Workload)间接声明的。 为考察POD的基本知识,可以从 K8S 系统的 POD开始。 ``` $ kubectl get pods -A NAMESPACE NAME READY STATUS RESTARTS AGE kube-system coredns-64897985d-r8qzp 1/1 Running 0 28h kube-system etcd-minikube 1/1 Running 0 28h kube-system metrics-server-7d54f9b645-q4rs9 1/1 Running 0 68m kubernetes-dashboard kubernetes-dashboard-6f75b5c656-9jc2k 1/1 Running 0 28h ``` 上述命令列出了所有的POD。 > -A 指示 全部名空间 --all-namespace=true。 > 使用 -o wide 可显示 POD运行的 Node 信息。 查看某一个 POD 的信息: ``` $ kubectl describe pod/coredns-64897985d-r8qzp -n kube-system |more Name: coredns-64897985d-r8qzp Namespace: kube-system Node: minikube/192.168.49.2 Start Time: Tue, 03 Jan 2023 11:04:50 +0800 Labels: k8s-app=kube-dns pod-template-hash=64897985d Status: Running IP: 10.244.0.2 IPs: IP: 10.244.0.2 Controlled By: ReplicaSet/coredns-64897985d Containers: coredns: Container ID: docker://f033279baf864e1bb892c9b1823b237c56c4788a5b2cc1a05dd4d970618004d2 Image: k8s.gcr.io/coredns/coredns:v1.8.6 Image ID: docker-pullable://k8s.gcr.io/coredns/coredns@sha256:5b6ec0d6de9baaf3e92d0f66cd96a25b9edbce8716f5f15dcd1a6 16b3abd590e Ports: 53/UDP, 53/TCP, 9153/TCP Host Ports: 0/UDP, 0/TCP, 0/TCP Args: -conf /etc/coredns/Corefile Liveness: http-get http://:8080/health delay=60s timeout=5s period=10s #success=1 #failure=5 Readiness: http-get http://:8181/ready delay=0s timeout=1s period=10s #success=1 #failure=3 Environment: Mounts: /etc/coredns from config-volume (ro) /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-krjlm (ro) ``` 以上是部分信息,可见该POD 的名字,名空间,运行节点名称,Labels等信息。 还有一部分是 Containers,其中定义了一个容器: * image: k8s.gcr.io/coredns/coredns:v1.8.6。 * 容器暴露(expose)三个端口: 53/UDP,53/TCP,9153、TCP。 * Liveness/Readiness: 这个类似于Swarm里的 Health probe 了。 * Enviroment:环境变量 * Mounts: 加载的卷。 * Ports: 相当于 容器 的 expose. 可见 K8S 的 Container 信息 类似于 Docker Compose 形式。 > 这个命令类似于`docker container inspect `, 注意其参数包括 pods 表明对象类型。这与docker 命令的差别是,docker 是按照不同的对象类型组织命令: `docker inspect `,而 K8S 则使用相同的命令来 处理不同的 对象类型,这得益于其 *声明式* 管理方式(`kubectl describe `)。 可以使用底层的 Docker 命令来查看相应的container信息 ``` # 如果使用 minikube,需要先进入 minikube 容器: # docker exec -it minikube bash $ docker ps | grep coredns f033279baf86 a4ca41631cc7 "/coredns -conf /etc…" 29 hours ago Up 29 hours k8s_coredns_coredns-64897985d-r8qzp_kube-system_5d5fc278-ebbf-4a9a-a2e2-0827b4d2953d_0 8dec9f4942c3 k8s.gcr.io/pause:3.6 "/pause" 29 hours ago Up 29 hours k8s_POD_coredns-64897985d-r8qzp_kube-system_5d5fc278-ebbf-4a9a-a2e2-0827b4d2953d_0 ``` 从命令输出可见,K8S 创建了一个容器,注意其 ID 正好是 `f033279baf86`,和describe中的`Container ID: docker://f033279baf86` 一致。 另一个容器 使用 k8s.gcr.io/pause 镜像,这是 该Pod 的管理容器。 #### 2.2.2 创建Pod ```yaml # samples/ngix-pod.yaml apiVersion: v1 kind: Pod metadata: name: nginx spec: containers: - name: nginx image: nginx:alpine ports: - containerPort: 80 ``` 该文件声明了一个K8S对象: * 一个 Pod: 通过`kind: Pod`指定对象类型 * 名为`nginx`: 通过metadata.name: nginx指定 * 包含一个容器:通过spec.container 指定。 * 容器名称:nginx * 镜像名称:nginx:alpine * 端口: 暴露端口 80。 在K8S部署该Pod: ``` # 部署 pod $ kubectl apply -f samples/nginx-pod.yaml pod/nginx created # 查看该pod的事件 $ kubectl events pod/nginx LAST SEEN TYPE REASON OBJECT MESSAGE 70s Normal Scheduled Pod/nginx Successfully assigned default/nginx to minikube-m02 67s Normal Pulling Pod/nginx Pulling image "nginx:alpine" 37s Normal Pulled Pod/nginx Successfully pulled image "nginx:alpine" in 30.19601896s 36s Normal Created Pod/nginx Created container nginx 35s Normal Started Pod/nginx Started container nginx # 参看Pod状态 wangjf@DESKTOP-60J9GOH:~/mvnprojects/kube$ kubectl get pod/nginx -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx 1/1 Running 0 5m16s 10.244.1.6 minikube-m02 ``` 在events中可见,通过拉取镜像,创建容器,启动容器几个步骤后,Pod有一个实例在(`minikube-m02`)运行了。 登录到minikube-m02节点上,查看 docker 信息: ``` $ docker ps | grep nginx 6e4b75299e4a nginx "/docker-entrypoint.…" 5 minutes ago Up 5 minutes k8s_nginx_nginx_default_12d44aa9-a386-4865-bb23-df89fa4b29ff_0 6495c47b7556 k8s.gcr.io/pause:3.6 "/pause" 6 minutes ago Up 6 minutes k8s_POD_nginx_default_12d44aa9-a386-4865-bb23-df89fa4b29ff_0 ``` 可见,nginx 和对应的 pause 容器 已经运行了。 #### 2.2.3 Pod Probe 为了更好的体现Pod服务状态,为其添加Probe。 K8S支持三种Probe探针: - liveness:服务是否存活。如该探针返回失败,则Pod状态为 Failure,持续一段时间后将被调度重启。 - readiness: 服务是否就绪。如该探针返回失败,则不会向该Pod发送流量(即不对外提供服务)。 - startup:服务是否启动。设置该探针后,liveness/readniess探针无效,这个配置用于启动较慢的服务。 官方文档的这段解释很精准到位: > 如果你的应用程序对后端服务有严格的依赖性,你可以同时实现存活态和就绪态探针。 当应用程序本身是健康的,存活态探针检测通过后,就绪态探针会额外检查每个所需的后端服务是否可用。 这可以帮助你避免将流量导向只能返回错误信息的 Pod。 K8S Probe 和 Swarm 的 heal check 类似。 下面在nginx上配置探针,这里可以体现出K8S对容器管理的到位之处。Swarm的Heal check 实现方式是在 目标容器中执行命令。比如:`curl -f localhost:80/ ...`。这就需要在业务容器中配置这些 Utility。K8S 支持三种检查方式: * exec:即在目标容器执行命令,同 Swarm一致。 * httpGet: 使用http get方式访问容器。 * tcpSocket: 访问端口,如容器监听了该端口,即认为成功。 * gRPC:执行gRPC调用。 这里使用httpGet和tcpSocket两种方式: ```yaml --- spec: containers: - name: nginx image: nginx:alpine ports: - containerPort: 80 livenessProbe: httpGet: path: / port: 80 initialDelaySeconds: 2 periodSeconds: 1 readinessProbe: tcpSocket: port: 80 initialDelaySeconds: 3 periodSeconds: 1 restartPolicy: OnFailure ``` > 注意 `restartPolicy` 是 spec 的子属性,和containers平级。 删除旧Pod并重新部署pod(这是因为Pod不允许更新这些配置)。 ``` $ kubectl delete pod/nginx pod "nginx" deleted $ kubectl apply -f samples/nginx-pod.yaml pod/nginx created $ kubectl events pod/nginx LAST SEEN TYPE REASON OBJECT MESSAGE 46m Normal Scheduled Pod/nginx Successfully assigned default/nginx to minikube-m02 46m Normal Pulling Pod/nginx Pulling image "nginx:alpine" 46m Normal Pulled Pod/nginx Successfully pulled image "nginx:alpine" in 30.19601896s 46m Normal Created Pod/nginx Created container nginx 46m Normal Started Pod/nginx Started container nginx 4m51s Normal Killing Pod/nginx Stopping container nginx 4m31s Normal Scheduled Pod/nginx Successfully assigned default/nginx to minikube-m02 4m30s Normal Pulled Pod/nginx Container image "nginx:alpine" already present on machine 4m30s Normal Created Pod/nginx Created container nginx 4m29s Normal Started Pod/nginx Started container nginx ``` 来看Probe的效果: ``` $ kubectl describe pod nginx | grep ness Liveness: http-get http://:80/ delay=2s timeout=1s period=1s #success=1 #failure=3 Readiness: tcp-socket :80 delay=3s timeout=1s period=1s #success=1 #failure=3 ``` 还可以使用logs查看nginx日志: ``` # --tail=3 的意思是显示最后三行日志 $ kubectl logs pod/nginx --tail=3 10.244.1.1 - - [04/Jan/2023:12:54:50 +0000] "GET / HTTP/1.1" 200 615 "-" "kube-probe/1.23" "-" 10.244.1.1 - - [04/Jan/2023:12:54:51 +0000] "GET / HTTP/1.1" 200 615 "-" "kube-probe/1.23" "-" 10.244.1.1 - - [04/Jan/2023:12:54:52 +0000] "GET / HTTP/1.1" 200 615 "-" "kube-probe/1.23" "-" ``` 可见,每秒钟都访问了一次。 HttpGet方式的Health check为应用容器管理提供了很多方便。 #### 2.2.4 发布服务 此时的nginx容器尚不能对外提供服务,需要为其配置一个网络服务来对外暴露服务。 K8S 的Service采用声明式管理,因此,需要单独创建一个 Service,并为该Service指定其"使用的" POD。 ```yaml apiVersion: 1 # samples/ngix-service.yaml apiVersion: v1 kind: Service metadata: name: web-service spec: type: NodePort selector: app/name: nginx ports: - protocol: TCP nodePort: 31001 port: 8080 targetPort: 80 ``` 这个service里面仅包括一个selector 和 一个 ports。 ports用来控制集群外暴露端口、集群内服务端口和容器端口。 * nodePort: 集群外端口,在Node上开放的监听端口。K8S对NodePort范围约定为30000-32767。 * port:集群内部端口,仅用于内部服务。配合ClusterIP可访问该服务。 * targetPort: 容器expose的端口号。 部署该服务: ``` $ kubectl apply -f samples/nginx-service.yaml service web-service created $ kubectl describe service/web-service Name: web-service Namespace: default Labels: Annotations: Selector: app/name=nginx Type: NodePort IP Family Policy: SingleStack IP Families: IPv4 IP: 10.98.134.132 IPs: 10.98.134.132 Port: 8080/TCP TargetPort: 80/TCP NodePort: 31001/TCP Endpoints: Session Affinity: None External Traffic Policy: Cluster Events: ``` 注意其中的`Endpoints `,这表明,该服务尚未接入任何Endpoint,也就是说,这个服务虽然绑定了31001端口,但并不知道要将该流量转发到哪里。 Service如何确定Endpoint呢?答案是使用**selector**来选择Pod。上文有`selector app/name=nginx`, 这就定义了一个selector,app/name是label,因此,web-service 将选中所有 labels中包含`app/name=nginx` 的pod。 目前 `Endpoints ` 当然因为没有选中符合条件的 Pod。 因此,需要给Pod添加Labels。 ```yaml metadata: name: nginx labels: app/name: nginx ``` 更新Pod: ``` $ kubectl apply -f samples/nginx-pod.yaml pod/nginx configured $ kubectl get pod/nginx -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx 1/1 Running 0 33m 10.244.1.8 minikube-m02 $ kubectl describe service/web-service | grep Endpoint Endpoints: 10.244.1.8:80 ``` Pod nginx的IP 是 10.244.1.8,expose端口为80,而web-service 的 EndPoints 中包含了 10.244.1.8:80,这就说明 service 已经选中了 Pod。 ``` $ curl lcoalhost:31001 ...

Thank you for using nginx.

... ``` 如果使用minikube需要先登入容器 ``` $ docker exec -it minikube bash ``` 或者,先参看minikube容器的ip。 ``` $ docker network inspect minikube -f "{{json .Containers }}" | jq { "d3113c438e41c9196b3d3513f7e938d9f1c5d39c0fbca70600ddf08b34cc564a": { "Name": "minikube-m02", "EndpointID": "08da4a1895268ef52bf48d09af81c23bd786a4c9e859b3fa9290df083bbd5893", "MacAddress": "02:42:c0:a8:31:03", "IPv4Address": "192.168.49.3/24", "IPv6Address": "" }, "f89bc797ed5cb3cef55e14974c38e8d01609deed0c0df68c2368f7bc4ff19f8d": { "Name": "minikube", "EndpointID": "1b512b7ab74e24ed7ba7834e8ec961955e7a092ecfb0ab9302792ee3efd67dfe", "MacAddress": "02:42:c0:a8:31:02", "IPv4Address": "192.168.49.2/24", "IPv6Address": "" } } ``` 可见两个minikube节点容器的IP 分别是 192.168.49.2 和 .3。 ``` $ curl 192.168.49.3:31001 ...

Thank you for using nginx.

... $ curl 192.168.49.2:31001 ...

Thank you for using nginx.

... ``` 访问任意一个node的31001端口,都可以访问到web-service并最终访问到pod nginx。这和Docker Swarm的效果是一致的。 #### 2.2.5 Pod多个副本 使用Deployment来创建多副本POD集合(ReplicaSet)。 ```yaml # samples/ngix-deployment.yaml apiVersion: v1 kind: Deployment metadata: name: nginx-rs spec: replicas: 3 selector: matchLabels: app/name: nginx ``` 上文定义了一个Deployment,名字为 nginx-rs。其spec中包含: * replicas:副本数量3。 * selector: 选择器,这里定义的是 label 选择器,选择标签是 app/name=nginx 的 Pod。 那么,这个ReplicaSet中的Pod如何定义呢?使用Pod Template来定义,顾名思义,模板就是指用来创建一系列Pod副本的template。 直接将 `nginx-pod.yaml`中的信息复制到 template 下: ```yaml template: metadata: name: nginx labels: app/name: nginx spec: containers: - name: nginx image: nginx:alpine ports: - containerPort: 80 livenessProbe: httpGet: path: / port: 80 initialDelaySeconds: 2 periodSeconds: 30 # restartPolicy: OnFailure ``` 使用 kubectl 部署: ``` $ kubectl apply -f samples/nginx-deployment.yaml deployment.apps/nginx-rs created $ kubectl get deploy/nginx-rs -o wide NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR nginx-rs 3/3 3 3 31s nginx nginx:alpine app/name=nginx $ kubectl describe deploy/nginx-rs Name: nginx-rs Namespace: default CreationTimestamp: Thu, 05 Jan 2023 11:55:47 +0800 Labels: Annotations: deployment.kubernetes.io/revision: 1 Selector: app/name=nginx Replicas: 3 desired | 3 updated | 3 total | 3 available | 0 unavailable StrategyType: RollingUpdate MinReadySeconds: 0 RollingUpdateStrategy: 25% max unavailable, 25% max surge Pod Template: ... Conditions: ... OldReplicaSets: NewReplicaSet: nginx-rs-7fddddcdb8 (3/3 replicas created) Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal ScalingReplicaSet 2m9s deployment-controller Scaled up replica set nginx-rs-7fddddcdb8 to 3 ``` 可见,deployment 实质上是创建了一个 ReplicaSet: ``` $ kubectl get rs/nginx-rs-7fddddcdb8 -o wide NAME DESIRED CURRENT READY AGE CONTAINERS IMAGES SELECTOR nginx-rs-7fddddcdb8 3 3 3 4m30s nginx nginx:alpine app/name=nginx,pod-template-hash=7fddddcdb8 # 再看对应的 pod(使用label选择 pod) $ kubectl get pod -l app/name=nginx -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx 1/1 Running 0 114m 10.244.1.8 minikube-m02 nginx-rs-7fddddcdb8-f2ct7 1/1 Running 0 5m52s 10.244.1.9 minikube-m02 nginx-rs-7fddddcdb8-vns9f 1/1 Running 0 5m52s 10.244.1.10 minikube-m02 nginx-rs-7fddddcdb8-zlb2t 1/1 Running 0 5m52s 10.244.0.3 minikube ``` 注意这里也包括了 之前单独创建的 pod。 那么,web-service是否也会匹配这几个pod做为Endpoints呢? ``` $ kubectl describe service/web-service | grep Endpoint Endpoints: 10.244.0.3:80,10.244.1.10:80,10.244.1.8:80 + 1 more... ``` 由此可以体会到 Service 基于 selector 匹配的强大之处。 ReplicaSet支持缩放,包括自动缩放。这里演示如何进行缩放。 为跟踪缩放过程,打开一个窗口,执行: ``` $ kubectl get pod -w -l app/name=nginx NAME READY STATUS RESTARTS AGE nginx-rs-7fddddcdb8-8prpj 1/1 Running 0 79m nginx-rs-7fddddcdb8-zlb2t 1/1 Running 0 3h29m ``` 该命令会一直跟踪nginx pod 的执行情况。 另一个窗口进行缩放: ``` $ kubectl scale deployment/nginx-rs --replicas=5 deployment.apps/nginx-rs scaled wangjf@DESKTOP-60J9GOH:~/mvnprojects/kube$ kubectl get deploy/nginx-rs NAME READY UP-TO-DATE AVAILABLE AGE nginx-rs 5/5 5 5 130m ``` 此时,第一个窗口信息大概是: ``` $ kubectl get pod -w -l app/name=nginx NAME READY STATUS RESTARTS AGE nginx-rs-7fddddcdb8-5gxq4 1/1 Running 0 2m46s nginx-rs-7fddddcdb8-8prpj 1/1 Running 0 82m nginx-rs-7fddddcdb8-zlb2t 1/1 Running 0 3h32m nginx-rs-7fddddcdb8-dgb8s 0/1 Pending 0 0s nginx-rs-7fddddcdb8-vfn2g 0/1 Pending 0 0s nginx-rs-7fddddcdb8-dgb8s 0/1 ContainerCreating 0 0s nginx-rs-7fddddcdb8-vfn2g 0/1 ContainerCreating 0 0s nginx-rs-7fddddcdb8-dgb8s 1/1 Running 0 3s nginx-rs-7fddddcdb8-vfn2g 1/1 Running 0 3s ``` 可见,原有3个副本,scale时,先创建两个Pod,再创建Pod容器,再启动容器。 再将其缩减为 2 个副本: ``` $ kubectl scale deployment/nginx-rs --replicas=2 deployment.apps/nginx-rs scaled ``` 第一个窗口输出: ``` nginx-rs-7fddddcdb8-vfn2g 1/1 Terminating 0 2m15s nginx-rs-7fddddcdb8-5gxq4 1/1 Terminating 0 5m7s nginx-rs-7fddddcdb8-vfn2g 0/1 Terminating 0 2m16s nginx-rs-7fddddcdb8-5gxq4 0/1 Terminating 0 5m9s nginx-rs-7fddddcdb8-dgb8s 0/1 Terminating 0 2m17s ``` 删除了三个Pod。 当然也可以通过修改yaml文件,将replicas 改为 5、2 进行缩放。 #### 2.2.6 小结 通过对 Pod、Service、Deployment 的简单介绍。可以对K8S基本概念有初步的认识。 K8S底层基于容器技术实现(containerd 而非局限于docker),因此,通过与Docker Swarm的简要对比可以帮助简化概念、加强理解。 ### 2.3 卷 Volume #### 2.3.1 对比 K8S 支持卷作为 容器存储。当然,Docker也支持,但与K8S相比,Docker的Volume过于简陋了。 K8S 对 Docker Volume 的最大改进是支持动态分配(PVC):即当Pod创建时进行按需分配,并可依据Pod要求保持、复用其数据(StatefulSet)。 #### 2.3.2 K8S 卷类型 K8S官方文档对卷的分类比较模糊,这里按照用途进行了归并: * 临时卷:随POD创建、消亡的临时存储,如:emptyDir,可用于映射内存临时存储。 * 投射卷:Projection,Kube内部使用它用于挂载密钥,host、resolver,config等数据文件。projection 的特点是可以将多个卷/文件 `投射`到一个 卷 目录下。 * 持久卷:这是应用经常使用的卷。持久卷可以使用Provisioner 供应者 进行动态制备,大部分Provisioner里会作为服务组件部署在K8S集群中,供应用使用。 #### 2.3.2 K8S 卷驱动 卷驱动driver是 Docker 的概念,K8S里对应的是 Provisioner 供应者。 其他的存储比如:NFS、Ceph以及各大云供应商提供存储,一般是网络存储了,则比较适合 StatefulSet 应用。 此类存储有两种方式引入,一种是使用K8S自带的(in-tree)驱动,另一种是使用CSI 驱动。从趋势上看,K8S会逐步放弃in-tree驱动,一方面减少K8S社区压力,另一方面第三方厂家提供的CSI驱动从性能、兼容性、更新频度几方面都是优于in-tree的,再有,CSI是推行中的规范。 各大云厂商均会提供动态制备的卷驱动。 #### 2.3.3 Local 卷 Local卷使用节点的本地目录作为卷,因此这种卷很简单,它的问题也很明显:不会在节点之间迁移,因此,可以用作 无状态应用的卷。 > Loacl 卷 相当于 Docker 中的 olume。 > 还有一个 HostPath卷,相当于 Docker 中的 bind Vloume,但不建议在集群中使用。 K8S使用StorageClass 术语来描述存储卷的分类,分类里会引用provisioner。官方推荐使用storageClass来区分不同类型的存储,这里使用官方的例子,首先建立一个storage class。 ```yaml # samples/local-storage-class.yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: local-storage provisioner: kubernetes.io/no-provisioner volumeBindingMode: WaitForFirstConsumer ``` 上文声明了一个 local-storage。no-provisioner 表示不需要使用供应商,这也意味着不能用于动态创建卷。 ``` $ kubectl apply -f samples/local-storage-class.yaml storageclass.storage.k8s.io/local-storage created $ kubectl get sc NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE local-storage kubernetes.io/no-provisioner Delete WaitForFirstConsumer false 95s standard (default) k8s.io/minikube-hostpath Delete Immediate false 2d10h ``` 这里的 kubectl get sc , sc是 storageclsss 的简写形式。 local-storage 创建后,就可以创建一个Local持久卷: ```yaml apiVersion: v1 kind: PersistentVolume metadata: name: local-pv labels: storage/type: local spec: storageClassName: local-storage capacity: storage: 100Mi accessModes: - ReadWriteOnce local: path: "/var/data/local" nodeAffinity: required: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - minikube ``` Local 卷依赖节点本地文件系统,因此,在定义时需要限定某一个节点上,同样,使用Local卷的Pod,也会被分配到该节点上。 当然,也可以在每个节点都建立相同目录。 在K8S中部署: ``` $ kubectl apply -f samples/local-pv.yaml persistentvolume/local-pv-volume created wangjf@DESKTOP-60J9GOH:~/mvnprojects/kube$ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE local-pv 100Mi RWO Retain Available local-storage 5s ``` `get pv` 中的 pv 是 persistent volume 的缩写。 注意,StorageClass定义中使用了 `WaitForFirstConsumer`,因此,此时pv状态并不是 BOUND,当第一个用户请求卷时,才会被绑定。 卷声明后,再声明一组持久存储申请对象(PVC,Persistent Volume Claim)。PVC描述了Pod在持久卷里申请存储空间的方法。 ``` # samples/local-pvc.yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: local-pvc spec: storageClassName: local-storage volumeName: local-pv accessModes: - ReadWriteOnce resources: requests: storage: 10Mi ``` 注意,这里通过 `volumeName: local-pv` 指定了从local-pv中申请。如果不指定,K8S会依据 StorageClass和容量需求来分配一个卷(如果存在多个卷的话)。 ``` $ kubectl apply -f samples/local-pvc.yaml persistentvolumeclaim/local-pvc created $ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE local-pvc Bound local-pv 100Mi RWO local-storage 3m34s $ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE local-pv 100Mi RWO Retain Bound default/local-pvc local-storage 3m45s ``` PVC建立后,与local-pv卷绑定起来了。 再通过PVC将卷挂载到Pod上。修改nginx deployment 即可(本文使用了新的文件nginx-ls-deployment.yaml): ``` spec: volumes: - name: local-path persistentVolumeClaim: claimName: local-pvc containers: ... volumeMounts: - mountPath: "/usr/share/nginx/html/local" name: local-path ``` 在 pod template中添加了PVC,并在容器中挂载。挂载路径为 nginx html 目录下的 local。 部署该Deployment: ``` $ kubectl apply -f samples/nginx-ls-deployment.yaml deployment.apps/nginx-rs created $ kubectl get pods -l app/name=nginx -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx-rs-6d49fbb644-62nsr 1/1 Running 0 17s 10.244.0.6 minikube nginx-rs-6d49fbb644-pqn4r 1/1 Running 0 17s 10.244.0.7 minikube nginx-rs-6d49fbb644-sbc5b 1/1 Running 0 17s 10.244.0.8 minikube ``` 注意,这时已经将全部副本都调度到 minikube节点了(因为 Local 卷 使用了这个节点)。 可以登入minikube 检查卷挂载情况: ``` $ docker exec -it minikube bash root@minikube:/# ls /var/data/local root@minikube:/# echo "Hello minikube!" > /var/data/local/index.html root@minikube:/# curl localhost:31001/local/ Hello minikube! ``` 再查看 pvc的使用情况: ``` $ kubectl describe pvc local-pvc Name: local-pvc Namespace: default StorageClass: local-storage Status: Bound Volume: local-pv Labels: Annotations: pv.kubernetes.io/bind-completed: yes Finalizers: [kubernetes.io/pvc-protection] Capacity: 100Mi Access Modes: RWO VolumeMode: Filesystem Used By: nginx-rs-6d49fbb644-62nsr nginx-rs-6d49fbb644-pqn4r nginx-rs-6d49fbb644-sbc5b ``` 可见三个Pod在使用。 > 可以使用 subPathExpression来为 Pod 分配单独的 目录。 可以注意到,每个Pod都挂载同一个目录,这种行为和Docker 的 Volume 很接近了。 #### 2.3.3 使用 NFS 卷 NFS 存储是较容易搭建环境的,因此,这里以 NFS 为例。 首先建立NFS存储环境。 ``` $ sudo apt install nfs-kernel-server -y # 查看服务状态 $ systemctl status nfs-kernel-server ● nfs-server.service - NFS server and services Loaded: loaded (/lib/systemd/system/nfs-server.service; enabled; vendor preset: enabled) Active: active (exited) since Thu 2023-01-05 20:21:12 CST; 1min 23s ago Main PID: 1970339 (code=exited, status=0/SUCCESS) Tasks: 0 (limit: 3488) CGroup: /system.slice/nfs-server.service Jan 05 20:21:11 DESKTOP-60J9GOH systemd[1]: Starting NFS server and services... Jan 05 20:21:12 DESKTOP-60J9GOH systemd[1]: Started NFS server and services. # ``` 建立目录/var/data/nfs 目录作为NFS共享目录。并在/etc/exports下添加定义: ``` $ sudo mkdir -p /var/data/nfs $ sudo chmod 777 /var/data/nfs $ sudo vi /etc/exports # 添加目录 /var/data/nfs *(rw,sync,no_subtree_check) #重新加载导出配置 $ sudo exportfs -ar $ showmount -e Export list for DESKTOP-60J9GOH: /var/data/nfs * ``` 至此配置完成,已经建立了NFS的共享目录。 同样的,使用 storageClass 来标志 NFS 卷。 这里将全部nfs卷定义放在一个文件`samples/nfs-volume.yaml`里来定义。 ```yaml apiVersion: v1 kind: PersistentVolume metadata: name: nfs-pv labels: storage/type: nfs spec: storageClassName: nfs-static-storage capacity: storage: 200Mi accessModes: - ReadWriteOnce nfs: path: "/var/data/nfs" server: 172.21.57.232 readOnly: false ``` 这里配置了 nfs 的共享路径和server 地址。 将nfs配置在 K8S中部署: ``` $ kubectl apply -f samples/nfs-volume.yaml storageclass.storage.k8s.io/nfs-static-storage created persistentvolume/nfs-pv created persistentvolumeclaim/nfs-pvc created wangjf@DESKTOP-60J9GOH:~/mvnprojects/kube$ kubectl get sc NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE local-storage kubernetes.io/no-provisioner Delete WaitForFirstConsumer false 15h nfs-static-storage kubernetes.io/no-provisioner Delete WaitForFirstConsumer false 10s standard (default) k8s.io/minikube-hostpath Delete Immediate false 3d1h wangjf@DESKTOP-60J9GOH:~/mvnprojects/kube$ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE local-pv 100Mi RWO Retain Bound default/local-pvc local-storage 97m nfs-pv 200Mi RWO Retain Available nfs-static-storage 13s wangjf@DESKTOP-60J9GOH:~/mvnprojects/kube$ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE local-pvc Bound local-pv 100Mi RWO local-storage 97m nfs-pvc Pending nfs-static-storage 22s ``` 在Deployment 中添加 nfs Mount 信息: ```yaml volumes: - name: nfs-path persistentVolumeClaim: claimName: nfs-pvc ... containers: ... volumeMounts: - mountPath: "/usr/share/nginx/html/nfs" name: nfs-path ``` 添加了nfs卷。重新部署: ``` $ kubectl apply -f samples/nginx-nfs-deployment.yaml deployment.apps/nginx-rs configured # 再看pvc状态: $ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE local-pvc Bound local-pv 100Mi RWO local-storage 170m nfs-pvc Bound nfs-pv 200Mi RWO nfs-static-storage 72m ``` 如使用 `kubectl get pods -w `则会发现原Pod都被删除并启动了新的Pod。 同样,在nfs的共享目录下添加一个文件: ``` $ echo "Hello NFS Volume!" > /var/data/nfs/index.html $ curl 192.168.49.3:31001/nfs/ Hello NFS Volume! ``` 可见已经将nfs 卷 mount到 pod了。 再看nfs卷挂载情况: ``` $ docker exec minikube nfsstat -m /var/lib/kubelet/pods/ada94928-f965-44f0-aa76-42e3702bd8d4/volumes/kubernetes.io~nfs/nfs-pv from 172.21.57.232:/var/data/nfs Flags: rw,relatime,vers=4.1,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=192.168.49.2,local_lock=none,addr=172.21.57.232 /var/lib/kubelet/pods/f951a38b-0093-4598-a75d-2bba197145c9/volumes/kubernetes.io~nfs/nfs-pv from 172.21.57.232:/var/data/nfs Flags: rw,relatime,vers=4.1,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=192.168.49.2,local_lock=none,addr=172.21.57.232 /var/lib/kubelet/pods/f29eeaf5-6d92-4089-9bf1-9f5ef8d2cc83/volumes/kubernetes.io~nfs/nfs-pv from 172.21.57.232:/var/data/nfs Flags: rw,relatime,vers=4.1,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=192.168.49.2,local_lock=none,addr=172.21.57.232 ``` 三个 Pod 分别挂载了nfs-pv。 关于持久卷的动态制备,将结合StatefulSet来介绍。 ### 2.4 Ingress K8S内部使用DNS为每一个对象(Pod、Service等)提供域名服务,因此,集群内部可以使用稳定的域名来访问内部服务。 K8S对外的入口可以使用Service(上文有简要介绍),也可以使用Ingress暴露端口。 #### 2.4.1 Ingress及Ingress Controller Ingress 是入口的意思。在Swarm中,Ingress是用于集群内服务发现的overlay网络服务,而K8S的Ingress侧重于外部对集群访问的入口控制。 K8S通过Service对象暴露端口,可以供外部访问,但其端口是随机的,即使人工指定,也限定在30000范围端口。同时,多个服务需要对外暴露多个端口,这对外部负载均衡配置是有点繁琐的,在微服务架构下,这一点更为明显。 我们知道,微服务可以使用网关服务(Gateway)来代理内部微服务,将不同的请求 rewrite 到 不同的 服务上。 K8S的Ingress提供了类似的能力。典型的 Ingress 模式如下图: ```mermaid graph LR c([Web Client]) subgraph k8s[K8S Cluster] ig[Ingress] svc[Service] p1[Pod] p2[Pod] svc2[Service] p3[Pod] p4[Pod] end c --Ingress Loadbalancer--> ig -- Route Rules --> svc svc --> p1 svc --> p2 ig -- Route Rules --> svc2 svc2 --> p3 svc2 --> p4 ``` 先来看一个官方ingress示例声明: ``` apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: ingress-wildcard-host spec: rules: - host: "foo.bar.com" http: paths: - pathType: Prefix path: "/bar" backend: service: name: service1 port: number: 80 - host: "*.foo.com" http: paths: - pathType: Prefix path: "/foo" backend: service: name: service2 port: number: 80 ``` rules中定义了两个路由规则: - host=foo.bar.com path=/bar:转发至backend 服务 service1。 - host=*.foo.com path=/foo:转发到service2 。 这个路由规则和Spring Gateway很相似,host匹配header中的 'Host', pathType Prefix 匹配 请求URL的前缀。一旦匹配,就将请求转发至相应的服务。 当然也可以不匹配 host, 仅使用path做匹配,此时不写host属性即可。 注意这里的 backend.service.port,是指Service的内部端口(因为一个Service可以配置多个端口的)。 Ingress仅支持 HTTP/HTTPS,Ingress 默认使用80和443标准端口号,这个端口号无法在ingress声明中配置,只能依赖于Ingress Controller。 #### 2.4.2 Ingress Controller Ingress控制器是完成ingress处理的服务,可以使用多种IngressControler,典型的控制器是 Nginx Ingress Controller(Nginx强大的无法回避)。 不同的Ingress 控制器,通过 Ingress Class 声明和引用。IngressClass 类似于 StorageClass,也仅仅是一个分类标识名称。 ``` $ kubectl get ingressClass NAME CONTROLLER PARAMETERS AGE nginx k8s.io/ingress-nginx 14h ``` 可以查看到 ingress class 有 nginx。 *注意*: > Nginx ingress 在 minikube 需要通过 addons启动。 ``` $ minikube addons enable ingress ``` 查看 nginx的配置: ``` $ kubectl describe ingressclass nginx Name: nginx Labels: app.kubernetes.io/component=controller app.kubernetes.io/instance=ingress-nginx app.kubernetes.io/name=ingress-nginx Annotations: ingressclass.kubernetes.io/is-default-class: true Controller: k8s.io/ingress-nginx Events: ``` 注意这里设置了is-default-class: true的注解,该注解表示nginx是默认的Ingress Class。 ``` $ kubectl get deploy -A -o wide | grep ingress-nginx ingress-nginx ingress-nginx-controller 1/1 1 1 15h controller k8s.gcr.io/ingress-nginx/controller:v1.2.1 app.kubernetes.io/component=controller,app.kubernetes.io/instance=ingress-nginx,app.kubernetes.io/name=ingress-nginx $ kubectl get deploy -n ingress-nginx NAME READY UP-TO-DATE AVAILABLE AGE ingress-nginx-controller 1/1 1 1 15h $ kubectl describe deploy ingress-nginx-controller -n ingress-nginx Name: ingress-nginx-controller Namespace: ingress-nginx ... Replicas: 1 desired | 1 updated | 1 total | 1 available | 0 unavailable StrategyType: RollingUpdate Pod Template: ... Service Account: ingress-nginx Containers: controller: Image: k8s.gcr.io/ingress-nginx/controller:v1.2.1 Ports: 80/TCP, 443/TCP, 8443/TCP Host Ports: 80/TCP, 443/TCP, 0/TCP Args: /nginx-ingress-controller ... ``` 可见ingress nginx Controller开放的端口(Host Ports)是 80, 443 端口。 nginx controller 使用 Nginx 进行反向代理,这样就比较容易理解 ingress 的实现方式了,无非是将 路由规则rules 内容转换成 Nginx 的 相应upstream配置。 #### 2.4.3 Ingress 示例 使用 nginx ingressClass 来创建一个ingress: ```yaml # samples/example-ingress.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: example-ingress spec: ingressClassName: nginx rules: - host: "s1.example.net" http: paths: - pathType: Prefix path: "/" backend: service: name: web-service port: number: 80 - host: "*.example.net" http: paths: - pathType: Prefix path: "/nfs" backend: service: name: web-service port: number: 80 ``` 这里定义了两个路由规则,一个是`s1.example.net/` 转发至 web-service。另一个是 `*.example.net/nfs/` 转发至 web-service。 部署ingress: ``` $ kubectl apply -f samples/example-ingress.yaml ingress.networking.k8s.io/example-ingress created $ kubectl get ingress NAME CLASS HOSTS ADDRESS PORTS AGE example-ingress nginx s1.example.net,*.example.net 192.168.49.2 80 8m40s ``` 域名可以在本机 /etc/hosts上配置,IP指向节点IP或ClusterIP。 ``` 192.168.49.2 s1.example.net 192.168.49.2 s2.example.net ``` 使用curl 进行测试: ``` $ curl s1.example.net ... Welcome to nginx! ... $ curl s1.example.net/local/ Hello minikube! $ curl s1.example.net/nfs/ Hello NFS Volume! $ curl s2.example.net/nfs/ Hello NFS Volume! $ curl s2.example.net/local/

404 Not Found

$ curl 192.168.49.2/

404 Not Found

``` 这样可以验证转发的效果了。 但要注意,ingress-nginx 仅做转发,没有做像 Spring gateway 那样的 URL rewite。Ingress的目的是提供多服务集成入口。所以,通常使用 host 虚拟主机方式转发即可。 ### 2.5 集群内 DNS K8S 内部使用DNS 服务,并为所有的Pod 提供 DNS解析。 Pod 和 Service 都具有DNS名称,其中 Service 的DNS名称是稳定的,而POD的名称并不稳定。 K8S会为每个POD 生成一个 resolv.conf 来指定其DNS服务。如: ``` $ kubectl get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx-rs-5697d79d5d-5r8l7 1/1 Running 0 24h 10.244.0.10 minikube nginx-rs-5697d79d5d-g4xq8 1/1 Running 0 24h 10.244.0.11 minikube nginx-rs-5697d79d5d-tn9sw 1/1 Running 0 24h 10.244.0.9 minikube # 选择一个 pod 访问其 resolv.conf 文件: $ kubectl exec nginx-rs-5697d79d5d-5r8l7 -- cat /etc/resolv.conf nameserver 10.96.0.10 search default.svc.cluster.local svc.cluster.local cluster.local options ndots:5 ``` dns 搜索路径为 三个层次, - cluster.local: 这是集群的域名。 - svc.cluster.local: 这是 Service 的使用的域名。 - default: 这是Pod所在名空间名字。 Pod 的域名是 `IP地址.namespace.pod.集群域名`。本例中 Pod Ip是 10.244.0.10,将其转换为 - 分割方式: ``` 10-244-0-10.default.pod.cluster.local 10-244-0-11.default.pod.cluster.local 10-244-0-9.default.pod.cluster.local ``` 使用 nslookup 来访问其域名: ``` $ kubectl exec nginx-rs-5697d79d5d-5r8l7 -- nslookup 10-244-0-9.default.pod.cluster.local Server: 10.96.0.10 Address: 10.96.0.10:53 Name: 10-244-0-9.default.pod.cluster.local Address: 10.244.0.9 ``` 如果Pod绑定了Service,则可以从另一个域名来访问: ``` # IP地址.服务名.namespace.svc.集群域名 $ kubectl exec nginx-rs-5697d79d5d-5r8l7 -- nslookup \ 10-244-0-9.web-service.default.svc.cluster.l ocal Server: 10.96.0.10 Address: 10.96.0.10:53 Name: 10-244-0-9.web-service.default.svc.cluster.local Address: 10.244.0.9 # 查询所有Pod $ kubectl exec nginx-rs-5697d79d5d-5r8l7 -- nslookup *.web-service.default.svc.cluster.local Server: 10.96.0.10 Address: 10.96.0.10:53 Name: *.web-service.default.svc.cluster.local Address: 10.244.0.10 Name: *.web-service.default.svc.cluster.local Address: 10.244.0.9 Name: *.web-service.default.svc.cluster.local Address: 10.244.0.11 ``` 而服务的 DNS 就是 `服务名.namespace.svc.集群域名` : ``` $ kubectl exec nginx-rs-5697d79d5d-5r8l7 -- nslookup web-service.default.svc.cluster.local Server: 10.96.0.10 Address: 10.96.0.10:53 Name: web-service.default.svc.cluster.local Address: 10.98.134.132 # 简写 域名 匹配 search list 也是可以的: $ kubectl exec nginx-rs-5697d79d5d-5r8l7 -- nslookup -d web-service Server: 10.96.0.10 Address: 10.96.0.10:53 Query #0 completed in 6ms: authoritative answer: Name: web-service.default.svc.cluster.local Address: 10.98.134.132 ``` 因此,如果在Pod中需要访问Service,同一个名空间的情况下,只需要使用service name,不同名空间的,添加 serviceName.namespace 就可以了。 > 与Swarm相比,Swarm允许参数化定制 Container 的 hostname来访问服务,K8S则不允许,要求使用Service来暴露服务,如需要给Pod分配稳定的DNS名字,需要使用StatefulSet。 ### 2.6 ConfigMap Docker 提供 Config 可以进行 配置类数据的管理,K8S 使用 ConfigMap 来提供此类功能。 ConfigMap 实质上是把文件内容保存起来,并可以在Pod中使用。常见的用途是用于应用的配置文件。 #### 2.6.1 声明 ConfigMap ConfigMap同样使用YAML文件声明: ```yaml apiVersion: v1 kind: ConfigMap metadata: name: nginx-files data: sample-env.txt: | TYPE=nginx STR=normalText # 类文件键 config.html: |

Hello ConfigMap!!

``` 同样使用 apply 来创建ConfigMap: ``` $ kubectl apply -f samples/config/nginx-files-config.yaml configmap/nginx-files created $ kubectl get configmap NAME DATA AGE kube-root-ca.crt 1 4d4h nginx-files 2 16s $ kubectl describe configmap nginx-files Name: nginx-files Namespace: default Labels: Annotations: Data ==== config.html: ----

Hello ConfigMap!!

sample-env.txt: ---- TYPE=nginx STR=normalText BinaryData ==== Events: ``` 当然也可以直接使用文件来创建ConfigMap: ``` kubectl create configmap nginx-files2 \ --from-file=samples/config/sample-env.txt \ --from-file=samples/config/config.html configmap/nginx-files2 created $ kubectl describe configmap nginx-files2 Name: nginx-files2 Namespace: default Labels: Annotations: Data ==== sample-env.txt: ---- TYPE=nginx STR=normalText config.html: ----

Hello ConfigMap!!

``` 这两种方式的内容是相同。 #### 2.6.2 Pod 挂载 ConfigMap 文件 Pod可以将 ConfigMap 做为文件挂载,使用方法和卷类似。 ```yaml # samples/ngix-config-deployment.yaml spec: volumes: - name: config-html configMap: name: nginx-files items: - key: config.html path: index.html ... containers: ... volumeMounts: - mountPath: "/usr/share/nginx/html/config" name: config-html ``` 上文使用 `nginx-files` ConfigMap 中的 `key: config.html` 内容作为文件`path: index.html`。 该文件mount到 "/usr/share/nginx/html/config"。 部署deployment: ``` $ kubectl apply -f samples/nginx-config-deployment.yaml deployment.apps/nginx-rs configured $ curl s1.example.net/config/

Hello ConfigMap!!

``` 可见ConfigMap已经挂载好了。 #### 2.6.3 Pod 引用 ConfigMap 变量 Pod内的Container可以使用环境变量,在PodTemplate中可以直接定义变量,也可以引用ConfigMap。 环境变量可以单独写文件中,形如: ```yaml TYPE=nginx STR=normalText ``` 这里可以使用 `#` 作为行注释。 使用命令参数 `--from-env-file` 创建 configMap: ``` $ kubectl create cm nginx-envs --from-env-file=samples/config/sample-env.txt configmap/nginx-envs created $ kubectl describe cm nginx-envs Name: nginx-envs Namespace: default Labels: Annotations: Data ==== STR: ---- normalText TYPE: ---- nginx ``` 这样就可以在Pod container 中引用变量: ``` yaml containers: - name: nginx image: nginx:alpine ports: - containerPort: 80 env: # 1 - name: CM_SOME_ENV value: SOME_VALUE - name: CM_SAMPLE_ENV_TXT # 2 valueFrom: configMapKeyRef: name: nginx-envs key: STR - name: CM_VAR # 3 value: "CM_INCLUDE_$(CM_SOME_ENV)_VALUE" - name: CM_UNDEFINE_ENV valueFrom: configMapKeyRef: name: nginx-envs key: undefined # 4 optional: true # 5 envFrom: # 6 - prefix: CM_ configMapRef: name: nginx-envs ``` 上文表现了环境变量定义的各种方法: 1. 使用 **name/value** 直接定义。 2. 使用 **CongfigMapKeyRef** 来引用环境变量值。 3. 使用 **$(VAR_NAME)** 引用其他变量值。 4. 使用 **optional = true** 来定义可选变量,避免因configMap或 configMap Key 不存在时 出现错误。 5. 使用 **envFrom** 将整个ConfigMap 中的 Key/Value 全部作为环境变量。 6. 使用 **prefix: CM_** 在ConfigMap的 key 前附加前缀'CM_'。 > 注意:修改环境变量会导致 所有 pod 重新创建。 ``` $ kubectl apply -f samples/nginx-config-deployment.yaml deployment.apps/nginx-rs configured $ kubectl get pods NAME READY STATUS RESTARTS AGE nginx-rs-d6774464b-6xbwc 1/1 Running 0 5m3s nginx-rs-d6774464b-fr5zb 1/1 Running 0 5m6s nginx-rs-d6774464b-gl2tp 1/1 Running 0 5m10s # 选择一个 参看环境变量 $ kubectl describe pod nginx-rs-7f7fd8c66c-9599c | grep CM_ nginx-envs ConfigMap with prefix 'CM_' Optional: false CM_SOME_ENV: SOME_VALUE CM_SAMPLE_ENV_TXT: Optional: false CM_VAR: CM_INCLUDE_$(CM_SOME_ENV)_VALUE CM_UNDEFINE_ENV: Optional: true # 查看实际取值: $ kubectl exec nginx-rs-7f7fd8c66c-9599c -- sh -c set | grep CM_ CM_SAMPLE_ENV_TXT='normalText' CM_SOME_ENV='SOME_VALUE' CM_STR='normalText' CM_TYPE='nginx' CM_VAR='CM_INCLUDE_SOME_VALUE_VALUE' ``` ### 2.7 Secret ### 2.8 StatefulSet StatefulSet 是和 Stateless相对的 ReplicaSet,Stateful 是指每个副本都需要保持其状态,这里的状态包括: * 唯一:hostname 和 dns 域名是稳定、唯一的。命名使用 spec.metadata.name + 序号 (0, 1,2, ...)。 * 粘性:使用的卷会按照副本序号保持,并在副本更新、重建时,根据副本序号重新绑定之前的卷。 * 有序:副本的启动顺序严格按照 副本序号的顺序,0 启动后才会依次启动 1, 2, 3...。收缩、删除、重启时与之相反。 这几点都是依赖于 副本 序号的,可以说,StatefulSet 是一种管理机制,和Deployment 不同,Deployment 的命名使用hash,DNS 域名 使用 IP,存储随机分配,更没有启动顺序(参考 RollingUpdate), #### 2.8.1 简易例子 既然是K8S的管理机制的差异,那么在 StatefulSet 和 Deployment 的声明上基本没啥差别,可以直接从deployment修改: ```yaml # from samples/ngix-nfs-deployment.yaml # to samples/nginx-statefulset.yml apiVersion: apps/v1 # 修改 Kind # kind: Deployment kind: StatefulSet metadata: # name: nginx-rs # 修改名字 name: nginx-sts labels: app/name: nginx spec: replicas: 3 selector: matchLabels: app/name: nginx # 必须添加一个服务 serviceName: web-service ... ``` 修改了 Kind 和metadata.name ,添加了 spec.serviceName。 StatefulSet必须绑定一个服务,这个服务会为其分配 DNS。 使用 kubectl apply 部署: ``` $ kubectl apply -f samples/nginx-statefulset.yaml statefulset.apps/nginx-sts created $ kubectl get po NAME READY STATUS RESTARTS AGE nginx-rs-7f7fd8c66c-9599c 1/1 Running 0 18h nginx-rs-7f7fd8c66c-d2kxz 1/1 Running 0 18h nginx-rs-7f7fd8c66c-fqk4r 1/1 Running 0 18h nginx-sts-0 1/1 Running 0 12m nginx-sts-1 1/1 Running 0 11m nginx-sts-2 1/1 Running 0 11m ``` 注意看 nginx-sts 的pod,其名称是 metadata.name-<序号>。 AGE列 的时间可以看出,副本 0 是最先创建的,之后才依次创建 1, 2。 > 序号是从0 开始的,可以使用 spec.ordinals 修改。如:`ordinals: 1`。 再来看DNS: ``` $ kubectl exec -it nginx-sts-0 -- sh / # cat /etc/hosts # Kubernetes-managed hosts file. 127.0.0.1 localhost ::1 localhost ip6-localhost ip6-loopback fe00::0 ip6-localnet fe00::0 ip6-mcastprefix fe00::1 ip6-allnodes fe00::2 ip6-allrouters 10.244.1.22 nginx-sts-0.web-service.default.svc.cluster.local nginx-sts-0 / # nslookup nginx-sts-1.web-service.default.svc.cluster.local Server: 10.96.0.10 Address: 10.96.0.10:53 Name: nginx-sts-1.web-service.default.svc.cluster.local Address: 10.244.1.23 / # ping nginx-sts-2.web-service -c 2 PING nginx-sts-2.web-service (10.244.1.24): 56 data bytes 64 bytes from 10.244.1.24: seq=0 ttl=63 time=0.264 ms 64 bytes from 10.244.1.24: seq=1 ttl=63 time=0.073 ms --- nginx-sts-2.web-service ping statistics --- 2 packets transmitted, 2 packets received, 0% packet loss round-trip min/avg/max = 0.073/0.168/0.264 ms ``` Stateful 的DNS和其他Pod没有差别,其区别在于,pod name 和 hostname是稳定的。 启动一个窗口执行 `kubectl get pods -w ` 用于观察 pod的状态变化。 另一个窗口执行: ``` $ kubectl delete po nginx-sts-0 pod "nginx-sts-0" deleted $ $ kubectl get pod -w NAME READY STATUS RESTARTS AGE nginx-sts-0 1/1 Running 0 22m nginx-sts-1 1/1 Running 0 22m nginx-sts-2 1/1 Running 0 22m # delete nginx-sts-0 1/1 Terminating 0 23m nginx-sts-0 0/1 Terminating 0 23m nginx-sts-0 0/1 Pending 0 0s nginx-sts-0 0/1 ContainerCreating 0 0s nginx-sts-0 1/1 Running 0 2s # scale 0 nginx-sts-2 1/1 Terminating 0 29m nginx-sts-2 0/1 Terminating 0 29m nginx-sts-1 1/1 Terminating 0 5m46s nginx-sts-1 0/1 Terminating 0 5m47s nginx-sts-0 1/1 Terminating 0 6m9s nginx-sts-0 0/1 Terminating 0 6m10s # scale 3 nginx-sts-0 0/1 Pending 0 0s nginx-sts-0 0/1 ContainerCreating 0 0s nginx-sts-0 1/1 Running 0 2s nginx-sts-1 0/1 Pending 0 0s nginx-sts-1 0/1 ContainerCreating 0 1s nginx-sts-1 1/1 Running 0 3s nginx-sts-2 0/1 Pending 0 0s nginx-sts-2 0/1 ContainerCreating 0 0s nginx-sts-2 1/1 Running 0 2s ``` 可见,删除后自动创建的 Pod 名称不变,收缩时是从 最高序号 开始,扩展时 是从最低序号开始。这就是 StatefulSet 的唯一、粘性、有序的特点。 #### 2.8.2 VolumeClaimTemplate 存储 StatefulSet 存储 可以采用 VolumeClaimTemplate 进行模板化申请。这里申请的存储应以storageClass为限定。这涉及到provisioner动态制备。 VolumeClaimTemplate 就是一个 persistentVolumeClaim 的模板,声明内容类似。 在上一个例子中将 container 中定义 的 persistentVolumeClaim 移到 spec.VolumeClaimTemplate中: ```yaml # samples/nginx-nfs-statefulset.yml ... template: metadata: name: nginx labels: app/name: nginx spec: # 不使用预定义的 pvc,改用 pvc template # volumes: # - name: nfs-path # persistentVolumeClaim: # claimName: nfs-pvc containers: ... volumeMounts: - mountPath: "/usr/share/nginx/html/nfs" name: nfs-path volumeClaimTemplates: # copy from nfs-pvc - metadata: # 名称和 volumeMounts中保持一致 name: nfs-path spec: accessModes: [ "ReadWriteOnce" ] storageClassName: nfs-static-storage resources: requests: storage: 100Mi ``` 将声明保存为 `samples/nginx-nfs-statefulset.yml` 并部署: ``` # 首先删除之前的 statefulset。 $ kubectl delete sts/nginx-sts statefulset.apps "nginx-sts" deleted $ kubectl apply -f samples/nginx-nfs-statefulset.yaml statefulset.apps/nginx-sts created ``` > 注意:删除原 stateful 而不是 apply 修改,是因为新增的 *volumeClaimTemplate* 不允许 修改,只能重建。 > 也可以使用 `kubectl replace ...` 。 这时查看 pod 状态: ``` $ kubectl get pod -w NAME READY STATUS RESTARTS AGE nginx-sts-0 0/1 Pending 0 2m46s ``` 可见 nginx-sts-0 一直处在 **Pending** 状态,检查Pending原因: ``` $ kubectl events sts/nginx-sts 5m6s Normal SuccessfulCreate StatefulSet/nginx-sts create Claim nfs-path-nginx-sts-0 Pod nginx-sts-0 in StatefulSet nginx-sts success 5m6s Normal SuccessfulCreate StatefulSet/nginx-sts create Pod nginx-sts-0 in StatefulSet nginx-sts successful 0s (x21 over 5m) Normal WaitForPodScheduled PersistentVolumeClaim/nfs-path-nginx-sts-0 waiting for pod nginx-sts-0 to be scheduled 0s (x6 over 5m20s) Warning FailedScheduling Pod/nginx-sts-0 0/2 nodes are available: 2 node(s) didn't find available persistent volumes to bind. ``` 可见,问题是: **0/2 nodes are available: 2 node(s) didn't find available persistent volumes to bind.** 也就是说,在两个节点上都找不到可用的 PV。 ``` $ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE local-pv 100Mi RWO Retain Bound default/local-pvc local-storage 2d2h nfs-pv 200Mi RWO Retain Bound default/nfs-pvc nfs-static-storage 2d ``` 当前定义的两个 PV都已经分配了。因此,需要再进一步定义几个NFS的PV。 > 为了简单起见,这里不再创建新的 NFS export 目录,仅用于演示效果。 将nfs-volume中的 PV 定义复制一份,并修改`metadata.name`,如: ```yaml # samples/nfs-pvlist.yaml --- apiVersion: v1 kind: PersistentVolume metadata: name: nfs-pv1 labels: storage/type: nfs spec: storageClassName: nfs-static-storage capacity: storage: 200Mi accessModes: - ReadWriteOnce nfs: path: "/var/data/nfs" server: 172.21.57.232 readOnly: false ``` 这里把名字改成了 `pv1` 路径还是指向 `/var/data/nfs`。如果建立了其他的NFS目录,这里可以修改成对应的目录。 部署 该 PV: ``` $ kubectl apply -f samples/nfs-pvlist.yaml $ kubectl get pv -w NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE local-pv 100Mi RWO Retain Bound default/local-pvc local-storage 2d2h nfs-pv 200Mi RWO Retain Bound default/nfs-pvc nfs-static-storage 2d1h nfs-pv1 200Mi RWO Retain Available nfs-static-storage 1m nfs-pv1 200Mi RWO Retain Bound default/nfs-path-nginx-sts-0 nfs-static-storage 1m ``` 当建立了新的 PV (storageClass=nfs-static-storage)之后,就被 nginx statefulSet 获取并绑定了。再看 `get pod -w` 窗口: ``` nginx-sts-0 0/1 Pending 0 0s nginx-sts-0 0/1 Pending 0 1s nginx-sts-0 0/1 ContainerCreating 0 1s nginx-sts-0 1/1 Running 0 3s nginx-sts-1 0/1 Pending 0 0s ``` 可见,nginx-sts-0 已经建立完成,因此,立即启动了 nginx-sts-1,没有空闲的PV,因此,sts-1 进入 Pending状态。 只要依次再创建两个PV就可以满足 nginx-sts-1 和 nginx-sts-2 的使用了,这里采用一个创建小技巧: ``` $ kubectl create -f samples/nfs-pvlist.yaml --edit -o yaml ``` ```yaml # Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 kind: PersistentVolume metadata: labels: storage/type: nfs name: nfs-pv1 spec: accessModes: - ReadWriteOnce capacity: storage: 200Mi nfs: path: /var/data/nfs readOnly: false server: 172.21.57.232 storageClassName: nfs-static-storage ~ # 这里会出现一个 VI 窗口,在VI 中将 nfs-pv1 修改 nfs-pv2,保存退出。 ``` 这会建立一个 nfs-pv2。按此方法在建立 pv3,可见 PV 状态: ``` $ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE nfs-pv 200Mi RWO Retain Bound default/nfs-pvc nfs-static-storage 2d1h nfs-pv1 200Mi RWO Retain Bound default/nfs-path-nginx-sts-0 nfs-static-storage 27m nfs-pv2 200Mi RWO Retain Bound default/nfs-path-nginx-sts-1 nfs-static-storage 27m nfs-pv3 200Mi RWO Retain Bound default/nfs-path-nginx-sts-2 nfs-static-storage 33s ``` 删除一个Pod,观察变化: ``` $ kubectl delete pod/nginx-sts-1 pod "nginx-sts-1" deleted $ kubectl get pvc $ kubectl get pvc -w NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE nfs-path-nginx-sts-0 Bound nfs-pv1 200Mi RWO nfs-static-storage 31m nfs-path-nginx-sts-1 Bound nfs-pv2 200Mi RWO nfs-static-storage 31m nfs-path-nginx-sts-2 Bound nfs-pv3 200Mi RWO nfs-static-storage 30m ``` 注意,PVC并没有被删除,当 pod sts-1自动启动后,仍将使用 nfs-path-nginx-sts-1, 从而 挂载 nfs-pv2。 > 如果 PV 和 VolumeClaimTemplate 中的 accessMode设定为 ReadWriteMany,这就允许多一个PV被多个PVC领用,此时将领用同一个PV。 > 注意VolumeClaimTemplate 定义了 `storageClass`,因此,只会申请同类的PV。 > 申请PV时会检查满足 request的 容量的 PV。 > VolumeClaimTemplate 不允许更改,因此,如果需要修改的话,必须删除 StatefulSet重新建立。由于默认的 `persistentVolumeClaimRetentionPolicy` 策略为`Retain`,即删除时会保留PVC,因此,即使删除了StatefulSet,相应PVC/PV都保持绑定状态,需要手动删除PVC。 本例使用了静态分配PV,可以看出使用VolumeClaimTemplate之后,每一个Pod都会创建一个新的 PVC 对象,并挂载单独的卷。 这样就可以实现每个Pod存储的独立、唯一、保持。 如果生产环境使用的StorageClass支持动态制备,则应使用动态制备的存储申请方案。后文将介绍 NFS 动态制备。 ### 2.9 K8S API 在许多对象的声明中,都可以看到: ```yaml apiVersion: apps/v1 --- apiVersion: v1 --- apiVersion: storage.k8s.io/v1 ``` 为什么每个`apiVersion`的取值是不同的,有些还带有目录? 这里的 apiVersion 是指 k8s API 的版本信息,附带了不同API的分类名称。这样讲尚不直观,来看下文的详细说明。 #### 2.9.1 kube-apiserver 所有Kubernetes的操作(如:kubectl create\apply\set\get ...),都是对通过访问 kube-apiserver 来完成的,kube-apiserver是一组 REST 服务,完成对K8S各类对象的管理。 先来看看kube-apiserver如何启动的: ``` $ kubectl get pod -n kube-system | grep apiserver kube-apiserver-minikube 1/1 Running 0 5d5h # minikube 中的 apiserver pod 。 $ kubectl get pod -n kube-system kube-apiserver-minikube -o yaml ``` ```yaml # ... spec: containers: - command: - kube-apiserver - --advertise-address=192.168.49.2 - --allow-privileged=true - --authorization-mode=Node,RBAC - --client-ca-file=/var/lib/minikube/certs/ca.crt - --enable-admission-plugins=NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,DefaultTolerationSeconds,NodeRestriction,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,ResourceQuota - --enable-bootstrap-token-auth=true - --etcd-servers=https://127.0.0.1:2379 - --secure-port=8443 image: k8s.gcr.io/kube-apiserver:v1.23.8 # ... ``` 重点看 `--advertise-address=192.168.49.2` 和 `--secure-port=8443`, 这就是API Server的地址。 使用kubectl 正确配置后,也可使用 kubectl cluster-info 来显示API-server 地址: ``` $ kubectl cluster-info Kubernetes control plane is running at https://127.0.0.1:49174 CoreDNS is running at https://127.0.0.1:49174/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy ``` 其中 control plane 地址,就是 api-server。 这两个地址不同是因为 minikube 运行在 docker 容器中(docker on docker ...)虚拟的节点上。 kubectl 就是使用这个这个地址来访问API Server的。 #### 2.9.2 API 分组 K8S的 API 很多,按照其功能、管理的对象类型进行了分组。在学习之前,可以先使用proxy访问API Server。 ``` $ kubectl proxy Starting to serve on 127.0.0.1:8001 # 另开窗口 $ curl http://localhost:8001/ ``` ```json { "paths": [ "/.well-known/openid-configuration", "/api", "/api/v1", "/apis", "/apis/admissionregistration.k8s.io/v1", "/apis/apiextensions.k8s.io", "/apis/apiextensions.k8s.io/v1", "/apis/apiregistration.k8s.io/v1", "/apis/apps/v1", // ... "/apis/rbac.authorization.k8s.io", "/apis/rbac.authorization.k8s.io/v1", "/apis/scheduling.k8s.io/v1", "/apis/storage.k8s.io", "/apis/storage.k8s.io/v1", "/apis/storage.k8s.io/v1beta1", "/healthz", "/healthz/autoregister-completion", "/healthz/etcd", "/healthz/log", "/healthz/ping", // ... ] } ``` K8S很友好,会返回一系列的 URL 路径,这些路径有三种, * /api/v1: 这是核心API, v1 是版本号。 * /apis/apps/v1: apis开头的是api分组,后面是 分组名称(GROUP),在后面v1是版本号(VERSION)。 * /healthz/ livnessz 等:这是容器健康等API。 这里重点讨论 api和apis。 > K8S会保留API的多个版本,如`/apis/storage.k8s.io/v1` 和 `/apis/storage.k8s.io/v1beta1`。 可以看到,YAML文件中的`apiVersion` 值得就是 GROUP/VERSION。 核心API 的GROUP不需要写。因此,是 `apiVerison: v1`。 另一个命令可以看到全部的 api 分组信息: ``` $ kubectl api-versions admissionregistration.k8s.io/v1 apiextensions.k8s.io/v1 apiregistration.k8s.io/v1 apps/v1 ... rbac.authorization.k8s.io/v1 scheduling.k8s.io/v1 storage.k8s.io/v1 storage.k8s.io/v1beta1 v1 ``` #### 2.9.3 API 和 资源(RESOURCE) REST API 核心理念,是使用URI表示 **资源**,使用 HTTP METHOD (get/post/delete。。。) 表示动作。 K8S API管理的资源分为很多种类型(RESOURCE_TYPE),每个资源的名字在同类中是唯一的。URL 规则中使用了RESOURCE_TYPE和RESOURCE_NAME 来标志 一个具体的资源。不指定NAME时,意味着获取该类型的全部资源。 先来试一下: ``` $ curl http://localhost:8001/api/v1/namespaces/default ``` ```json { "kind": "Namespace", "apiVersion": "v1", "metadata": { "name": "default", "uid": "d8c2057e-67d7-45d0-8e41-388d646efda0", "resourceVersion": "207", "creationTimestamp": "2023-01-03T03:04:06Z", "labels": { "kubernetes.io/metadata.name": "default" }, // ... "status": { "phase": "Active" } } ``` 这个 URL 中,各部分含义: - /api : 核心API - /v1 : 版本号 VERSION - /namespace:RESOURCE_TYPE 资源类型是 名空间。 - /default : RESOURCE_NAME 名空间名称是 default。 CURL 默认使用 GET , GET 在 REST里的语义就是 **查看** 。因此,K8S 返回了 名空间 `default`的 状态信息,相当于:`kubectl get namespace default -o json` 分组 API 也遵循相同的 规则, 如: ``` $ curl http://localhost:8001/apis/networking.k8s.io/v1/ingressclasses/nginx/ ``` ```json { "kind": "IngressClass", "apiVersion": "networking.k8s.io/v1", "metadata": { "name": "nginx", "uid": "e44fbc9d-e745-44d7-92fc-b5d8af9f9880", "resourceVersion": "232990", "generation": 1, "creationTimestamp": "2023-01-06T11:29:13Z", "labels": { "app.kubernetes.io/component": "controller", "app.kubernetes.io/instance": "ingress-nginx", "app.kubernetes.io/name": "ingress-nginx" } }, // ... "spec": { "controller": "k8s.io/ingress-nginx" } } ``` 这个 URL 中,各部分含义: - /apis : API分组 - /networking.k8s.io: 分组名称 GROUP - /v1 : 版本号 VERSION - /ingressclasses:RESOURCE_TYPE 资源类型是 ingressClass。 - /nginx : RESOURCE_NAME ingressClasses 是 nginx。 K8S 的 资源 有些是从属于 Namespace 的,有些从属于 集群的(即不属于任何 namespace)。上面两个例子都是集群资源。当需要访问名空间资源时,需要在 RESOURCE_TYPE前加上 `namespaces/所属名空间名(NAMESPACE)`。形如`/apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCE_TYPE/RESOURCE_NAME/`如: ``` $ curl http://localhost:8001/apis/apps/v1/namespaces/default/deployments/nginx-rs ``` 这个 URL 中,各部分含义: * /apis : API分组 * /apps : 分组名称 GROUP - /v1 : 版本号 VERSION - /deployments:RESOURCE_TYPE 资源类型是 deployment。 - namespaces: 表示选择某一namespaces - default: NAMESPACE, namespace 的名称。 - /nginx-rs : RESOURCE_NAME 是 nginx-rs。 #### 2.9.4 API 的 操作类型(verbs) 每个资源的API操作都使用REST风格定义。下文摘自官方资料: > 几乎所有对象资源类型都支持标准 HTTP 动词 - GET、POST、PUT、PATCH 和 DELETE。 Kubernetes 也使用自己的动词,这些动词通常写成小写,以区别于 HTTP 动词。 > Kubernetes 使用术语 list 来描述返回资源集合, 以区别于通常称为 get 的单个资源检索。 如果你发送带有 ?watch 查询参数的 HTTP GET 请求, Kubernetes 将其称为 watch 而不是 get(有关详细信息,请参阅快速检测更改)。 > 对于 PUT 请求,Kubernetes 在内部根据现有对象的状态将它们分类为 create 或 update。 update 不同于 patch;patch 的 HTTP 动词是 PATCH。 > ubernetes API 允许客户端对对象或集合发出初始请求,然后跟踪自该初始请求以来的更改:watch。 客户端可以发送 list 或者 get 请求,然后发出后续 watch 请求。 #### 2.9.5 查看资源清单 API 管理哪些资源,资源是否属于名空间,可以使用该命令: ``` $ kubectl api-resources -o wide ``` | **NAME** | **SHORTNAMES** | **APIVERSION** | **NAMESPACED** | **KIND** | **VERBS** | **CATEGORIES** | | --- | --- | --- | --- |--- |--- |--- | |configmaps |cm | v1 | true | ConfigMap | create,delete, deletecollection, get,list,patch, update,watch | | |endpoints| ep | v1 | true | Endpoints | create,delete, deletecollection, get,list,patch, update,watch | | | events | ev | v1| true | Event | create,delete, deletecollection, get,list,patch ,update,watch | | | namespaces | ns | v1 | false | Namespace | create,delete, get,list,patch, update,watch | | | nodes | no| v1| false | Node | create,delete, deletecollection, get,list,patch, update,watch | | | persistentvolumeclaims | pvc | v1 | true | PersistentVolumeClaim | create,delete, deletecollection, get,list,patch, update,watch | | | persistentvolumes | pv | v1 | false | PersistentVolume | create,delete, deletecollection, get,list,patch, update,watch | | | pods | po | v1 | true | Pod | create,delete, deletecollection, get,list,patch, update,watch | all | | | 这里只是截取了一些。命令输出中 各列 含义如下: * NAME: API 中的 RESOURCE_TYPE。同时也是 `kubectl get <资源类型>` 中的 资源类型。 * SHORTNAME: `kubectl get <资源类型>` 中的 资源类型 简写。 * APIVERSION: API 的版本号。 * NAMESPACED: 资源是否从属于 namespace(true/false)。 * KIND: 在 yaml 声明的 Kind 字段值。 * VERBS: 资源支持的 操作。 ### 2.10 资源管理方式 K8S 提供的 声明式 对象管理方式,将对象(资源)都以 YAML/JSON 格式进行声明,这大大简化了资源的管理,使资源的可读性很强。 #### 2.10.1 Kubectl apply 通常的管理方法是 使用 `kubectl apply -f <文件名>`来进行管理。`apply` 意味着 `应用`,根据文件中的资源Kind/name等确定是否该`create` 还是 `update`。 kubectl apply 还支持使用目录,将同一个目录下的yaml文件全部检查一遍,并apply其中的对象。 kubectl apply 可以使用 -R 参数来指定递归执行其子目录下的yaml文件。 这样就大大方便了一组应用的声明、部署(尤其是,K8S对资源的关联是松耦合的,当资源申请不到时,会挂起申请者,当资源具备时,再进行Create,因此,YAML文件的执行顺序并不是很要紧,特例是 Service 和 Pod之间,需要先创建 Service,这时POD 中的 SERVICE_ 环境变量才会生效)。 而在 kubectl get 命令中,也支持使用 `-f -R` 参数,获取文件中定义的对象。比如: ``` $ kubectl get -f samples/nfs-volume.yaml NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE storageclass.storage.k8s.io/nfs-static-storage kubernetes.io/no-provisioner Delete WaitForFirstConsumer false 2d22h NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE persistentvolume/nfs-pv 200Mi RWO Retain Bound default/nfs-pvc nfs-static-storage 2d22h NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE persistentvolumeclaim/nfs-pvc Bound nfs-pv 200Mi RWO nfs-static-storage 2d22h ``` #### 2.10.2 API Server 如前所述,kubectl 所有操作均通过 API Server 完成的,因此,可以直接使用 API Server。 如果Pod中应用需要检索、管理 K8S对象时,可以使用API server。此时建议使用 K8S 提供的 各语言环境 API库,如 Java 库。 此时需要进行鉴权。参见下文的 **安全** 。 #### 2.10.3 kubectl create 某些简单的 资源 可以直接采用 kubectl create命令来创建,如namespace: ``` $ kubectl create ns ``` 复杂的资源就不建议这样了。 因此类似的还有 kubectl set / patch /label 等用于修改 资源 声明的命令。 #### 2.10.4 修改 修改的主要方法自然是修改 YAML 文件,但也有简便的方法可以快速编辑。如: ``` $ kubectl edit ``` 这命令会打开一个 编辑窗口,编辑结束后,Kubectl 将 apply 修改后的内容。 #### 2.10.5 获取YAML 使用 `kubectl get -o yaml ` 可以取得 资源的 YAML 定义。 可以将该信息保存在文件中。 #### 2.10.6 历史及版本 K8S 应用 常用的部署形式是 Delployment、StatefulSet、DaemonSet(Job 是一次性任务,不需要历史管理)。 这些 对象 由于版本、配置变化等,会多次编辑、发布,K8S 记录了这些历史版本信息。 ``` $ kubectl rollout history deploy nginx-rs deployment.apps/nginx-rs REVISION CHANGE-CAUSE 3 4 5 6 7 8 9 10 ``` 可见该 deployment 已经 有这么多版本了(注意,scale 也会记录一个版本)。 K8S 允许查看任意版本的对象内容: ``` kubectl rollout history deploy nginx-rs --revision=5 -o yaml ``` CHANGE-CAUSE 通过 annotation 注解 `kubernetes.io/change-cause` 来定义: ```yaml # samples/ngix-nfs-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: nginx-rs annotations: kubernetes.io/change-cause: add change-cause example ``` Apply 该文件后,再查询 可见: ``` $ kubectl apply -f samples/nginx-nfs-deployment.yaml deployment.apps/nginx-rs configured $ kubectl rollout history deploy nginx-rs deployment.apps/nginx-rs REVISION CHANGE-CAUSE 4 5 6 7 8 9 10 11 add change-cause example ``` #### 2.10.7 diff 比较 当编辑了资源声明 YAML 之后,可以使用该命令检查 文件定义与 K8S 当前版本资源的差异,例如,把 `sample/nfs-volume.yaml` 中的 storage: 200Mi 改成 300: ``` $ kubectl diff -f samples/nfs-volume.yaml diff -u -N /tmp/LIVE-2986072598/v1.PersistentVolume..nfs-pv /tmp/MERGED-3148704781/v1.PersistentVolume..nfs-pv --- /tmp/LIVE-2986072598/v1.PersistentVolume..nfs-pv 2023-01-09 11:23:36.125010080 +0800 +++ /tmp/MERGED-3148704781/v1.PersistentVolume..nfs-pv 2023-01-09 11:23:36.125010080 +0800 @@ -17,7 +17,7 @@ accessModes: - ReadWriteOnce capacity: - storage: 200Mi + storage: 300Mi claimRef: apiVersion: v1 kind: PersistentVolumeClaim ``` 这是一个方便实用的功能。 ## 3. 安全 K8S 的安全管理分为两部分: * 集群管理的安全:控制从外部访问集群API-Server的安全,使用用户认证机制。 * 集群内部的安全:控制集群内应用(POD)访问AP-Server的安全。使用ServiceAccount即RBAC或ABAC。 ### 3.1 用户认证 K8S 的用户认证方式采用签名证书。这是因为 K8S 的核心是 api-server,而访问api-server 的方式就是通过HTTPS,使用客户端证书进行身份认证是很合理的方法。 K8S 的用户是外部管理的,即,K8S并不直接管理用户,也不会考虑用户的来源,用户信息的安全,这是由K8S系统外的安全措施来保证的。 使用客户端证书进行身份认证的方式是: 1. 客户端生成密钥对。 2. 使用K8S 的 CA证书进行签名(K8S会生成一个CA证书,也可以从外部导入一个CA证书)。 3. 客户端需要保护好证书安全。 4. 客户端访问api-server时携带该证书(典型的应用是使用`kubectl`)。 5. api-server 使用鉴权模块进行身份认证(K8S还支持其他鉴权机制如token,本文仅介绍证书机制)。 6. 身份认证通过后,api-server 通过 RBAC或ABAC进行鉴权,检查该客户端是否具备访问该资源的权限。 7. 根据鉴权结果执行操作(允许或拒绝)。 #### 3.1.2 Kubectl 查看客户端认证信息 本文仅介绍kubectl的认证机制,首先看一下 kubectl 如何使用客户端证书。 先用命令查看一下 kubectl 当前配置信息: ``` $ kubectl config view --minify ``` ```yaml apiVersion: v1 clusters: - cluster: certificate-authority: ~/.minikube/ca.crt name: cluster_info server: https://127.0.0.1:49174 name: minikube contexts: - context: cluster: minikube namespace: default user: minikube name: minikube current-context: minikube kind: Config preferences: {} users: - name: minikube user: client-certificate: ~/.minikube/profiles/minikube/client.crt client-key: ~/.minikube/profiles/minikube/client.key ``` 可见config中定义了: * 一个 cluster minikube,它的CA根证书保存在 `~/.minikube/ca.crt`。 * 当前上下文context: 上下文默认的 cluster/user/namespace等。 * 用户信息:名为`minikube`的user,以及其使用的客户端证书及私钥。 重点关注user 的 client-certificate和client-key。 使用 openssl 查看 CA 证书内容: ``` $ openssl x509 -in ./ca.crt -noout -text Certificate: Data: # 证书颁发者Issuer和Subject都是 minikubeCA。 Issuer: CN = minikubeCA Subject: CN = minikubeCA Subject Public Key Info: Public Key Algorithm: rsaEncryption X509v3 extensions: X509v3 Basic Constraints: critical # 表示这是一个CA证书 CA:TRUE ... ``` 再看 client-certificate 客户端证书: ``` $ openssl x509 -in ./profiles/minikube/client.crt -noout -text Certificate: Data: # 颁发者是 minikubeCA Issuer: CN = minikubeCA # 主体 Subject Subject: O = system:masters, CN = minikube-user X509v3 extensions: X509v3 Basic Constraints: critical CA:FALSE ``` client.cert是由 minikubeCA签发的。来分析一下Subject(主体) 内容,这很重要: * O: organization 组织,在这里代表 K8S 的 **用户组** * CN: 通用名Common Name,在这里代表 K8S 的**用户名** system:master用户组是特殊用户组,通常只能建立普通用户。 这样,当 kubectl 访问时,会携带CA 签发的证书,这时 api-server 才会认证该证书。 > 注意,私钥的安全是由用户自己来保证的。 #### 3.1.2 用户组Group和用户User 证书中的O 和 CN 代表用户组和用户名。 K8S内部定义的组以 `system:`开头。当然可以自定义组名称,但应避免使用 `system:` 前缀。 已知的K8S内部组包括: * system:master, 这是K8S超级用户组,该组的用户不进行鉴权,即拥有全部资源的访问权限。 * system:authenticated, 已认证用户组,当用户通过身份认证后,即在该组中。默认可以访问自身资源。 * system:unauthenticated,未认证用户组。 * system:serviceaccounts, K8S 服务账户(ServiceAccount)组。在下文RBAC会讲到。 还有其他的组是K8S 组件创建的组,不在讨论范围内。 用户可以自己定义一个组,比如:某个应用管理组,这种组通常会绑定到某个具体名空间的管理权限。 本例中将使用 app 组,用户名 为 appadmin,绑定的namespace 是 app。 #### 3.1.3 生成用户证书并签名 生成用户证书的过程包括两部分: 1. 使用任意工具(本例使用openssl)生成RSA 密钥。 2. 创建一个 CSR 证书签名请求。 2. K8S CA使用CSR生成证书并对证书进行签名。 首先用openssl生成密钥: ``` $ cd ~/ # 初始化随机数 $ openssl rand -writerand .rnd $ cd ~/.kube $ mkdir -p profiles/appadmin $ cd profiles/appadmin # 生成 2048 bits RSA key $ openssl genrsa -out appadmin.key 2048 Generating RSA private key, 2048 bit long modulus (2 primes) ...............................+++++ ..............+++++ e is 65537 (0x010001) ``` 有了公钥和私钥之后,就需要创建CSR(Certficate Signing Request)。 本例使用openssl使用命令行创建CSR, 创建CSR时需要指定: * key: 即appadmin.key * 证书Subject域信息: 即 `O=app` 和 `CN=appadmin`。 > CSR包含Subject域数据、公钥,并对其使用 私钥 签名。 使用openssl建立 CSR: ``` $ openssl req \ -new \ -key appadmin.key \ -out appadmin.csr \ -subj "/O=app/CN=appadmin" $ openssl req -subject -in appadmin.csr -noout subject=O = app, CN = appadmin ``` 本例中持有CA私钥(minikube CA的私钥和证书放在一个目录下,名为ca.key),因此可以直接使用`openssl x509 -req` 命令来生成CA签名证书。 K8S 也提供了生成签名证书的方法,本例使用该方法。 K8S 的方式是创建一个 CertificateSigningRequest (CSR) 对象并提交到 K8S api-server,由具有管理权限的用户批准该申请,即可生成签名证书。 ```yaml # samples/auth/appadmin-csr.yaml apiVersion: certificates.k8s.io/v1 kind: CertificateSigningRequest metadata: # 申请的名字,并非用户名。 name: appadmin-req spec: # 申请内容,将appadmin.csr内容使用base64编码。 request: # 签名者是内置的,用于约束证书用途,本例中申请用于 api-server 的客户端认证。 signerName: kubernetes.io/kube-apiserver-client # 证书过期时间,默认为一年。这里的单位是秒。 expirationSeconds: 8640000 # 100 day # 证书用途,必须包括 'client auth' usages: - client auth ``` request的内容可以通过`base64`命令进行编码: ``` $ base64 appadmin.csr -w 0 LS0tLS1CRUdJTiBDRVJU ..... ``` 将结果复制到文件中即可。 > 上例使用了不换行的(-w 0) base64,换行的文本可以 使用 `request: |` 多行文本模式来编写。 在kubectl 提交该请求: ``` $ kubectl apply -f samples/auth/appadmin-csr.yaml certificatesigningrequest.certificates.k8s.io/appadmin-req created ``` 参看并批准该请求: ``` $ kubectl get csr NAME AGE SIGNERNAME REQUESTOR REQUESTEDDURATION CONDITION appadmin-req 48s kubernetes.io/kube-apiserver-client minikube-user 100d Pending $ kubectl certificate approve appadmin-req certificatesigningrequest.certificates.k8s.io/appadmin-req approved # 也可以用 deny 拒绝。 $ kubectl get csr NAME AGE SIGNERNAME REQUESTOR REQUESTEDDURATION CONDITION appadmin-req 3m23s kubernetes.io/kube-apiserver-client minikube-user 100d Approved,Issued # 申请已批准,证书已签发。 ``` 注意其中`REQUESTOR minikube-user`,和之前看到的 client.crt 中的 `CN`域一致。 签发的证书保存在 csr 申请的 status.certificate 字段中: ``` $ kubectl get csr appadmin-req -o yaml ``` ```yaml apiVersion: certificates.k8s.io/v1 kind: CertificateSigningRequest metadata: annotations: creationTimestamp: "2023-01-10T02:26:57Z" name: appadmin-req resourceVersion: "526427" uid: 11dc047f-d141-4f59-a67d-4fbec80985f3 spec: expirationSeconds: 8640000 groups: - system:masters - system:authenticated request: LS0tLS ... signerName: kubernetes.io/kube-apiserver-client usages: - client auth username: minikube-user status: certificate: LS0tLS1CRUdJT... conditions: - lastTransitionTime: "2023-01-10T02:29:21Z" lastUpdateTime: "2023-01-10T02:29:21Z" message: This CSR was approved by kubectl certificate approve. reason: KubectlApprove status: "True" type: Approved ``` 可以将status.certificate复制并使用base64解码,其内容是一个X509证书。或使用下面的命令直接将其保存: ``` $ kubectl get csr appadmin-req -o jsonpath='{.status.certificate}' | base64 -d > ~/.kube/profiles/appadmin/appadmin-client.crt $ openssl x509 -in ~/.kube/profiles/appadmin/appadmin-client.crt -noout -text Certificate: Data: Version: 3 (0x2) Serial Number: 2e:94:73:b4:d1:6f:0e:36:5c:91:28:87:d7:77:0c:d5 Signature Algorithm: sha256WithRSAEncryption Issuer: CN = minikubeCA Validity Not Before: Jan 10 02:24:21 2023 GMT Not After : Apr 20 02:24:21 2023 GMT Subject: O = app, CN = appadmin ... ``` #### 3.1.4 使用appadmin用户 需要配置 kubectl 使用 appadmin 用户证书及密钥。 ``` $ cd ~/.kube/profiles/appadmin/ # 指定 用户名,证书,私钥。 $ kubectl config set-credentials appadmin --client-certificate=appadmin-client.crt --client-key=appadmin.key User "appadmin" set. $ kubectl config view | grep user -3 ``` ```yaml ... users: - name: appadmin user: client-certificate: profiles/appadmin/appadmin-client.crt client-key: profiles/appadmin/appadmin.key - name: minikube user: client-certificate: ~/.minikube/profiles/minikube/client.crt client-key: ~/.minikube/profiles/minikube/client.key ``` > 由于 profiles目录在 ~/.kube目录下,因此这里使用了相对路径。 设置一个使用 appadmin 的 kube context 并切换: ``` $ kubectl config set-context mk-app --cluster=minikube --user=appadmin --namespace=default Context "mk-app" created. $ kubectl config set current-context mk-app Property "current-context" set. $ kubectl config get-contexts CURRENT NAME CLUSTER AUTHINFO NAMESPACE minikube minikube minikube default * mk-app minikube appadmin default ``` 试着访问一下K8S资源: ``` $ kubectl get pods Error from server (Forbidden): pods is forbidden: User "appadmin" cannot list resource "pods" in API group "" in the namespace "default" ``` appadmin用户被拒绝了,这时因为它没有list pods 的授权。 下一章 RBAC中介绍如何授权。 > 用户的`$HOME`目录下有一个`.kube/config` 文件,kubectl 的配置信息就保存在这个文件中。 ### 3.2 RBAC RBAC 是 Role Based Authorization Control 的缩写。意思是基于角色的授权管理。 回顾 api-server 的章节,其中定义了: * api-group: API分组,每组API管理一部分资源。 * resource: 资源,资源有些是按照名空间进行管理。有些是Cluster级别的。 * verbs: 操作,对资源的操作动词,如:create,get,list,delete,patch等。 授权的客体已经很明确了,就是: api-group/namespace/resource_type/resource/verbs。即:对何种资源进行何种操作。 授权的主体一般是`用户`。上文介绍了基于证书的用户,注意这是从集群外部访问api-server的用户,在集群内部的各组件、应用访问api-server时,使用`ServiceAccount`。 另一个授权主体是 Group(即:上文中的 Subject O 定义的名字),组用户会继承组的授权。组和用户的定义完全由证书中的 Subject O/CN决定,无法通过其他方式改变。 因此,授权主体包括: * 用户: user,是指外部使用证书(或令牌)进行身份认证的用户。 * 组:group。 * ServiceAccount: 集群内部管理的账户。 只需要建立 授权主体-客体 的关系,就可以完成授权和鉴权了。 RBAC 管理方法采用了 Role 角色 来建立 `主体-客体` 关系: * 建立Role。 * 建立Role和客体的关系。 * 建立Role和主体的绑定关系(RoleBinding)。 * 主体继承了Role的授权。 ```mermaid graph TB rb[RoleBinding] rb -->rr[RoleRef] --> r[Role] rs[rules] --> ag[apiGroups] --> res[resources] --> rsn[resourceName] rs--> vb[verbs] s[Subjects] s --> g[Group] s--> u[User] s--> sa[ServiceAccount] r-->rs rb-->s ``` #### 3.2.2 Role Role作为顶级对象,同样使用YAML来声明,先使用命令查看一下role的资源信息: ``` $ kubectl api-resources |grep Role clusterrolebindings rbac.authorization.k8s.io/v1 false ClusterRoleBinding clusterroles rbac.authorization.k8s.io/v1 false ClusterRole rolebindings rbac.authorization.k8s.io/v1 true RoleBinding roles rbac.authorization.k8s.io/v1 true Role ``` Role 由 `rbac.authorization.k8s.io/v1` api管理,Kind是`Role`,由此可以写出yaml文件头: ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: appadmin-role ``` Role中需要使用rules来定义一组授权规则,rule 相当于 `客体`,遵循 上面提到的 `api-group/namespace/resource_type/verbs` 的结构: ```yaml rules: - apiGroups: ["", "apps"] resources: ["pods", "delployments", "statefulsets"] verbs: - get - list - watch ``` 这个规则定义了对核心API(使用"")和"apps"的资源 pods, deployments, statefullsets 的 get/list/watch授权(均为查看性质的授权)。 还可以再rule中使用resourceNames 来指定具体资源的名字。通常并不需要这样做。 由于Role是namespace级别的,因此,必须指定一个namespace。可以在YAML中定义 `metadata.namespace`,也可以在apply时使用当前名空间。 appadmin 的职责是管理应用的部署,因此,它需要对上述资源有修改权限,可以使用命令查看资源有哪些verbs: ``` $ kubectl api-resources -o wide | grep statefulsets statefulsets sts apps/v1 true StatefulSet create,delete,deletecollection,get,list,patch,update,watch all ``` 因此,需要在verbs加上: ```yaml verbs: - create - delete - deletecollection - get - list - patch - update - watch ``` 或者用通配符: ```yaml verbs: ["*"] ``` 还需要赋予其他对象的查看权限,比如,允许查看 RBAC的资源: ```yaml rules: - apiGroups: ["rbac.authorization.k8s.io"] resources: ["*"] verbs: ["get", "list"] ``` 使用apply在default名空间创建该 Role: ``` # 先切换回 minikube 用户,否则无权限操作。 $ kubectl config set current-context minikube $ kubectl apply -f samples/auth/appadmin-role.yaml role.rbac.authorization.k8s.io/appadmin-role created $ kubectl get roles NAME CREATED AT appadmin-role 2023-01-10T06:53:57Z ``` 下一步将 Role 绑定 到 用户 appadmin #### 3.2.3 RoleBinding K8S 使用 RoleBinding 建立 Role 和 用户(或其他`主体`)的关系。 > 可以使用命令 `kubectl explain RoleBinding` 来查看资源说明。 同样使用YAML声明RoleBinding: ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: appadmin-rb subjects: # 注意:这里使用 apiGroup, 而非 apiGroups. - apiGroup: rbac.authorization.k8s.io # 主体 User appadmin kind: User name: appadmin roleRef: # 角色 appadmin-role name: appadmin-role apiGroup: rbac.authorization.k8s.io kind: Role ``` 在 kubectl apply: ``` $ kubectl apply -f samples/auth/appadmin-role.yaml role.rbac.authorization.k8s.io/appadmin-role unchanged rolebinding.rbac.authorization.k8s.io/appadmin-rb created $ kubectl get rolebindings NAME ROLE AGE appadmin-rb Role/appadmin-role 20s ``` 这样就可以使用 appadmin用户来操作: ``` $ kubectl config set current-context mk-app Property "current-context" set. $ kubectl get pods NAME READY STATUS RESTARTS AGE nginx-rs-5697d79d5d-96vnx 1/1 Running 0 27h nginx-rs-5697d79d5d-fztjj 1/1 Running 0 27h nginx-rs-5697d79d5d-nw2qk 1/1 Running 0 27h nginx-sts-0 1/1 Running 0 2d1h nginx-sts-1 1/1 Running 0 2d $ kubectl delete pod/nginx-rs-5697d79d5d-96vnx pod "nginx-rs-5697d79d5d-96vnx" deleted ``` 对namespace default 的授权已经实现。 #### 3.2.1 ServiceAccount **ServiceAccount**是集群内部的用户,因此,完全由K8S管理。 集群内的组件、应用通过设置ServiceAccount来启用授权。workload 资源(pod/deploy/sts/ds等)都可以设置。设置ServiceAccount后,pod应用访问api-server时,将使用ServiceAccount鉴权。 比如:某个应用需要检测其他服务是否就绪,这时需要访问api-server,那么它需要有对这个资源的 get/list授权。 ServiceAccount广泛用于集群组件,比如,DNS 组件就需要监视(watch)所有pod, workload, service,这样才能在新Pod建立之后,更新域名信息。 本节仅简要介绍一下ServiceAccount。 新建ServiceAccount同样采用YAML声明。 ```yaml apiVersion: v1 kind: ServiceAccount metadata: name: accountName automountServiceAccountToken: true secrets: [] ``` 也可以直接用命令创建: ``` kubectl create sa ``` 对 Service Account 授权仍使用 RoleBinding, 将SA信息填写在其中的 subjects即可: ```yaml subjects: - apiGroup: "" kind: ServiceAccount name: ``` `automountServiceAccountToken` 是指将ServiceAccount使用的认证信息以Secrets文件形式挂载到pod,这样pod就可以以其身份来认证鉴权了。 要使用ServiceAccount,在Pod声明中使用 serviceAccountName定义,如: ```yaml apiVersion: apps/v1 kind: Deployment spec: template: serviceAccountName: ``` #### 3.2.4 ClusterRole/ClusterRolebinding CulsterRole和ClusterRoleBinding 于 Role/Rolebinding 类似,通常是用于管理非namespace的资源。 也可以在RoleBinding中引用ClusterRole,将其`复制`到namespace中,这时ClusterRole就相当于一个模板。 ### 3.3 模拟用户 kubectl 提供了一种模拟用户的命令,可以比较方便的检查用户和ServiceAccount的授权情况。 使用 `kubectl --as='username/ServiceAccountName'` 选项即可。 如: ``` $ kubectl get service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 443/TCP 7d5h web-service NodePort 10.98.134.132 8080:31001/TCP 5d19h $ kubectl get service --as=appadmin Error from server (Forbidden): services is forbidden: User "appadmin" cannot list resource "services" in API group "" in the namespace "default" ``` 还可以用 kubectl auth can-i 来检查授权。 ## 4. 存储动态制备 ### 4.1 动态制备概念 所谓动态制备,是指不需要预先创建PV,而是在PVC申请PV时,根据`StorageClass`查找Provisioner,由Provisioner创建新的PV来满足PVC需求。 整个流程大致如下: 1. StorageClass指定的Provisioner会以组件Pod方式运行在K8S中。 2. Provisioner监视(watch)所有符合StorageClass的PVC。 2. Pod 通过 PVC 申请 PV。 3. K8S 检查PVC中的StorageClass。获取符合条件的PV。 4. 如果没有满足条件的PV,K8S将Pod挂起,并等待合适的PV(被其他Pod释放或新的PV创建)。 5. Provisioner监视到挂起的PVC后,根据自身配置情况,创建新的PV。 6. 如第三步,K8S将新建PV分配给Pod。 7. Pod运行结束后,根据PVC的Policy来决定是否保留或删除PV。 8. 删除PV的请求被Provisioner监视到,执行实际的删除/空间回收。 ### 4.2 Provisioner 部署形式 由动态制备的流程可知,实现一个Provisioner的核心是Provisoner组件Pod,而组件与K8S其他组件、应用是解耦的。即: * K8S并不需要关心Provisioner是否存在,如何运行,也不对其进行管理。两者之间的关联仅仅是StorageClass指定的 Provsioner 名字。 * Provisoner不需要关心应用Pod的具体信息,而仅依赖于PVC。 * K8S /Provisioner/ 应用POD三者之间,通过Provisioner 监视 PVC 建立联系。 PVC 到 Provisoner的链路如下: ```mermaid graph LR p[Provisioner] sc[StorageClass] pv[PersistentVolume] pvc[PersistentVolumeClaim] pod[Pod] pod --> pvc --> sc --> p --> pv ``` 下图更详细的描述几者之间的关系: ```mermaid graph LR p[Provisioner] sc[StorageClass] pv[PersistentVolume] pvc[PersistentVolumeClaim] pvct[VolumeCliamTemplate] pod[Pod] sc --provisioner--> p pvc --storageClassName--> sc pvc --Claim request--> pv pvct --Create--> pvc pod --Declare--> pvc pod --Declare--> pvct p --Watch--> pvc p --Create--> pv ``` 因此,Provisioner的工作内容实际是: 1. 监视(Watch) StorageClass: 找到包含provisiner名字的sc。 2. 监视(Watch) PVC:找到包含应用Provisioner的PVC。 2. 根据PVC创建PV。 3. 监视PV,当PV解绑后,可控制回收PV或删除PV。 4. 假如Provisoner重启动了,需要监视PV来获取已经动态制备的PV(因此,通常会在动态制备PV上添加一些标志性的Label)。 这里要注意的是,Pod 绑定 PV 的过程,是K8S完成的,而非 Provisioner。 这些工作都需要访问 api-server,那么,Provision就需要恰当的授权。至少需要: ```yaml rules: - apiGroups: [""] resources: - persistentvolumeclaims verbs: - get - list - watch - apiGroups: [""] resources: - persistentvolumes verbs: ["get", "list", "watch", "create","delete", "update"] # verbs: ["*"] - apiGroups: ["storage.k8s.io"] resources: - storageclasses verbs: ["get", "list", "watch"] ``` 在 RBAC中需要创建一个 ServiceAccount,一个 Role(或ClusterRole)和相应的RoleBinding(ClusterRoleBinding)。 由于 PVC 属于 namespace 资源,因此,假如多个名空间需要使用该Provsioner,还需要在每个名空间分别授权。 Provisioner当然可以运行在K8S之外,但在K8S集群内运行更佳,因此,大部分Provisioner还需要部署一个Deployment(或DaemonSet)。 如果Provisioner是有状态的,需要保存一些数据,那么部署会复杂一些,比如:使用DaemonSet结合Local来存储较少的数据。 ### 4.3 NFS动态制备 本例采用nfs-subdir-provisioner作为动态制备器。从名称可以看出,该provisioner是使用了子目录(subdir) 的方式进行动态制备。即:以一个NFS目录作为待分配的空间,在其中建立子目录来产生动态制备的PV。 这种方式实现原理简单,缺陷也很明显:Provisioner 绑定了一个 NFS目录,当需要部署多个NFS目录时,就需要创建不同的 Pdovisoner Pod,使用不同的Provisoner名称加以区分。 个人觉得更理想的方法是在 provisoner的 paramter中指定 NFS 目录信息。这样通过不同的stroageClass来区分NFS目录更为合理。比如: ```yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-client- provisioner: k8s-sigs.io/nfs-subdir-external-provisioner parameters: archiveOnDelete: "false" nfsServer: nfsExport: storage: 100Gi ``` 但并没有这样实现,大约是因为基于NFS存储并非主流云存储方式,因此没有费心去处理这些细节。 这从部署资料也能看出来,推荐将其部署在应用相同的名空间中。这样使用方便。 #### 4.3.1 下载nfs-subdir 该项目主页为 https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner/。 可以使用 git clone 该项目。 ``` git clone https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner.git ``` #### 4.3.2 部署说明 官方提供了 helm Chart,本例中为了更好的理解 Provisioner,使用手动安装。 #### 4.3.3 准备 NFS export dir 上例中已经创建了 NFS Server 和目录。可以新建一个 目录,也可以就用原来的目录。 为简单起见,使用原来的目录。 #### 4.3.4 部署文件说明 Clone项目后,手动部署文件保存在 `deploy/`目录下,在`depoly/objects/`目录下还有一组按对象单独编写的声明文件。本例使用 `objects`目录下,这样部署文件的结构更清晰。 ``` $ cd /deploy/objects $ tree ├── README.md ├── class.yaml # StorageClass ├── clusterrole.yaml # ClusterRole ├── clusterrolebinding.yaml # ClusterRoleBinding ├── deployment.yaml # Deployment ├── role.yaml # Role ├── rolebinding.yaml # RoleBinding └── serviceaccount.yaml # ServiceAccount ``` 核心文件是 deployment.yaml 用来部署Provisioner POD。 class.yaml 是StorageClass的示例文件。 其他文件是RBAC所需的授权资源。 ##### 4.3.4.1 RBAC 文件 前文介绍了Provisoner所需的资源授权,来看一下 nfs-subdir的ClusterRole授权: ```yaml kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: nfs-client-provisioner-runner rules: - apiGroups: [""] resources: ["nodes"] verbs: ["get", "list", "watch"] - apiGroups: [""] resources: ["persistentvolumes"] verbs: ["get", "list", "watch", "create", "delete"] - apiGroups: [""] resources: ["persistentvolumeclaims"] verbs: ["get", "list", "watch", "update"] - apiGroups: ["storage.k8s.io"] resources: ["storageclasses"] verbs: ["get", "list", "watch"] - apiGroups: [""] resources: ["events"] verbs: ["create", "update", "patch"] ``` 该授权中增加了node和event的授权。 > 由于 persistentVolume是集群级别(非名空间)资源,因此,需要使用ClusterRole。 > role.yaml的用途,推测应为 Leader election的。 serviceaccount.yaml定义了nfs-client-provisioner 账户。 clusterrolebinding.yaml将ClusterRole 绑定到 ServiceAccount: ```yaml kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: run-nfs-client-provisioner subjects: - kind: ServiceAccount name: nfs-client-provisioner # replace with namespace where provisioner is deployed namespace: default roleRef: kind: ClusterRole name: nfs-client-provisioner-runner apiGroup: rbac.authorization.k8s.io ``` ##### 4.3.4.2 StorageClass文件 class.yaml定义了storageClss 的示例: ```yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-client provisioner: k8s-sigs.io/nfs-subdir-external-provisioner # or choose another name, must match deployment's env PROVISIONER_NAME' parameters: archiveOnDelete: "false" ``` nfs-subdir 有三个paramter, 用于控制PV删除时的数据处理方式,archive是指将删除的PV目录归档。 这里定义的 metadata.name 和 provisioner 名字都是可以修改的。 注意应保持 provisioner 和 deployment 中的 环境变量 `PROVISIONER_NAME` 一致。 ##### 4.3.4.3 Deployment 文件 重点看 deployment.yaml 中的 pod template 部分: ```yaml spec: # 使用 nfs-client-provisioner 账户运用 serviceAccountName: nfs-client-provisioner containers: - name: nfs-client-provisioner image: k8s.gcr.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2 volumeMounts: - name: nfs-client-root mountPath: /persistentvolumes env: - name: PROVISIONER_NAME value: k8s-sigs.io/nfs-subdir-external-provisioner - name: NFS_SERVER value: 10.3.243.101 - name: NFS_PATH value: /ifs/kubernetes volumes: - name: nfs-client-root nfs: server: 10.3.243.101 path: /ifs/kubernetes ``` 可以看到,nfs-subdir的实现方式,是将一个NFS目录作为Volume挂载到容器,再为PVC建立subdir的PV。 部署前,需要修改这几项内容,指向预备好的NFS Server。 ##### 4.3.4.4 修改namespace 上述文件中均已绑定namespace为default,如需部署在其他名空间,需要将其修改为自定义的名空间。 #### 4.3.5 部署nfs-subdir-provisoner 本例中已将相关文件复制到samples/nfs-subdir/, 并修改其中的deployment.yaml 中的 NFS 信息: ```yaml spec: serviceAccountName: nfs-client-provisioner containers: - name: nfs-client-provisioner # 无法使用k8s.gcr.io,使用dockerio的镜像 image: dyrnq/nfs-subdir-external-provisioner:v4.0.2 # image: k8s.gcr.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2 volumeMounts: - name: nfs-client-root mountPath: /persistentvolumes env: - name: PROVISIONER_NAME value: k8s-sigs.io/nfs-subdir-external-provisioner - name: NFS_SERVER value: nfs-server - name: NFS_PATH value: /var/data/nfs volumes: - name: nfs-client-root nfs: path: /var/data/nfs server: nfs-server ``` 本例中使用 `nfs-server`代替ip地址。需要在节点的/etc/hosts中添加主机名如: ``` 172.24.129.138 nfs-server ``` 使用kubectl部署目录中的全部yaml 即可,: ``` $ cd samples/nfs-subdir/ $ kubectl apply -f samples/nfs-subdir/ storageclass.storage.k8s.io/nfs-client created clusterrole.rbac.authorization.k8s.io/nfs-client-provisioner-runner created clusterrolebinding.rbac.authorization.k8s.io/run-nfs-client-provisioner created deployment.apps/nfs-client-provisioner created role.rbac.authorization.k8s.io/leader-locking-nfs-client-provisioner created rolebinding.rbac.authorization.k8s.io/leader-locking-nfs-client-provisioner created serviceaccount/nfs-client-provisioner created $ kubectl get pods NAME READY STATUS RESTARTS AGE nfs-client-provisioner-b8fdbfc6b-82wbd 1/1 Running 0 8s ``` #### 4.3.6 测试 只需要修改之前的 StatefulSet 的 StorageClass 即可: ```yaml # samples/nginx-nfs-subdir-statefulset.yaml volumeClaimTemplates: - metadata: name: nfs-path spec: accessModes: [ "ReadWriteOnce" ] storageClassName: nfs-client resources: requests: storage: 100Mi ``` 部署即可: ``` # 删除原nginx-sts $ kubectl delete -f samples/nginx-nfs-statefulset.yaml statefulset.apps "nginx-sts" deleted $ kubectl apply -f samples/nginx-nfs-subdir-statefulset.yaml statefulset.apps/nginx-sts created $ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE local-pvc Bound local-pv 100Mi RWO local-storage 5d3h nfs-path-nginx-sts-0 Bound nfs-pv1 200Mi RWO nfs-static-storage 3d nfs-path-nginx-sts-1 Bound nfs-pv2 200Mi RWO nfs-static-storage 3d nfs-path-nginx-sts-2 Bound nfs-pv3 200Mi RWO nfs-static-storage 3d nfs-path-nginx-sts-3 Pending nfs-static-storage 2d2h ``` 注意到 nginx-sts 仍然使用了旧的PV, 这是因为 StatefulSet 保留了PVC, 因此, 重启后仍使用该pvc. 需要手动删除pvc. ``` # 先缩至0 $ kubectl scale sts/nginx-sts --replicas=0 statefulset.apps/nginx-sts scaled $ kubectl delete pvc -l app/name=nginx persistentvolumeclaim "nfs-path-nginx-sts-0" deleted persistentvolumeclaim "nfs-path-nginx-sts-1" deleted persistentvolumeclaim "nfs-path-nginx-sts-2" deleted persistentvolumeclaim "nfs-path-nginx-sts-3" deleted # 再改回 3 $ kubectl scale sts/nginx-sts --replicas=3 statefulset.apps/nginx-sts scaled $ kubectl get pvc -l app/name=nginx NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE nfs-path-nginx-sts-0 Bound pvc-28d05ee3-bdeb-47d1-9f1a-75e961ddff99 100Mi RWO nfs-client 15s nfs-path-nginx-sts-1 Bound pvc-d9e8d3bb-35be-49b7-b2d1-353886ebd4c3 100Mi RWO nfs-client 11s nfs-path-nginx-sts-2 Bound pvc-b889b9e3-1253-474b-bb78-7deeaf994bf1 100Mi RWO nfs-client 7s ``` 可以看一下NFS服务的目录: ``` $ ls /var/data/nfs/ default-nfs-path-nginx-sts-0-pvc-28d05ee3-bdeb-47d1-9f1a-75e961ddff99 default-nfs-path-nginx-sts-1-pvc-d9e8d3bb-35be-49b7-b2d1-353886ebd4c3 default-nfs-path-nginx-sts-2-pvc-b889b9e3-1253-474b-bb78-7deeaf994bf1 ``` 可以看到建立了三个子目录。 ## 5. Helm ### 5.1 Helm简介 Helm 是 K8S 资源包管理工具,也是官方推荐使用的工具。 在前一个例子中,使用 nfs-subdir 的时候,需要将 namespace 修改为想要的内容。会不会有此疑问: * K8S YAML文件为什么不能使用变量呢? 想要更方便的复用K8S资源文件,必须提供方便的可配置的方法。Helm就应运而生了。 Helm 分为两部分功能: 1. 包管理:提供了一套组织K8S资源文件的结构,称之为包,Helm提供仓库(repo),允许用户上传、下载K8S资源包。Helm 还提供包依赖、包版本管理。简言之,就是 K8S 的 apt/yum。 2. 模板技术:提供了扩展的Golang template,可以方便的对一组资源文件进行模板化编写。 仓库和包管理的功能,使得Helm社区壮大,可以在Helm的官方仓库找到大量的K8S资源包。许多开源产品也提供Helm安装包,这些包可以简单配置后使用,也可以对其进行修改。 掌握Helm的重点是如何利用模板技术,编写简洁的,方便复用的 K8S资源包。 可参考官方文档:https://helm.sh/zh/docs/ ### 5.2 Helm 基础 #### 5.2.1 安装 Helm 使用 Go 语言编写,继承了一贯简单风格,仅有一个可执行文件`helm`。 直接从 github release页面下载即可: ``` $ curl -LO https://get.helm.sh/helm-v3.10.3-linux-amd64.tar.gz ``` 将其解压并移动到 /usr/bin或其他系统路径。安装即可完成。 #### 5.2.2 包结构 使用 helm 创建一个样例包: ``` $ helm create nginx-sts Creating nginx-sts $ tree nginx-sts/ nginx-sts/ ├── Chart.yaml # Chart 声明 ├── charts # 依赖子chart ├── templates # 模板目录,资源等放在该目录下 │   ├── NOTES.txt # 说明文件 │   ├── _helpers.tpl # 用于声明命名模板 │   ├── deployment.yaml # deployment 样例 │   ├── hpa.yaml # 自动缩放控制器 样例 │   ├── ingress.yaml # ingress 样例 │   ├── service.yaml # Service 样例 │   ├── serviceaccount.yaml # 服务账户 样例 │   └── tests # 测试 │   └── test-connection.yaml # 测试 Service 样例 └── values.yaml # Chart 使用的 变量 数据 ``` templates目录下的文件在安装时都会作为模板进行处理。 下文将分章节说明各部分文件的用途。 #### 5.2.3 values.yaml values.yaml中包含了`配置`数据,在Helm文档称之为`configuration values`。 ``` $ cat values.yaml ``` ```yaml # Default values for nginx-sts. # This is a YAML-formatted file. # Declare variables to be passed into your templates. replicaCount: 1 image: repository: nginx pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. tag: "" imagePullSecrets: [] nameOverride: "" fullnameOverride: "" # ... ``` 这里定义的变量,可以转换为Values.的成员变量,比如: ``` {{ .Values.replicaCount }} {{ .Values.image.repository }} {{ .Values.imagePullSecrets.[0] }} ``` 在Helm提供的样例Values基础上是补充所需的变量推荐的方法。 根据这些变量的名字,大致就能猜到它的用途,比如: * imagePullSecrets是用于定义 containers 镜像服务器认证信息的。 * replicaCount是用于deployment或StatefulSet 的replicas。 #### 5.2.4 Chart.yaml Chart.yaml 声明 Helm 包。Helm 包就叫做`Chart`。 ```yaml apiVersion: v2 name: nginx-sts description: A Helm chart for Kubernetes # A chart can be either an 'application' or a 'library' chart. type: application # This is the chart version. This version number should be incremented each time you make changes version: 0.1.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. appVersion: "1.16.0" ``` Chart形式类似于K8S的资源声明。 其中的 `version` 是指 Chart版本号,使用版本号的规则,Chart发布时回控制。`appVersion` 是 应用的版本号。这两者的差异是,假设部署一个Prometheus资源,那么,`appVersion`应该是 Prometheus 的版本号。而 `version` 是 Chart 包的版本号。 #### 5.2.5 charts 依赖 在Charts.yaml 中没有描述 子 Chart (sub charts) 的信息。下面简要介绍一下: ```yaml dependencies: # chart 必要条件列表 (可选) - name: chart名称 (nginx) version: chart版本 ("1.2.3") repository: (可选)仓库URL ("https://example.com/charts") 或别名 ("@repo-name") condition: (可选) 解析为布尔值的yaml路径,用于启用/禁用chart (e.g. subchart1.enabled ) tags: # (可选) - 用于一次启用/禁用 一组chart的tag import-values: # (可选) - ImportValue 保存源值到导入父键的映射。每项可以是字符串或者一对子/父列表项 alias: (可选) chart中使用的别名。当你要多次添加相同的chart时会很有用 ``` 使用 `dependencies` 罗列子chart信息。包括 名称,版本,以及所在的Helm仓库。 其他内容并不常用。 定义了dependencies 后,使用命令: `helm dependency update` 或简写 `helm dep up` 就可以将子Chart下载到charts目录下。 在helm执行安装时,会自动安装子chart。(也可以使用 tags , condition 等选项来控制是否安装) 在values.yaml中可以定义子chart的配置数据,使用sub chart 的名字,如: ```yaml subChartName: data1: data2: # 或者用别名 subChartAliasName: data1: ``` SubChart模板执行时,`.Values`的内容是 `subChartName`。 #### 5.2.6 templates Helm使用Golang 模板定义所有的资源文件,看一下deployment.yaml例子: ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "nginx-sts.fullname" . }} labels: {{- include "nginx-sts.labels" . | nindent 4 }} spec: {{- if not .Values.autoscaling.enabled }} replicas: {{ .Values.replicaCount }} {{- end }} selector: matchLabels: {{- include "nginx-sts.selectorLabels" . | nindent 6 }} # ``` 看这个例子比较复杂,其中大量使用了`命名模版`。但从含义上大致能看出来模板的意思。比如: * `name: {{ include "nginx-sts.fullname" . }}` include 是包含 一个`命名模板`。 * `replicas: {{ .Values.replicaCount }}` ,使用 values.yaml 中的 replicaCount 替换。 * `{{- if }} ... {{- end }}`,这是 if 流程控制。 下文会简要介绍go模板的语法和常用方法。 ### 5.3 Helm/Go模板 Go 模板是 Go语言发明的一种模板技术。广泛应用于Go生态。Go 模板的最大优点是可以自由的引入go语言函数。 GO 模板语句使用 `{{ }}` 包含,其中的内容将作为模板进行渲染(替换)。 Go 模板技术较容易掌握,Helm官方文档有详尽的介绍和例子。Helm对go模板进行了一些扩展(主要是增加了函数)。 Helm 模板主要包括以下内容: * 访问对象:go 的对象传递至模板中,可以访问其成员。 * 函数及管道:在 go 中调用函数,或使用管道调用函数。 * 流程控制:if-else 、range 循环。 * 变量:使用 `$varname:= ` 来声明变量。 * 命名模板:可以理解为子模版。使用 define 定义命名模板,使用 include 或 template来执行命名模板。 #### 5.3.1 访问对象(Helm内置对象) Helm 向模板传递的对象称之为`内置对象`。包括: * Values: Values.yaml中定义了配置数据。 * Chart: Chart.yaml的配置。 * Release: Helm 执行版本发布的数据。 * Template: 当前模板的信息。如:Name(模板文件名),BasePath(路径)等。 * Capabilities: 提供K8S集群的版本等支持性信息。 另有工具类:Files,提供一组函数用于访问资源文件。 在模板中访问对象的方法是:` {{ .Chart.name }} `, 其中 `.` 指示`当前对象`。后面的则是按照成员层次访问。 `当前对象`随着作用域会发生变化: * with: 使用with 会改变当前对象, * 如:`{{ with .Chart }} {{ .name }} {{ end }}。将.Chart设定为当前变量,类似于JavaScript 的with。 * range: 使用range 循环时,循环作用域内的当前对象,是range的迭代item。 * 如: `{{ range .Values.imagePullSecrets }} {{ . }} {{ end }}`,range 作用域中的 `.`代表.Values.imagePullSecrets 列表的 item。 > 可以向命名模板传递对象,该对象将作为模板中的`当前对象`。 在模板输出中渲染对象的值的方法,就是直接使用 {{ .Chart.name }}。 #### 5.3.2 调用函数 GO 模板中的函数是除了if-else end range with 等保留字之外的任何 `名称`,均作为函数调用。 如:`{{ sum 1 2 }}`,sum 就是函数名, 1 2 是它的两个参数,相当于:`sum (1,2)` 。 > 因此也可以理解,为什么引用对象时需要使用 `.` 开头。 另一种调用方法是使用`管道`。类似于 linux shell 中的管道概念: * 一个命令的输出,作为另一个命令的输入。`cat etc/hosts | more` GO 模板的管道是: * 一个表达式(函数,对象,变量等)的返回值,作为 另一个函数的输入参数。当然,参数只能有一个。如: * {{ .Chart.name | upper | quote }}: 相当于 `quote ( upper ( .Chart.Name ) )`, 把名字大写,并加双引号。 #### 5.3.3 定义变量 GO 中可以定义变量,语法是:`{{ $varname := }}`。 变量是有作用域的,在range with 等作用域的变量仅在此范围内有效。 特殊变量 `$` 指代根对象,在任何作用域中,使用 `{{ $.Values... }}` 都是可以访问到 chart 的 Values.yaml数据的。 变量在range中比较有用: ``` toppings: |- {{- range $index, $topping := .Values.pizzaToppings }} {{ $index }}: {{ $topping }} {{- end }} # 对于列表类型的迭代,$index是item序号,$stopping 是 item 。 {{- range $key, $val := .Values.favorite }} {{ $key }}: {{ $val | quote }} {{- end }} # 对于 map 的迭代,$key 是 key键值, $val 是value. ``` #### 5.2.7 流程控制 循环 range 和 with 在上文已有介绍。 经典的 if - else 语法如下: ``` {{ if 表达式 }} {{ else }} {{ end }} ``` 表达式,在 Go 使用术语 pipeline。实质是指 if 后的条件是一系列函数,或者对象。 模板不使用 boolean 表达式,而是使用函数替代。如: * eq : 等于 * ne: 不等 * not : 非 * gt: * lt: * and: * or: * ... #### 5.2.8 计算 数学计算符号+-*/都没有,全部使用函数实现: * add * sub * ... #### 5.2.9 命名模板 可以使用 define 定义命名模板,使用 include 或 template来执行模板。 如: ``` {{ define "chart.labels"}} app/name: {{ .name }}-test app/version: "1.0" {{ end }} ``` 调用时: ``` {{ template "chart.labels" .Chart }} ``` 其中的 `.Chart` 很重要,template 将使用 .Chart 作为其 根对象。 通常会使用 include 而非 template 来调用 模板,这是因为 template 无法进行灵活的输出控制。 > 两者的区别:可以将 template 理解为无返回值的函数,在函数中直接输出结果。而include 理解为有返回值函数,可以使用管道对其结果进行处理。 #### 5.2.10 空白控制 YAML文件是使用空白来表示文件结构的,因此,模板中如何处理空白变得异常重要。。。 GO 模板提供了下列方法来控制: * `{{- .Chart.name }}`: 左边的 `-` 表示 删除左边的全部空白。 * `{{ .Chart.name -}}`: 右边的 `-` 表示 删除右边的空白,注意,包括换行符。 * `{{- .Chart.name | indent 6 }}`: 使用函数 indent 对文本进行 缩进,这里指定缩进6个空格。 可以看到官方的各种例子中大量使用上述技巧。 #### 5.2.11 常用函数 集合函数: * list: 创建一个集合 `$ l := list 1 2 3 4 5 6 ` * tuple: 创建一个 tuple `tuple "a" 1, "3" ` 字符串函数: * qoute: 加引号。sqoute是单引号。 * printf: 格式化 `printf "%s has %d dogs." .Name .NumberDogs` * cat: 用空格连接字符串,相当于`javascript join` * indent: 退格。 * contains,hasPrefix,hasSuffix:检查字符串中是否包含(或前缀、后缀)在另一个字符串。`contains "cat" "catch" `. * substr: 取子串,substr startIndex endIndex string,`substr 0 5 "hello world"` * trim: 去掉两端空格。 * trimAll, trimPrefix,trimSuffix: 移除全部(或前缀,后缀)字符, `trimAll "$" "$5.00"` * lower, upper, title, untitle,camelcase,snakecase,kebabcase:各类大小写转换。 base64: * b64enc: base64编码 * b64dec: base64解码 ### 5.3 Helm 创建示例 本节使用Helm 创建之前的nginx-nfs-subdir-sts。 #### 5.3.1 内容 nginx-nfs-subdir-sts 中包括: * StatefulSet * nfs-subdir-provisioner * StorageClass * Servive * Ingress * Namespace 使用一个Ingress + Service 来对外暴露 StatefulSet。 StatefulSet 使用 StorageClass 引用 nfs-subdir-provisioner。 另外,创建一个Namespace来自动创建名空间。 #### 5.3.2 开始 仍使用nginx-sts作为helm 名称。可以使用 `helm create nginx-sts` 创建。 #### 5.3.3 添加nfs-subdir依赖 在Chart.yml中添加依赖: ```yaml dependencies: - name: nfs-subdir-external-provisioner repository: https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/ version: 4.0.16 ``` 下载依赖包: ``` $ cd helm nginx-sts $ helm dep up Getting updates for unmanaged Helm repositories... ...Successfully got an update from the "https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/" chart repository Saving 1 charts Downloading nfs-subdir-external-provisioner from repo https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/ Deleting outdated charts $ ls charts nfs-subdir-external-provisioner-4.0.16.tgz ``` `nfs-subdir` chart下载成功,打包的格式是 tgz。 可以将该包接开,并查看其中的 values.yaml,这里包含安装时的配置数据。从中摘了一些需要使用的数据项: ```yaml image: # 由于难以访问google镜像,此处需要修改。 repository: k8s.gcr.io/sig-storage/nfs-subdir-external-provisioner tag: v4.0.2 pullPolicy: IfNotPresent nfs: # nfs服务器的地址,需要配置。 server: # nfs export 路径,需要配置 path: /nfs-storage mountOptions: volumeName: nfs-subdir-external-provisioner-root # Reclaim policy for the main nfs volume reclaimPolicy: Retain # For creating the StorageClass automatically: storageClass: create: true # Set a provisioner name. If unset, a name will be generated. # provisionerName: # Set StorageClass as the default StorageClass # Ignored if storageClass.create is false defaultClass: false # Set a StorageClass name # Ignored if storageClass.create is false # 在VolumeClaimTemplate需要使用 storage class 名字 name: nfs-client ``` 在values.yaml中定义 上述数据: ```yaml nfs-subdir-external-provisioner: image: repository: dyrnq/nfs-subdir-external-provisioner tag: v4.0.2 nfs: server: nfs-server path: /var/data/nfs ``` 使用 sub chart 的名字,或别名,定义其配置数据, `nfs-subdir-external-provisioner` 包含的配置数据将和values.yaml 中的数据合并。 storageclass 的值,也可以定义在values.yaml中,但这里想要演示以下 import-values 的功能:因此,在Chart.yaml添加: ```yaml dependencies: - name: nfs-subdir-external-provisioner repository: https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/ version: 4.0.16 import-values: - child: storageClass parent: nfsStorageClass ``` 增加了 `import-values` 声明,将 sub chart 的 `storageClass`数据引入到 parent Chart 的 `nfsStorageClass`,这样就可以使用 `.Values.nfsStorageClass.name` 来引用。 #### 5.3.4 声明 StatefulSet 可以在原有的 StatefulSet `nginx-nfs-subdir-statefulset.yaml` 中修改就可以完成。 ##### 5.3.4.1 metadata 首先来看,metadata部分: ```yaml apiVersion: apps/v1 kind: StatefulSet metadata: name: {{ include "nginx-sts.fullname" . }} labels: {{- include "nginx-sts.labels" . | nindent 4 }} ``` 这里使用了两个`命名模板`,这两个模板定义在自动生成的 _helper.tpl中: ```yaml {{/* Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} {{- define "nginx-sts.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} {{- $name := default .Chart.Name .Values.nameOverride }} {{- if contains $name .Release.Name }} {{- .Release.Name | trunc 63 | trimSuffix "-" }} {{- else }} {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} {{- end }} {{- end }} {{- end }} ``` 对nginx-sts-fullname说明: * `{{/* */}}`: 注释,不会渲染。 * `{{- define "nginx-sts.fullname" -}}`: 声明`命名模板`。注意前后`-`会删除所有空格包括换行符。 * `{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}`:trunc 63 是截断字符串保留最多63个字符。 * `{{- $name := default .Chart.Name .Values.nameOverride }}`: default 是指当第一个参数为空时,使用第二参数值。还可写作:`.ChartName | default .Values.nameOverride` * 关注上述流程控制语句的空白处理。 这是一个Helm推荐模板,使用Release.name ,Chart.name 和 nameOrride来定义最终的资源名称。 ```yaml {{/* Common labels */}} {{- define "nginx-sts.labels" -}} helm.sh/chart: {{ include "nginx-sts.chart" . }} {{ include "nginx-sts.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "nginx-sts.selectorLabels" -}} app.kubernetes.io/name: {{ include "nginx-sts.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} ``` 对象的标签,这里使用了 K8S 官网的 建议 使用的 标签。 由于 app.kubernetes.io名字比较长,可以根据自己组织的命名来修改,如: `app./name`。或者直接写作 `app/name`这种比较短的模式。 本例中将去掉k8s.io的名字,这样在使用命令行时比较方便。 当然,这些模板不是必须使用的,只是一种惯例。初期会提高学习难度,如果觉得麻烦,可以简化。 另,可以加上一个storage class 标签。那么,可以直接在其中增加: ```yaml {{- if .Values.nfsStorageClass.name }} app/storage: {{ if .Values.nfsStorageClass.name }} {{- end }} ``` ##### 5.3.4.2 spec spec部分: ```yaml spec: serviceName: {{ .Values.service.name }} {{- if not .Values.autoscaling.enabled }} replicas: {{ .Values.replicaCount }} {{- end }} selector: matchLabels: {{- include "nginx-sts.selectorLabels" . | nindent 6 }} volumeClaimTemplates: - metadata: name: {{ .Values.volumeMount.name }} spec: accessModes: [ "ReadWriteOnce" ] storageClassName: {{ .Values.nfsStorageClass.name }} resources: requests: storage: 100Mi ``` 这部分 matchLabels内容都是相同的。使用了三个配置: ``` service: name: web-service type: ClusterIP port: 80 volumeMount: name: nfs-path path: "/usr/share/nginx/html/nfs" ``` ##### 5.3.4.3 pod template Podtemplate 部分可以采用标准模板来实现: ```yaml template: metadata: {{- with .Values.podAnnotations }} annotations: {{- toYaml . | nindent 8 }} {{- end }} labels: {{- include "nginx-sts.selectorLabels" . | nindent 8 }} ``` labels 使用了 selectorLabels。 container部分: ```yaml spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} containers: - name: {{ .Chart.Name }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - name: http containerPort: {{ .Values.service.port }} protocol: TCP livenessProbe: httpGet: path: / port: 80 initialDelaySeconds: 2 periodSeconds: 30 volumeMounts: - mountPath: {{ .Values.volumeMount.path }} name: {{ .Values.volumeMount.name }} ``` 其中引用了 Image 和 vloumeMount等配置数据。 `toYaml` 的作用是将对象按yaml格式渲染。注意`indent`的使用,必须符合缩进要求。 #### 5.3.5 service Service 内容很简单,使用Helm默认service.yaml即可: ```yaml apiVersion: v1 kind: Service metadata: name: {{ include "nginx-sts.fullname" . }} labels: {{- include "nginx-sts.labels" . | nindent 4 }} spec: type: {{ .Values.service.type }} ports: - port: {{ .Values.service.port }} targetPort: http protocol: TCP name: http selector: {{- include "nginx-sts.selectorLabels" . | nindent 4 }} ``` 这里的service type 在values中定义,使用了ClusterIP。ClusterIP 是将 服务集中在一个虚拟IP 上。 注意 selector 使用和其他资源相同的 labels。这样,在使用 kubectl get 时可以使用一致的查询方式。 #### 5.3.5 ingress ingress 基本上,使用Helm默认的ingress.yaml就可以了,只是需要在values.yaml中补充hosts映射。 如: ```yaml # valaues.yaml ingress: enabled: true className: nginx annotations: # kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" hosts: - host: "s1.example.net" http: paths: - pathType: Prefix path: "/" - host: "*.example.net" http: paths: - pathType: Prefix path: "/nfs" ``` 默认的backend 是 Service.yaml定义的服务。因此,仅把host/path定义即可。 > ingress.yaml官方模板比较复杂,可以按照自己的文件直接修改模板化即可。 #### 5.3.5 ignore Helm会将templates目录下所有yaml文件都作为K8S资源文件提交至K8S,本例中,deployment 和 hpa 都没有使用。一种方法是将之删除,另一种办法是使用 `.helmignore` 文件。 类似与 `.gitignore`,`.helmignore`用来忽略不需要的文件。 ```yaml # 在文件中补充 deployment.yaml hpa.yaml ``` 在部署时,helm 会忽略ignore 的文件。 #### 5.3.5 部署 使用 helm 部署时,会引入一个新的概念:Release。 由于Helm Chart 是一个资源包文件,采用了模板化的手段,很容易使用不同配置数据部署多个实例,每一个部署的实例,都是一个Release。Release时指定的名称,用于识别不同的实例。 如使用名称 my 来部署例子: ``` $ helm install my ./nginx-sts NAME: my LAST DEPLOYED: Sat Jan 14 19:33:36 2023 NAMESPACE: default STATUS: deployed REVISION: 1 NOTES: 1. Get the application URL by running these commands: http://s1.example.net/ http://*.example.net/nfs $ helm list NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION my default 1 2023-01-14 19:33:36.628034145 +0800 CST deployed nginx-sts-0.1.0 1.16.0 ``` 看一下 my 的资源: ``` $ kubectl get all -l helm.sh/chart=nginx-sts-0.1.0 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/my-nginx-sts ClusterIP 10.99.27.212 80/TCP 12m NAME READY AGE statefulset.apps/my-nginx-sts 3/3 12m ``` 使用 label `helm.sh/chart=nginx-sts-0.1.0` 来筛选,。 注意发布后的资源名称变成了 `my-nginx-sts`,相当于 release 和 chart 名字的组合。这是 Helm 标准模板的写法。当然可以按照自己的需求进行修改。但标准的做法是一种很好的实践。 Helm 可以对 release 进行管理,包括:install、delete、upgrade。 可以查看 helm 管理的资源信息: ``` $ kubectl get svc/my-nginx-sts -o jsonpath="{.metadata.managedFields}" | jq ``` ```json [ { "apiVersion": "v1", "fieldsType": "FieldsV1", "fieldsV1": { "f:metadata": { "f:annotations": { ".": {}, "f:meta.helm.sh/release-name": {}, "f:meta.helm.sh/release-namespace": {} }, "f:labels": { ".": {}, "f:app.kubernetes.io/managed-by": {}, "f:app.kubernetes.ioa/managed-by": {}, "f:app/instance": {}, "f:app/name": {}, "f:app/version": {}, "f:helm.sh/chart": {} } } // ... ] ``` 资源的 managedFields 包含了 Helm 管理的 注解、标签等信息。Helm通过这些标签可以追溯Release的资源。 #### 5.4 调试及其他 在编写 Chart 过程中,注意使用增量方式进行测试,避免出现 YAML 格式问题。测试方式为: ``` $ helm lint nginx-sts ==> Linting nginx-sts [INFO] Chart.yaml: icon is recommended 1 chart(s) linted, 0 chart(s) failed # 或 $ helm install --dryrun ``` `dryrun` 不会真正的向 K8S 发布资源,只是会进行K8S资源的语法检查,错误信息会提示到具体的模板行,根据这一信息可以进行修改。 还可以使用: ``` $ helm template --dryrun ``` 这将输出模板渲染结果。将其保存后可以检查错误信息。 当安装后也可以从K8S 获取资源信息,如: ``` helm get manifest ``` helm 使用 kubectl 相同的 `.kube/config` 环境配置。 > TODO: Helm 官网信息完备,本部分介绍的不充分(比如:文件处理)。 ## 6. 监控 ### 6.1 监控服务概况 系统监控通用架构是采用下述组件架构: ```mermaid graph BT c((Collector)) s[Monitor-TSDB] ui[Visualization UI] a[Alert] c -- Push/Pull --> s ui -.-> s a -.-> s ``` 各组件包括: * Collector: 采集器,部署在目标上,收集监视数据(metrics) * Monitor:监视服务,从Collector抓取metrics数据(或Collector推送),保存在 TSDB中。 * Visualization: 监视metric的可视化。 * Alert: 告警,配置告警规则,并可推送告警信息至外部。 流行的K8S的监控架构也是如此: * Collector: cAdvisor,采集节点及容器的metric。 * Monitor: 自然是 Prometheus。 * 可视化:Granfana。 * Alert:Prometheus自带AlertManager。 ### 6.2 监控方案 #### 6.2.1 cAdvidor cAdvisor 是google提供的容器指标采集程序。K8S已经将其集成在Kubelet服务中,也就是说,不在需要单独安装cAdvisor。 cAdvisor通过api-server对外提供metrics,当然支持Prometheus格式的metrics。 K8S的cAdvisor接口集成至核心api的node资源中,通过访问api-server即可得到节点和容器的metrics。 ``` $ kubectl get --raw /api/v1/nodes/minikube/proxy/metrics/cadvisor | grep my-nginx-sts | grep cpu_load | tail -2 container_cpu_load_average_10s{container="nginx-sts",id="/docker/f89bc797ed5cb3cef55e14974c38e8d01609deed0c0df68c2368f7bc4ff19f8d/kubepods/besteffort/pod61ff6ed8-6cc5-4232-8733-1cc3e1d7c601/d5a3babf817595d825e53f0090e9f42541d67dfec47f1027d41ee45018d27d37",image="sha256:ae893c58d83fe2bd391fbec97f5576c9a34fea55b4ee9daf15feb9620b14b226",name="k8s_nginx-sts_my-nginx-sts-2_default_61ff6ed8-6cc5-4232-8733-1cc3e1d7c601_0",namespace="default",pod="my-nginx-sts-2"} 0 1673786242701 container_cpu_load_average_10s{container="nginx-sts",id="/kubepods/besteffort/pod61ff6ed8-6cc5-4232-8733-1cc3e1d7c601/d5a3babf817595d825e53f0090e9f42541d67dfec47f1027d41ee45018d27d37",image="sha256:ae893c58d83fe2bd391fbec97f5576c9a34fea55b4ee9daf15feb9620b14b226",name="k8s_nginx-sts_my-nginx-sts-2_default_61ff6ed8-6cc5-4232-8733-1cc3e1d7c601_0",namespace="default",pod="my-nginx-sts-2"} 0 1673786233004 ``` 上述命令可以看到 POD my-nginx-sts-2的cpu使用情况, 使用kubectl访问api-server 即可得到metric数据, 其URL为:`/api/v1/nodes//proxy/metrics/cadvisor`。 还可以通过 kubelet 端口访问,`/metrics/cadvisor` #### 6.2.2 kube-state-metrics cadvisor 暴露的是 容器相关的性能指标,而非 K8S 资源 ,如: Deployment/Statefullset 等状态是无法得到的,作为补充,`kue-state-metrics` 会更为方便, #### 6.2.3 Prometheus Prometheus是目前主流的 监控系统 TSBD。它具备水平扩展的能力,大量的exportor可以深入监视不同的应用系统。 Prometheus定时抓取数据,并提供PromQL进行聚合查询。部署Prometheus的重点是如何采集数据。 Prometheus通过配置http/s端点来抓取数据,配置内容称为 scrape_config,如: ```yaml # The job name assigned to scraped metrics by default. job_name: static_config: # The targets specified by the static config. targets: [ - '' ] ``` static_config可以配置静态的(不变的)scrape endpiont。而在K8S中,绝大部分Metrics是从Pod暴露的,而Pod的IP是随机分配的,(虽然 StatefullSet DNS是稳定的,但并不能涵盖所有的需求)。 因此,Prometheus 提供了动态的配置机制,称之为 Service Discovery,简称SD,用来支持动态配置。特别针对 K8S 提供了 kubernetes_sd_configs 。如: ```yaml job_name: "kubernetes-cadvisor" kubernetes_sd_configs: role: node scheme: https metrics_path: /metrics/cadvisor ``` 这是选择了 node kubelet 的 metric ,可以使用 `/metrics`或 `/metrics/cadvisor`来抓取数据。 也可以选择某些POD的端口进行监控。这时需要使用`role: pod`。 #### 6.2.4 Grafana Grafana 与 Prometheus 的集成是成熟的方案。使用Grafana 通过 PromQL 创建监视可视化界面, Grafana 提供了大量的模板,稍加修改即可满足需求。 #### 6.2.5 Prometheus Operator Prometheus Operator 是 为简化Prometheus部署、配置而产生的开源项目,Prometheus的配置文件很复杂,Prometheus Operator 利用K8S 的CRD声明和发布Prometheus配置方案,并提供了很多便利的手段。比如:Operator只需要声明一个 PodMinitor 就可以 自动生成并配置 对特定Pod endpoint metric 抓取。而无需考虑如何捕获Pod端点信息,如何将其加入 Prometheus scrape config 等。 #### 6.2.6 小结 为更深入了解 K8S metric接口 和 Prometheus 的配置机制,将先从不借助 Operator手动配置 Prometheus 开始。此后再介绍 Operator 的原理(K8S CRD)和部署示例。 最终的部署会包括对 K8S 各服务组件的监视(/metrics和 /metrics/cadvisor),K8S资源监视(kube-state-metrics),应用metrics监视(nginx sidecard模式),以及 Prometheus Operator 部署方案。 ### 6.3 手动部署 Prometheus Prometheus 首先是一个 TSDB 时序数据库,它需要持久化数据存储,因此,如果要部署多个实例实现高可用,应考虑使用StatefulSet(也可以使用)。 对K8S 组件的监视,需要了解组件暴露的 endpoint 并将其配置在 Prometheus中。 #### 6.3.1 编制 Prometheus Statefulset 利用之前的知识,编辑一个 Statefulset YAML如下: ```yaml # samples/monitor/prometheus-sts.yml apiVersion: apps/v1 kind: StatefulSet metadata: name: prometheus labels: app/name: prometheus spec: replicas: 1 selector: matchLabels: app/name: prometheus serviceName: prom-service # ... ``` replicas 可以设定为`2`实现高可用。 container 设置,使用了 readiness和liveness: ```yaml containers: - name: prometheus image: prom/prometheus:v2.41.0 ports: - containerPort: 9090 livenessProbe: httpGet: path: /-/healthy port: 9090 initialDelaySeconds: 2 periodSeconds: 30 readinessProbe: httpGet: path: /-/ready port: 9090 volumeMounts: - mountPath: "/prometheus" name: nfs-prom ``` volume 使用了 NFS-subdir 的 vct: ```yaml volumeClaimTemplates: - metadata: name: nfs-prom spec: accessModes: [ "ReadWriteOnce" ] storageClassName: nfs-client resources: requests: storage: 400Mi ``` 另需要创建一个prom-service: ```yaml # samples/monitor/prometheus-svc.yaml apiVersion: v1 kind: Service metadata: name: prom-service spec: type: NodePort selector: app/name: prometheus ports: - protocol: TCP port: 9090 nodePort: 31090 targetPort: 9090 ``` 使用kubectl 部署即可。`kubectl apply -f samples/monitor/` #### 6.3.2 port-forward 开发阶段可以使用 port-forward 命令针对Pod/服务进行端口转发,方便调试: ``` $ kubectl port-forward svc/prom-service 9090:9090 Forwarding from 127.0.0.1:9090 -> 9090 Forwarding from [::1]:9090 -> 9090 ``` 从 localhost:9090可访问到prom-service服务. 从`Status-targets`中可以看到 prometheus 实例已经启动了。 #### 6.3.3 访问 Kubelet metrics ##### 6.3.3.1 外部访问 kubelet metrics 有两种途径可以访问,第一是从api-server代理,访问: ``` # kubelet https:///api/v1/nodes/minikube/proxy/metrics # 节点 cadvisor https:///api/v1/nodes/minikube/proxy/metrics/cadvisor ``` 另一个途径是访问kubelet 端口。 ``` https:///metrics https:///metrics/advisor ``` 首先尝试 api-server,为简化HTTPS认证,可使用 kubectl proxy命令代理一个http端口。访问HTTPS的话,需要客户端证书。使用 kubectl 可以查看到证书,在 3. 安全 部分已经说明了。 ``` $ kubectl config view apiVersion: v1 clusters: - cluster: certificate-authority: ~/.minikube/ca.crt extensions: - extension: last-update: Sat, 14 Jan 2023 18:52:52 CST provider: minikube.sigs.k8s.io version: v1.28.0 name: cluster_info server: https://127.0.0.1:49154 name: minikube ... - name: minikube user: client-certificate: ~/.minikube/profiles/minikube/client.crt client-key: ~/.minikube/profiles/minikube/client.key ``` api-server 地址和用户证书、密钥文件都可以看到。使用 curl 可以访问: ``` $ curl -k https://127.0.0.1:49154/api/v1/nodes/minikube/proxy/metrics/cadvisor \ --cert ~/.minikube/profiles/minikube/client.crt \ --key ~/.minikube/profiles/minikube/client.key ``` 这里使用`-k`跳过服务器证书认证环节。如需认证可指定 ca : `--cacert ~/.minikube/ca.crt`。 非 lolcalhost 地址可以使用命令查看: ``` $ kubectl describe svc/kubernetes Name: kubernetes Namespace: default Labels: component=apiserver provider=kubernetes Annotations: Selector: Type: ClusterIP IP Family Policy: SingleStack IP Families: IPv4 IP: 10.96.0.1 IPs: 10.96.0.1 Port: https 443/TCP TargetPort: 8443/TCP Endpoints: 192.168.49.2:8443 Session Affinity: None Events: ``` 集群外可访问`192.168.49.2:8443`,集群内访问:`10.96.0.1:443` 。 kubelet 运行在每个节点上,默认使用 10250端口提供服务,可以通过节点IP和该端口访问,客户端证书与上文一致: ``` curl -k https://192.168.49.2:10250/metrics \ --cert ~/.minikube/profiles/minikube/client.crt \ --key ~/.minikube/profiles/minikube/client.key ``` ##### 6.3.3.2 Pod 内访问 另一种安全认证方式是使用token,将token添加在HTTP-HEADER中。 在Pod中无法直接访问客户端证书,代之以 ServiceAccount 的 Token。 官方的例子如下: ```sh # 指向内部 API 服务器的主机名 APISERVER=https://kubernetes.default.svc # 服务账号令牌的路径 SERVICEACCOUNT=/var/run/secrets/kubernetes.io/serviceaccount # 读取 Pod 的名字空间 NAMESPACE=$(cat ${SERVICEACCOUNT}/namespace) # 读取服务账号的持有者令牌 TOKEN=$(cat ${SERVICEACCOUNT}/token) # 引用内部证书机构(CA) CACERT=${SERVICEACCOUNT}/ca.crt # 使用令牌访问 API curl --cacert ${CACERT} --header "Authorization: Bearer ${TOKEN}" -X GET ${APISERVER}/api ``` 当Pod启动时,会将ServiceAccount 的令牌 和 CA 证书挂载在 `/var/run/secrets/kubernetes.io/serviceaccount`目录下。通过 https 头中添加认证Token来访问api-server。 #### 6.3.4 配置 prometheus.yaml 只需要将上述地址和认证信息配置在 prometheus 中即可实现监控。为此,修改 Prometheus默认的配置文件/etc/prometheus/prometheus.yml。 ``` # 类似于 docker cp ,从 pod 复制文件。 $ kubectl cp prometheus-0:/etc/prometheus/prometheus.yml samples/monitor/prometheus.yml ``` 编辑 配置文件: ```yaml - job_name: "kubelet" static_configs: - targets: - "192.168.49.2:10250" - "192.168.49.3:10250" scheme: https metrics_path: /metrics tls_config: ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt # cert_file: /var/lib/kubelet/pki/kubelet-client-current.pem # key_file: /var/lib/kubelet/pki/kubelet-client-current.pem # 相当于 curl -k 参数 insecure_skip_verify: true authorization: type: Bearer credentials_file: /var/run/secrets/kubernetes.io/serviceaccount/token ``` 使用 `insecure_skip_verify: true` 来跳过对CA的验证,因为CA签发使用的是域名,并非节点IP。 #### 6.3.5 配置RBAC权限 之前部署的prometheus使用ServiceAccount default并没有访问节点API权限,需要创建新的账户并分配权限。为此创建一个RBAC文件(为方便起见,把ClusterRole ClusterRoleBinding ServiceAccount 写在一个文件中)。 由于节点(nodes)属于集群资源,因此需要使用ClusterRole。 ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: prometheus rules: - apiGroups: [""] resources: - nodes - nodes/metrics - nodes/proxy verbs: - "get" - "list" - "watch" - nonResourceURLs: - "/metrics" - "/metrics/cadvisor" verbs: - get ``` 由于 `/metrics`不属于资源,因此需要使用 `nonResourceURLs`声明。 `nodes/proxy`用于访问api-server代理时。 还需要将 Service Account 指定到 prometheus statefulSet中。 ```yaml spec: serviceAccountName: prometheus ``` apply 上述资源。 #### 6.3.6 配置文件刷新 Prometheus 可以通过两种方式热重启,重新加载配置文件。 1. 使用 `--web.enable-lifecycle=true`启动prometheus,可访问POST /-/reload热重启。 2. 发送signal HUP。`kill -HUP `。 本例使用signal方式: ``` $ kubectl cp samples/monitor/prometheus.yml prometheus-0:/etc/prometheus/prometheus.yml $ kubectl exec prometheus-0 -- kill -HUP 1 ``` > K8S 容器 pid 是1。可通过ps 命令查看。 curl访问prometheus api: ``` $ curl 192.168.49.2:31090/api/v1/targets | jq ``` ```json { "status": "success", "data": { "activeTargets": [ { "labels": { "instance": "192.168.49.3:10250", "job": "kubelet" }, "scrapePool": "kubelet", "scrapeUrl": "https://192.168.49.3:10250/metrics", "globalUrl": "https://192.168.49.3:10250/metrics", "lastError": "", "lastScrape": "2023-01-23T11:36:06.924859999Z", "lastScrapeDuration": 0.048961602, "health": "up", "scrapeInterval": "15s", "scrapeTimeout": "10s" }, { "labels": { "instance": "192.168.49.2:10250", "job": "kubelet" }, "scrapePool": "kubelet", "scrapeUrl": "https://192.168.49.2:10250/metrics", "globalUrl": "https://192.168.49.2:10250/metrics", "lastError": "", "lastScrape": "2023-01-23T11:36:04.687527635Z", "lastScrapeDuration": 0.092662603, "health": "up", "scrapeInterval": "15s", "scrapeTimeout": "10s" }, { "labels": { "instance": "localhost:9090", "job": "prometheus" } } ], "droppedTargets": [] } } ``` 抓取Kubelet metrics 服务已经生效。 #### 6.3.7 使用ConfigMap 可以使用configMap作为配置文件,这样启动时会自动挂载该文件。 ``` $ kubectl create cm prometheus-yaml --from-file=samples/monitor/ prometheus.yml configmap/prometheus-yaml created $ kubectl get cm prometheus-yaml -o yaml apiVersion: v1 data: prometheus.yml: "# my global config\nglobal:\n scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.\n evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.\n # scrape_timeout is set to the global default (10s).\n\n# Alertmanager configuration\nalerting:\n .... kind: ConfigMap metadata: creationTimestamp: "2023-01-24T03:25:10Z" name: promtheus-yaml namespace: default resourceVersion: "1932188" uid: 9cd346ee-3249-42d9-b020-d5a6ce1331f4 ``` 编辑prometheus-sts 添加ConfigMap: ```yaml spec: containers: - name: prometheus # .... volumeMounts: - mountPath: "/prometheus" name: nfs-prom - mountPath: "/etc/prometheus" name: config-yaml volumes: - name: config-yaml configMap: name: prometheus-yaml items: - key: prometheus.yml path: prometheus.yml ``` 将configMap的 prometheus.yml mount 到 /etc/prometheus目录下。这样就可以通过修改configMap来刷新配置文件。 ### 6.4 使用`kubernetes_sd_config` Prometheus 对 K8S 和 Dockers等容器类编排工具都提供了动态配置的功能。 `kubernetes_sd_config`可以通过简单配置来实现动态发现K8S节点、服务、Pod等ip地址的能力(`sd`是`Service Discovery` 的缩写)。 #### 6.4.1 role = node 使用不同的`role`来引用K8S 的节点、服务、POD,之前的配置文件可以简化为: ```yaml - job_name: "kubelet" kubernetes_sd_config: - role: node scheme: https metrics_path: /metrics tls_config: ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt # 相当于 curl -k 参数 insecure_skip_verify: true authorization: type: Bearer credentials_file: /var/run/secrets/kubernetes.io/serviceaccount/token ``` 注意,把 `static_configs targets` 修改为 `kubernetes_sd_config`,动态产生 targets。其他认证等信息仍保持不变。 更新该configMap: ``` $ kubectl delete cm prometheus-yaml $ kubectl create cm prometheus-yaml --from-file=prometheus.yml=samples/monitor/prometheus-sd.yml $ kubectl exec prometheus-0 -- kill -HUP 1 $ kubectl logs prometheus-0 ts=2023-01-24T10:03:02.691Z caller=main.go:1234 level=info msg="Completed loading of configuration file" filename=/etc/prometheus/prometheus.yml totalDuration=17.6796ms db_storage=3.2µs remote_storage=3.6µs web_handler=1.1µs query_engine=1.9µs scrape=16.117ms scrape_sd=683.4µs notify=25.9µs notify_sd=16.3µs rules=2.1µs tracing=9.2µs $ curl 192.168.49.2:31090/api/v1/targets | jq ``` #### 6.4.2 role = pod 发现POD目标时,可指定 role : pod。sd_config会返回符合条件的pod地址和信息。 此时Prometheus 需要具备 对 pod 的访问权限,因此,需在prometheus-rbac.yaml 添加: ```yaml rules: - apiGroups: [""] resources: - nodes - nodes/metrics - nodes/proxy - pods verbs: - "get" - "list" - "watch" ``` 本例来访问 prometheus: ```yaml - job_name: "prom-test" kubernetes_sd_configs: - role: pod selectors: - role: pod label: app/name=prometheus ``` 注意,这里用了 `selectors label` 来选择 一组 pod。其方式与K8S的标签选择相同。 使用 curl 访问 targets后,可得到prom-test的信息: ```json { "discoveredLabels": { "__address__": "10.244.1.7:9090", "__meta_kubernetes_namespace": "default", "__meta_kubernetes_pod_container_image": "prom/prometheus:v2.41.0", "__meta_kubernetes_pod_container_init": "false", "__meta_kubernetes_pod_container_name": "prometheus", "__meta_kubernetes_pod_container_port_number": "9090", "__meta_kubernetes_pod_container_port_protocol": "TCP", "__meta_kubernetes_pod_controller_kind": "StatefulSet", "__meta_kubernetes_pod_controller_name": "prometheus", "__meta_kubernetes_pod_host_ip": "192.168.49.3", "__meta_kubernetes_pod_ip": "10.244.1.7", "__meta_kubernetes_pod_label_app_name": "prometheus", "__meta_kubernetes_pod_label_controller_revision_hash": "prometheus-755bccf47f", "__meta_kubernetes_pod_label_statefulset_kubernetes_io_pod_name": "prometheus-0", "__meta_kubernetes_pod_labelpresent_app_name": "true", "__meta_kubernetes_pod_labelpresent_controller_revision_hash": "true", "__meta_kubernetes_pod_labelpresent_statefulset_kubernetes_io_pod_name": "true", "__meta_kubernetes_pod_name": "prometheus-0", "__meta_kubernetes_pod_node_name": "minikube-m02", "__meta_kubernetes_pod_phase": "Running", "__meta_kubernetes_pod_ready": "true", "__meta_kubernetes_pod_uid": "13ff59c3-e089-48bb-9939-c4aef51050bc", "__metrics_path__": "/metrics", "__scheme__": "http", "__scrape_interval__": "15s", "__scrape_timeout__": "10s", "job": "prom-test" }, "labels": { "instance": "10.244.1.7:9090", "job": "prom-test" }, "scrapePool": "prom-test", "scrapeUrl": "http://10.244.1.7:9090/metrics", "globalUrl": "http://10.244.1.7:9090/metrics", "lastError": "", "lastScrape": "2023-01-24T12:02:57.253072866Z", "lastScrapeDuration": 0.046549599, "health": "up", "scrapeInterval": "15s", "scrapeTimeout": "10s" } ``` 服务发现生成抓取URL: `http://10.244.1.7:9090/metrics`,使用了 pod IP + containerPort + /metrics 组合而成。 在discoverLabels中,包含一组 `__meta_kuberbetes_pod_` 开头的 标签,这些标签包括了 pod 的各项信息,可以使用这些信息来进行 relabling。 #### 6.4.3 role = service 与 pod 类似,这里不进行演示。 需增加权限 services 。 #### 6.4.5 配置监视 nginx Prometheus支持使用 Exporter 获取nginx的监视数据。最简单的 exportor 是 `nginx/nginx-prometheus-exporter`。exporter需要nginx stub_status模块支持。 nginx stub_status 模块提供连接数等简要指标信息。需要在 nginx 配置中添加: ``` location /nginx_status{ stub_status; } ``` 因此,需要为nginx镜像配置一个 ConfigMap。 exporter可以使用单独的容器运行。本例中利用K8S Pod 的sidecard功能,即:在一个pod中部署多个容器,这些镜像之间会共享网络和存储。 Pod template 中 conatiners 是一个数组,因此,在其中添加exporter镜像即可。 本例基于 helm 进行修改。 ##### 6.4.5.1 添加values.yaml配置 在 values.yaml中添加关于 exporter 的配置项: ```yaml exporter: repository: nginx/nginx-prometheus-exporter pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. tag: "0.11" port: 9113 uri: "/nginx_status" ``` 这里定义了 image,端口 以及 stub_status 开放的 uri。 ##### 6.4.5.2 添加 container 在 statefulset.yaml中添加 exporter container: ```yaml - name: {{ .Chart.Name }}-exporter securityContext: {{- toYaml .Values.securityContext | nindent 12 }} image: {{ .Values.exporter.repository }}:{{ .Values.exporter.tag | default "latest" }} imagePullPolicy: {{ .Values.exporter.pullPolicy }} args: - -nginx.scrape-uri=http://localhost:{{ .Values.service.port }}{{ .Values.exporter.uri | default "/nginx_status" }} ports: - name: metrics containerPort: {{ .Values.exporter.port }} protocol: TCP livenessProbe: httpGet: path: / port: metrics resources: {{- toYaml .Values.resources | nindent 12 }} ``` container名字使用了 `{{ .Chart.Name }}-exporter`,本例中为 `nginx-sts-exporter`。 image 部分引用了 `.Values.exporter` 的配置。 exporter的启动参数中,添加 `-nginx.scrape-uri`,注意,多个container之间访问可使用localhost。此配置的URI为:`http://localhost:80/nginx_status`。 ##### 6.4.5.3 添加 ConfigMap 把nginx配置文件复制出来,编写 ConfigMap。这里使用了`range`循环,文件名定义在 configMap.files中: ```yaml configMap: create: true files: - default.conf mountPath: /etc/nginx/conf.d ``` 多个文件可以挂载在一个目录下,本例挂载在 /etc/nginx/conf.d目录下。 使用该信息可以创建 CM: ```yaml data: {{- range .Values.configMap.files }} {{ ( $.Files.Glob . ).AsConfig | indent 2 }} {{- end }} {{- end -}} ``` 这里使用了 Files 获取文件内容,AsConfig函数,将其保存成 `文件名 : 文件内容` 的形式。 ##### 6.4.5.4 mount ConfigMap 在 nginx 容器配置中增加 volumes 和 mount 信息: ```yaml volumes: - name: {{ .Chart.Name }}-cm configMap: name: {{ include "nginx-sts.fullname" . }} items: {{- range .Values.configMap.files }} - key: {{ . }} path: {{ . }} {{- end }} ``` > key 名称 即 path 文件名。 ```yaml volumeMounts: {{- if .Values.configMap.create }} - mountPath: {{ .Values.configMap.mountPath }} name: {{ .Chart.Name }}-cm {{- end }} ``` ##### 6.4.5.5 更新验证 至此,配置完成,使用 helm upgrade 更新 release即可。 ``` $ cd helm $ helm upgrade my nginx-sts Release "my" has been upgraded. Happy Helming! NAME: my LAST DEPLOYED: Wed Jan 25 14:58:16 2023 NAMESPACE: default STATUS: deployed REVISION: 5 NOTES: 1. Get the application URL by running these commands: http://s1.example.net/ http://*.example.net/nfs ``` 执行curl检查配置是否正确: ``` $ kubectl exec my-nginx-sts-0 -- curl localhost:9113/metrics Defaulted container "nginx-sts" out of: nginx-sts, nginx-sts-exporter # HELP nginx_connections_accepted Accepted client connections # TYPE nginx_connections_accepted counter nginx_connections_accepted 508 # HELP nginx_connections_active Active client connections # TYPE nginx_connections_active gauge nginx_connections_active 1 # HELP nginx_connections_handled Handled client connections # TYPE nginx_connections_handled counter ... $ kubectl exec my-nginx-sts-0 -- curl localhost:80/nginx_status Defaulted container "nginx-sts" out of: nginx-sts, nginx-sts-exporter Active connections: 1 server accepts handled requests 512 512 513 Reading: 0 Writing: 1 Waiting: 0 ``` 可见,exporter已经可以工作。 ##### 6.4.5.6 配置 prometheus 这里的重点是筛选出exporter 容器,跳过 nginx 容器,以及如何定义端口号。 exporter使用 9113 端口,当然可以使用 relabel 直接修改 `__address__`。 首先通过 label selector 来选择 nginx-sts 的容器: ```yaml - job_name: "nginx-exporter" kubernetes_sd_configs: - role: pod selectors: - role: pod label: app/name=nginx-sts ``` 再通过 port name 是否是 metric 来判断该容器是否需要保留。如: ```yaml relabel_configs: - source_labels: [__meta_kubernetes_pod_container_port_name] action: keep regex: metrics ``` 注意,仅仅保留 container_port_name == metrics 的 conatainer 和 端口号,此时生成的 URL 就是 `:`。本例中就是 `:9113` 。 更新该配置,并热启动 prometheus 。 访问 prometheus /api/v1/targets 可以看到已经发现了三个exporter实例。 ### 6.5 Alertmanager Prometheus 使用 AlertRules 和 alert manager 配置并管理告警信息。Alertmanager 可以将告警进行分组、抑制、静默操作,并可将告警信息Push到recievers(如:邮件SMTP,Webhook,微信,钉钉)。 #### 6.5.1 运行 Alertmanager 简便起见,直接运行一个 alertmanager : ``` # 类似于 docker run 启动POD。 $ kubectl run alertmanager --image=prom/alertmanager:v0.25.0 --port=9030 pod/alertmanager created # 为其打上标签 $ kubectl label pod/alertmanager app/name=alertmanager pod/alertmanager labeled $ kubectl get pod -L app/name NAME READY STATUS RESTARTS AGE NAME alertmanager 1/1 Running 0 14h alertmanager ``` > 使用 `-L