文章

快速搭建高可用CI工具:基于K8S的Jenkins Master-Slave架构

主流CI工具对比

以下持续集成工具本人进行使用后做出的一些总结:

image-20240322213131415

不同项目需要根据企业要求选择合适的CI工具,例如:企业中采购了gitlab作为所有项目的代码仓库,可以尝试使用Gitlab CI。如果采购了Github企业版本,使用GitHub Actions也是一个不错的选择。同理,企业云战略选择Azure云,完全可以尝试Azure DevOps一站式解决Code Repository以及CI Pipeline。不要局限于特定的工具,按需求按场景进行调整即可。

Jenkins是老牌的CI工具,这里先以Jenkins为例,为大家快速演示如何搭建企业级的CI工具。

高可用 Jenkins 集群

高负载的情况下,经常出现不可用等情况时,是非常影响项目团队的开发效率和体验的。那么可以尝试结合K8S来搭建高可用的Jenkins集群。

image-20240322214844581

这里可以看到通过启动Slave Pod来执行不同的Task任务,这样我们可以通过Docker Image来自定义任何想要的镜像,如:支持Java语言、Golang语言、Node等等。甚至可以嵌套容器将MySQL、Redis等有状态的中间件使用容器的方式运行,用来满足执行集成测试或者API测试。

同理,目前很多CI工具也可以看到类似的设计,如:Drone CI工具,也是分为了 Drone Server和Drone Runner。

流水线的执行流程

image-20240322220228738

举个🌰

环境准备

  • Kubernetes 1.26.5

  • Kubectl

  • Helm v3.12.1

  • Text Editor (Yaml)

安装Jenkins Master

官方配置微调

以下是我通过Helm微调后后的Jenkins Master的部署脚本,调整的地方:

1)替换定制化Jenkins镜像:这里替换成我自己定制化的镜像,和官方的区别在于将主要的一些plugins直接打包进了Docker Image中,而不用在Yaml文件中,每次部署都需要重新下载,经常出现网络断开无法下载的情况,非常恶心。

插件列表如下:

git
credentials-binding
configuration-as-code
build-timestamp
build-timeout
htmlpublisher
build-monitor-plugin
kubernetes

2)关闭Agent Flag、调整Storage Class、调整javaOpts等,具体可以查看部署文件。

部署脚本

jenkins-local-pv.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-pv-jenkins
  labels:
    app: local-pv-jenkins
spec:
  capacity:
    storage: 8Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  storageClassName: local-storage
  persistentVolumeReclaimPolicy: Retain
  local:
    path: /Users/eric/Documents/data/jenkins
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - lima-rancher-desktop # <changeme>

local-jenkins-values.yaml

​
# https://github.com/jenkinsci/helm-charts/blob/main/charts/jenkins/values.yaml
# helm pull jenkins/jenkins --version 3.0.2 --untar
​
controller:
  componentName: "jenkins-master"
  image: "amuguelove/jenkins"
  tag: "alpine-plugins-20210425"
  imagePullPolicy: "IfNotPresent"
  imagePullSecretName:
​
  serviceType: ClusterIP
​
  adminSecret: true
  adminUser: "admin"
  # adminPassword: "xxx"   ## <defaults to random>
  admin:
    existingSecret: ""  # changeme
    userKey: jenkins-admin-user
    passwordKey: jenkins-admin-password
​
  resources:
    requests:
      cpu: "50m"
      memory: "512Mi"
    limits:
      cpu: "2000m"
      memory: "4096Mi"
​
  javaOpts: >-
    -Djenkins.install.runSetupWizard=false
    -Duser.timezone=Asia/Shanghai
    -Dhudson.slaves.NodeProvisioner.initialDelay=0
    -Dhudson.slaves.NodeProvisioner.MARGIN=50
    -Dhudson.slaves.NodeProvisioner.MARGIN0=0.85
    -DsessionTimeout=1440
    -DsessionEviction=43200
