基于jenkins持续集成k8s

CI/CD流程介绍

在本次集成 CI/CD 工具的过程中,我们先对比一下传统环境与 Kubernetes 环境下的流程差异:

传统环境:

  • CI 流程:
    开发人员提交代码到 GitLab 仓库 → Jenkins 触发构建 → 进行漏洞扫描、编译代码 → 将构建产物推送至 Nexus 仓库 → 部署至测试环境。
  • CD 流程:
    Jenkins 从 Nexus 仓库拉取构建产物 → 部署至生产环境。

Kubernetes 环境:

  • CI 流程:
    开发人员提交代码到 GitLab 仓库 → Jenkins 触发构建 → 进行漏洞扫描、编译代码 → 构建 Docker 镜像并推送至 Harbor 仓库 → 部署至 Kubernetes 测试环境。
  • CD 流程:
    Jenkins 从 Harbor 仓库拉取对应的镜像 → 部署应用至 Kubernetes 生产环境。

Slave介绍

静态 Slave(静态 Agent)

静态SLave:需要固定的节点,配置其对应环境,手动注册到Master,然后执行任务,任务完成节点处于空闲等待状态;

优势:

  1. 能够分担主节点上的压力,加快构建速度(所有任务都由Master执行,造成构建速度缓慢,且任务多会出现排队现象
  2. 能够将特定的任务在特定的主机上运行(比如:不同的任务需要不同的编译环境)

痛点:

  1. Master 发生单点故障时,整个Jenkins都没办法使用;
  2. 每个Slave的环境不一样,用于完成不同项目的编译打包工作,但这些不同环境的配置管理及维护都特别困难;
  3. 有的Slave构建任务频繁,可能出现排队等待,而有的Slave又处于空闲状态,所以会出现资源分配严重不均衡;
  4. 因为每个Slave都需要一台虚拟机,当Slave空间时,等于就是空跑,资源浪费明显;

动态 Slave(动态 Agent)

动态Slave:由Master动态创建Slave的Pod,自动注册到Master,然后执行任务,任务结束Pod自动销毁;

优势:

  1. 定义:动态 slave 是按需创建和销毁的构建节点。这些节点根据构建任务的需求动态启动,任务完成后可以自动销毁。
  2. 配置:通常通过云插件(如 Amazon EC2、Kubernetes 插件)来实现。这些插件允许 Jenkins 根据需求动态创建和销毁虚拟机或容器化的 agent。
  3. 资源分配:资源仅在需要时被占用,不使用时可以释放,从而提高资源利用率并降低成本。
  4. 适用场景:适用于构建任务不太频繁或者资源需求波动较大的环境。特别是在云环境中,动态 slave 可以帮助实现更加灵活和成本有效的构建流程。

总的来说,静态 slave 更适合那些需要长时间运行和持续可用的环境,而动态 slave 则更适合那些对资源使用有弹性需求的情况。动态 slave 的使用越来越普遍

动态slave工作流程:

从图上可以看到 Jenkins Master 和 Jenkins Slave 以 Pod 形式运行在 Kubernetes 集群的 Node 上,Master 运行在其中一个节点,并且将其配置数据存储到一个 Volume 上去,Slave 运行在各个节点上,并且它不是一直处于运行状态,它会按照需求动态的创建并自动删除。
这种方式的工作流程大致为:当 Jenkins Master 接受到 Build 请求时,会根据配置的 Label 动态创建一个运行在 Pod 中的 Jenkins Slave 并注册到 Master 上,当运行完 Job 后,这个 Slave 会被注销并且这个 Pod 也会自动删除,恢复到最初状态。

准备工作

服务部署可参考

配置cloud节点

镜像构建服务部署

开启agent节点通道

系统管理-全局安全配置-代理

TCP port for inbound agents

需要手动开启指定端口,用于k8s连接jenkins

配置k8s认证

k8s证书准备

两种方式

  1. 使用 rbac授权,token的方式连接k8s
  2. 使用admin证书。这里我使用第二种
sz  ~/.kube/config  #将证书上传到集群外

配置cloud

系统管理-clouds-new cloud

  • kubernetes地址:为k8s api server地址,通过调用apiserver操作k8s。可通过以下来查看:
[root@master01 jenkins]# kubectl cluster-info
Kubernetes control plane is running at https://10.0.0.200:16443
CoreDNS is running at https://10.0.0.200:16443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
  • 凭据:kubernetes plugin可以通过key或凭据的方式与k8s进行认证,方便起见,我们采用凭据的方式,使用我们此前创建的<font style="color:rgb(192, 52, 29);background-color:rgb(251, 229, 225);">secret file</font>凭据,此时我们需要。
  • kubernetes命令空间:使用默认的default
  • 点击连接测试,可以看到k8s已经连接成功
  • Jenkins 地址 : jenkins的访问地址
  • Jenkins 通道 :用于与 Kubernetes 集群建立 CI/CD 连接

配置jnlp镜像

这里我们使用的是内网环境需要手动配置jnlp镜像

添加镜像secret

kubectl create secret docker-registry harbor-admin --namespace=default --docker-server=harbor.jiajia.com --docker-username=admin --docker-password=123456
[root@master01 jenkins]# kubectl get secret | grep harbor
harbor-admin                         kubernetes.io/dockerconfigjson        1      33m

  • 名称:pod名称,在k8s中实际名称为jenkins-slave-随机值。
  • 标签列表:此处标签即标识Jenkins agent的,如流水线中agent定义调度在哪个slave上运行。当然此处我们也可不配置,kubernetes plugin将会默认使用<font style="color:rgb(192, 52, 29);background-color:rgb(251, 229, 225);">jenkins/jnlp-slave:alpine</font>镜像创建。但是kubernetes-plugin官方已<font style="color:rgb(192, 52, 29);background-color:rgb(251, 229, 225);">停止维护</font>此镜像,而统一使用<font style="color:rgb(192, 52, 29);background-color:rgb(251, 229, 225);">jenkins/inbound-agent</font>。因此我们需要进行重新设置。 名称:pod中容器的名称,注意此处必须设置为jnlp,才能对镜像重写使用<font style="color:rgb(192, 52, 29);background-color:rgb(251, 229, 225);">jenkins/inbound-agent</font>,否则将会出现以下问题:k8s同时拉取<font style="color:rgb(192, 52, 29);background-color:rgb(251, 229, 225);">jenkins/inbound-agent</font><font style="color:rgb(192, 52, 29);background-color:rgb(251, 229, 225);">jenkins/jnlp-slave:alpine</font>两个镜像,第一个为重写后的实际使用镜像,第二个为默认镜像,导致jenkins-slave无法正常运行,不断重复构建。
  • Docker镜像:当名称设置为jnlp后,<font style="color:rgb(192, 52, 29);background-color:rgb(251, 229, 225);">harbor.jiajia.com/dev/inbound-agent:latest</font>即为重写后的镜像,否则默认使用<font style="color:rgb(192, 52, 29);background-color:rgb(251, 229, 225);">jenkins/jnlp-slave:alpine</font>
  • 工作目录:Jenkins slave的默认工作目录,构建时将会在此目录下创建workspace。
  • 运行的命令和命令参数: 其中
  • 运行的命令必须要留空,否则会重写镜像的默认entrypoint,导致agent 无法连接到master,下面我们会进行演示说明。
  • 资源限制:默认的容器是没有资源限制的,我们在此添加了cpu和memory限制,大家可根据实际情况进行修改。

测试节点

我们这里创建一个流水线测试一下是否可用

pipeline {
    // 使用k8s拉起slave
    agent {
        kubernetes {
            cloud 'k8s'
            inheritFrom 'jenkins-slave'
            namespace 'default'
        }
    }

    stages {
        stage('输出pods名称') {
            steps {
                sh 'hostname'
            }
        }

        stage('等待时间') {
            steps {
                sh 'sleep 3'
            }
        }
    }
}

可以看到已经构建成功

制作pod模板镜像

jnlp镜像是用来连接JenkinsMaster以及共享Master的WORKSPACE,但该镜像并没有maven、docker、kubectl等常用命令,为此我们需要定制几个镜像,后期通过Pipeline将不同的任务交由同一个Pod的不同的容器来执行。

maven镜像

用于代码构建编译打包,会把Ruoyi相关依赖包打到基础镜像内,避免分层构建失败

下载修改好的源

#下载修改好的源 
wget https://linux.oldxu.net/settings_docker.xml

编写Dockerfile

[root@master01 maven]# git clone https://gitee.com/y_project/RuoYi-Cloud.git
    [root@master01 maven]# cd RuoYi-Cloud
[root@master01 maven]# git checkout v3.6.3
[root@master01 maven]# cd ../
[root@master01 maven]# cat Dockerfile 
FROM maven:3.8.6-openjdk-8
ADD ./RuoYi-Cloud /opt/RuoYi-Cloud
ADD ./settings_docker.xml /usr/share/maven/conf/settings.xml
RUN /bin/cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
cd /opt/RuoYi-Cloud && mvn clean install -DskipTests && \
rm -rf /opt/RuoYi-Cloud

构建

docker build -t  harbor.jiajia.com/dev/jenkins-maven:3.8.6 .
    docker push harbor.jiajia.com/dev/jenkins-maven:3.8.6

sonar镜像

拉取镜像

[root@node02 ~]# docker pull emeraldsquad/sonar-scanner:2.3.0
[root@node02 ~]# docker tag emeraldsquad/sonar-scanner:2.3.0   harbor.jiajia.com/dev/sonar-scanner:2.3.0  
[root@node02 ~]# docker push   harbor.jiajia.com/dev/sonar-scanner:2.3.0

nodejs镜像

编写Dockerfile

[root@master01 nodejs]# cat Dockerfile 
# 使用基础镜像
FROM centos:7

# 安装 gzip 和 wget 工具使用 RPM 包
COPY gzip-1.5-11.el7_9.x86_64.rpm /tmp/
    COPY wget-1.14-18.el7_6.1.x86_64.rpm /tmp/
    RUN yum localinstall -y /tmp/gzip-1.5-11.el7_9.x86_64.rpm /tmp/wget-1.14-18.el7_6.1.x86_64.rpm && \
yum clean all && \
rm -f /tmp/gzip-1.5-11.el7_9.x86_64.rpm /tmp/wget-1.14-18.el7_6.1.x86_64.rpm

# 创建必要的目录
RUN mkdir -p /data/setup/ /data/prog/

    # 下载并解压 Node.js,如果文件和目录不存在
RUN cd /data/setup/ && \
if [ ! -f /data/setup/node-v12.18.3-linux-x64.tar.xz ]; then \
wget http://down.yu1991.com/ruoyi/node-v12.18.3-linux-x64.tar.xz; \
    fi && \
if [ ! -d /data/prog/node-v12.18.3-linux-x64 ]; then \
tar -xf /data/setup/node-v12.18.3-linux-x64.tar.xz -C /data/prog/; \
fi && \
chown -R root:root /data/prog/node-v12.18.3-linux-x64 && \
ln -snf /data/prog/node-v12.18.3-linux-x64 /data/prog/node

# 创建 npm 和 node 的软链接
RUN ln -s /data/prog/node/bin/npm /usr/bin/npm && \
ln -s /data/prog/node/bin/node /usr/bin/node

# 设置工作目录
WORKDIR /data/prog

构建

docker build  -t harbor.jiajia.com/dev/jenkins-nodejs:12.18.3 .
    docker push harbor.jiajia.com/dev/jenkins-nodejs:12.18.3

docker镜像

拉取镜像

docker pull docker:20.10
docker tag docker:20.10 harbor.jiajia.com/dev/docker:20.10
docker push harbor.jiajia.com/dev/docker:20.10

kubectl镜像

添加源

cat << EOF > ./kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/
    enabled=1
gpgcheck=0
EOF

编写Dockerfile

FROM centos:7

# 1、调整时区
RUN /bin/cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo 'Asia/Shanghai' >/etc/timezone && \
rm -f  /etc/yum.repos.d/*

# 2、添加yum源
ADD ./kubernetes.repo /etc/yum.repos.d/kubernetes.repo

# 3、安装Kubectl
RUN yum makecache && \
    yum install kubectl-1.23.17 -y && \
    yum clean all

构建

docker build -t harbor.jiajia.com/dev/jenkins-kubectl:1.23.17 .
    docker push harbor.jiajia.com/dev/jenkins-kubectl:1.23.17 

测试模板镜像

我们新建一个流水线测试一下模板

pipeline {
    agent {
        kubernetes { 
            // 自动创建 Slave Pod 执行以下动作
            cloud 'k8s'
            inheritFrom 'jenkins-slave'
            namespace 'default'
            yaml '''
                apiVersion: v1
                kind: Pod
                spec:
                  containers:
                  - name: maven
                    image: harbor.jiajia.com/dev/jenkins-maven:3.8.6
                    imagePullPolicy: IfNotPresent
                    command: ["cat"]
                    tty: true
                    volumeMounts:
                    - name: data
                      mountPath: /root/.m2
                  - name: nodejs
                    image: harbor.jiajia.com/dev/jenkins-nodejs:12.18.3
                    imagePullPolicy: IfNotPresent
                    command: ["cat"]
                    tty: true
                  - name: sonar
                    image: harbor.jiajia.com/dev/sonar-scanner:2.3.0
                    imagePullPolicy: IfNotPresent
                    command: ["cat"]
                    tty: true
                  - name: docker
                    image: docker:20.10
                    imagePullPolicy: IfNotPresent
                    command: ["cat"]
                    tty: true
                    volumeMounts:
                    - name: dockersocket
                      mountPath: /run/docker.sock
                  - name: kubectl
                    image: harbor.jiajia.com/dev/jenkins-kubectl:1.23.17
                    imagePullPolicy: IfNotPresent
                    tty: true
                    command: ["cat"]
                  volumes:
                  - name: data
                    nfs:
                      server: 10.0.0.11 #存储maven仓库依赖
                      path: /nfs/data/maven
                  - name: dockersocket
                    hostPath:
                      path: /run/docker.sock
            '''
        }
    }
    stages {
        stage('maven测试') {
            steps {
                container('maven') {
                    sh 'mvn -version'
                }
            }
        }
        stage('nodejs测试') {
            steps {
                container('nodejs') {
                    sh 'node -v'
                }
            }
        }
        stage('docker测试') {
            steps {
                container('docker') {
                    sh 'docker ps'
                }
            }
        }
        stage('kubectl测试') {
            steps {
                container('kubectl') {
                    sh 'kubectl version'
                }
            }
        }
    }
}

CI阶段流程

获取代码

在 Kubernetes 上拉取代码时,可以参考以下方案:

  1. Slave Pod 如何访问 GitLab:
    1. 通过 GitLab 的 Service 地址直接访问项目。
    2. 使用 CoreDNS 配置自定义域名解析,以便更方便地访问 GitLab。
  2. GitLab 项目是私有的,如何处理:
    • 在 Jenkins 上配置好与 GitLab 对应的认证信息,以确保能够顺利访问私有项目。
  3. 编写stage

配置域名解析

kubectl edit configmaps -n kube-system coredns	
添加如下部分
hosts {
    10.10.10.22 gitlab.jiajia.com
    fallthrough
}

配置后进入容器测试是否可以正确解析

配置gitlab认证

获取gitlab的token,点击create会自动生成

jenkins添加凭据

凭据ID必须是全局唯一,这里为了后期方便使用,我命名为gitlab_token,凭据ID一旦创建不可更改

配置stage

        stage("获取代码") {
            steps {
                container('maven') {
                    echo "拉取代码"
                    checkout scmGit(
                    branches: [[name: '*/master']], 
                    extensions: [], 
                    userRemoteConfigs: [[credentialsId: 'git-root', 
                    url: "${GitLab_Url}${PROJECT_NAME}.git"]])
                }
            }
        }

