1、Jenkins概念 1.1 前言 Jenkins 是一个开源的自动化构建工具,用于持续集成和持续交付。它可以帮助开发团队自动化构建、测试和部署软件,提高软件开发的效率和质量。Jenkins 具有以下特点:
开源:Jenkins 是一个开源软件,可以免费使用。
易用:Jenkins 提供了丰富的插件和模板,可以快速地搭建自动化构建和部署流程。
可扩展:Jenkins 支持插件扩展,可以满足不同项目的需求。
支持多种编程语言:Jenkins 支持多种编程语言,如 Java、Python、Ruby 等。
支持多种构建工具:Jenkins 支持多种构建工具,如 Maven、Gradle、Ansible 等。
支持多种部署方式:Jenkins 支持多种部署方式,如 SCM 仓库、Git 仓库、SVN 仓库等。
支持多种测试工具:Jenkins 支持多种测试工具,如 Selenium、JUnit、SonarQube 等。
总之,Jenkins 是一个功能强大的自动化构建工具,可以帮助开发团队提高软件开发的效率和质量。
在生产环境中我们往往会在物理机或者虚拟机上部署jenkins,但是这种部署方式会有一些痛点,如下:
主 Master 发生单点故障时,整个流程都不可用了
每个 Slave 的配置环境不一样,来完成不同语言的编译打包等操作,但是这些差异化的配置导致管理起来非常不方便,维护起来也是比较费劲
资源分配不均衡,有的 Slave 要运行的 job 出现排队等待,而有的 Slave 处于空闲状态
资源有浪费,每台 Slave 可能是物理机或者虚拟机,当 Slave 处于空闲状态时,也不会完全释放掉资源。
扩展性差:在物理机或虚拟机上部署 Jenkins,当 Jenkins 需要扩展时,需要购买更多的服务器或虚拟机,扩展性较差。
正因为上面的这些种种痛点,我们渴望一种更高效更可靠的方式来完成这个 CI/CD 流程,而 Docker容器技术能很好的解决这个痛点,又特别是在 Kubernetes 集群环境下面能够更好来解决上面的问题。
优势
基于云原生现有K8s集群来解决问题,充分的利用现有资源,无需再申请新虚机;
Slave可在构建任务来之时动态创建,工作结束后自动销毁,释放资源;
可通过K8s原生来管理Jenkins的调度策略,防止Slave调度分配不均匀;
通过云原生控制器来管理Jenkins配置,后期比较利于维护、扩展;
Jenkins 小概率意外宕机场景,通过K8s的机制可以自愈;
劣势
增加了系统复杂度;
有一定技术壁垒;
实现需要时间;
1.2 Master-Slave 工作原理 下图是基于 Kubernetes 搭建 Jenkins 集群的简单示意图:
从图中可以看出,Jenkins Master和Jenkins Slave以Pod的形式运行在Kubernetes集群的节点上。Master运行在其中的一个节点上,并将其配置数据存储到一个Volume中,而Slave运行在各个节点上,并且它并不总是处于运行状态,它会根据需求动态地创建并自动删除。
这种方法的工作流程大致如下:当Jenkins Master收到一个构建请求时,它会根据配置的Label动态地创建一个运行在Pod中的Jenkins Slave并将其注册到Master上。在运行完Job之后,这个Slave会被注销,Pod也会自动删除,恢复到原始状态。
设计优势
动态伸缩
合理的使用资源,每次运行 Job 时,会自动创建一个 Jenkins Slave,Job 完成后, Slave 自动注销并删除容器,资源自动释放,而且 Kubernetes 会根据每个资源的使用情况,动态分配 Slave 到空闲的节点上创建,降低出现因某节点资源利用率高, 还排队等待在该节点的情况。
服务高可用
当 Jenkins Master 出现故障时,Kubernetes 会自动创建一个新的 Jenkins Master 容器,并且将 Volume 分配给新创建的容器,保证数据不丢失,从而达到集群服务高可用。
扩展性好
当 Kubernetes 集群的资源严重不足而导致 Job 排队等待时,可以很容易的添加一 个 Kubernetes Node 到集群中,从而实现扩展。
2、Jenkins Master部署 在kubernetes中我们可以通过helm或者自定义控制器文件部署,本文我们介绍自定义控制器文件部署方式。
Jenkins使用NFS做数据持久化
首先部署NFS服务器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 $ sudo apt-get update $ sudo apt-get install nfs-kernel-server $ sudo yum install nfs-utils sudo mkdir /nfsshare $ sudo chmod 777 /nfsshare $ sudo nano /etc/exports $ /nfsshare *(rw,sync ,no_root_squash) $ sudo systemctl start nfs-kernel-server sudo systemctl enable nfs-kernel-server $ sudo apt-get install nfs-kernel-client $ sudo yum install nfs-utils $ showmount -e 192.168.1.0
创建PV、PVC,为Jenkins提供数据持久化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 $ vim jenkins-pvc.yml --- apiVersion: v1 kind: PersistentVolume metadata: name: jenkins-pv spec: capacity: storage: 10Gi accessModes: - ReadWriteMany persistentVolumeReclaimPolicy: Delete nfs: server: 10.0 .1 .2 path: /volume1/k8s/ha-nfs/jenkins --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: jenkins-pvc namespace: devops spec: accessModes: - ReadWriteMany resources: requests: storage: 10Gi
应用Jenkins-pvc.yaml时,需要确认没有默认的storageclass,否则不会直接使用文件内的nfs路径,而是使用默认storageclass进行pvc创建
创建角色授权
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 $ jenkins-rbac.yaml apiVersion: v1 kind: ServiceAccount metadata: name: jenkins-sa namespace: devops --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: jenkins-cr rules: - apiGroups: ["extensions" , "apps" ] resources: ["deployments" ] verbs: ["create" , "delete" , "get" , "list" , "watch" , "patch" , "update" ] - apiGroups: ["" ] resources: ["services" ] verbs: ["create" , "delete" , "get" , "list" , "watch" , "patch" , "update" ] - apiGroups: ["" ] resources: ["pods" ] verbs: ["create" ,"delete" ,"get" ,"list" ,"patch" ,"update" ,"watch" ] - apiGroups: ["" ] resources: ["pods/exec" ] verbs: ["create" ,"delete" ,"get" ,"list" ,"patch" ,"update" ,"watch" ] - apiGroups: ["" ] resources: ["pods/log" ] verbs: ["get" ,"list" ,"watch" ] - apiGroups: ["" ] resources: ["secrets" ] verbs: ["get" ]--- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: jenkins-crd roleRef: kind: ClusterRole name: jenkins-cr apiGroup: rbac.authorization.k8s.io subjects: - kind: ServiceAccount name: jenkins-sa namespace: devops
在Kubernetes中部署Jenkins,新建Deployment
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 $ vim jenkins-deploy.yaml --- apiVersion: apps/v1 kind: Deployment metadata: name: jenkins namespace: devops spec: selector: matchLabels: app: jenkins template: metadata: labels: app: jenkins spec: terminationGracePeriodSeconds: 10 serviceAccount: jenkins-sa containers: - name: jenkins image: jenkins/jenkins:lts imagePullPolicy: IfNotPresent ports: - containerPort: 8080 name: web protocol: TCP - containerPort: 50000 name: agent protocol: TCP resources: limits: cpu: 2000m memory: 3Gi requests: cpu: 500m memory: 1Gi livenessProbe: httpGet: path: /login port: 8080 initialDelaySeconds: 60 timeoutSeconds: 5 failureThreshold: 12 readinessProbe: httpGet: path: /login port: 8080 initialDelaySeconds: 60 timeoutSeconds: 5 failureThreshold: 12 volumeMounts: - name: jenkinshome mountPath: /var/jenkins_home env: - name: JAVA_OPTS value: -XshowSettings:vm -Dhudson.slaves.NodeProvisioner.initialDelay=0 -Dhudson.slaves.NodeProvisioner.MARGIN=50 -Duser.timezone=Asia/Shanghai -Dhudson.slaves.NodeProvisioner.MARGIN0=0.85 securityContext: runAsUser: 0 volumes: - name: jenkinshome persistentVolumeClaim: claimName: jenkins-pvc
默认情况下,Jenkins生成代理是保守的。
例如,如果队列中有两个构建,它不会立即生成两个执行器。它将生成一个执行器,并等待某个时间释放第一个执行器,然后再决定生成第二个执行器。Jenkins确保它生成的每个执行器都得到了最大限度的利用。
如果你想覆盖这个行为,并生成一个为每个构建队列不等待的执行器,所以在Jenkins启动时候添加这些参数:
-Dhudson.slaves.NodeProvisioner.initialDelay=0
-Dhudson.slaves.NodeProvisioner.MARGIN=50
-Dhudson.slaves.NodeProvisioner.MARGIN0=0.85
创建集群访问入口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 $ vim jenkins-svc.yaml --- apiVersion: v1 kind: Service metadata: name: jenkins namespace: devops labels: app: jenkins spec: selector: app: jenkins type: NodePort ports: - name: web port: 8080 targetPort: 8080 nodePort: 32000 - name: agent port: 50000 targetPort: 50000
部署Jenkins
1 $ kubectl apply -f jenkins-rbac.yaml -f jenkins-pvc.yaml -f jenkins-deploy.yaml -f jenkins-svc.yaml
初始化的密码我们可以在 jenkins 的容器的日志中进行查看,也可以通过指定数据位置查看:
1 $ kubectl exec -it jenkins-xxxxx -ndevops -- cat /var/jenkins_home/secrets/initialAdminPassword
访问
在配置文件中,我们配置的端口类型为NodePort,所以我们使用k8s节点ip+端口即可访问
1 2 3 4 $ kubectl get svc -n devops NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE jenkins NodePort 10.15.151.20 <none> 8080:32000/TCP,50000:31510/TCP 16d
地址: http://nodeip:32000
3、Jenkins Master配置 3.1 插件安装 “系统管理” –> “插件管理” –> “可选插件”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Chinese 中文 Gitlab Git Parameter Extended Choice Parameter 构建job时实用插件 Docker Groovy Kubernetes Pipeline Config File Provider active choices kubernetes Continuous Deploy http request build user vars description setter Describe With Params Build Name and Description Setter Pipeline Stage View
新版本jenkins中已弃用kubernetes Continuous Deploy,可以通过从本地系统中上传插件文件进行安装,下载地址:https://oss.zhoumx.net/kubernetes_Continuous_Deploy.hpi
3.2 全局凭据 (unrestricted)
1、gitlab-auth-passwd,类型:Username with password
2、Harbor,类型:Username with password
3、kubeconfig,类型: Secret file
Kubeconfig : 如果是kubeadm部署的,配置文件:
1 $ cat /root/.kube/config
3.3 在Jenkins上添加k8s 系统管理 –> 节点管理 –> Configure Cloud
如果jenkins是直接部署在k8s之内的,就比较简单,不需要填写证书,直接这样,如图:
如果jenkins部署在集群外,新建一个按照3.2步骤新建一个凭据,通过ingress将k8s api接口暴露,配置DNS解析只想LoadBalancer。
点击Test Connection,如果出现 Connection test successful 或图中的提示信息证明 Jenkins 已经可以和 Kubernetes 系统正常通信
4、Jenkins Slave部署 4.1 Slave Container 设计思路 我们都知道,基于k8s拉起的服务,实际上都是以Pod形式存在的,而Pod是个容器组, 最终服务实际是以Pod内的Container来支撑运行的。那么针对Slave的应用场景, Container应该如何设计?
Jenkins-Master在构建Job的时候,Kubernetes会创建Jenkins-Slave的Pod来完成 Job的构建
我们选择运行Jenkins-Slave的镜像为官方推荐镜像:jenkins/inbound-agent:latest,但是这个镜像里面并没有Maven 环境,为了方便使用,我们需要自定义一个新的镜像
这里我们使用docker进行slave的构建
Dockerfile文件内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 FROM jenkins/inbound-agent:latestUSER rootCOPY apache-maven-3.3.9-bin.tar.gz . RUN tar -zxf apache-maven-3.3.9-bin.tar.gz && \ mv apache-maven-3.3.9 /usr/local && \ rm -f apache-maven-3.3.9-bin.tar.gz && \ ln -s /usr/local/apache-maven-3.3.9/bin/mvn /usr/bin/mvn && \ ln -s /usr/local/apache-maven-3.3.9 /usr/local/apache-maven && \ mkdir -p /usr/local/apache-maven/repo COPY settings.xml /usr/local/apache-maven/conf/settings.xml USER jenkins
构建镜像
1 docker build -t jenkins-slave-maven:v1 .
上传harbor仓库(公共镜像全部放在/library/下)
1 docker tag jenkins-slave-maven:v1 harbor.zhoumx.cc/library/jenkins-slave-maven:v1
为了不侵入宿主机Node上的docker服务,那么目前最佳的思路是要引入Docker in Docker技术来完成:
1 docker tag docker:stable harbor.zhoumx.cc/library/docker:stable
4.2 测试验证 新建任务
脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def git_address = "https://gitlab.zhoumx.net/kubernetes/springdemo.git" def git_auth = "d94c267d-20be-4778-bfad-2a26f568a508" podTemplate(label: 'jenkins-slave' , cloud: 'kubernetes' , containers: [ containerTemplate( name: 'jnlp' , image: "harbor.zhoumx.cc/library/jenkins-slave-maven:v1" ) ] ) { node("jenkins-slave" ){ stage('拉取代码' ){ checkout([$class: 'GitSCM' , branches: [[name: '*/main' ]], extensions: [], userRemoteConfigs: [[credentialsId: "${git_auth}" , url: "${git_address}" ]]]) } } }
执行结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 Running on jenkins-slave-r5q0z-sx1s4 in /home/jenkins/agent/workspace/test-pipeline [Pipeline] { [Pipeline] stage [Pipeline] { (拉取代码) [Pipeline] checkout The recommended git tool is: NONE using credential d94c267d-20be-4778-bfad-2a26f568a508 Cloning the remote Git repository Avoid second fetch Cloning repository https://gitlab.zhoumx.net/kubernetes/springdemo.git > git init /home/jenkins/agent/workspace/test-pipeline Fetching upstream changes from https://gitlab.zhoumx.net/kubernetes/springdemo.git > git --version > git --version using GIT_ASKPASS to set credentials Gitlab-user > git fetch --tags --force --progress -- https://gitlab.zhoumx.net/kubernetes/springdemo.git +refs/heads/*:refs/remotes/origin/* > git config remote.origin.url https://gitlab.zhoumx.net/kubernetes/springdemo.git > git config --add remote.origin.fetch +refs/heads/*:refs/remotes/origin/* > git rev-parse refs/remotes/origin/main^{commit} Checking out Revision 910acad93981159051ee0d095742162e1bae2ce5 (refs/remotes/origin/main) Commit message: "更新.gitlab-ci.yml文件" > git config core.sparsecheckout > git checkout -f 910acad93981159051ee0d095742162e1bae2ce5 > git rev-list --no-walk 910acad93981159051ee0d095742162e1bae2ce5 [Pipeline] } [Pipeline] // stage [Pipeline] } [Pipeline] // node [Pipeline] } [Pipeline] // podTemplate [Pipeline] End of Pipeline Finished: SUCCESS