1. CI/CD 概述

1.1 CI(持续集成)

持续集成是一种软件开发实践,通过自动化工具对代码进行编译、测试和打包,减少人工干预,提高构建效率。它的核心理念是将代码频繁地集成到共享存储库中,并通过自动化构建和测试流程来验证代码的正确性。

核心价值:

  • 为开发人员提供即时反馈

  • 帮助快速定位并修复问题

  • 加速软件开发周期

  • 提高软件质量

1.2 CD(持续交付)

持续交付是一种软件开发实践,通过自动化工具建立一套自动化的流水线,将应用程序部署到不同的环境中(开发环境、测试环境和生产环境等)。

核心价值:

  • 自动化执行构建、测试、部署和发布

  • 确保软件随时随地可靠交付

  • 缩短交付周期

  • 降低发布风险

1.3 CI/CD 关系

  • 持续集成持续交付的基础

  • CI 提高代码质量和可维护性,为 CD 提供基础

  • CD 促进更快代码交付,推动 CI 实施

  • 两者相辅相成,共同推动软件开发持续改进

2. 项目架构

2.1 技术栈

  • Jenkins - 自动化服务器

  • SonarQube - 代码质量检查

  • Harbor - 镜像仓库

  • Container - 容器运行时

  • Kubernetes - 容器编排平台

2.2 项目目标

搭建完整的 CI/CD 管道,模拟实际生产环境项目开发部署流程,实现:

  • 持续集成

  • 持续交付

  • 持续部署

3. 环境部署

3.1 基础设施

所有服务均运行在 Kubernetes 集群上,使用 NFS 共享存储。

部署文档:

4. 权限配置

4.1 Harbor 配置

项目创建:

  • 创建 spring_boot_demo 项目

  • 设置项目为私有

用户管理:

bash

# 创建普通用户 cuiliang
# 配置项目用户权限
# 设置角色为开发者

权限说明:

角色 权限
访客 对项目有只读权限
开发人员 对项目有读写权限
维护人员 读写权限 + 创建 webhook 权限
项目管理员 完整权限,包括用户管理

4.2 GitLab 配置

群组管理:

  • 创建 develop 开发组

  • 组权限设置为私有

项目管理:

  • 创建 sprint boot demo 项目

  • 项目类型为私有

用户权限:

bash

# 创建普通用户 cuiliang
# 将用户添加到 develop 组
# 设置角色为 Developer
# 配置分支权限

5. Jenkins 配置

5.1 插件安装

插件名称 功能描述 参考文档
GitLab Plugin GitLab 集成 https://www.cuiliangblog.cn/detail/section/127410630
SonarQube Scanner 代码质量检查 https://www.cuiliangblog.cn/detail/section/165534414
Kubernetes Plugin K8s 集成 https://www.cuiliangblog.cn/detail/section/127230452
Email Extension 邮件推送 https://www.cuiliangblog.cn/detail/section/133029974
Version Number 版本号管理 https://plugins.jenkins.io/versionnumber/
Content Replace 文件内容替换 https://plugins.jenkins.io/content-replace/

5.2 Jenkins Slave 镜像制作

Dockerfile:

dockerfile

FROM jenkins/inbound-agent:latest-jdk17
USER root
COPY kubectl /usr/bin/kubectl
COPY nerdctl /usr/bin/nerdctl
COPY buildctl /usr/bin/buildctl

构建命令:

bash

docker build -t harbor.local.com/cicd/jenkins-slave:v1.0 .

6. 动态 Slave 架构

6.1 架构优势

  • 按需创建:根据构建需求动态创建 Slave

  • 自动清理:Job 完成后自动删除 Slave Pod

  • 资源优化:避免资源长期占用

  • 环境隔离:每次构建都是全新的环境

6.2 工作流程

  1. Jenkins Master 接收 Build 请求

  2. 根据配置的 Label 动态创建 Jenkins Slave Pod

  3. Slave 注册到 Master 并执行 Job

  4. Job 完成后注销 Slave 并删除 Pod

  5. 系统恢复到初始状态

7. 流水线配置

7.1 Job 配置

构建触发器:

  • 配置 Webhook,代码提交时触发构建

  • 支持分支过滤

流水线定义:

  • 从 SCM 获取 Jenkinsfile

  • 脚本路径:Jenkinsfile-k8s.groovy

7.2 多环境支持

环境 分支 命名空间 域名
测试环境 test test demo.test.com
生产环境 master prod demo.local.com