漏洞扫描

通过 Pod 模板中的 Sonar 容器访问 SonarQube 服务端,可以参考以下步骤:

  1. DNS 解析:
    • 配置 DNS 解析,使得 Slave Pod 能够正确访问 SonarQube 服务端。
    • 这里SonarQube部署在集群外可以直接通过ip访问
  2. Jenkins 集成 SonarQube:
    • 在 Jenkins 中集成 SonarQube,确保 Jenkins 能够与 SonarQube 通信。
  3. 编写 Jenkins Pipeline Stage:
    • 在 Jenkins Pipeline 中编写一个 Stage,用于调用 Sonar-Scanner 执行代码扫描,并将结果上传到 SonarQube 服务端。

配置sonarqube

安装sonarqube,这里已经提前安装好了,默认安装成功后访问 浏览器 http://部署的服务器IP:9090 ,默认账号 admin 密码admin

配置token

token: 2253419796b4417f2200453f99351ca2e65bbfe4

配置jenkins

创建凭据

安装SonarQube插件

SonarQube

配置jenkins对接sonar

系统管理-系统配置-添加add sonarqube

编写Stage

注:withSonarQubeEnv 负责提供 SonarQube 环境变量

stage('代码扫描') {
    steps { 
        withSonarQubeEnv('sonarqube'){
            container('sonar'){
                sh 'ls -l'
                sh 'sonar-scanner \
                -Dsonar.projectKey=${PROJECT_NAME} \
                -Dsonar.java.binaries=src \
                -Dsonar.sources=.'
            }
        }
    }
}  

