本文总结了我在工作中在云原生环境下对jenkins的使用,主要介绍了jenkins和kubernetes、docker、helm的结合,并使用一个angular项目和maven项目作为案例,分别展示了前后端的构建流程。

由于整理仓促,涉及到的内容时间跨度较长,难免有些细节不够严谨,仅供参考。

部署Jenkins

按照顺序依次创建以下k8s资源,在集群中快速部署一套比较简单的jenkins环境。

命名空间

apiVersion: v1
kind: Namespace
metadata:
  name: jenkins

service account

apiVersion: v1
kind: ServiceAccount
metadata:
  name: jenkins
  namespace: jenkins
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: jenkins
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/v1beta1
kind: ClusterRoleBinding
metadata:
  name: jenkins
  namespace: jenkins
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: jenkins
subjects:
  - kind: ServiceAccount
    name: jenkins
    namespace: jenkins

service

apiVersion: v1
kind: Service
metadata:
  name: jenkins
  namespace: jenkins
  labels:
    app: jenkins
spec:
  selector:
    app: jenkins
  type: NodePort
  ports:
    - name: web
      port: 8080
      targetPort: web
      # nodeport方式暴露访问地址
      nodePort: 30020
    - name: agent
      port: 50000
      targetPort: agent

pvc

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: jenkins
  namespace: jenkins
spec:
  # 使用cephfs作为pvc的动态提供者
  storageClassName: cephfs
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 5G

deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: jenkins
  namespace: jenkins
spec:
  selector:
    matchLabels:
      app: jenkins
  template:
    metadata:
      labels:
        app: jenkins
    spec:
      terminationGracePeriodSeconds: 10
      serviceAccount: jenkins
      # 用initContainer修复pvc数据目录的权限,确保容器进程可写
      initContainers:
        - name: permission-fixer
          image: alpine
          command: ["sh", "-c", "chown -R 1000 /cephfs"]
          volumeMounts:
            - name: jenkinshome
              mountPath: /cephfs
      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: 1000m
              memory: 1Gi
            requests:
              cpu: 500m
              memory: 512Mi
          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
              subPath: jenkins2
              mountPath: /var/jenkins_home
      volumes:
        - name: jenkinshome
          persistentVolumeClaim:
            claimName: jenkins

确认jenkins pod状态为running,且探针正常后,访问node节点上的http://node-ip:30020,进入jenkins web控制台。

初始化Jenkins

从pod 标准输出中找到解锁码,输入。

image-20200713004246987 image-20200713004345619

选择默认插件配置即可,开始插件安装过程。

image-20200713004437082

最后设置账号密码信息,完成基本配置登入jenkins控制台。

换源

如果集群所在内网没有配置路由器实现自由访问某些网络资源,可以考虑换源以加速插件下载速度

  1. 根据jenkins deployment持久化的情况,用shell进入pvc中的jenkins配置目录中,修改hudson.model.UpdateCenter.xml

    <?xml version='1.1' encoding='UTF-8'?>
    <sites>
      <site>
        <id>default</id>
        <url>http://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json</url>
      </site>
    </sites>
  2. 还需要替换两处

    sed -i 's/https:\/\/updates.jenkins.io\/download/http:\/\/mirrors.tuna.tsinghua.edu.cn\/jenkins/g ./updates/default.json
    sed -i 's/http:\/\/www.google.com/https:\/\/www.baidu.com/g' ./updates/default.json

通用设置

image-20211202152527186