​
  installPlugins: []
​
  JCasC:
    defaultConfig: true
    configScripts: {}
    securityRealm: |-
      local:
        allowsSignup: false
        enableCaptcha: false
        users:
        - id: "${chart-admin-username}"
          name: "Jenkins Admin"
          password: "${chart-admin-password}"
    # Ignored if authorizationStrategy is defined in controller.JCasC.configScripts
    authorizationStrategy: |-
      loggedInUsersCanDoAnything:
        allowAnonymousRead: false
​
  ingress:
   enabled: false
​
agent:
  enabled: false
​
persistence:
  enabled: true  # changeme
  storageClass: "local-storage" # changeme
  size: "8Gi"
​
serviceAccount:
  create: true
  name: jenkins-master
​
rbac:
  create: true

执行脚本

# 准备
kubectl create namespace jenkins
kubectl apply -f jenkins-local-pv.yaml
​
# 安装Jenkins
helm upgrade --install \
  jenkins-master jenkins/jenkins \
  -n jenkins \
  -f local-jenkins-values.yaml \
  --version 3.3.18

执行结果

Release "jenkins-master" does not exist. Installing it now.
NAME: jenkins-master
LAST DEPLOYED: Sat Mar 23 10:06:23 2024
NAMESPACE: jenkins
STATUS: deployed
REVISION: 1
NOTES:
1. Get your 'admin' user password by running:
  kubectl exec --namespace jenkins -it svc/jenkins-master -c jenkins -- /bin/cat /run/secrets/chart-admin-password && echo
2. Get the Jenkins URL to visit by running these commands in the same shell:
  echo http://127.0.0.1:8080
  kubectl --namespace jenkins port-forward svc/jenkins-master 8080:8080
​
3. Login with the password from step 1 and the username: admin
4. Configure security realm and authorization strategy
5. Use Jenkins Configuration as Code by specifying configScripts in your values.yaml file, see documentation: http:///configuration-as-code and examples: https://github.com/jenkinsci/configuration-as-code-plugin/tree/master/demos
​
For more information on running Jenkins on Kubernetes, visit:
https://cloud.google.com/solutions/jenkins-on-container-engine
​
For more information about Jenkins Configuration as Code, visit:
https://jenkins.io/projects/jcasc/

登陆

### 使用host主机8080端口登陆
kubectl --namespace jenkins port-forward svc/jenkins-master 8080:8080
​
### 查看admin用户的登陆密码
kubectl exec --namespace jenkins -it svc/jenkins-master -c jenkins -- /bin/cat /run/secrets/chart-admin-password && echo
4nrtG6h2l1n3AE25aZt6th

通过http://localhost:8080访问Jenkins web服务了。

配置Jenknis Slave

接下来我们就需要来配置 Jenkins,让它能够动态的生成 Slave 的 Pod。

第一步:安装Kubernetes Plugin(定制化镜像中有,可忽略),点击 Manage Jenkins -> Manage Plugins -> Available -> Kubernetes plugin 勾选安装即可。

jenkins-slave-step1

第二步:填写 Kubernetes Cloud Details,点击 Manage Jenkins —> Configure System —> (拖到最下方)Add a new cloud —> 选择 Kubernetes

jenkins-slave-step2

这里需要点击Test Connection去验证是否可以连接你的Kubernetes集群。

第三步:填写Pod Template和Container Template

jenkins-slave-step3注意这里的名称最好都一致,例如我这边都使用jnlp-agent,之前因为名字不同导致无法完美运行。

第四步:添加卷信息

jenkins-slave-step4

可以看到这里分别挂载了主机的docker,kubectl等工具,其中Mount path根据实际情况进行调整。

如果碰到没有权限执行/var/run/docker.sock,可登陆到对应的node节点或者本地机器进行授权:

chmod 666 /var/run/docker.sock

可以使用docker context ls 查看本地的docker sock.

第五步:配置权限

image-20240323111944631