代码编译

编写stage

stage('编译代码'){
    steps {
        container('maven'){
            sh 'pwd && ls -l'
            sh 'mvn package -Dmaven.test.skip=true'
        }
    }
}

构建镜像

在构建镜像的过程中,可以按照以下优化步骤进行:

  1. 配置域名解析:
    • 配置 DNS 解析,使得 Slave Pod 能够顺利访问 Harbor 仓库。
  2. 配置凭据:
    • 在 Jenkins 中配置好访问 Harbor 的凭据,确保推送镜像时能够正确进行认证。
  3. 构建 Docker 镜像:
    • 使用 docker build 命令,根据 Dockerfile 构建 Docker 镜像。
  4. 推送镜像到 Harbor:
    • 将构建好的 Docker 镜像推送到 Harbor 仓库中。

配置域名解析

上可以参考获取代码部分

配置harbor凭据

编写stage

dockfile文件存放在镜像仓库

        stage('生成镜像Tag') {
            steps { 
                container('maven') {
                    script {
                    //本次git提交的commid     (git log -n1 --pretty=format:'%h')
                    env.COMMITID = sh(returnStdout: true, script: "git log -n1 --pretty=format:'%h'").trim()
                    //构建的时间   (date +%Y%m%d_%H%M%S)
                    env.BuildTime = sh(returnStdout: true, script: "date +%Y%m%d_%H%M%S").trim()
            
                    //完整的镜像Tag   (c106654_20221115_133911)
                    env.ImageTag = COMMITID + "_" +  BuildTime
                    }        
                    sh 'echo "镜像的Tag: ${ImageTag}"'
                }
            }
        }
        stage('镜像构建') {
            steps { 
                container('docker') {
                withCredentials([usernamePassword(credentialsId: "HARBOR_ID", passwordVariable: 'HARBOR_PASSWORD', usernameVariable: 'HARBOR_USER')]) {
                  //登陆harbor
                  sh 'echo "${HARBOR_PASSWORD}" | docker login  ${HarBor_Url} -u "${HARBOR_USER}" --password-stdin'
                  //构建镜像
                  sh 'docker build -t ${HarBor_Url}/${Pro}/${PROJECT_NAME}:${ImageTag} .'
                  //推送镜像
                  sh 'docker push ${HarBor_Url}/${Pro}/${PROJECT_NAME}:${ImageTag}'
                  //删除镜像
                  sh 'docker rmi ${HarBor_Url}/${Pro}/${PROJECT_NAME}:${ImageTag}' 
                 }
                }
            }
        }