集成k8s

  • 目标:实现jenkins调用k8s接口,操作docker、k8s,在动态生成的pod中完成ci、cd流程,最后销毁动态生成的pod。
  1. 在jenkins中安装k8s插件。

    image-20200713005145897
  2. 选择可选插件,并输入kubernetes搜索,找到如下图叫做kubernetes的插件(安装后在已安装插件列表里名字是kubernetes plugin,可以用简介和版本号进行区分)。

    image-20200713005323832
  3. 等待安装完成。

  4. 增加slave的集群信息:系统配置拉到页面尾部,在cloud项目下点超链接进入新页面配置集群信息。新增cloud,选择kubernetes。

    image-20200713005531851 image-20200713013037463 image-20200713005555216
  5. 配置jenkins构建用的slave pod所在k8s集群的连接方式:kubernetes地址为集群自带的default命名空间的kubernetes service,直接按图填即可。最后测试连接,确保连接成功。

    image-20200713005823021
  6. 根据jenkins部署的命名空间和service名字填写jenkins地址。

    image-20200713005955948
  7. 增加一个pod template,标签列表需要记住,以后的ci/cd需要增加该标签才会调用该集群进行构建。其他按图填即可,该容器镜像是预配置好的镜像,包含了docker和kubectl客户端程序。

    image-20200713010348114
  8. 增加host path类型的卷,分别为host上的docker socket和kubeconfig

    image-20200713010447627 image-20200713010519512
  9. 填入部署时的sa,使模板pod有权限操作相应的资源

    image-20200713010643616

验证动态pod构建

  1. jenkins左侧新建item,选择free style,注意标签写入上一步里的标签,进行匹配,方能触发动态构建。

    image-20200713010928028

  2. 增加实际构建时的操作,此处执行shell命令,证明动态构建的pod可以操作docker和k8s。在构建中选择执行shell,输入以下shell命令

    echo "测试 Kubernetes 动态生成 jenkins slave"
    echo "==============docker in docker==========="
    docker info
    
    echo "=============kubectl============="
    kubectl get pods
    image-20200713013910045
  3. 保存后,进入该构建的主页面,点击左侧的立即构建,手动触发一次构建。此时观察pod的动态,可以看到新的pod被自动创建、运行、最终被销毁。

    image-20200713012034705
  4. build完成后,左侧build history中会出现可点击的链接,左侧点击控制台输出可以看到本次构建的标准输出,可以看到动态构建pod成功运行了docker info获取了host上的docker信息,成功用kubectl获取了集群pod信息。

    image-20200713014237471

至此完成了jenkins和k8s集成的初步配置。接下来可以开始针对具体的工作任务进行详细的应用和进一步的优化配置。

实际应用

内网及代理配置

  1. 私有服务尽量使用ip,可避免集群中域名解析的困扰
  2. 私有服务如果需要用域名解析,可以通过hostAlias修改jenkins pod及jenkins slave pod的hosts
  3. 如果配置了上述的内部域名,同时配置了访问公网的代理,记得再no_proxy里加入内部域名

插件

  1. Config File Provider:管理配置文件,并在jenkinsfile中引用,实际构建时会映射到工程目录里
  2. Pipeline Utility Steps:读取yaml文件,在jenkinsfile中引用。使用该插件可以方便地整合helm,实现以helm chart的values.yaml为唯一的配置文件

构建项目配置

本文构建项目以流水线类型的项目为主,并可以建立文件夹,进行分类管理。

在流水线类型的项目中,主要需要关注的几个点如下:

  1. 源代码仓库相关:先在系统管理-安全-credentials里保存git用户名和密码

  2. 构建参数:勾选参数化构建,可以设定构建时的输入参数。例如:

    1. 分支:在代码仓库配置中使用*/${branch}引用该参数

                            <img src="https://cdn.wubw.fun/typora/211201-101428-image-20211201101428440.png" alt="image-20211201101428440" style="zoom:50%;" />
      1. 命名空间:同1,可以设定namespace参数,使用户可以指定部署到哪个命名空间

jenkinsfile编写

经过众多配置,终于来到了核心的jenkinsfile的编写。本文展示我对jenkinsfile应用的几个案例,以供学习参考。

由于slave pod运行在k8s中,所以通过配置不同的pod template,可以实现对各种各样构建任务的支持。

