【编者的话】Kubernetes的出现让容器的调度管理变的简单,但是Kubernetes本身的“部署难”又让很多人止步,对该技术的普及也有一定的影响。文章首先,从Kubernetes的架构角度,为大家讲解一下从零部署一个生产可用的Kubernetes集群,并对Kubelet TLS Bootstrapping的原理进行讲解。
先说一下我们生产环境Kubernets集群部署,为方便后期维护及减少一层Docker封装,我们选择采用二进制方式部署Kubernetes集群。(kubeadm方式部署全部运行在Docker容器中、证书时长限制等;虽然降低部署门槛,但屏蔽了很多细节,遇到问题很难排查等。)
Kubernets集群部署流程:
Kubernetes集群部署要点:
我已经将Kubernetes集群安装过程写成ansible-playbook,实现一键部署、扩容Kubernets 高可用集群 。Playbook包含了系统初始化、签发证书、部署etcd、部署Kubernetes、替换集群证书等。
我们DevOps平台对接了Ansible Playbook,可在DevOps平台上操作部署、扩容,配合工单系统,完成全自动化流程。
DevOps平台操作部署、扩容Kubernetes集群流程:
同时也可以使用以下命令进行操作:
通过以下命令即可将一批刚刚安装好系统、配置好网络的机器部署为一套生产可用的Kubernetes集群:
ansible-playbook k8s.yml -i inventory
扩容Master节点:
ansible-playbook k8s.yml -i inventory -t init -l master ansible-playbook k8s.yml -i inventory -t cert,install_master
扩容Node节点:
ansible-playbook k8s.yml -i inventory -t init -l node ansible-playbook k8s.yml -i inventory -t cert,install_node
通过Playbook部署集群,可以在很短的时间内完成集群扩容。生产上使用本Playbook最高一次性扩容过50个Node节点。
这里先简单介绍下Kubelet TLS Bootstrapping和etcd的自动备份,其他部分就不再进一步讲解,具体详细部署大家可参考《 Kubernetes高可用集群之二进制部署 》。
在Kubernetes集群中,工作节点上的组件(kubelet和kube-proxy)需要与Kubernetes主组件通信,特别是kube-apiserver。为了确保通信保持私密,不受干扰,并确保群集的每个组件与另一个受信任组件通信,我们强烈建议在节点上使用客户端TLS证书。
引导这些组件的正常过程,特别是需要证书的工作节点,因此它们可以与kube-apiserver安全地通信,这可能是一个具有挑战性的过程,因为它通常超出了Kubernetes的范围,需要大量的额外工作。反过来,这可能会使初始化或扩展集群变得具有挑战性。
为了简化流程,从版本1.4开始,Kubernetes引入了证书请求和签名API以简化流程。
Kubernetes开启TLS认证后,每个N ode节点的kubelet都要使用由kube-apiserver的CA签发证书,才能与kube-apiserver进行通信,如果接入比较多的节点,为每个节点生成证书不太现实,Bootstrapping就是让kubelet先使用一个低权限用户连接到kube-apiserver,然后由kube-controller-manager为kubelet动态签发证书。再配合RBAC指定用户拥有访问哪些API的权限。
kubelet启动后会发起CSR请求,kube-apiserver收到CSR请求后,对其中的Token进行认证,认证通过后将请求的user设置为 system:bootstrap:<Token ID>
,group设置为 system:bootstrappers
,这一过程称为Bootstrap Token Auth。
kubelet发起的CSR请求都是由kube-controller-manager来做实际签发的,对于kube-controller-manager来说,TLS bootstrapping下kubelet发起的CSR请求大致分为以下三种:
O=system:nodes
和 CN=system:node:(node name)
形式发起的CSR请求 使用Bootstrap Token时整个启动引导过程:
Bootstrap Token Secret
,替换以前使用 token.csv
文件 bootstrap.kubeconfig
以供kubelet启动时使用 bootstrap.kubeconfig
并使用其中的 TLS Bootstrapping Token 完成首次证书申请 Bootstrap Token Secret
,或等待Controller Manager待其过期后删除,以防止被恶意利用 下面讲一下etcd备份。
etcd是Kubernetes集群中的一个十分重要的组件,用于保存集群所有的网络配置和对象的状态信息。一旦出现问题或者数据丢失,影响将无法估计。所以,我们一定要做好etcd的数据备份。
这里利用Kubernetes的cronjob对etcd进行备份,并挂载外部存储,将数据备份在外部存储上。
以下是使用cronjob备份的yaml:
apiVersion: batch/v1beta1 kind: CronJob metadata: name: etcd-backup spec: schedule: "10 1 * * *" concurrencyPolicy: Allow failedJobsHistoryLimit: 3 successfulJobsHistoryLimit: 3 jobTemplate: spec: template: spec: containers: - name: etcd-backup imagePullPolicy: IfNotPresent image: k8sre/etcd:v3.3.15 env: - name: ENDPOINTS value: "https://172.16.70.131:2379,https://172.16.70.132:2379,https://172.16.70.133:2379" command: - /bin/bash - -c - | set -ex ETCDCTL_API=3 /opt/etcd/etcdctl / --endpoints=$ENDPOINTS / --cacert=/certs/ca.pem / --cert=/certs/etcd.pem / --key=/certs/etcd.key / snapshot save /data/backup/snapshot-$(date +"%Y%m%d").db volumeMounts: - name: etcd-backup mountPath: /data/backup - name: etcd-certs mountPath: /certs restartPolicy: OnFailure volumes: - name: etcd-backup persistentVolumeClaim: claimName: etcd-backup - name: etcd-certs secret: secretName: etcd-certs
下面来讲一下,我们的CI/CD是如何实现的。
先给大家看下我们大概的架构图:
我们业务主要的开发语言包含Java、PHP、Golang、NodeJs以及少部分Python,涉及开发语言较广,CI、CD需求各种各样等原因,我们选择使用Jenkins Pipeline来完成CI、CD的相关工作,全部逻辑通过Groovy语言编写。并通过Jenkins Agent的方式适配公司所有开发语言代码的CICD。同时支持虚拟机和Kubernetes部署。
Jenkins Master及所有agent都部署在Kubernetes集群,并使用PVC挂载一块极速NAS作为数据同步。下面给大家看下我们部分部署yaml:
jenkins-master.yaml
apiVersion: v1 kind: ServiceAccount metadata: name: jenkins-master namespace: jenkins --- apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: name: jenkins-master roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin subjects: - kind: ServiceAccount name: jenkins-master namespace: jenkins --- apiVersion: apps/v1 kind: StatefulSet metadata: name: jenkins-master namespace: jenkins spec: serviceName: "jenkins-master" replicas: 1 selector: matchLabels: app: jenkins-master template: metadata: labels: app: jenkins-master spec: serviceAccount: jenkins-master affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: app operator: In values: - jenkins containers: - name: jenkins image: k8sre/jenkins:master imagePullPolicy: Always securityContext: runAsUser: 0 privileged: true ports: - containerPort: 8080 name: http protocol: TCP - containerPort: 50000 name: agentport protocol: TCP resources: limits: cpu: 2 memory: 4Gi requests: cpu: 2 memory: 4Gi livenessProbe: tcpSocket: port: 8080 initialDelaySeconds: 10 timeoutSeconds: 3 periodSeconds: 5 readinessProbe: tcpSocket: port: 8080 initialDelaySeconds: 10 timeoutSeconds: 3 periodSeconds: 5 volumeMounts: - name: jenkins-data mountPath: /data/jenkins - name: sshkey mountPath: /root/.ssh/id_rsa subPath: id_rsa readOnly: true - name: jenkins-docker mountPath: /var/run/docker.sock - name: jenkins-maven mountPath: /usr/share/java/maven-3/conf/settings.xml subPath: settings.xml readOnly: true volumes: - name: sshkey configMap: defaultMode: 0600 name: sshkey - name: jenkins-docker hostPath: path: /var/run/docker.sock type: Socket - name: jenkins-maven configMap: name: jenkins-maven - name: jenkins-data persistentVolumeClaim: claimName: jenkins --- apiVersion: v1 kind: Service metadata: name: jenkins-master namespace: jenkins spec: ports: - name: web port: 8080 protocol: TCP targetPort: 8080 - name: agent port: 50000 protocol: TCP targetPort: 50000 selector: app: jenkins-master
agent-go.yaml
apiVersion: v1 kind: ServiceAccount metadata: name: agent-go namespace: jenkins --- apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: name: agent-go roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin subjects: - kind: ServiceAccount name: agent-go namespace: jenkins --- apiVersion: apps/v1 kind: StatefulSet metadata: name: agent-go namespace: jenkins spec: serviceName: "agent-go" replicas: 1 selector: matchLabels: app: agent-go template: metadata: labels: app: agent-go spec: serviceAccount: agent-go affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: role operator: In values: - jenkins containers: - name: jenkins image: k8sre/jenkins:go imagePullPolicy: Always securityContext: runAsUser: 600 privileged: true env: - name: "ENV_NAME" value: "go" - name: "JKS_SECRET" value: "xxxxxxxxxxxxxx" - name: "JKS_DOMAINNAME" value: "https://ci.k8sre.com" - name: "JKS_ARGS" value: "-Xmx4g -Xms4g" - name: "JKS_DIR" value: "/data/jenkins" volumeMounts: - mountPath: /data/jenkins name: jenkins-data volumes: - name: jenkins-data persistentVolumeClaim: claimName: jenkins
下图是我们目前Jenkins在使用的节点:
我们CI/CD的流程:
以下是CI/CD的流程图:
Jenkins Pipeline测试环境阶段视图:
一些亮点功能:
有很多案例使用gitlab-ci实现的CI/CD,我们通过调研发现gitlab-ci有一定的侵入性,而且Pipeline支持比较弱。我们使用Jenkins Pipeline完成了公司全部的CI/CD需求,其中还包含了运维相关系统的部署、安卓打包及渠道包打包、IOS打包,并提供了二维码展示,方便测试或运营人员快速下载安装。
使用Groovy完成整套流程,方便、易用便于维护,但是其中处理逻辑相对复杂,需要对业务非常了解,并且掌握Groovy语言。同时选择使用Helm部署也遇到不少问题,Release名称在不同namespace不能重用(helm3已经支持),模版使用APIVersion与实际部署资源对象APIVersion不一致(影响不大),同版本代码部署不更新(通过添加自定义annotation实现支持)等等。另外,我也测试了Helm 3,前期bug太多,属于完全不可用状态,最近新出了alpha2版本,修复了很多bug,可以考虑测试使用了。
Q:请问是通过Jenkinsfile来控制版本管理吗,是否使用了Jenkins library,多个环境的情况下,是部署一个Jenkins Master,还是每个Kubernetes集群都带有一个Jenkins Master?
A:Jenkinsfile实现整个CI/CD流程,使用Git对Jenkinsfile代码进行版本管理;目前没有使用Jenkins library,但是Jenkins library的方式正在重写过程中,还没有完成;多环境的实现是只在一个集群部署一套Jenkins Master+Jenkins Agent,不同的环境通过不同Agent实现。
Q:每个Jenkins的Job里写一个Jenkinsfile的repo?这样是不是太浪费了。每个repo就一个Jenkinsfile文件(多环境可能有多个分支)。我们是直接写成了Jenkinsfile模板,然后Jenkins构建参数传入的。不知道老师是如何权衡的?
A:不是的,所有Job使用一个Jenkinsfile。每个Job就是传递一些基础参数,大多数配如编译命令,部署配置等,都是在DevOps平台先行配置好之后,触发Job构建之后从DevOps拉取参数配置。
Q:为什么拉取代码后就要进行Sonar代码扫描呢?研发的代码都没有集成编译验证,扫描代码有什么意义?
A:不是拉取代码前进行的扫描,是拉取代码完成后进行的扫描。方便对接多语言。这个看取舍的,我们因为涉及语言种类比较多,不太容易顾全所有的语言,而且前期我们PHP语言的业务较多。
Q:你们生产环境也关闭防火墙了吗?不用防火墙吗?还是你们用的其他的安全措施?
A:我们生产环境防火墙是关闭的,因为我们使用的是公有云环境,策略都是通过公有云安全组实现的。
Q:同Ansible集成那部分怎么实现的?中间涉及到传参数,例如IP地址,端口号,服务名称,是通过什么控制的?
A:基础环境信息,都在inventory里维护。其他的一些参数放在一个group_vars中。
Q:Node节点使用的是动态token还是apiserver内置的静态的token进行Bootstrap的?
A:动态token,不在apiserver的配置里指定token.csv。
Q:Jenkinsfile使用文档较少,可以提供在此遇到的坑列举几个典型吗?谢谢!
A:使用的话,可以去参考有一本Groovy的书。遇到的坑的话,大多数是CI/CD逻辑的问题,比如说,我们会有一个容器运行用户的配置,实际业务场景中有使用root的,有使用普通用户的,还会针对不同的环境适配不同的用户配置,逻辑处理出错就会导致实际部署后,Pod运行异常。
Q:请问主机系统初始化这一块对系统的发行版和内核版本有什么要求和建议?看到一些Docker的问题是因为系统内核版本太低 (3.10内核kmem account泄漏bugs导致节点NotReady),请问你们是如何选择的?
A:目前仅支持Redhat、CentOS,内核版本是3.10,没有进行升级。以我们集群运行这么长时间看,虽然有NotReady现象,但是概率比较小,固没对考虑对内核升级。
Q: 请问下集群规模是怎么样的,Node节点是怎么做资源保护的?
A:目前600个Node节点,我们目前是监控整体集群水位,在达到60%左右就会对集群进行扩容。
Q:如何实现灰度发布以及蓝绿部署?
A:基于Ingress实现的,部分是使用公有云的负载均衡,通过API实现切换。
Q:目前我们使用的gitlab-ci-runner部署于Kubernetes之外实现CI/CD。发现gitlab-ci在实际使用中,经常会遇到卡死报错。请问下,相比Jenkins 做CI/CD是会有什么优势,之前并没有使用过Jenkins?
A:gitlab-ci生产环境中,我们也没有使用,我们调研的结果是:1、有侵入性;2、Pipeline功能较弱,但是有一个好处是遇到错误好像还可以继续执行。Jenkins遇到错误会中断流程。
Q:基于kubeadm+Calico,空闲时CPU占用达到30-40%是否正常?
A:实际使用中没有使用过kubeadm部署,因为封装东西太多,不易于排查问题。空闲时CPU到30-40,需要具体情况分析。
Q:600个Node节点都遇到过什么问题, 有什么需要注意的?
A:前期网络上遇到的问题比较多,还包含calico bug。后面多数是一些业务使用上导致的问题,还有业务增量之后引发的各种问题。
Q:请问是怎么配置的多个不同功能的Jenkins-slave Pod的还有Jenkins-slave镜像怎么做的,还有一个任务中有发布和回滚怎么做呢,老师的CI/CD人工干预的地方在哪里?
A:Jenkins-slave镜像实际就是把Jenkins的agent.jar运行在容器中。发布就如同前面所讲。回滚最终是调用helm rollback。CI/CD人工干预的话都是通过配置项来控制的。
以上内容根据2019年9月3日晚微信群分享内容整理。 分享人 马松林,现任DaoCloud交付架构师,负责Kubernetes、DCE、DMP、中间件、EFK平台的架构设计、技术方案评审等 。 DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiese,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。