服务部署

编写stage

        stage("服务发布") {
            steps {
                container('kubectl') {
                    sh 'kubectl version'
                    withCredentials([file(credentialsId: 'k8s-admin', variable: 'KUBECONFIG')]) {
                    sh 'mkdir -p ~/.kube && cp ${KUBECONFIG} ~/.kube/config'

                    sh '''
                        sed -i "s#{namespace}#default#g" deploy.yaml
                        sed -i "s#{image}#${HarBor_Url}/${Pro}/${PROJECT_NAME}:${ImageTag}#g" deploy.yaml
                        kubectl apply -f deploy.yaml
                    '''
              }
                }
            }
        }

完整流水线

pipeline {
    agent {
        kubernetes { 
            cloud 'k8s'
            inheritFrom 'jenkins-slave'
            namespace 'default'
            yaml '''
                apiVersion: v1
                kind: Pod
                spec:
                  containers:
                  - name: maven
                    image: harbor.jiajia.com/dev/jenkins-maven:3.8.7
                    imagePullPolicy: Always
                    command: ["cat"]
                    tty: true
                    volumeMounts:
                    - name: data
                      mountPath: /root/.m2
                  - name: nodejs
                    image: harbor.jiajia.com/dev/jenkins-nodejs:12.18.3
                    imagePullPolicy: IfNotPresent
                    command: ["cat"]
                    tty: true
                  - name: sonar
                    image: harbor.jiajia.com/dev/sonar-scanner:2.3.0
                    imagePullPolicy: IfNotPresent
                    command: ["cat"]
                    tty: true
                  - name: docker
                    image: docker:20.10
                    imagePullPolicy: IfNotPresent
                    command: ["cat"]
                    tty: true
                    volumeMounts:
                    - name: dockersocket
                      mountPath: /run/docker.sock
                  - name: kubectl
                    image: harbor.jiajia.com/dev/jenkins-kubectl:1.23.17
                    imagePullPolicy: IfNotPresent
                    tty: true
                    command: ["cat"]
                  volumes:
                  - name: data
                    nfs:
                      server: 10.0.0.11
                      path: /nfs/data/maven
                  - name: dockersocket
                    hostPath:
                      path: /run/docker.sock
            '''
        }
    }
    environment {
        HarBor_Url="harbor.jiajia.com"
        Pro="ruoyi"
        GitLab_Url="http://gitlab.jiajia.top/root/"
    }
    parameters {
        choice choices: ['ruoyi-auth', 'ruoyi-system', 'ruoyi-gateway', 'ruoyi-ui'], 
        description: '请选择项目构建', 
        name: 'PROJECT_NAME'
    }
    stages {
        stage("获取代码") {
            steps {
                container('maven') {
                    echo "拉取代码"
                    checkout scmGit(
                    branches: [[name: '*/master']], 
                    extensions: [], 
                    userRemoteConfigs: [[credentialsId: 'git-root', 
                    url: "${GitLab_Url}${PROJECT_NAME}.git"]])
                }
            }
        }
        stage('获取构建信息') {
            steps {
                script {
                    currentBuild.displayName = "${BUILD_NUMBER}-${PROJECT_NAME}"
                    currentBuild.description = "提交者: ${BUILD_USER} <br> 提交者ID:${BUILD_USER_ID} <br> 提交时间:${BUILD_TIMESTAMP} <br> 构建分支:${PROJECT_NAME}"
                }
            }
        }
        stage('代码扫描') {
            steps { 
                withSonarQubeEnv('sonarqube'){
                    container('sonar'){
                        sh 'ls -l'
                        sh 'sonar-scanner -Dsonar.projectKey=${PROJECT_NAME} -Dsonar.java.binaries=src -Dsonar.sources=.'
                    }
                }
            }
        }     
        stage('代码编译') {
            steps { 
                container('maven') {
                    sh 'pwd'
                }
            }
        }
        stage('生成镜像Tag') {
            steps { 
                container('maven') {
                    script {
                        env.COMMITID = sh(returnStdout: true, script: "git log -n1 --pretty=format:'%h'").trim()
                        env.BuildTime = sh(returnStdout: true, script: "date +%Y%m%d_%H%M%S").trim()
                        env.ImageTag = COMMITID + "_" +  BuildTime
                    }        
                    sh 'echo "镜像的Tag: ${ImageTag}"'
                }
            }
        }
        stage('镜像构建') {
            steps { 
                container('docker') {
                    withCredentials([usernamePassword(credentialsId: "HARBOR_ID", passwordVariable: 'HARBOR_PASSWORD', usernameVariable: 'HARBOR_USER')]) {
                        sh 'echo "${HARBOR_PASSWORD}" | docker login  ${HarBor_Url} -u "${HARBOR_USER}" --password-stdin'
                        sh 'docker build -t ${HarBor_Url}/${Pro}/${PROJECT_NAME}:${ImageTag} .'
                        sh 'docker push ${HarBor_Url}/${Pro}/${PROJECT_NAME}:${ImageTag}'
                        sh 'docker rmi ${HarBor_Url}/${Pro}/${PROJECT_NAME}:${ImageTag}' 
                    }
                }
            }
        }
        stage("服务发布") {
            steps {
                container('kubectl') {
                    sh 'kubectl version'
                    withCredentials([file(credentialsId: 'k8s-admin', variable: 'KUBECONFIG')]) {
                        sh 'mkdir -p ~/.kube && cp ${KUBECONFIG} ~/.kube/config'
                        sh '''
                            sed -i "s#{namespace}#default#g" deploy.yaml
                            sed -i "s#{image}#${HarBor_Url}/${Pro}/${PROJECT_NAME}:${ImageTag}#g" deploy.yaml
                            kubectl apply -f deploy.yaml
                        '''
                    }
                }
            }
        }
    }
    post { 
        always { 
            dingtalk (
                robot: "dev",
                type: 'MARKDOWN',
                title: "${PROJECT_NAME} 构建${currentBuild.result}",
                text: [
                    "## [${PROJECT_NAME} 构建${currentBuild.result}提醒](${BUILD_URL}console)",
                    "---",
                    "- 项目名称:${PROJECT_NAME} ",
                    "- 构建编号:${BUILD_NUMBER} ",
                    "- 构建分支:master ",
                    "- 构建人:${BUILD_USER} ",
                    "- 构建URL:${BUILD_URL} ",
                    "- 构建结果:${currentBuild.result} ",
                    "- 构建环境:${env.PLATFORM} ",
                    "- 构建时间:${BUILD_TIMESTAMP} ",
                    "- 构建持续时间:${currentBuild.duration/1000}秒"
                ]
            )
        }
    }
}