8. 完整 Jenkinsfile

groovy

pipeline {
    agent {
        kubernetes {
            yaml '''
apiVersion: v1
kind: Pod
metadata:
  labels:
    app: jenkins-slave
spec:
  containers:
  - name: jnlp
    image: harbor.local.com/cicd/jenkins-slave:v1.0
    resources:
      limits:
        memory: "512Mi"
        cpu: "500m"
    securityContext:
      privileged: true
    volumeMounts:
    - name: buildkit
      mountPath: "/run/buildkit/"
    - name: containerd
      mountPath: "/run/containerd/containerd.sock"
    - name: kube-config
      mountPath: "/root/.kube/"
      readOnly: true
  - name: maven
    image: harbor.local.com/cicd/maven:3.9.3
    resources:
      limits:
        memory: "512Mi"
        cpu: "500m"
    command:
      - 'sleep'
    args:
      - '9999'
    volumeMounts:
      - name: maven-data
        mountPath: "/root/.m2"
  - name: buildkitd
    image: harbor.local.com/cicd/buildkit:v0.13.2
    resources:
      limits:
        memory: "256Mi"
        cpu: "500m"
    securityContext:
      privileged: true
    volumeMounts:
    - name: buildkit
      mountPath: "/run/buildkit/"
    - name: buildkit-data
      mountPath: "/var/lib/buildkit/"
    - name: containerd
      mountPath: "/run/containerd/containerd.sock"
  volumes:
  - name: maven-data
    persistentVolumeClaim:
      claimName: jenkins-maven
  - name: buildkit
    hostPath:
      path: /run/buildkit/
  - name: buildkit-data
    hostPath:
      path: /var/lib/buildkit/
  - name: containerd
    hostPath:
      path: /run/containerd/containerd.sock
  - name: kube-config
    secret:
      secretName: kube-config
            '''
      retries 2
        }
    }
    environment {
        HARBOR_CRED = "harbor-cuiliang-password"
        IMAGE_NAME = ""
        IMAGE_APP = "demo"
        branchName = ""
    }
    stages {
        stage('拉取代码') {
            environment {
                GITLAB_CRED = "gitlab-cuiliang-password"
                GITLAB_URL = "http://gitlab.cicd.svc/develop/sprint_boot_demo.git"
            }
            steps {
                echo '开始拉取代码'
                checkout scmGit(branches: [[name: '*/*']], extensions: [], userRemoteConfigs: [[credentialsId: "${GITLAB_CRED}", url: "${GITLAB_URL}"]])
                script {
                    def branch = env.GIT_BRANCH ?: 'master'
                    branchName = branch.split('/')[-1]
                }
                echo '拉取代码完成'
            }
        }
        stage('编译打包') {
            steps {
                container('maven') {
                    echo '开始编译打包'
                    sh 'mvn clean package'
                    echo '编译打包完成'
                }
            }
        }
        stage('代码审查') {
            environment {
                SONARQUBE_SCANNER = "SonarQubeScanner"
                SONARQUBE_SERVER = "SonarQubeServer"
            }
            steps {
                echo '开始代码审查'
                script {
                    def scannerHome = tool "${SONARQUBE_SCANNER}"
                    withSonarQubeEnv("${SONARQUBE_SERVER}") {
                        sh "${scannerHome}/bin/sonar-scanner"
                    }
                }
                echo '代码审查完成'
            }
        }
        stage('构建镜像') {
            environment {
                HARBOR_URL = "harbor.local.com"
                HARBOR_PROJECT = "spring_boot_demo"
                IMAGE_TAG = ''
                IMAGE_NAME = ''
            }
            steps {
                echo '开始构建镜像'
                script {
                    if (branchName == 'master') {
                        IMAGE_TAG = VersionNumber versionPrefix: 'p', versionNumberString: '${BUILD_DATE_FORMATTED, "yyMMdd"}.${BUILDS_TODAY}'
                    } else if (branchName == 'test') {
                        IMAGE_TAG = VersionNumber versionPrefix: 't', versionNumberString: '${BUILD_DATE_FORMATTED, "yyMMdd"}.${BUILDS_TODAY}'
                    } else {
                        error("Unsupported branch: ${params.BRANCH}")
                    }
                    IMAGE_NAME = "${HARBOR_URL}/${HARBOR_PROJECT}/${IMAGE_APP}:${IMAGE_TAG}"
                    sh "nerdctl build --insecure-registry -t ${IMAGE_NAME} . "
                }
                echo '构建镜像完成'
                echo '开始推送镜像'
                withCredentials([usernamePassword(credentialsId: "${HARBOR_CRED}", passwordVariable: 'HARBOR_PASSWORD', usernameVariable: 'HARBOR_USERNAME')]) {
                    sh """nerdctl login --insecure-registry ${HARBOR_URL} -u ${HARBOR_USERNAME} -p ${HARBOR_PASSWORD}
          nerdctl push --insecure-registry ${IMAGE_NAME}"""
                }
                echo '推送镜像完成'
                echo '开始删除镜像'
                script {
                    sh "nerdctl rmi -f ${IMAGE_NAME}"
                }
                echo '删除镜像完成'
            }
        }
        stage('项目部署') {
            environment {
                YAML_NAME = "k8s.yaml"
            }
            steps {
                echo '开始修改资源清单'
                script {
                    if (branchName == 'master' ) {
                        NAME_SPACE = 'prod'
                        DOMAIN_NAME = 'demo.local.com'
                    } else if (branchName == 'test') {
                        NAME_SPACE = 'test'
                        DOMAIN_NAME = 'demo.test.com'
                    } else {
                        error("Unsupported branch: ${params.BRANCH}")
                    }
                }
                contentReplace(configs: [fileContentReplaceConfig(configs: [fileContentReplaceItemConfig(replace: "${IMAGE_NAME}", search: 'IMAGE_NAME'),
                                                                            fileContentReplaceItemConfig(replace: "${NAME_SPACE}", search: 'NAME_SPACE'),
                                                                            fileContentReplaceItemConfig(replace: "${DOMAIN_NAME}", search: 'DOMAIN_NAME')],
                        fileEncoding: 'UTF-8',
                        filePath: "${YAML_NAME}",
                        lineSeparator: 'Unix')])
                echo '修改资源清单完成'
                sh "cat ${YAML_NAME}"
                echo '开始部署资源清单'
                sh "kubectl apply -f ${YAML_NAME}"
                echo '部署资源清单完成'
            }
        }
    }
    post {
        always {
            echo '开始发送邮件通知'
            emailext(subject: '构建通知:${PROJECT_NAME} - Build # ${BUILD_NUMBER} - ${BUILD_STATUS}!',
                    body: '${FILE,path="email.html"}',
                    to: 'cuiliang0302@qq.com')
            echo '邮件通知发送完成'
        }
    }
}

