文章目录
- 前言
- k8s组件与操作流程
- k8s组件
- 创建pod
- k8s
- 代码&&打包
- k8s yaml
- deployment
- service
- k8s volumes
- demo
- CI
- gitlab
- CI runner
- CD
- 配置git repository
- 安装argo
- 创建argo cd的配置yaml
- argocd和helm结合
- argocd hook
- argocd 发布
- RBAC
- operator
- helm
- prometheus && grafna
- prometheus
- metric
前言
本文章主要是记录一套完善的CICD流水线,从golang代码到打包—>到发布(argo)到k8s上,其中包含日志的存储(es),服务监控(Prometheus),监控可视化(grafana)
k8s组件与操作流程
k8s组件
k8s分成control panel和data panel,这2个panel都是node组成,control panel顾名思义就是我们的master节点,data panel就是我们的工作(work)节点…
在data panel中主要是由kubelet,kubelet用于和master节点(master的api server)进行通讯
而在control panel中有多个组件分别为etcd,api server,scheduler,controller manager
- etcd:用于存储所有配置,比如我们的pod的状态等各个resource obj的理想状态(通过yaml设置的)
- scheduler:被api-server激活后用于调度resource obj到work node上
- controller manager:一个大的loop,用于监控集群中所有的对象的状态,并且和etcd中存储的状态做比较,不一样就按照etcd中存储的理想状态进行动态调整
- api-server:非常重要,api-server链接所有的组件(etcd,scheduler,controller manager,kubelet which peer),所有的组件都要通过这个api-server进行通讯,且kubectl用户端也是通过api-server与k8s集群通讯
如上图
注意!!!,我们的集群是kubeadm一键搭建的,什么coredns,etcd,kube-apiserver都以pod的形式搭建在k8s集群中,我们当然可以将其拿出来在liuux上额外的安装
创建pod
当我们通过yaml文件(kube apply -f ...
)创建多个pod的时候流程如下
- 首先客户端通过连接api-server运行yaml文件生成manifest,这manifest有我们指定的pod配置,比如多少份(replicas 3)等等传递给etcd,etcd存储这些配置文件,并且创建3个pod(manifest配置的),此时pod状态为pending
- etcd创建完对应的pod后回传消息给api-server,告诉他完成pod初始创建,需要将他调度到work node上,api-server此时唤醒scheduler
- scheduler被唤醒后选择work node(也是通过api-server查找etcd看node的信息),然后将选择的node发送给api-server,最后scheduler接着沉睡
- api-server接到消息后传递给对应worker node的kubelet,让这些worker node通过kubelet创建pod,此时pod的状态是creating(更新到etcd中)
- 当work-node的kubelet创建完pod后将创建完的消息发送给api-server,api-server再通知etcd,让etcd将对应的pod状态改为running
k8s
不介绍了,教程一大堆,这里默认安装成功,注意的是,我的环境下,使用的是containerd(container runtime), docker(container engine),一台master,2台slaver
container engine vs container runtime
首先container runtime是container engine的重要组成部分
我们一个容器生命周期包含,拉镜像,运行,存储,销毁等等,这个容器的生命周期都是由container engine来维护,但是容器最终是要运行在os上,既然要运行在os上那么必不可少的一些syscall(unix为例子),比如mount(),clone(),这些syscall是container engine必须要操作的,因此这一部分与os密切相关(如何在os上存储如何在os上运行等)的功能叫做container runtime,必须要注意的是,container runtime必须要遵守OCI标准,OCI标准是k8s所提出来的(不确定),containerd就是一个非常出色的container runtime,其核心功能用runc完成
containerd是docker的一部分,之前的新闻说k8s不再支持docker,而是转向支持containerd是什么情况,首先docker是先于k8s发行,在k8s发布后,没有多余可以选择的产品,所以选择了docker作为容器引擎(当时的docker的runtime就是containerd),此后因为k8s产品理念(模块化),使用者可以自己搭配组件,以达到自己的使用目的,并且当时市面上出现了多个container runtime,此时k8s提出了CRI(container runtime interface)标准,k8s的流程是由kubelete发送CRI给container runtime进行容器运行,生命周期由k8s掌管,但是为了兼容之前的版本(docker作为container runtime),k8s为docker额外写一个组件叫做dockerdim(因为之前的版本CRI是直接发送给docker而非containerd,虽然说containerd支持oci且是docker的一部分,但是docker不支持,所以需要dockerdim将oci翻译成docker的api进行下发),之后在1.21版本后k8s不再支持dockerdim,解决方案是绕过docker直接将oci下发给containerd(containerd支持oci),因为k8s不需要docker去操作容器生命周期,k8s可以直接通过oci直接与containerd去接管
代码&&打包
很简单的golang代码,实现功能就是提供get请求,并且服务端记录日志等待后续的输出
package mainimport("net/http""fmt""log""os"
)func handler_for_hello_get(rw http.ResponseWriter, r *http.Request){file,_ := os.OpenFile("access_log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)infologger := log.New(file, "INFO", log.Ldate|log.Ltime|log.Lshortfile)if r.Method != "GET"{fmt.Fprintf(rw, "handle get method only!!!\n")infologger.Println("connect from ", r.RemoteAddr, " Failed!!!")file.Close()return}fmt.Fprintf(rw, "hello!!!\n")infologger.Println("connect from ", r.RemoteAddr, " Success!!!")file.Close()
}func main(){add := ":8080"fmt.Printf("start server at %v\n", add)http.HandleFunc("/hello", handler_for_hello_get)log.Fatal(http.ListenAndServe(add, nil))
}
当我们写完代码后需要写dockerfile,然后打包,这里注意的是,我们就一个代码,没有设计到第三方库,所以不需要什么go.mod和go.sum文件,所以dockerfile的时候不用copy go.mod和go.sum(本来也没有),dockerfile如下
#golang 1.20 image
FROM golang:1.20# set workdir
WORKDIR /app#copy go.mod and go.sum but golang code we have doesn't import third-part package, so we #
#COPY go.mod go.sum ./# same as front
#RUN go mod download# copy golang source code to image
COPY *.go ./EXPOSE 8081CMD ["go", "run", "server.go"]
开始build打包,打包后的镜像名字为honkytonkman/server_in_k8s,注意/
后是镜像名字,/
前是用户名字,后面我们docker login的时候login的用户名一定要是待推送image /
前面的名字,不然就出错
root@k8s-master:~/k8s_demo# docker build . -t honkytonkman/server_in_k8s
[+] Building 2.0s (8/8) FINISHED docker:default=> [internal] load build definition from Dockerfile 0.0s=> => transferring dockerfile: 351B 0.0s=> [internal] load .dockerignore 0.0s=> => transferring context: 2B 0.0s=> [internal] load metadata for docker.io/library/golang:1.20 2.0s=> [1/3] FROM docker.io/library/golang:1.20@sha256:bc5f0b5e43282627279fe5262ae275fecb3d2eae3b33977a7fd200c7a760d6f1 0.0s=> [internal] load build context 0.0s=> => transferring context: 31B 0.0s=> CACHED [2/3] WORKDIR /app 0.0s=> CACHED [3/3] COPY *.go ./ 0.0s=> exporting to image 0.0s=> => exporting layers 0.0s=> => writing image sha256:2f86a57b12cc98f087c73ad5cfe126129131718bcc7b64a2a01567f8b513a26e 0.0s=> => naming to docker.io/zhr/server_in_k8s 0.0s
打包后查看镜像
root@k8s-master:~/k8s_demo# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
honkytonkman/server_in_k8s latest 2f86a57b12cc 5 minutes ago 845MB
如果生产环境下这些都是自动化的(CI)然后传入公司的镜像仓库,但是我们这里是自己的环境所以我打算上传到dockerhub的公共仓库中,再在k8s中拉取下来
root@k8s-master:~/k8s_demo# docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: honkytonkman
Password:
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-storeLogin Succeeded
push镜像
root@k8s-master:~/k8s_demo# docker push honkytonkman/server_in_k8s
k8s yaml
deployment
k8s有多个object,比如什么Pod,Deployment,Service等等,他们可以通过yaml进行配置,可以通过命令行进行配置,这里主要讲述通过yaml进行配置
pod和Deployment有啥区别呢?首先在生产环境我们会很少碰到只用Pod的情况,因为一个服务不仅仅是Pod,他需要指定replicate的数量,他需要指定持久存储(可能),他需要指定template(就是Pod)等等,而Pod就仅仅是Pod,而Service object主要是为多个replicate的Pod分配一个虚拟ip供外部访问
无论什么object的yaml,他们都分为3类,分别是
-
metadata
metadata就是创建pod指定的metadata比如label,name之类的
-
specification
在yaml中简写为spec,主要是pod的配置,根据kind的不同而不同
-
status
这个是k8s自己生成的,我们期望Pod的status在spec:中指定了,但是运行过程中Pod的真正status由k8s查看Pod得出,k8s不断地比较这2个status,不过不一样比如我们规定replicats为2但是实际的replicats为1,所以k8s会发现status不一样,然后自动调整以达到我们spec规定的状态,具体实现这个操作的k8s组件是
controller
下面是我们的deployment
apiVersion: apps/v1
kind: Deployment
metadata:name: serverlabels:app: web
spec:selector:matchLabels:app: webreplicas: 5strategy:type: RollingUpdatetemplate:metadata:labels:app: webspec:containers:- name: serverimage: honkytonkman/server_in_k8sports:- containerPort: 8080
上述的Pod只有一个container,假如一个Pod有多个container,那么多个container共享IPC和Network namespace,也就是说一个Pod中多个container可以通过localhost进行访问,process namespace默认不共享,但是我们可以通过设置
spec: #这个是pod的specshareProcessNamespace: truecontainers: ...
当然deployment可也通过
kubectl
进行设置
是不是很奇怪
metadata
里面的label
,spec
里面的selector
,还有selector
中的matchlabel
,这里会详细的解释
首先要明确的是label
是metadata
里面的属性,且label
是k/v格式,一个node可以有label
,一个service之类的object可以有label
,在外面的label是设置整个deplyment的属性,而在template中的label是设置pod的属性(template就是设置pod的container还有镜像什么的),在selector中matchLabels选中kv属性为app: web
的pod运行,正好我们template中设置了labels为app: web,所以运行他
然后我们运行这个deployment
root@k8s-master:~/k8s_demo# kubectl apply -f server_deplyment.yaml
deployment.apps/server configured
然后查看deplyment,发现我们创建的5个pod都正常运行,然后我们看是那些什么pod
root@k8s-master:~/k8s_demo# kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
server 5/5 5 5 47m
我们可以通过刚刚给pod设置的label进行pod查找
root@k8s-master:~/k8s_demo# kubectl get pods --selector app=web
NAME READY STATUS RESTARTS AGE
server-7776bd5fb9-48lck 1/1 Running 0 17m
server-7776bd5fb9-7t4mh 1/1 Running 0 18m
server-7776bd5fb9-8pjvc 1/1 Running 0 17m
server-7776bd5fb9-ftw8n 1/1 Running 0 18m
server-7776bd5fb9-x8knr 1/1 Running 0 17m
也可以根据名字进行查找,因为pod的命明规则是deployment.yaml中设置的名字再加上pod id,我们的deployment的名字是server所以
root@k8s-master:~/k8s_demo# kubectl get pods | grep ^server-
server-7776bd5fb9-48lck 1/1 Running 0 20m
server-7776bd5fb9-7t4mh 1/1 Running 0 20m
server-7776bd5fb9-8pjvc 1/1 Running 0 20m
server-7776bd5fb9-ftw8n 1/1 Running 0 20m
server-7776bd5fb9-x8knr 1/1 Running 0 20m
我们查看pod的详细信息
root@k8s-master:~/k8s_demo# kubectl describe pod server-7776bd5fb9-48lck
Name: server-7776bd5fb9-48lck
Namespace: default
Priority: 0
Service Account: default
Node: k8s-slaver1/192.168.152.132
Start Time: Wed, 09 Aug 2023 11:52:12 +0800
Labels: app=webpod-template-hash=7776bd5fb9
Annotations: <none>
Status: Running
IP: 10.244.1.6
IPs:IP: 10.244.1.6
Controlled By: ReplicaSet/server-7776bd5fb9
Containers:server:Container ID: containerd://3668c62a0caa0d2caad516cf46b3b3115e6f52ec3196a0a041fb6ee438f42f00Image: honkytonkman/server_in_k8sImage ID: docker.io/honkytonkman/server_in_k8s@sha256:9ca2b7dc0aaace89f0e914a1b87bfd5eab24cfbf87ff1bb61a45962eab9ff825Port: 8080/TCPHost Port: 0/TCPState: RunningStarted: Wed, 09 Aug 2023 11:52:17 +0800Ready: TrueRestart Count: 0Environment: <none>Mounts:/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-xnmnx (ro)
Conditions:Type StatusInitialized TrueReady TrueContainersReady TruePodScheduled True
Volumes:kube-api-access-xnmnx:Type: Projected (a volume that contains injected data from multiple sources)TokenExpirationSeconds: 3607ConfigMapName: kube-root-ca.crtConfigMapOptional: <nil>DownwardAPI: true
QoS Class: BestEffort
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300snode.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:Type Reason Age From Message---- ------ ---- ---- -------Normal Scheduled 22m default-scheduler Successfully assigned default/server-7776bd5fb9-48lck to k8s-slaver1Normal Pulling 22m kubelet Pulling image "honkytonkman/server_in_k8s"Normal Pulled 22m kubelet Successfully pulled image "honkytonkman/server_in_k8s" in 2.46795589s (3.418282616s including waiting)Normal Created 22m kubelet Created container serverNormal Started 22m kubelet Started container server
然后发现ip和端口后我们直接测试链接是否pod提供的服务正常
root@k8s-master:~/k8s_demo# curl 10.244.1.6:8080/hello
hello!!!
我们详细(没有describe那么详细)的看deployment中pod的信息
root@k8s-master:~/k8s_demo# kubectl get pods -o wide --selector app=web
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
server-7776bd5fb9-48lck 1/1 Running 0 28m 10.244.1.6 k8s-slaver1 <none> <none>
server-7776bd5fb9-7t4mh 1/1 Running 0 28m 10.244.1.4 k8s-slaver1 <none> <none>
server-7776bd5fb9-8pjvc 1/1 Running 0 28m 10.244.1.5 k8s-slaver1 <none> <none>
server-7776bd5fb9-ftw8n 1/1 Running 0 28m 10.244.2.6 k8s-slaver2 <none> <none>
server-7776bd5fb9-x8knr 1/1 Running 0 28m 10.244.2.7 k8s-slaver2 <none> <none>
发现有3个node被调度到k8s-server1上,如果k8s-slaver2是一台高新能机器,且deployment的pod运行时也需要高性能(或者说k8s-slaver2属于一个database服务的专属服务器,这个depolyment是用于部署database服务的)我们需要将deployment的所有pod都转移到k8s-slaver2中,这怎实现呢?用labels
首先给k8s-slaver2打上标签
root@k8s-master:~/k8s_demo# kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
k8s-master Ready control-plane 41h v1.27.4 192.168.152.131 <none> Ubuntu 20.04.6 LTS 5.15.0-78-generic containerd://1.6.21
k8s-slaver1 Ready <none> 41h v1.27.4 192.168.152.132 <none> Ubuntu 20.04.6 LTS 5.15.0-78-generic containerd://1.7.2
k8s-slaver2 Ready <none> 41h v1.27.4 192.168.152.133 <none> Ubuntu 20.04.6 LTS 5.15.0-78-generic containerd://1.7.2root@k8s-master:~/k8s_demo# kubectl label nodes k8s-slaver2 group=database
node/k8s-slaver2 labeled
然后在deployment上加nodeSelector
apiVersion: apps/v1
kind: Deployment
metadata:name: serverlabels:app: web
spec:selector:matchLabels:app: webreplicas: 5strategy:type: RollingUpdatetemplate:metadata:labels:group: databaseapp: webspec:containers:- name: serverimage: honkytonkman/server_in_k8sports:- containerPort: 8080nodeSelector:group: database
重新apply deployment,因为在spec中设置了策略是滚动更新(RollingUpdate
),也就是说,先创建新pod再删除原有的pod
如果我们设置了滚动更新,假设有5个Pod副本(replicas:5),手动删除一个,那么删除后会立马自动创建一个新的pod,以达到replicas=5这个status,因为开始讲了k8s会无时无刻的比较生产环境中的pod的状态(因为刚刚删除了一个pod此时replicas为4),和我们配置的理想状态(replicas为5)k8s发现不一样立马调整生产环境中的状态以达到我们配置的理想状态,这也叫self-heal
root@k8s-master:~/k8s_demo# kubectl apply -f server_deplyment.yaml
deployment.apps/server configured
root@k8s-master:~/k8s_demo# kubectl get pods -o wide --show-labels
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES LABELS
server-59f9bd6776-9tlpq 1/1 Running 0 42s 10.244.2.15 k8s-slaver2 <none> <none> app=web,group=database,pod-template-hash=59f9bd6776
server-59f9bd6776-bjlw4 1/1 Running 0 50s 10.244.2.12 k8s-slaver2 <none> <none> app=web,group=database,pod-template-hash=59f9bd6776
server-59f9bd6776-xtsvr 1/1 Running 0 50s 10.244.2.11 k8s-slaver2 <none> <none> app=web,group=database,pod-template-hash=59f9bd6776
server-59f9bd6776-xwkkv 1/1 Running 0 45s 10.244.2.14 k8s-slaver2 <none> <none> app=web,group=database,pod-template-hash=59f9bd6776
server-59f9bd6776-z8gg6 1/1 Running 0 50s 10.244.2.13 k8s-slaver2 <none> <none> app=web,group=database,pod-template-hash=59f9bd6776
最后发现所有的pod都被调度到k8s-slaver2上了
service
因为deployment创建了5个pod每个pod都有一个单独的ip,并且提供相同的功能,我们目前的需求是要5个pod同时通过一个ip对外提供服务,所以此时service就出现了,简单讲service就是为一个服务的多个pod提供一个虚拟ip供外界访问
不过service有2种提供虚拟ip的方式,一个是NodePort,一个是ClusterIP
- NodePort:就是使用本node的ip,不过会随机开一个端口,通过访问本node的端口达到对service的访问
- ClusterIP:故名思意就是分配一个集群ip,通过集群ip访问
可能你还有疑问,如果用yaml文件进行部署,service和deployment是2个不同的yaml文件,如何确定service和deployment这2个object可以匹配上?注意deployment的最外层的matedata中我们设置了labels,这个labels是针对这个deployment文件的label,而在service的yaml中也有一个key是selector,这个selector可以选中特定deployment的label,这样2者就匹配上了,上面的deployment的label是app: web
,这里我们的service就选中他即可
service.yaml文件如下
apiVersion: v1
kind: Service
metadata:name: service-obj-for-servicelabels:app: webtype: service
spec:type: ClusterIP #use cluster ipselector:app: webports:- protocol: TCPport: 8081 #对外的ip端口targetPort: 8080 #pod内需要映射出去的端口
然后使用这个yaml
root@k8s-master:~/k8s_demo# kubectl apply -f server_service.yaml
service/service-obj-for-service created
root@k8s-master:~/k8s_demo#
root@k8s-master:~/k8s_demo#
root@k8s-master:~/k8s_demo# kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 2d16h
service-obj-for-service ClusterIP 10.107.202.64 <none> 8081/TCP 8s
通过分配的集群ip进行访问
root@k8s-master:~/k8s_demo# curl 10.107.202.64:8081/hello
hello!!!
但是我们并不能确定是否后端的5个pod是否平均分担负载,因为代码只返回hello,没有返回当前主机名,所以等后面会通过gitlab+argo的方式搭建一条完整的CICD流水线,当代码改动自动的CI(打包dockerfile–>push打包后的镜像到镜像仓库中),自动的CD(使用helm2模板自动部署到argo中)
然后我们尝试使用nodeport的方式进行访问
更改service的yaml为NodePort模式
apiVersion: v1
kind: Service
metadata:name: service-obj-for-servicelabels:app: webtype: service
spec:type: NodePort #use cluster ipselector:app: webports:- protocol: TCPport: 8081 #对外的ip端口targetPort: 8080 #pod内需要映射出去的端口
使用yaml
root@k8s-master:~/k8s_demo# kubectl apply -f server_service.yaml
然后查看
root@k8s-master:~/k8s_demo# kubectl describe service service-obj-for-service
root@k8s-master:~/k8s_demo# kubectl describe service service-obj-for-service
Name: service-obj-for-service
Namespace: default
Labels: app=webtype=service
Annotations: <none>
Selector: app=web
Type: NodePort
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.107.202.64
IPs: 10.107.202.64
Port: <unset> 8081/TCP
TargetPort: 8080/TCP
NodePort: <unset> 31168/TCP
Endpoints: 10.244.1.15:8080,10.244.1.18:8080,10.244.1.19:8080 + 2 more...
Session Affinity: None
External Traffic Policy: Cluster
Events: <none>
root@k8s-master:~/k8s_demo#
发现本机自动地暴露了一个tcp端口(31168),分配的cluster的ip没有变还是10.107.202.64:8081
这个时候我们再通过本机node的ip访问成功
root@k8s-master:~/k8s_demo# curl 192.168.152.131:31168/hello
hello!!!
此时流量访问的流程是
client----->k8s_master_IP:31168------>cluster_virtual_ip:8081--->Multiple_Pod:8080
type还可以设置为LoadBalancer
这个我们后面实验
k8s volumes
这里我们只是学习学习,并不发布到cicd中去
首先我们直到容器时stateless的,它并不保存他运行时的状态,所以传统上来说,容器并不用作数据库应用,但是在k8s中则不一样,k8s中提供了一个object叫做PersistentVolume
,它可以使用外部的存储,比如nfs,比如公有云提供的云存储服务,也可以使用本地磁盘等等存储服务进行pod的数据存储,并且当pod被删除和重新创建后可以通过configmap等object再重新的指向PresistentVolume
,在k8s volumes中有3个重要的object分别是PersistentVolume,PersistentVolumeClaim,StorageClass
Ephemeral volume types have a lifetime of a pod, but persistent volumes exist beyond the lifetime of a pod. When a pod ceases to exist, Kubernetes destroys ephemeral volumes; however, Kubernetes does not destroy persistent volumes. For any kind of volume in a given pod, data is preserved across container restarts.
PersistentVolume并不属于任何一个namespace,而是属于整个集群!!!,所以我们的Pod在使用的时候需要使用PersistentVolumeClaim(PVC),PVC向PersistentVolume申请存储,然后给Pod用(PVC有namespace)
StorageClass和PersistentVolume一样游离于namespace之外
demo
本地临时存储
demo的过程,我们的deplyment只会创造一个pod,然后在slaver1创建,然后在pod中写入文件,最后我们再将pod迁移到slaver2中,再查看pod的零时存储的文件是否还在,我们迁移不会使用kubectl drain
驱逐node上的pod,再kubectl cordon
使节点不可调度,因为我们的node上还有其他的pod需要使用,让slaver2承受所有太难了,所以我们的方案是在deployment上使用matchlabel匹配slaver2的label
deployment如下
apiVersion: apps/v1
kind: Deployment
metadata: name: test-pod-deployment
spec:replicas: 1selector:matchLabels:app: test-podtemplate:metadata:labels: #set pod labelapp: test-podspec:volumes:- name: cache-volumeemptyDir: {}containers:- image: nginxname: test-pod-emptydirvolumeMounts:- mountPath: /cache #mount to pod /cachename: cache-volume
然后apply
root@k8s-master:~/storage# kubectl apply -f nginx-localstorage-deployment.yml
deployment.apps/test-pod-deployment created
root@k8s-master:~/storage# kubectl get pod -o wide | grep test-pod
test-pod-deployment-74f78694b9-9wshr 1/1 Running 0 70s 10.244.2.141 k8s-slaver2 <none> <none>
发现存在于slaver2上,我们进去看看,并且在dir中存入
root@k8s-master:~/storage# kubectl exec -it test-pod-deployment-74f78694b9-9wshr -- /bin/bash
root@test-pod-deployment-74f78694b9-9wshr:/# cd /cache/
root@test-pod-deployment-74f78694b9-9wshr:/cache# echo "for test emptydir" > test.txt
root@test-pod-deployment-74f78694b9-9wshr:/cache# cat /cache/test.txt
for test emptydir
root@test-pod-deployment-74f78694b9-9wshr:/cache# exit
exit
然后我们进行驱逐操作,首先是看slaver1的labels(以免我们使用kubectl label
手动给node打标签)
root@k8s-master:~/storage# kubectl get nodes --show-labels
NAME STATUS ROLES AGE VERSION LABELS
k8s-master Ready control-plane 54d v1.27.4 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-master,kubernetes.io/os=linux,node-role.kubernetes.io/control-plane=,node.kubernetes.io/exclude-from-external-load-balancers=
k8s-slaver1 Ready <none> 54d v1.27.4 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-slaver1,kubernetes.io/os=linux
k8s-slaver2 Ready <none> 54d v1.27.4 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,group=database,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-slaver2,kubernetes.io/os=linux
然后我们决定使用slaver1的kubernetes.io/hostname=k8s-slaver1
这个标签。所以改动我们的deployment加上nodeSelector
apiVersion: apps/v1
kind: Deployment
metadata: name: test-pod-deployment
spec:replicas: 1selector:matchLabels:app: test-podtemplate:metadata:labels: #set pod labelapp: test-podspec:nodeSelector:kubernetes.io/hostname: k8s-slaver1volumes:- name: cache-volumeemptyDir: {}containers:- image: nginxname: test-pod-emptydirvolumeMounts:- mountPath: /cache #mount to pod /cachename: cache-volume
再进行apply,最后查看
root@k8s-master:~/storage# kubectl get pod -o wide | grep test-pod
test-pod-deployment-564f5bf74f-tg2md 1/1 Running 0 2m45s 10.244.1.160 k8s-slaver1 <none> <none>
pod已经跑到我们的slaver1上了,最后我们看test.txt还在吗
root@k8s-master:~/storage# kubectl exec -it test-pod-deployment-564f5bf74f-tg2md -- /bin/bash
root@test-pod-deployment-564f5bf74f-tg2md:/# cd /cache
root@test-pod-deployment-564f5bf74f-tg2md:/cache# ls
root@test-pod-deployment-564f5bf74f-tg2md:/cache#
啥也没有
说明我们的local存储再经过调度到其他的节点后,原本存储的东西已经都没有了
因为pod是stateless的,经过调度,原pod宿主机上的存储数据都会被删除,要想数据还保存在,我们推荐使用pv和pvc实现持久化存储
持久化存储
我们首先在master上配置nfs,所有的数据都会被存入nfs中,然后再通过pvc和pv的方式共享出来,实现stateful,无论pod怎么调度,数据都还在
安装nfs不讲了
直接开始写pv如下
apiVersion: v1
kind: PersistentVolume
metadata:name: pv-nfs-datanamespace: default
spec:accessModes:- ReadWriteManycapacity:storage: 1GipersistentVolumeReclaimPolicy: Retain #retain代表对应pvc被删除的时候,保留pv声明的数据,且不被其他pvc重复使用,delete代表对应的pvc被删除的时候对应的pv和pv指向的存储对象(aws等对象)也被删除,recycle代表pvc被删除的时候pv对应的存储对象数据被删除,且被其他的pvc复用nfs:server: 192.168.152.131path: /data
root@k8s-master:~/storage# kubectl apply -f nfs-pv.yml
persistentvolume/pv-nfs-data created
root@k8s-master:~/storage#
root@k8s-master:~/storage#
root@k8s-master:~/storage# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pv-nfs-data 1Gi RWX Retain Available 25m
然后我们再声明对应的pvc,pvc就有namespace的概念了,且pvc必须要和pv一个名字不然不能bound
apiVersion: v1
kind: PersistentVolumeClaim
metadata: name: pvc-nfs-data # same as pv's namenamespace: default
spec:accessModes:- ReadWriteManyresources:requests:storage: 1Gi
查看
root@k8s-master:~/storage# kubectl apply -f nfs-pvc.yml
persistentvolumeclaim/pvc-nfs-data1 created
root@k8s-master:~/storage# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
pvc-nfs-data Bound pv-nfs-data 1Gi RWX 27s 6s
然后我们直接在nfs中创建文件
root@k8s-master:~/storage# showmount -e
Export list for k8s-master:
/data *
root@k8s-master:~/storage# cd /data
root@k8s-master:/data# echo "presistent storage test" > test.txt
然后改动deployment,再apply
apiVersion: apps/v1
kind: Deployment
metadata: name: test-pod-deployment
spec:replicas: 1selector:matchLabels:app: test-podtemplate:metadata:labels: #set pod labelapp: test-podspec:nodeSelector:kubernetes.io/hostname: k8s-slaver1volumes:- name: persistent-volume#emptyDir: {}persistentVolumeClaim:claimName: pvc-nfs-datacontainers:- image: nginxname: test-pod-emptydirvolumeMounts:- mountPath: /cache #mount to pod /cachename: persistent-volume
发现文件已经在里面
root@k8s-master:~/storage# kubectl apply -f nginx-localstorage-deployment.yml
deployment.apps/test-pod-deployment configured
root@k8s-master:~/storage#
root@k8s-master:~/storage#
root@k8s-master:~/storage#
root@k8s-master:~/storage#
root@k8s-master:~/storage# kubectl get pod -o wide | grep test
test-pod-deployment-66cb996cbb-jb9xr 1/1 Running 0 48s 10.244.1.161 k8s-slaver1 <none> <none>
test-pv-pod 0/1 Pending 0 41d <none> <none> <none> <none>
root@k8s-master:~/storage# kubectl exec -it test-pod-deployment-66cb996cbb-jb9xr -- /bin/bash
root@test-pod-deployment-66cb996cbb-jb9xr:/# cd /cache/
root@test-pod-deployment-66cb996cbb-jb9xr:/cache# ls
test.txt
root@test-pod-deployment-66cb996cbb-jb9xr:/cache# cat test.txt
presistent storage test
然后再将pod迁移到slaver2中(更改deployment文件的nodeselect)
apiVersion: apps/v1
kind: Deployment
metadata: name: test-pod-deployment
spec:replicas: 1selector:matchLabels:app: test-podtemplate:metadata:labels: #set pod labelapp: test-podspec:nodeSelector:kubernetes.io/hostname: k8s-slaver2volumes:- name: persistent-volume#emptyDir: {}persistentVolumeClaim:claimName: pvc-nfs-datacontainers:- image: nginxname: test-pod-emptydirvolumeMounts:- mountPath: /cache #mount to pod /cachename: persistent-volume
最后查看发现无论pod怎么迁移数据还是存在
root@k8s-master:~/storage# kubectl apply -f nginx-localstorage-deployment.yml
deployment.apps/test-pod-deployment configured
root@k8s-master:~/storage#
root@k8s-master:~/storage#
root@k8s-master:~/storage# kubectl get pod -o wide | grep test-pod
test-pod-deployment-74df69669c-cbvzz 1/1 Running 0 14s 10.244.2.142 k8s-slaver2 <none> <none>
root@k8s-master:~/storage# kubectl exec -it test-pod-deployment-74df69669c-cbvzz -- /bin/bash
root@test-pod-deployment-74df69669c-cbvzz:/# cd /cache/
root@test-pod-deployment-74df69669c-cbvzz:/cache# ls
test.txt
root@test-pod-deployment-74df69669c-cbvzz:/cache# cat test.txt
presistent storage test
root@test-pod-deployment-74df69669c-cbvzz:/cache#
动态pv
这里主要是讲sc(storageclass),sc更加的具有弹性,sc可以根据存储类型去定义,比如一个ssd存储一个sc,一个nfs一个sc,然后用户在使用的时候(pvc)可以指定sc,然后由sc自动的生成pv(根据sc设置的类别)
但是sc需要依赖第三方的插件,比如nfs的第三方插件,我们先通过helm下载nfs的插件
CI
这里CI就是用gitlab
进行自动化的构建打包镜像,然后推送到dockerhub
中,而gitlab
我们就不自己搭建了,而是直接用gitlab.com
gitlab
首先gitlab有着极其丰富的功能,包含代码的版本管理,还有就是今天的重头戏CI(当然gitlab也支持CD,不过我们只用gitlab做CI)
在CI之前,我们的业务代码就是上面的go语言写的web server要先上传到gitlab中,具体怎么上传教程非常多,就不介绍了
当代码到了gitlab仓库后进入gitlab主页的代码项目中,点击左边的Build再点击Pipeline Editor进行CI Pipeline的编写(如果是自己搭建,且版本有点老Pipeline Editor在左边的CICD选项中,点击即可下拉选中Pipeline Editor)
点击后会出现一个在线编辑的界面,让我编辑Pipeline的yml文件,这个文件会自动的创建
Gitlab的CI Pipeline的yml配置文件非常简单,Pipeline由2部分组成,分别是stage和job,一个stage中包含多个job,比如我们一套代码流程(Pipeline)包含编译–>测试–>发布,其中编译,测试,发布都可以看成stage,而Job就是具体执行的操作,比如一个测试stage中可以分成2个job,分别是单元测试job和压力测试job,编译stage可以就只有一个job,比如build-job类似,在gitlab Pipeline yml语法中job和stages都是key,其中stages定义了多个stage(数组形式),job定义了从属的stage和做的具体操作(script),在test的时候job运行具体的命令,当命令返回非0的时候pipeline才会停止
我们开始明确了Gitlab只做CI,那么Pipeline stage可以分成4部分,分别是build–>test–>package–>push_to_dockerhub
注意!!!每个job执行完毕后都会恢复现场,比如编译后的文件会一一删除,进入的目录会一一退出
因为我们要将golang的源代码编译成二进制文件,所以简单的go run就不行了,这里我们改一下golang的源码,添加go.mod
go.mod文件如下
module main
然后就可以进行编译成二进制文件了,此时只需要push这个新的文件到gitlab中即可,然后我们更新需求,因为有2个branch,一个dev,一个main,我们设置了main不准被commit,只准commit到dev,再由dev发merge request到main才行,所以前2个stage可以main和dev
.gitlab-ci.yml文件如下
stages: # List of stages for jobs, and their order of execution- build- test- package- push_to_dockerhubbuild-job: # This job runs in the build stage, which runs first.stage: buildscript:- echo ${CI_PIPELINE_SOURCE}- echo ${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}- cd src- echo "build golang code to binary file"- /usr/local/go/bin/go build # 因为我们的runner在本地的机器shell运行(excutor是shell)此时会以gitlab-ci这个用户登录然后运行我们的pipeline,我们最好加上go的全路径因为我的PATH写在root用户的bashrc下test-job: # This job runs in the test stage.stage: test # It only starts when the job in the build stage completes successfully.script:- echo "Running tests... "- pwd && ls -al- cd src- /usr/local/go/bin/go build #因为上一个job执行完毕后会删除job在运行中产生的文件- ./main& #后台运行编译后的二进制文件- echo "access url and get respond_file"- curl -I 127.0.0.1:8080/hello >> respond_file- echo "check if file exist and return 200"- test -f respond_file && grep "HTTP/1.1 200 OK" respond_file- echo "test complete,and kill ./main process"- kill `ps -aux | grep ./main | head -1 | awk '{print $2}'`package-job: # This job also runs in the package stage.stage: package rules:- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_REF_NAME == "main"' when: manual #手动执行,代表着这个job不会自动执行需要工作人员手动点执行才会执行allow_failure: falsescript:- echo "start to package"- cd src- docker build . -t honkytonkman/server_in_k8spush-job: # This job runs in the deploy stage.stage: push_to_dockerhub # It only runs when *both* jobs in the test stage complete successfully.rules:- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_REF_NAME == "main"' when: manual #手动执行,代表着这个job不会自动执行需要工作人员手动点执行才会执行allow_failure: falsescript:- echo "login to dockerhub"- docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} #login dockerhub- echo "push image"- docker push honkytonkman/server_in_k8s
我们可以看到CI Pipeline的yml文件中有2个变量,这个变量并没有在yml文件中设置,因为这些都是机密变量分别保存了密码和账号,所以我们将这些变量设置为预变量,预变量在gitlab的setting的cicd中 设置,变量可以选择环境范围(mask variable开启将会对这些变量在job log中加密,protect variable代表将会在受保护的分支上运行)
我们也可以通过设置rules来规定我们的job由什么branch执行,什么时候执行,匹配commit的log,匹配等等信息官网的文章解释的非常好
为什么package-job和push-job中的CI_PIPELINE_SOURCE 设置的是push而不是merge_request_event,因为merge_request_event指的是这个分指发起了mr才会触发pipeline,而我们是merge合并后触发,所以是push,如果要用merge_request_event就可以在dev分支中使用,当dev发起mr的时候触发merge_request_event,关于这种predefine的变量在官网解释的非常好
还有一点需要注意的是job的rules里面如果if匹配到并且后面没有跟when: never那么就执行此job,没有匹配到if就不执行这个job,如果if后面跟了个when: never,说明if后面的条件被匹配到了就不执行这个job,反之执行执行
CI runner
当我们更改后发现pipeline执行失败,说是账户没有绑定信用卡…这个时候Gitlab给了2个选项要么绑定信用卡说明你是valid user,要么自己搭建CI runner然后绑定gitlab…
开始安装CI runner,我们将CI runner安装在k8s master机器上(注意是装在裸机上,而非容器)
root@k8s-master:~# curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash
root@k8s-master:~# sudo apt-get install gitlab-runner
然后在gitlab官网上创建gitlab runner(左边的bar的Settings–>CICD—>Runners)上面有3种runner,分别是
-
group runner
一个group 可以包含多个project,这个project共享这个group runner,换句话说由这个group种的所有project由这个group runner调度
-
project runner
针对每个project单独的runner,我们就用这个,用于我们的project
-
shared runner
对所有的project共享
创建完成后会有注册命令,后面包含了url和token我们复制粘贴到待运行runner的机器上就行如下
root@k8s-master:~# gitlab-runner register --url https://gitlab.com --token XXXXXXXX
记住我们是本地机器直接运行ci runner,要选择excutor为shell而非container,virtual machine,当注册后,机器上会自动的创建gitlab-runner
用户,当job在机器上跑的时候以这个用户的身份运行job中的命令(scrip下面)
因为我们的job有打包镜像,push镜像到dockerhub的操作,所以其中需要使用到docker命令,但是gitlab-runner
用户没有权限执行docker命令所以我们要将gitlab-runner
用户加入到docker组中
root@k8s-master:/home# usermod -aG docker gitlab-runner
然后可以查看CI runner
gitlab-runner@k8s-master:/home$ gitlab-ci-multi-runner list
Runtime platform arch=amd64 os=linux pid=49375 revision=782e15da version=16.2.0
Listing configured runners ConfigFile=/home/gitlab-runner/.gitlab-runner/config.toml
然后就可以在gitlab上运行pipeline(当我们mr之后的时候就会自动的运行)
CD
入前面介绍过了,我们CD使用的工具是argo,为什么不用gitlab继续做部署?首先想一个问题,我们的gitlab runner在其他的机器上运行(非k8s集群机器),如果要部署k8s,还不是通过kubectl命令进行部署,但是runner运行的机器需要由k8s集群的权限才能运行,假如有50台机器运行gitlab runner,那么意味着要给这50台机器上k8s的权限…而argo避免了这个问题的产生,因为argo直接部署在K8S之中,一个完整的CICD流程如下
commit code repository change triger gitCI pipeline after CI done push image to dockerhub or change manifest file
coder ----------------> gitlab code repository-------------------------------------------->gitlab CI Runner---------------------------------------------------------------------->dockerhub/k8s manifest file
上述的流程都是通过预先配置自动触发集成
要将每个project(应用)的配置文件与源代码放在不同的git repository中
配置文件指的是deployment.yaml,service.yaml,secret.yaml等等,这样做的好处是,当我们的配置文件做了更改,此时我们不应该跑上述的CI(因为project的源代码没变),所以我们要将应用的配置文件和source code放在不同的git repository中,所以我们也要对这不同的git repository有不同业务逻辑的pipeline,当装载配置文件的给git repository改变了(k8s manifest file,这个无论是helm还是其他方式生成改动的)都会触发argo进行CD操作
argo的好处
- 只认git上的配置文件,也就是说当我们在集群中手动更改应用的状态(比如多加了一个pod),argo发现后会自动的还原(还原成git上配置文件的设定),换句话说argo CD无时不刻的在sync,git上的配置和k8s真正的状态
- 通过git管理配置文件,因为如果多个人同时操作k8s非常危险,而用git可以分多个branch,当要改动配置(merge到main branch,由main branch触发cd)此时需要提mr,由上级审核mr才行
- argo直接跑在k8s中,所以认证非常容易,且argo使用原生的k8s api,比如上述说的状态对比直接用的k8s的controller,等等这些操作对于cluster都是可见的,对于我们定位问题非常有帮助
配置git repository
这里我们将配制2个git repository,分别用于存储项目代码(golang_web_service
)和k8s,argo配置代码(golang_web_service_config
)
具体配置不做详细的叙述,这里直接看结果
golang_web_service_config repository的目录如下
.
├── argo_cd_config
└── k8s_config├── server_deplyment.yaml└── server_service.yaml
golang_web_service repository的目录如下
.
└── src├── access_log├── Dockerfile├── go.mod├── main├── respond_file└── server.go
安装argo
首先argo cd需要运行在argo自己的namespace中,我们先创建argo cd的namespace
root@k8s-master:~# kubectl create namespace argocd
namespace/argocd created
然后apply argo cd的安装yaml文件
root@k8s-master:~# kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
首先看这个文件,我们发现他的apiServer是apiextensions.k8s.io/v1
这是个啥?我们看一下手上的k8s集群有没有这个api
root@k8s-master:~# kubectl api-versions
admissionregistration.k8s.io/v1
apiextensions.k8s.io/v1
apiregistration.k8s.io/v1
apps/v1
argoproj.io/v1alpha1
authentication.k8s.io/v1
authorization.k8s.io/v1
autoscaling/v1
autoscaling/v2
batch/v1
certificates.k8s.io/v1
coordination.k8s.io/v1
discovery.k8s.io/v1
events.k8s.io/v1
flowcontrol.apiserver.k8s.io/v1beta2
flowcontrol.apiserver.k8s.io/v1beta3
networking.k8s.io/v1
node.k8s.io/v1
policy/v1
rbac.authorization.k8s.io/v1
scheduling.k8s.io/v1
storage.k8s.io/v1
v1
还真有,查了一下发现是自定义配置的,搭配kind为CustomResourceDefinition
使用,当我们使用这个后,根据apiVersion: apiextensions.k8s.io/v1
的配置k8s的api server会自动的创建一个RESTful api,具体的api地址是
/apis/<group>/<version>/<plural>
有了上述的配置再apply后,就可以更具这个apiVersion和kind自定义的创建yaml配置
具体看官网链接
有机会独开一章讲这个
因为argocd有ui,我们要登录,可以先看argocd的ip和端口,直接查看service
root@k8s-master:~# kubectl get service -n argocd
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
argocd-applicationset-controller ClusterIP 10.104.161.245 <none> 7000/TCP,8080/TCP 52m
argocd-dex-server ClusterIP 10.97.137.16 <none> 5556/TCP,5557/TCP,5558/TCP 52m
argocd-metrics ClusterIP 10.105.1.82 <none> 8082/TCP 52m
argocd-notifications-controller-metrics ClusterIP 10.96.102.116 <none> 9001/TCP 52m
argocd-redis ClusterIP 10.101.167.132 <none> 6379/TCP 52m
argocd-repo-server ClusterIP 10.96.231.179 <none> 8081/TCP,8084/TCP 52m
argocd-server ClusterIP 10.104.18.36 <none> 80/TCP,443/TCP 52m
argocd-server-metrics ClusterIP 10.100.207.255 <none> 8083/TCP 52m
发现在namespace为argocd中有个service是argocd-server,且他开放80和443端口,这一看就是web地址,但是我们要从外面访问,所以将service的443端口转发到k8s-master的8080端口上
root@k8s-master:~# kubectl port-forward -n argocd services/argocd-server 8080:443 --address="192.168.152.131" &
如果我们不指定–address那么默认绑定localhost,不能通过node的ip地址进行访问
当我们输入ip和端口后,需要输入用户名,用户名的账号为admin,密码需要进入secret里看
root@k8s-master:~# kubectl get secrets -n argocd
NAME TYPE DATA AGE
argocd-initial-admin-secret Opaque 1 93m
argocd-notifications-secret Opaque 0 94m
argocd-secret Opaque 5 94m
发现有一个secrets叫做argocd-inital-admin-secret,这个就是我们初次登录的密码,密码写在他的yaml配置中,我们查看这个secret的yaml
root@k8s-master:~# kubectl get secrets -n argocd argocd-initial-admin-secret -o yaml
apiVersion: v1
data:password: aWwxa2lGVjhlWGtzT1VNVg==
kind: Secret
metadata:creationTimestamp: "2023-08-14T02:39:15Z"name: argocd-initial-admin-secretnamespace: argocdresourceVersion: "288124"uid: 770c86e7-53dc-4ce0-9c81-3bd77ab8fcf8
type: Opaque
因为密码是base64加密(官网说的),所以我们对密码进行base64 decode得到我们的密码
root@k8s-master:~# echo "aWwxa2lGVjhlWGtzT1VNVg==" | base64 --decode
il1kiFV8eXksOUMV
输入后就可以登录
创建argo cd的配置yaml
首先这个配置的yaml放在config的repository中,argo应用这个repository后,默认3分钟sync一次config repository,sync就是比较这个config repository和当前k8s环境的配置有什么不同,有不同就根据配置自动的调整(self-heal)
在一切开始前我们查看当前有没有argo相关的api
root@k8s-master:~/k8s_demo_config/argo_cd_config# kubectl api-versions | grep argo
argoproj.io/v1alpha1
发现的确有,因为当我们安装argo的时候他自动的创建了相应的argo的api,我们随后argo的yaml配置文件就用这个api
然后在argo的ui中添加argocd需要sync的repository,并且输入账号和密码(因为我们是私密repository,不添加就会出错),在argo ui左边的bar中选中settings然后再选中Repositories,再点击左上角CONNECT REPO,在新的弹窗中输入repo的地址(可以通过https和ssh方式当作需要被sync的repo的url),最后输入账号和密码
首先在本地的config repository 创建argocd的application.yaml,最后push到远端
root@k8s-master:~/k8s_demo_config# ls
argo_cd_config k8s_config
root@k8s-master:~/k8s_demo_config# cd argo_cd_config/
root@k8s-master:~/k8s_demo_config/argo_cd_config# vim application.yaml
application.yaml如下
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:name: golang-servernamespace: argocd
spec:project: defaultsource:repoURL: https://gitlab.com/k8s_learn/golang_web_service_config.gittargetRevision: HEADpath: k8s_configdestination:server: https://kubernetes.default.svcnamespace: defaultsyncPolicy:syncOptions:- CreateNamespace=true #假如destination指定的部署namespace不存在那么我们创建一个automated:selfHeal: trueprune: true #当sync的时候argocd发现现环境k8s的实际的状态和,被sync的目录预设的的目录状态不同,那么argo会改变k8s环境中的状态和sync目录预设定的同步,如果是k8s实际分配资源多了,prune允许argocd删除资源
首先api argoproj.io/v1alpha1和kind Application之前讲了在安装argo的时候通过api apiextensions.k8s.io/v1和kind CustomResourceDefinition创建的object,所以我们可以使用k8s这种风格的yaml去配置argo
在matadata中定义本argo服务位于那个namespace中,在argo application创建后通过这条命令查看root@k8s-master:~/k8s_demo_config# kubectl get applications.argoproj.io -n argocd NAME SYNC STATUS HEALTH STATUS golang-server Synced Healthy
然后再spec中最重要的2个是source和destination
- source:定义我们需要sync的repo路径,HEAD指明我们跟踪的当前branch最新的commit,path定义我们的repo中具体的目录,这样我们可以只sync指定目录的变
destination:定义我们的服务部署到那里,比如指定namespace,serversyncPolicy指定了我们sync的过程中的策略
然后我们push到远端的仓库中,具体不做详细介绍
然后我们本地apply这个yaml
root@k8s-master:~/k8s_demo_config# kubectl apply -f argo_cd_config/argocd_application.yaml
然后通过argo ui中就可以快速的定位application
然后我们更改deployment的配置(将replicas改为3,也就是只有3个pod副本)然后push到远端的repo中看是否会自动的触发CD
因为每个3分钟argocd才会同步一次所以我们点了手动sync并且刷新最后发现的确触发了CD。
argocd和helm结合
argocd好像可以直接指定helm的repository,但是我试了一下午没有找到方法…但是其他的就比较简单,如下我们应用对应的配置在另外的仓库中,如下
root@k8s-master:~/k8s_demo_config# tree
.
├── argo_cd_config
│ └── argocd_application.yaml
├── golang_web_application
│ ├── charts
│ ├── Chart.yaml
│ ├── templates
│ │ ├── k8s_config
│ │ │ ├── NOTES.txt
│ │ │ ├── server_deplyment.yaml
│ │ │ └── server_service.yaml
│ │ └── NOTES.txt
│ └── values.yaml
└── promethues_monitor└── service.monitor.yaml6 directories, 8 files
我们的配置仓库就这3个目录,然后我们的argocd的配置文件(argocd_application.yaml
)如下
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:name: golang_servernamespace: argocd
spec:project: defaultsource:repoURL: https://gitlab.com/k8s_learn/golang_web_service_config.gittargetRevision: HEADpath: golang_web_applicationhelm:valueFiles:- values.yamldestination:server: https://kubernetes.default.svcnamespace: defaultsyncPolicy:syncOptions:- CreateNamespace=trueautomated:selfHeal: trueprune: true
当然上述可以通过argo ui直接配置(点application—>new application)更简单…
argocd hook
TODO
argocd 发布
TODO
RBAC
RBAC全称(Role-base access control)
首先我们客户端的命令到达k8s的control plant后,先是api-server,api-serve去看用户是否有权限操作这些资源,RBAC指定的资源对象就是常见的resource object,而操作有以下几个
- get
- list
- watch
- create
- patch
- update
- delete
- deletecollection
RBAC的操作也非常简单,首先是创建账户(serviceaccount),再是创建规则(role)规定那些apigroup的那些资源可以执行那些操作(verb),最后再用RoleBinding进行账户和role的绑定即可,看下面简单的例子
我们先是创建一个serviceaccount,名字为sa-example
apiVersion: v1
kind: ServiceAccount
metadata: name: sa-example
然后我们创建规则,允许对pod,service,endpoint做所有操作,但是对deployment只能create不能查看
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:name: role-example
rules:
- apiGroups:- ""resource:- pods- services- endpointsverbs:- '*'- apiGroups:- appsresource:- deploymentsverbs:- create
最后我们绑定他们
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: name: rolebind-example
roleRef:kind: RoleapiGroup: rbac.authorization.k8s.ioname: role-example
subjects:
- kind: ServiceAccountname: sa-examplenamespace: default
然后再deployment的spec(最里面的)指定serviceAccountName:
即可
operator
operator按照一句简单的话来说就是一组resource,比如CRD,deployment,service等等,opereator根据部署内容和功能也有自己的分级,级别越高,说明这个operator越厉害,稳定,智能,越低说明这个operator只能满足基本的需求,比如安装
- level 1: 基本安装
- level 2: level1之上支持对应用补丁和小的升级
- level 3: 构建整个应用完整的生命周期,包括应用生命周期,存储生命周期(什么时候使用存储,什么时候free使用的存储,还有什么备份,failure recover)
- level 4: 在level3之上又支持监控,日志分析,workload分析等等
- level 5: 在level3之上支持横向,纵向扩容,实现自动配置,等等高级功能
这么一看operator就像是软件的sre
operator前面说了是用户使用k8s的定义的一个扩展api进行resource object的创建自定义,具体流程如下
具体流程就是我们自己定义的operator yaml文件交由controller的api server,api-server发现api是CRD,然后根据yaml自定义的对象在集群中创建resource object
helm
helm在k8s中广泛应用,他到底是啥呢?官方解释helm是一个包管理器,包管理器我们第一反应是yum,apt之类的linux包管理器,也许是pip或者go model之类的包管理器,helm也是包管理器,包管理器到底是个什么东西呢?首选包管理器应该是一个集合,这个集合可以由二进制可执行文件组成(yum,apt之类的包管理器),也可以是代码组成(pip,go model),helm显然是由多个yaml配置文件组成的包管理器,我们可以通过helm手动的生成本地的repository(本地仓库或者叫做本地包管理器),或者直接使用远端的repository(别人提供的,或者是官方的)
包管理器这个概念运用于计算机各个领域,比如上述提到的yum这种成熟应用,或者运用于编写代码,引用第三方库的时候,他们都可以用各自的包管理器管理,helm这个project的目标就是对于k8s的yaml进行集中管理
helm还有一个非常常用的功能就是提供模板,比如一个yaml的某个值可以直接使用"宏"代替,这个"宏"的具体值定义在我们helm repository根目录的value.yaml或者其他的yaml中,并且helm提供直接安装被其管理的k8s配置yaml文件(helm自动的处理了依赖关系)
这么一看helm的确非常强大,所以我们开始将我们的gitlab上的config repository配置成helm repository
root@k8s-master:~/k8s_demo_config# helm create golang_web_application
Creating golang_web_application
然后我们查看helm自动为我们创建的文件
root@k8s-master:~/k8s_demo_config# tree
.
├── argo_cd_config
│ └── argocd_application.yaml
├── golang_web_application
│ ├── charts
│ ├── Chart.yaml
│ ├── templates
│ │ ├── deployment.yaml
│ │ ├── _helpers.tpl
│ │ ├── hpa.yaml
│ │ ├── ingress.yaml
│ │ ├── NOTES.txt
│ │ ├── serviceaccount.yaml
│ │ ├── service.yaml
│ │ └── tests
│ │ └── test-connection.yaml
│ └── values.yaml
└── k8s_config├── server_deplyment.yaml└── server_service.yaml6 directories, 13 files
发现他为我们自动的创建了一个叫做goalng_web_application的目录,进去看有template目录还有values.yaml,template就是我们放置我们k8s或者argo的配置yaml的,外面的values.yaml就是上述存储"宏"对应的值的文件,我们不用这个文件,取而代之的是自己创建的values-stage.yaml(后面我们会创建一个value-prod.yaml),charts目录用于存访依赖的目录,我们将本地的配置放到其正确位置中,确保我们后面push到gitlab中正确无误,文件目录如下
root@k8s-master:~/k8s_demo_config# tree
.
├── argo_cd_config
│ └── argocd_application.yaml
└── golang_web_application├── charts├── Chart.yaml├── templates│ ├── k8s_config│ │ ├── NOTES.txt│ │ ├── server_deplyment.yaml│ │ └── server_service.yaml│ └── NOTES.txt└── values-stage.yaml5 directories, 7 files
首先我们要了解template的编写规则,格式示例为{{ .Values.appName }}
,template都是以.Values
开头表示helm repository的values.yaml之类存访值的文件,appName
,代表values.yaml之类的文件中key的名字,所以{{ .Values.appName }}
的具体值定义在values.yaml之类的文件中的
appName: XXX
{{ .Values.configmap.name }}
的具体值定义在values.yaml之类的文件中的
configmap:name: XXX
了解后开始改动我们的k8s配置文件
root@k8s-master:~/k8s_demo_config/golang_web_application/templates/k8s_config# vim server_deplyment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:name: {{ .Values.K8sConfig.ServerName }}labels:app: {{ .Values.K8sConfig.ServerName }}
spec:selector:matchLabels:app: {{ .Values.K8sConfig.ServerName }}replicas: {{ .Values.K8sConfig.Replicas }}strategy:type: RollingUpdatetemplate:metadata:labels:group: databaseapp: {{ .Values.K8sConfig.ServerName }}spec:containers:- name: {{ .Values.K8sConfig.ServerName }}image: "{{ .Values.K8sConfig.Image.Name }}:{{ .Values.K8sConfig.Image.Tag }}"ports:- containerPort: {{ .Values.K8sConfig.ContainerPort }}#nodeSelector:#group: database
root@k8s-master:~/k8s_demo_config/golang_web_application/templates/k8s_config# vim server_service.yaml
apiVersion: v1
kind: Service
metadata:name: {{ .Values.K8sConfig.ServerName }}namespace: {{ .Values.K8sConfig.NameSpace }}labels:app: webtype: service
spec:type: NodePort #use cluster ipselector:app: {{ .Values.K8sConfig.ServerName }}ports:- protocol: TCPport: {{ .Values.K8sConfig.ServicePort }} #对外的ip端口targetPort: {{ .Values.K8sConfig.ContainerPort }} #pod内需要映射出去的端口
现在开始改values-stage.yaml文件
root@k8s-master:~/k8s_demo_config/golang_web_application# vim values-stage.yaml
K8sConfig:ServerName: golang_serverReplicas: 2ContainerPort: 8080ServicePort: 8081NameSpace: stageImage:Name: honkytonkman/server_in_k8sTag: latest
然后我们创建stage和prod2个namespace,用于存储2个不同的环境,我们的目的是先在stage上发布,再在prod上发布
root@k8s-master:~/k8s_demo_config# kubectl create namespace stage
namespace/stage created
root@k8s-master:~/k8s_demo_config# kubectl create namespace prod
namespace/prod created
然后我们指定我们的value文件直接运行安装我们的helm仓库(install会自动的部署我们的temple的object)
root@k8s-master:~/k8s_demo_config# helm install golang-web-application golang_web_application -f golang_web_application/values-stage.yaml
NAME: golang-web-application
LAST DEPLOYED: Wed Aug 16 12:55:05 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
然后我们查看我们的仓库再查看stage namespace下的所有pod和service是否已经创建
root@k8s-master:~/k8s_demo_config# helm ls
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
golang-web-application default 1 2023-08-16 12:55:05.205674113 +0800 CST deployed golang_web_application-0.1.0 1.16.0
root@k8s-master:~/k8s_demo_config#
root@k8s-master:~/k8s_demo_config#
root@k8s-master:~/k8s_demo_config# kubectl get pods -n stage
NAME READY STATUS RESTARTS AGE
golang-server-564b599b7-nzt2k 1/1 Running 0 5m6s
golang-server-564b599b7-wmnt8 1/1 Running 0 5m6s
root@k8s-master:~/k8s_demo_config# kubectl get service -n stage
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
golang-server NodePort 10.106.159.22 <none> 8081:31024/TCP 5m12s
发现都已经创建,此时我们有个问题,假如stage环境下的服务经过压力测试等一系列测试后发现没问题,我们要发布到生产环境中(prod)也就是prod这个namespace下,我们该怎么办,创建一个values-prod.yaml如下
root@k8s-master:~/k8s_demo_config# vim golang_web_application/values-prod.yaml
K8sConfig:NameSpace: prod
然后我们apply他,注意看我们的操作在install的时候必须指定2个文件分别是values-stageyaml和values-prod,且values-prod必须要在后面,因为helm规定了如果-f或者–values指定2个文件,且2个文件的某一个key/value有冲突,那么后面文件的冲突的key/value覆盖前面的value文件,所以我们这样写了后会用.Values.K8sConfig.NameSpace: prod
覆盖前面的.Value.K8sConfig.NameSpace: stage
这样我们的obj就发布到了prod环境中
root@k8s-master:~/k8s_demo_config# helm upgrade golang-web-application golang_web_application -f golang_web_application/values-stage.yaml -f golang_web_application/values-prod.yaml
Release "golang-web-application" has been upgraded. Happy Helming!
NAME: golang-web-application
LAST DEPLOYED: Wed Aug 16 13:08:23 2023
NAMESPACE: default
STATUS: deployed
REVISION: 2
TEST SUITE: None
然后我们查看prod环境,发现成果
root@k8s-master:~/k8s_demo_config# kubectl get pods -n prod
NAME READY STATUS RESTARTS AGE
golang-server-564b599b7-84hh2 1/1 Running 0 26s
golang-server-564b599b7-9h7qx 1/1 Running 0 26s
root@k8s-master:~/k8s_demo_config# kubectl get service -n prod
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
golang-server NodePort 10.96.38.211 <none> 8081:30810/TCP 32s
假设我们更改了服务的k8s obj配置只需要改对应的value即可,如下我们改了deplyment的数量只需要helm upgrade即可(helm update是更新库)
root@k8s-master:~/k8s_demo_config# vim golang_web_application/values-stage.yaml
K8sConfig:ServerName: golang-serverReplicas: 3ContainerPort: 8080ServicePort: 8081NameSpace: stageImage:Name: honkytonkman/server_in_k8sTag: latest
然后upgrade
root@k8s-master:~/k8s_demo_config# helm upgrade golang-web-application golang_web_application -f golang_web_application/values-stage.yaml -f golang_web_application/values-prod.yaml
Release "golang-web-application" has been upgraded. Happy Helming!
NAME: golang-web-application
LAST DEPLOYED: Wed Aug 16 13:15:40 2023
NAMESPACE: default
STATUS: deployed
REVISION: 3
TEST SUITE: None
发现pod变成了3个
root@k8s-master:~/k8s_demo_config# kubectl get pods -n prod NAME READY STATUS RESTARTS AGE
golang-server-564b599b7-84hh2 1/1 Running 0 7m23s
golang-server-564b599b7-9h7qx 1/1 Running 0 7m23s
golang-server-564b599b7-f7c4j 1/1 Running 0 7s
假设新版本出现了故障,我们需要立马回退然后debug直接helm rollback加上release name即可如下
先查看helm release name
root@k8s-master:~/k8s_demo_config# helm ls
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
golang-web-application default 3 2023-08-16 13:15:40.319255029 +0800 CST deployed golang_web_application-0.1.0 1.16.0
release name就是golang-web-application
然后rollback到最近的一版
root@k8s-master:~/k8s_demo_config# helm rollback golang-web-application
Rollback was a success! Happy Helming!
然后查看pod
root@k8s-master:~/k8s_demo_config# kubectl get pods -n prod
NAME READY STATUS RESTARTS AGE
golang-server-564b599b7-84hh2 1/1 Running 0 22m
golang-server-564b599b7-9h7qx 1/1 Running 0 22m
发现rollback成功
我们用git push的时候可以为我们待push的代码加上–commit描述我们的这次push的message,同理,helm也可以在helm install的时候–description即可
我们再改pod数量为1此时加上description描述我们此次的改动
root@k8s-master:~/k8s_demo_config# helm upgrade golang-web-application golang_web_application -f golang_web_application/values-stage.yaml -f golang_web_application/values-prod.yaml --description "set podv number to 1"
Release "golang-web-application" has been upgraded. Happy Helming!
NAME: golang-web-application
LAST DEPLOYED: Wed Aug 16 13:36:10 2023
NAMESPACE: default
STATUS: deployed
REVISION: 5
TEST SUITE: None
再查看history可以看到我们upgrade的message
root@k8s-master:~/k8s_demo_config# helm history golang-web-application
REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
1 Wed Aug 16 12:55:05 2023 superseded golang_web_application-0.1.0 1.16.0 Install complete
2 Wed Aug 16 13:08:23 2023 superseded golang_web_application-0.1.0 1.16.0 Upgrade complete
3 Wed Aug 16 13:15:40 2023 superseded golang_web_application-0.1.0 1.16.0 Upgrade complete
4 Wed Aug 16 13:30:19 2023 superseded golang_web_application-0.1.0 1.16.0 Rollback to 2
5 Wed Aug 16 13:36:10 2023 deployed golang_web_application-0.1.0 1.16.0 set podv number to 1
prometheus && grafna
prometheuse我们使用operator来进行搭建
prometheus
首先将Prometheus-operator从github下载到服务器上,再进入主目录执行下面命令安装Prometheus-operator自定义的crd(),后面我们要用这些crd资源创建对象管理Prometheus服务,就和deployment,pod,service一样
root@k8s-master:~/prometheus/prometheus-operator# kubectl create -f bundle.yaml
customresourcedefinition.apiextensions.k8s.io/alertmanagerconfigs.monitoring.coreos.com created
customresourcedefinition.apiextensions.k8s.io/alertmanagers.monitoring.coreos.com created
customresourcedefinition.apiextensions.k8s.io/podmonitors.monitoring.coreos.com created
customresourcedefinition.apiextensions.k8s.io/probes.monitoring.coreos.com created
customresourcedefinition.apiextensions.k8s.io/prometheuses.monitoring.coreos.com created
customresourcedefinition.apiextensions.k8s.io/prometheusrules.monitoring.coreos.com created
customresourcedefinition.apiextensions.k8s.io/servicemonitors.monitoring.coreos.com created
customresourcedefinition.apiextensions.k8s.io/thanosrulers.monitoring.coreos.com created
clusterrolebinding.rbac.authorization.k8s.io/prometheus-operator created
clusterrole.rbac.authorization.k8s.io/prometheus-operator created
deployment.apps/prometheus-operator created
serviceaccount/prometheus-operator created
service/prometheus-operator created
配置rbac,因为Prometheus operator(任何operator都一样)想接入到k8s集群中都需要配置权限
先创建serviceaccount(因为github里面有写好的安装包我们直接使用)
root@k8s-master:~/prometheus/prometheus-operator# kubectl apply -f example/rbac/prometheus-operator/prometheus-operator-service-account.yaml
创建role,同上,Prometheus-operator的github官网为我们提供了
root@k8s-master:~/prometheus/prometheus-operator# kubectl apply -f example/rbac/prometheus-operator/prometheus-operator-cluster-role.yaml
创建role bind,绑定service和role
root@k8s-master:~/prometheus/prometheus-operator# kubectl apply -f example/rbac/prometheus-operator/prometheus-operator-cluster-role-binding.yaml
查看一下rolebind是否绑定成功
root@k8s-master:~/prometheus/prometheus-operator# kubectl describe clusterrolebinding prometheus-operator
Name: prometheus-operator
Labels: app.kubernetes.io/component=controllerapp.kubernetes.io/name=prometheus-operatorapp.kubernetes.io/version=0.63.0
Annotations: <none>
Role:Kind: ClusterRoleName: prometheus-operator
Subjects:Kind Name Namespace---- ---- ---------ServiceAccount prometheus-operator default
然后使用Prometheus-operator自定义的Prometheus资源创造Prometheus obj
root@k8s-master:~/prometheus/prometheus-operator# kubectl apply -f prometheus.yaml
prometheus.monitoring.coreos.com/prometheus-operator created
这个是自己写的如下
apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:name: prometheus-operator
spec:serviceAccountName: prometheus-operator #一定要写之前创建绑定的serviceaccount,不然就会使用默认的serviceaccount default(创建namespace的时候默认创建)serviceMonitorNamespaceSelector: {} #指定监控的namespace范围{}匹配所有,可以往下面加mathchLabels指定标签serviceMonitorSelector: {} #指定监控的service范围{}匹配所有,可以往下面加mathchLabels指定标签podMonitorSelector: {} #指定监控的pod范围{}匹配所有,可以往下面加mathchLabels指定标签resources:requests:memory: 400Mi
然后进入代码目录中根据前面的service(golang-server)写对应的监控配置(当然也是用Prometheus-operator创建的crd resource)
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor #Prometheus crd定义的
metadata:name: golang-service-monitor
spec:endpoints:- port: web #这个是我们被监控service创建的endpoint指定namespaceSelector:any: true #找所有的namespace,也可以指定特定的namespaceselector:matchLabels:app: web #基于前面指定的namespace,再匹配特定的labels
然后apply他
root@k8s-master:~/k8s_demo_config/prometheus-monitor-config# kubectl apply -f service.monitor.yaml
这里非常关键
我们先看Prometheus-operator的pod(创建Prometheus资源对象的时候创建的)的日志
root@k8s-master:~/k8s_demo_config/prometheus-monitor-config# kubectl logs prometheus-prometheus-operator-0 | tail -f
输出如下的日志
ts=2023-08-30T08:55:57.341Z caller=klog.go:116 level=error component=k8s_client_runtime func=ErrorDepth
msg="pkg/mod/k8s.io/client-go@v0.26.1/tools/cache/reflector.go:169: Failed to watch *v1.Endpoints: failed to list
*v1.Endpoints: endpoints is forbidden: User \"system:serviceaccount:default:prometheus-operator\" cannot list
resource \"endpoints\" in API group \"\" at the cluster scope"
很明了的告诉了你,你名称为Prometheus-operator的serviceaccount没有权限去list service和endpoint对象!所以我们要在之前apply的role yaml中加上对应的verb(list操作)
- apiGroups:- ""resources:- services- services/finalizers- endpointsverbs:- list #这里- get- create- update- delete
然后再apply,发现还是报错,这个报错
ts=2023-08-30T09:18:36.476Z caller=klog.go:116 level=error component=k8s_client_runtime func=ErrorDepth
msg="pkg/mod/k8s.io/client-go@v0.26.1/tools/cache/reflector.go:169: Failed to watch *v1.Pod: unknown (get pods)"
这个报错是因为我们没有在服务中暴露/mertics
,不管他,直接进Prometheus的web界面,进之前先转发端口(9090端口)
root@k8s-master:~/k8s_demo_config/prometheus-monitor-config# kubectl get svc/prometheus-operated
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
prometheus-operated ClusterIP None <none> 9090/TCP 50m
root@k8s-master:~/k8s_demo_config/prometheus-monitor-config# kubectl port-forward svc/prometheus-operated 9090:9090 --address=${YOUR_HOST_IP}
登录后发现的确存在这个target
metric
Prometheus有4个类型的metric
- Counter:counter只能增加或者被设为0或者重新回归初值再增加
- Gauge:也代表一个数字,可以增加或者下降,比如代表温度之类的
- Histogram:
- Summary:
使用也非常的简单,在go中主要使用这2个库
"github.com/prometheus/client_golang/prometheus""github.com/prometheus/client_golang/prometheus/promhttp"
首先我们创建metric需要先注册metric,我们的这个metric是counter类型所以要注册counter类型metric(prometheus.NewCounter()
)
var http_request_total = prometheus.NewCounter(prometheus.CounterOpts{Name: "METRIC_NAME",Help: "METRIC_HELPER",},)
prometheus.MustRegister(http_request_total)
那么我们具体怎么对这个metric设置具体值呢?因为是counter所以他是从0开始增加且,只能增加,或者reset成0,我们需要在我们具体的处理或者统计函数中使用http_request_total.Inc()
就可以完成metric的自增,http_request_total 就是之前prometheus.NewCounter
的返回值