CD阶段流程

Jenkins 从 Harbor 仓库拉取对应的镜像 → 部署应用至 Kubernetes 生产环境。

对于获取镜像仓库标签我们可以配置 Jenkins 动态关联参数,以便从 Harbor 仓库获取镜像的最新标签,并自动关联到部署流程中,实现灵活的镜像版本管理

动态关联参数

内容提要:

  1. 安装 Active Choices Plugin-in 之前版本叫 Active Choices Plugin
  2. UI 应用 Active Choices Reactive Parameters
  3. Pipeline 脚本应用

安装 Active Choices Plugin
在用 Active Choices Reactive Parameters 之前,确保 Jenkins 上安装 Active Choices 插件。

安装成功后,打开 Job Configure 页面,勾上 This project is parameterized , 点开 Add Parameter,会看到多出下面三个选项。

配置harbor项目名称

选择Active Choices parameters-主动选择参数

return ["pi6000", "prome","ruoyi"]

配置harbor镜像名称

选择Active Choices Reactive Parameter 主动选择反应参数

import groovy.json.JsonSlurper

// 定义 curl 命令来获取 JSON 数据
def command = "curl -s -u admin:123456 -H 'Content-Type: application/json' -X GET https://192.168.1.20/api/v2.0/projects/${HarBor_Pro}/repositories --insecure"

// 执行命令并获取输出
def process = command.execute()
def output = process.text.trim()

// 打印输出以调试
println("Command Output: ${output}")

// 解析 JSON 数据
def jsonSlurper = new JsonSlurper()
def parsedJson = jsonSlurper.parseText(output)

// 提取仓库名称的最后部分
def repositoryNames = parsedJson.collect { 
    // 获取仓库名称
    def fullName = it.name
    
    // 提取最后部分
    def shortName = fullName.replaceAll(/.*\/([^\/]*)$/, '$1')
    
    return shortName
}

// 返回仓库名称列表
return repositoryNames

记得填写Referenced parameters获取HarBor_Pro参数的变化

获取镜像标签

选择 Active Choices Reactive Parameter 主动选择反应参数

def get_tag = [ "bash", "-c", "curl -s -uadmin:123456 -H 'Content-Type: application/json' -X GET https://192.168.1.20/v2/${HarBor_Pro}/${Image_Name}/tags/list --insecure | sed -r 's#(\\{.*\\[)(.*)(\\]\\})#\\2#g' | xargs -d ',' -n1 | xargs -n1 | sort -t '_' -k2 -k3 -nr | head -5"]

return get_tag.execute().text.tokenize("\n")

记得填写Referenced parameters获取HarBor_Pro参数的变化

HarBor_Pro,Image_Name

整体效果

测试镜像名称

测试一下完整镜像名称

pipeline {
  agent {
    kubernetes {
      cloud 'k8s'
      inheritFrom 'jenkins-slave'
     namespace 'default'
      yaml '''
        apiVersion: v1
        kind: Pod
        spec:
          imagePullSecrets:
          - name: harbor-admin
          containers:
          - name: kubectl
            image: harbor.jiajia.com/dev/jenkins-kubectl:1.23.17
            imagePullPolicy: IfNotPresent
            command: ["cat"]
            tty: true
      '''
    }
  }

  environment {
      Full_Image = "harbor.jiajia.com/${Harbor_Pro}/${Image_Name}:${Image_Tags}"
    }
  stages {
    stage('输出完整的镜像名称') {
      steps {
        sh 'echo 镜像名称-tag: ${Full_Image}'
      }
    }
 }
}