在本文的集成k8s部分,配置了一个基础的pod template,基于该pod template生成的动态pod使用了cnych/jenkins:jnlp6镜像,其中包含了kubect和docker客户端,因此在jenkinsfile中就可以使用kubectl和docker完成构建任务。

接下来分别展示前后端构建使用的pod template和jenkinsfile,注意二者需要配合使用。

后端

技术栈:spring->maven->docker image->k8s helm release

pod template

image-20211202154650775

接下来对后端的pod template一些关键点做简要说明:

  1. 名称随意填,但是标签列表要记下来,随后的jenkinsfile中通过标签引用该pod template生成动态pod用于构建。
  2. container template会设置两个容器,一个镜像是maven:3.6.3-jdk-8,用于编译java代码生成jar包;另一个容器使用了我自制的镜像ccr.ccs.tencentyun.com/lwabish/cloud-native-clients,其中包含了kubect、docker、helm三个客户端,可以将编译好的jar包打包成docker镜像、部署到集群。
  3. 使用hostPath volume将node上的docker socket和kubeconfig映射给slave pod直接使用。
  4. 代理的空闲存活时间:如果构建任务比较频繁,默认每次动态pod都销毁,反复重建影响效率。设置合适的存活时间方便pod重用。
  5. pod retention配置为on failure,在构建出问题时保留slave pod方便查看日志。
  6. 如果需要对slave pod的yaml文件进行一些额外的配置,而表单里没有,那么可以在raw yaml for the pod中填写,并选择merge。注意yaml层级要写全,不是只写需要的部分。
  7. 记得给slave pod指定sa。
jenkinsfile
pipeline{
    agent{
        // 这里对应pod template中的标签列表
        label "java"
    }

    environment {
        // 整合helm chart,从chart的values.yaml以及构建参数获取输入,结合一些简单的逻辑确定构建任务的各项环境参数
        config=readYaml (file: 'chart/values.yaml')
        // 构建参数里的namespace override chart里的namespace
        namespace="${namespace=='chart'?config.global.namespace:namespace}"
        // harbor仓库直接在chart里写死,通常不变
        harbor="${config.global.registry}"
        // 构建镜像的tag,和jenkins全局变量绑定,方便排查问题
        tag="${branch}-${BUILD_NUMBER}"
        // 完整的image tag
        image="${harbor}/${JOB_BASE_NAME}:${tag}"
    }

    stages{

        stage('prompt env'){
            steps{
                container('maven'){
                  sh """
                    echo ${namespace}
                    echo ${harbor}
                    echo ${tag}
                    echo ${image}
                    """
                }
            }
        }


        stage('maven compile'){
          steps{
            container('maven'){
              // config file provider插件中配置的config file id
              configFileProvider([configFile(fileId: "040a1234-40c0-466f-acf0-526de4aa8144", targetLocation: "settings.xml")]){
                sh """
                mvn clean package -U -Dmaven.test.skip=true --settings settings.xml
                """
              }
            }
          }
        }

      stage('build docker image'){
        steps{
          withCredentials([[$class: 'UsernamePasswordMultiBinding',
                            credentialsId: '24d21234-3f88-4648-b354-31c53ca0d0e3',
                            usernameVariable: 'DOCKER_HUB_USER',
                            passwordVariable: 'DOCKER_HUB_PASSWORD']]) {
            container('clients') {
              sh """
              echo ${DOCKER_HUB_PASSWORD} | docker login ${harbor} -u ${ DOCKER_HUB_USER } --password-stdin
              docker build -t ${image} ./target/ --build-arg=branch=${branch} --build-arg=commit=\$(git rev-parse --short HEAD) --build-arg=build_time=\$(date +'%Y-%m-%dT%H:%M:%S%Z')
              docker push ${image}
              docker rmi ${image}
              """
            }
          }
        }
      }


      stage('helm upgrade'){
        steps{
          container('clients') {
            sh """
            // helm 不允许release名称有/,替换成-
            releaseName=`echo ${JOB_NAME} | sed 's/\\//-/'`
            // helm chart中使用appVersion作为镜像tag,此处进行替换
            sed -i 's/appVersion: \\S*/appVersion: ${tag}/' ./chart/Chart.yaml
            helm upgrade -i -n ${namespace} --create-namespace --set global.registry=${harbor} \${releaseName} ./chart
            """
          }
        }
      }
    }
}