9. 项目演示

9.1 开发测试阶段

开发流程:

bash

# 切换到 test 分支
git checkout -b test origin/test

# 修改代码版本信息
vim src/main/java/com/example/springbootdemo/HelloWorldController.java

# 提交代码
git add .
git commit -m "test环境更新版本至v2"
git push

自动化流程触发:

  1. 自动创建 Jenkins Slave Pod

  2. 执行编译、打包、代码审查

  3. 构建测试环境镜像(标签格式:t231215.1)

  4. 部署到 test 命名空间

  5. 发送邮件通知

9.2 生产发布阶段

发布流程:

bash

# 切换到 master 分支
git checkout master

# 更新生产环境版本
vim src/main/java/com/example/springbootdemo/HelloWorldController.java

# 提交代码
git add .
git commit -m "生产环境更新版本至v2"
git push

自动化流程触发:

  1. 执行完整 CI/CD 流程

  2. 构建生产环境镜像(标签格式:p231215.1)

  3. 部署到 prod 命名空间

  4. 生产环境服务更新

10. 项目资源

代码仓库:

部署效果:

11. 总结

本项目成功实现了基于 Kubernetes 的完整 CI/CD 流水线,具备以下特点:

11.1 技术优势

  • 完整的自动化流程:从代码提交到生产部署全自动化

  • 多环境支持:测试环境和生产环境独立部署

  • 动态资源管理:按需创建和销毁构建资源

  • 代码质量保障:集成 SonarQube 代码审查

  • 安全可靠:私有镜像仓库和权限控制

11.2 业务价值

  • 提升开发效率:自动化流程减少人工操作

  • 提高软件质量:持续的代码审查和测试

  • 降低发布风险:标准化部署流程

  • 快速迭代能力:支持频繁的版本发布

通过这个项目,团队可以实现高效的持续集成和持续交付,为业务快速发展提供强有力的技术支撑。

Logo

一站式 AI 云服务平台

更多推荐