发布服务

pipeline {
  agent {
    kubernetes {
      cloud 'k8s'
      inheritFrom 'jenkins-slave'
     namespace 'default'
      yaml '''
        apiVersion: v1
        kind: Pod
        spec:
          imagePullSecrets:
          - name: harbor-admin
          containers:
          - name: kubectl
            image: harbor.jiajia.com/dev/jenkins-kubectl:1.23.17
            imagePullPolicy: IfNotPresent
            command: ["cat"]
            tty: true
      '''
    }
  }

  environment {
      Full_Image = "harbor.jiajia.com/${Harbor_Pro}/${Image_Name}:${Image_Tags}"
    }
  stages {
    stage('输出完整的镜像名称') {
      steps {
        sh 'echo 镜像名称-tag: ${Full_Image}'
      }
    }
    
    stage('部署应用至K8S') {
      steps {
          withCredentials([file(credentialsId: 'k8s-admin', variable: 'KUBECONFIG')]) {
            container('kubectl'){
              sh 'mkdir -p ~/.kube && cp ${KUBECONFIG} ~/.kube/config'
              sh 'kubectl set image deployment/${Image_Name} ${Image_Name}=${Full_Image} -n default'
            }
          }
      }
    }
}
}

快速回滚

    stage('快速回滚') {
      steps {
          withCredentials([file(credentialsId: 'k8s-admin', variable: 'KUBECONFIG')]) {
            container('kubectl'){
              script {
                timeout(time:1 , unit: 'HOURS'){
                  def UserInput = input message: '是否回退至上一个版本', parameters: [choice(choices: ['No', 'Yes'], name: 'rollback')]
                  if (UserInput == "Yes"){
                    sh 'mkdir -p ~/.kube && cp ${KUBECONFIG} ~/.kube/config'
                    sh 'kubectl rollout undo deployment ${Image_Name} -n default'
                  }else {
                    echo "没有选择回退"
                  }
                }
              }
            }
          }
      }
    }

发布服务构建

可以看到当前镜像版本已更新

kubectl get  pods  ruoyi-auth-6b66d64b7-9vbwf  -o yaml | grep  image

我们的流水线设置了1小时的超时限制。如果在此期间没有执行操作,默认情况下部署将不会回滚。现在,我们选择在超时后自动执行回滚操作。

查看回滚后镜像,可以看到已经快速回退到上一个版本

kubectl get  pods  ruoyi-auth-6b66d64b7-9vbwf  -o yaml | grep  image

完整流水线

pipeline {
  agent {
    kubernetes {
      cloud 'k8s'
      inheritFrom 'jenkins-slave'
     namespace 'default'
      yaml '''
        apiVersion: v1
        kind: Pod
        spec:
          imagePullSecrets:
          - name: harbor-admin
          containers:
          - name: kubectl
            image: harbor.jiajia.com/dev/jenkins-kubectl:1.23.17
            imagePullPolicy: IfNotPresent
            command: ["cat"]
            tty: true
      '''
    }
  }
  parameters {
        choice(
            choices: ['dev', 'prod'], 
            description: '请选择要部署的环境', 
            name: 'PLATFORM'
        )
    }
  environment {
      Full_Image = "harbor.jiajia.com/${Harbor_Pro}/${Image_Name}:${Image_Tags}"
      UserInput = ""
    }
  stages {
    stage('输出完整的镜像名称') {
      steps {
        sh 'echo 镜像名称-tag: ${Full_Image}'
      }
    }
    
    stage('部署应用至K8S') {
      steps {
          withCredentials([file(credentialsId: 'k8s-admin', variable: 'KUBECONFIG')]) {
            container('kubectl'){
              sh 'mkdir -p ~/.kube && cp ${KUBECONFIG} ~/.kube/config'
              sh 'kubectl set image deployment/${Image_Name} ${Image_Name}=${Full_Image} -n default'
            }
          }
      }
    }
    stage('快速回滚') {
      steps {
          withCredentials([file(credentialsId: 'k8s-admin', variable: 'KUBECONFIG')]) {
            container('kubectl'){
              script {
                timeout(time:1 , unit: 'HOURS'){
                  UserInput = input message: '是否回退至上一个版本', parameters: [choice(choices: ['No', 'Yes'], name: 'rollback')]
                  if (UserInput == "Yes"){
                    sh 'mkdir -p ~/.kube && cp ${KUBECONFIG} ~/.kube/config'
                    sh 'kubectl rollout undo deployment ${Image_Name} -n default'
                  }else {
                    echo "没有选择回退"
                  }
                }
              }
            }
          }
      }
    }
  }
    post { 
        always { 
            dingtalk (
                robot: "dev",
                type: 'MARKDOWN',
                title: "${Image_Name} 构建${currentBuild.result}",
                text: [
                    "## [${Image_Name} 构建${currentBuild.result}提醒](${BUILD_URL}console)",
                    "---",
                    "- 项目名称:${Image_Name} ",
                    "- 构建编号:${BUILD_NUMBER} ",
                    "- 构建人:${BUILD_USER} ",
                    "- 构建URL:${BUILD_URL} ",
                    "- 构建结果:${currentBuild.result} ",
                    "- 构建环境:${env.PLATFORM} ",
                    "- 构建时间:${BUILD_TIMESTAMP} ",
                    "- 构建镜像:${Full_Image} ",
                    "- 是否回滚:${UserInput} ",
                    "- 构建持续时间:${currentBuild.duration/1000}秒"
                ]
            )
        }
    }
}

生成动态关联参数流水线