大部分和前端部分相同,唯一的区别是maven构建部分,同样使用的也是未采用多阶段构建的dockerfile,并使用config file provider预配置了maven的settings文件,注入到slave pod中,以使用私有的maven仓库。

前端

技术栈:angular->docker image->k8s helm release

pod template

image-20211201103450410

pod templeate整体配置和后端一致,唯一的区别是把maven镜像换成了node镜像。

jenkinsfile
stage('获取依赖'){
  steps{
    container('node'){
      sh """
      // yarn 公网代理
      yarn config set proxy http://x.x.x.x:3128
      yarn config set https-proxy http://x.x.x.x:3128
      yarn config set registry https://registry.npm.taobao.org/
      yarn config set sass_binary_site https://npm.taobao.org/mirrors/node-sass/
      yarn
      """
    }
  }
}

stage('编译打包'){
  steps{
    container('node'){
      sh """
      node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --prod --configuration=production
      """
    }
  }
}

上面jenkinsfile中仅展示了和后端不一样的几个stage。

多用户及权限管理

安装插件Matrix Authorization Strategy,在系统设置-安全-管理用户中新建用户,在全局安全配置-授权策略中选择安全矩阵,即可以按照用户分配权限。

多任务批量执行

  1. 安装插件Multijob plugin。

  2. 系统设置-Maven项目配置-执行者数量默认为0,必须设置成大于0的数量才能开启构建,且大于1时可以支持多pod并发。

  3. 新建任务,类型选择MultiJob

  4. 在构建中为增加phase,每个phase下都可以引用多个其他job

  5. 注意引用job name时要加文件夹前缀

  6. 参数传递:multi job选择参数化构建,然后在子job的高级按钮中增加predefined paramters,通过${}语法将父job的参数传递给子job,例如:

    image-20211202172811342

    特别注意:${}里的变量名称不能出现中划线-,会导致变量不解析直接原样传递到子job中

  7. 并发度控制有多处配置需要考虑:

    1. 系统配置-maven项目配置-执行者数量
    2. 系统配置-cloud-Kubernetes集群-容器数量
    3. 系统配置-cloud-podTemplate-Concurrency Limit

排坑

  • 项目配置,scm的轻量级检出要取消勾选
  • 快速查询jenkins内置方法及变量:进入某个构建项目,左侧流水线语法
  • jenkinsfile有两种书写格式:脚本化流水线和声明式流水线,两种语法有区别,需要统一,不可交叉使用。
  • jenkinsfile调试效率不高,可通过以下几个方式尝试改进
    1. 失败的构建可以在build详情左侧点击回放,快速修改jenkinsfile后快速重试
    2. 我在使用idea和vscode两种编辑器编辑jenkinsfile时,发现其对groovy的lint等支持较少,可以在vscode中搜索jenkins相关插件,尝试remote lint或者remote debug的方式提高效率。这部分由于我司的jenkinsfile变更不算太多,就没有深入实践。
  • 某次升级插件后,遇到了Kubernetes插件的集群配置丢失的问题。因此升级插件前建议做好配置备份。
  • 插件更新如果受网络原因影响,也可以下载到本地,通过浏览器手动更新。插件出问题看jenkins pod日志,会提示插件依赖以及相关的插件url,本地下载很方便(前提是本地网络通畅)。

参考

文章目录-jenkins | 超级小豆丁 (mydlq.club)

基于 Jenkins、Gitlab、Harbor、Helm 和 Kubernetes 的 CI/CD(一)-阳明的博客|Kubernetes|Istio|Prometheus|Python|Golang|云原生 (qikqiak.com)

流水线语法 (jenkins.io)