这里填写jenkins-master ,是我们在jenkins master中创建的一个Service Account。如果不填的话,会导致执行kubectl命令会出现权限不足。

验证

新建一个Freestyle的Item:

jenkins-slave-test1

填写在Container Template中的Label:

jenkins-slave-test2

添加一个Build Shell:

echo '=== 测试动态jenkins slave === '
​
echo '=== 打印docker信息 ==='
docker info
​
echo '=== 获取pods ==='
kubectl get pods

jenkins-slave-test3

在Jenkins web中点击立即构建Build Now,在控制观察kuberntes Pod的状态:

jenkins-slave-test4

执行完毕后,控制台会有如下输出:

Building remotely on jnlp-agent-l58sk (jnlp-agent) in workspace /home/jenkins/agent/workspace/jnlp-demo
[jnlp-demo] $ /bin/sh -xe /tmp/jenkins4069627081916824161.sh
+ echo === 测试动态jenkins slave === 
=== 测试动态jenkins slave === 
+ echo === 打印docker信息 ===
=== 打印docker信息 ===
+ docker info
Containers: 20
 Running: 19
 Paused: 0
 Stopped: 1
Images: 13
Server Version: 18.06.3-ce
Storage Driver: overlay2
 Backing Filesystem: extfs
 Supports d_type: true
 Native Overlay Diff: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins:
 Volume: local
 Network: bridge host macvlan null overlay
 Log: awslogs fluentd gcplogs gelf journald json-file logentries splunk syslog
Swarm: inactive
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
containerd version: e6b3f5632f50dbc4e9cb6288d911bf4f5e95b18e (expected: 468a545b9edcd5932818eb9de8e72413e616e86e)
runc version: f56b4cbeadc407e715d9b2ba49e62185bd81cef4 (expected: a592beb5bc4c4092b1b1bac971afed27687340c5)
init version: fec3683
Security Options:
 seccomp
  Profile: default
Kernel Version: 4.14.105-19-0008
Operating System: CentOS Linux 7 (Core)
OSType: linux
Architecture: x86_64
CPUs: 2
Total Memory: 3.476GiB
Name: VM_0_12_centos
ID: 5EKE:FP2N:QFR7:BHIV:5LLJ:OUUK:CDV6:PRPI:5TW2:GAKT:JCMH:OFF3
Docker Root Dir: /var/lib/docker
Debug Mode (client): false
Debug Mode (server): false
Registry: https://index.docker.io/v1/
Labels:
Experimental: false
Insecure Registries:
 127.0.0.0/8
Registry Mirrors:
 https://mirror.ccs.tencentyun.com/
Live Restore Enabled: true
​
+ echo === 获取pods ===
=== 获取pods ===
+ kubectl get pods
NAME                       READY   STATUS    RESTARTS   AGE
jenkins-587dc776c6-79hlg   1/1     Running   0          23m
jnlp-agent-l58sk           2/2     Running   0          17s
Finished: SUCCESS

到这里基于Kubernetes的动态Jenkins Slave就搭建完毕了。这里演示了一个简单的demo,如果在项目中有Jenkinsfile,可以定义node属性,指定使用jnlp-agent即可使用slave构建你的项目了。

可能碰到的坑与解决思路

  1. Slave Pod执行完任务后,会进行Pod销毁,这时在CI过程中所产生的数据也将丢失,如:流水线过程中的各种报告(Jacoco覆盖率报告、测试报告等),需要将这些有用的数据发布到Master上。

  2. 每次执行一次流水线,编译代码时,前后端的pod都会去下载很多依赖,如:Jar,npm文件等,如果每次Slave Pod启动都要重新下载这些文件,那么流水线的执行时长将远比虚拟机部署方式要长,注意这些依赖文件的缓存,每次Slave Pod启动能够访问到这些文件。

  3. 云厂商的选择上,PV挂载的远程云磁盘,是否支持在线扩容?

License:  CC BY 4.0