properties([gitLabConnection(gitLabConnection: 'gitlab', jobCredentialId: ''), parameters([activeChoice(choiceType: 'PT_SINGLE_SELECT', description: '请选择项目名称', filterLength: 1, filterable: false, name: 'HarBor_Pro', randomName: 'choice-parameter-3118753064687', script: groovyScript(fallbackScript: [classpath: [], oldScript: '', sandbox: false, script: ''], script: [classpath: [], oldScript: '', sandbox: false, script: 'return ["pi6000", "prome","ruoyi"]'])), reactiveChoice(choiceType: 'PT_SINGLE_SELECT', description: '请选择仓库名称', filterLength: 1, filterable: false, name: 'Image_Name', randomName: 'choice-parameter-3118772227412', referencedParameters: 'HarBor_Pro', script: groovyScript(fallbackScript: [classpath: [], oldScript: '', sandbox: false, script: ''], script: [classpath: [], oldScript: '', sandbox: false, script: '''import groovy.json.JsonSlurper

// 定义 curl 命令来获取 JSON 数据
def command = "curl -s -u admin:123456 -H \'Content-Type: application/json\' -X GET https://192.168.1.20/api/v2.0/projects/${HarBor_Pro}/repositories --insecure"

// 执行命令并获取输出
def process = command.execute()
def output = process.text.trim()

// 打印输出以调试
println("Command Output: ${output}")

// 解析 JSON 数据
def jsonSlurper = new JsonSlurper()
def parsedJson = jsonSlurper.parseText(output)

// 提取仓库名称的最后部分
def repositoryNames = parsedJson.collect { 
    // 获取仓库名称
    def fullName = it.name
    
    // 提取最后部分
    def shortName = fullName.replaceAll(/.*\\/([^\\/]*)$/, \'$1\')
    
    return shortName
}

// 返回仓库名称列表
return repositoryNames'''])), reactiveChoice(choiceType: 'PT_SINGLE_SELECT', description: '请选择镜像标签', filterLength: 1, filterable: false, name: 'Image_Tags', randomName: 'choice-parameter-3118774362701', referencedParameters: 'HarBor_Pro,Image_Name', script: groovyScript(fallbackScript: [classpath: [], oldScript: '', sandbox: false, script: ''], script: [classpath: [], oldScript: '', sandbox: false, script: '''def get_tag = [ "bash", "-c", "curl -s -uadmin:123456 -H \'Content-Type: application/json\' -X GET https://192.168.1.20/v2/${HarBor_Pro}/${Image_Name}/tags/list --insecure | sed -r \'s#(\\\\{.*\\\\[)(.*)(\\\\]\\\\})#\\\\2#g\' | xargs -d \',\' -n1 | xargs -n1 | sort -t \'_\' -k2 -k3 -nr | head -5"]

return get_tag.execute().text.tokenize("\\n")''']))])])

生成完之后放在流水线最上面即可

完整流水线

properties([gitLabConnection(gitLabConnection: 'gitlab', jobCredentialId: ''), parameters([activeChoice(choiceType: 'PT_SINGLE_SELECT', description: '请选择项目名称', filterLength: 1, filterable: false, name: 'HarBor_Pro', randomName: 'choice-parameter-3118753064687', script: groovyScript(fallbackScript: [classpath: [], oldScript: '', sandbox: false, script: ''], script: [classpath: [], oldScript: '', sandbox: false, script: 'return ["pi6000", "prome","ruoyi"]'])), reactiveChoice(choiceType: 'PT_SINGLE_SELECT', description: '请选择仓库名称', filterLength: 1, filterable: false, name: 'Image_Name', randomName: 'choice-parameter-3118772227412', referencedParameters: 'HarBor_Pro', script: groovyScript(fallbackScript: [classpath: [], oldScript: '', sandbox: false, script: ''], script: [classpath: [], oldScript: '', sandbox: false, script: '''import groovy.json.JsonSlurper

// 定义 curl 命令来获取 JSON 数据
def command = "curl -s -u admin:123456 -H \'Content-Type: application/json\' -X GET https://192.168.1.20/api/v2.0/projects/${HarBor_Pro}/repositories --insecure"

// 执行命令并获取输出
def process = command.execute()
def output = process.text.trim()

// 打印输出以调试
println("Command Output: ${output}")

// 解析 JSON 数据
def jsonSlurper = new JsonSlurper()
def parsedJson = jsonSlurper.parseText(output)

// 提取仓库名称的最后部分
def repositoryNames = parsedJson.collect { 
    // 获取仓库名称
    def fullName = it.name
    
    // 提取最后部分
    def shortName = fullName.replaceAll(/.*\\/([^\\/]*)$/, \'$1\')
    
    return shortName
}

// 返回仓库名称列表
return repositoryNames'''])), reactiveChoice(choiceType: 'PT_SINGLE_SELECT', description: '请选择镜像标签', filterLength: 1, filterable: false, name: 'Image_Tags', randomName: 'choice-parameter-3118774362701', referencedParameters: 'HarBor_Pro,Image_Name', script: groovyScript(fallbackScript: [classpath: [], oldScript: '', sandbox: false, script: ''], script: [classpath: [], oldScript: '', sandbox: false, script: '''def get_tag = [ "bash", "-c", "curl -s -uadmin:123456 -H \'Content-Type: application/json\' -X GET https://192.168.1.20/v2/${HarBor_Pro}/${Image_Name}/tags/list --insecure | sed -r \'s#(\\\\{.*\\\\[)(.*)(\\\\]\\\\})#\\\\2#g\' | xargs -d \',\' -n1 | xargs -n1 | sort -t \'_\' -k2 -k3 -nr | head -5"]

return get_tag.execute().text.tokenize("\\n")''']))])])
pipeline {
  agent {
    kubernetes {
      cloud 'k8s'
      inheritFrom 'jenkins-slave'
     namespace 'default'
      yaml '''
        apiVersion: v1
        kind: Pod
        spec:
          imagePullSecrets:
          - name: harbor-admin
          containers:
          - name: kubectl
            image: harbor.jiajia.com/dev/jenkins-kubectl:1.23.17
            imagePullPolicy: IfNotPresent
            command: ["cat"]
            tty: true
      '''
    }
  }
  parameters {
        choice(
            choices: ['dev', 'prod'], 
            description: '请选择要部署的环境', 
            name: 'PLATFORM'
        )
    }
  environment {
      Full_Image = "harbor.jiajia.com/${Harbor_Pro}/${Image_Name}:${Image_Tags}"
      UserInput = ""
    }
  stages {
    stage('输出完整的镜像名称') {
      steps {
        sh 'echo 镜像名称-tag: ${Full_Image}'
      }
    }
    
    stage('部署应用至K8S') {
      steps {
          withCredentials([file(credentialsId: 'k8s-admin', variable: 'KUBECONFIG')]) {
            container('kubectl'){
              sh 'mkdir -p ~/.kube && cp ${KUBECONFIG} ~/.kube/config'
              sh 'kubectl set image deployment/${Image_Name} ${Image_Name}=${Full_Image} -n default'
            }
          }
      }
    }
    stage('快速回滚') {
      steps {
          withCredentials([file(credentialsId: 'k8s-admin', variable: 'KUBECONFIG')]) {
            container('kubectl'){
              script {
                timeout(time:1 , unit: 'HOURS'){
                  UserInput = input message: '是否回退至上一个版本', parameters: [choice(choices: ['No', 'Yes'], name: 'rollback')]
                  if (UserInput == "Yes"){
                    sh 'mkdir -p ~/.kube && cp ${KUBECONFIG} ~/.kube/config'
                    sh 'kubectl rollout undo deployment ${Image_Name} -n default'
                  }else {
                    echo "没有选择回退"
                  }
                }
              }
            }
          }
      }
    }
  }
    post { 
        always { 
            dingtalk (
                robot: "dev",
                type: 'MARKDOWN',
                title: "${Image_Name} 构建${currentBuild.result}",
                text: [
                    "## [${Image_Name} 构建${currentBuild.result}提醒](${BUILD_URL}console)",
                    "---",
                    "- 项目名称:${Image_Name} ",
                    "- 构建编号:${BUILD_NUMBER} ",
                    "- 构建人:${BUILD_USER} ",
                    "- 构建URL:${BUILD_URL} ",
                    "- 构建结果:${currentBuild.result} ",
                        "- 构建环境:${env.PLATFORM} ",
                    "- 构建时间:${BUILD_TIMESTAMP} ",
                    "- 构建镜像:${Full_Image} ",
                    "- 是否回滚:${UserInput} ",
                    "- 构建持续时间:${currentBuild.duration/1000}秒"
                ]
            )
        }
    }
}

基于Lark Notice优化构建通知

简介

<font style="color:rgb(31, 35, 40);">lark-notice-plugin</font> 是一个用于 <font style="color:rgb(31, 35, 40);">Jenkins</font><font style="color:rgb(31, 35, 40);">构建通知机器人</font> 通知插件,可以将 <font style="color:rgb(31, 35, 40);">Jenkins</font> 构建过程以及结果通知推送到 <font style="color:rgb(31, 35, 40);">Lark</font><font style="color:rgb(31, 35, 40);">飞书</font><font style="color:rgb(31, 35, 40);">钉钉</font> 协作平台。 可配置多个的通知时机,包括 <font style="color:rgb(31, 35, 40);">构建启动时</font><font style="color:rgb(31, 35, 40);">构建中断</font><font style="color:rgb(31, 35, 40);">构建失败</font><font style="color:rgb(31, 35, 40);">构建成功时</font><font style="color:rgb(31, 35, 40);">构建不稳定</font>等。 支持多种不同类型的消息,包括 <font style="color:rgb(31, 35, 40);">文本消息</font><font style="color:rgb(31, 35, 40);">图片消息</font><font style="color:rgb(31, 35, 40);">群名片消息</font><font style="color:rgb(31, 35, 40);">富文本消息</font><font style="color:rgb(31, 35, 40);">卡片消息</font>; 同时该插件还提供了<font style="color:rgb(31, 35, 40);">自定义模板</font><font style="color:rgb(31, 35, 40);">变量</font>的功能,使您能够根据自己的需求来定制通知消息的内容和格式。

下载插件

目前不支持在线安装

Jenkins版本要求:2.414.3+

#离线安装
cd /var/lib/jenkins/plugins
wget https://gitee.com/xm721806280/lark-notice-plugin/releases/download/v2.0.0/lark-notice.hpi
systemctl restart jenkins

控制台验证

机器人配置

打开 Manage Jenkins 页面,找到 Lark Notice 配置项,如下图所示:

默认全部勾选

点击新增机器人

测试是否正常

增加stage

    stage('发送卡片消息') {
      steps {
        echo '发送卡片消息...'
      }
      post {
        success {
          dingTalk (
            robot: '6081ef0eaec970f0ef3e5f2799f46a8b0e3c343744ec90e990f212b07910f03c',
            type: 'CARD',
            title: '📢 Jenkins 构建通知',
            text: [
              "## <font color='green'>📢 Jenkins 构建通知</font>",
              "---",
              "📋 **任务名称**:${Image_Name}  ",
              "🔢 **任务编号**:[${BUILD_DISPLAY_NAME}](${BUILD_URL})  ",
              "🌟 **构建状态**:  <font color='green'>${currentBuild.currentResult}</font>  ",
              "🕐 **构建用时**: ${currentBuild.duration/1000}秒  ",
              "😘 **构建镜像**: ${Full_Image}  ",
              "😎 **构建环境**: ${env.PLATFORM}  ",
              "😝 **是否回滚**: ${UserInput}  ",
              "👤 **执  行 者**: ${env.BUILD_USER}  ",
              '![图片](https://p5.toutiaoimg.com/origin/pgc-image/4f42eb331c604c5c8a7acd5c833e0208?from=pc)  '
            ],
            atAll: true,
            buttons: [
              [
                title: "更改记录",
                url: "${BUILD_URL}changes"
              ],
              [
                title: "控制台",
                type: "danger",
                url: "${BUILD_URL}console"
              ]
            ]
          )
        }
      }
    }
posted @ 2024-09-18 16:11  &UnstopPable  阅读(18)  评论(0编辑  收藏  举报