企业级Jenkins Pipeline 实战 Docker构建+Ansible发布

企业级实现

之前文中展示的例子是一条非常标准、具备"准企业级"水准的 Jenkins Pipeline,且架构设计非常清晰。

它不仅涵盖了 CI/CD 的核心流程,还通过引入 "第三个仓库(Ansible 剧本库)" 实现了代码与运维逻辑的解耦,这是很多中大型企业在 DevOps 成熟度提升阶段会采用的架构。以下是从企业级标准角度对 Pipeline 进行的深度评估和架构分析:

当前设计

目前的架构属于 "三位一体" 的分布式构建部署模型:

  1. 代码源(Source):前端、后端两个独立的 Git 仓库。
  2. 制品源(Artifact):Nexus 私服(同时存放 Maven Jar 和 Raw Zip)。
  3. 流程源(Orchestration)Ansible Playbooks 仓库(第三个仓库) ------ 这是你架构中最亮眼的设计

这种架构完美遵循了 "关注点分离" 原则:Jenkins 只负责编译和上传,不负责具体的部署逻辑;Ansible 负责环境差异和部署细节。 但是问题就是将前后端耦合在同一个 Jenkinsfile 中的典型痛点就是:如果我只想发布前端,这个时候前后端代码都会拉去打包一遍造成:资源浪费和风险增加

所以将流水线拆分为"前端专用"和"后端专用"两个 Job,同时让它们指向同一个 Ansible 剧本仓库,正是很多中大型企业推崇的架构。这不仅是企业级的做法,更是目前主流云原生架构(如 GitLab CI/CD、Jenkins Shared Libraries)的标准实践。

在企业级 DevOps 实践中,核心原则是 "逻辑复用""关注点分离"

将"部署逻辑"(Ansible 剧本)抽离出来作为一套公共资产,供不同的流水线调用,是解决重复代码和维护噩梦的最佳方案。

构建后端

1.ansible的主机清单稍微调整了一下,由于我的机器不够所以使用前端服务器作为灰度发布的主机组,然后提交到git的ansible仓库

ini 复制代码
[root@jenkins-slave staging]# pwd
/root/ansible/inventory/staging
[root@jenkins-slave staging]# cat hosts.ini 
[backend-test]
172.20.10.6 ansible_port=22 ansible_user=deploy
172.20.10.5 ansible_port=22 ansible_user=deploy

2.测试(灰度)后端变量:inventory/staging/group_vars/backend-test.yml

ini 复制代码
---
# 测试环境变量覆盖
server_name: "jenkins-demo-backend-1.0.0.jar"
heap_size: "1024m"
dest_path: "/data/application"
server_port: 8080
upload_path: "/data/upload"
backup_path: "/data/backup"


#定义Nexus仓库的信息
nexus_url: "http://172.20.10.6:8081"
nexus_repo: "app-backend"
repo_path: "com/example/jenkins-demo-backend/1.0.0"
nexus_user: "admin"
nexus_password: "admin123"

3.修改剧本加入了灰度发布,通过jenkins变量来传入参数,识别是全量还是灰度

ini 复制代码
[root@jenkins-slave ops-ansible-playbooks]# cat deploy.yml 
---
# ==========================================
# Play 1: 后端服务通用更新 (合并了全量和灰度)带回滚
# ==========================================
- name: 滚动更新后端服务
  hosts: "{{ target_group | default('backend-servers') }}" # 关键点:这里使用变量 target_group,如果没有传入,默认为 backend-servers
  remote_user: deploy
  become: yes
  become_method: sudo       #使用 sudo 方式提权
  gather_facts: yes

  # --- 滚动更新策略 ---
  serial: 1                # 生产环境两台机器,这里设为1表示每次只更1台,实现零停机
  max_fail_percentage: 0   # 只要有1台失败,立即终止

  vars:
    # 动态生成备份文件名,例如:app.jar.bak_20260509_1630,使用ansible中的 date, hour, minute, second 拼接
    backup_suffix: "bak_{{ ansible_date_time.date }}_{{ ansible_date_time.hour }}{{ ansible_date_time.minute }}{{ ansible_date_time.second }}"
    backup_file: "{{ backup_path }}/{{ server_name }}.{{ backup_suffix }}"
    # 拼接完整的 Nexus 下载链接
    nexus_download_url: "{{ nexus_url }}/repository/{{ nexus_repo }}/{{ repo_path }}/{{ server_name }}"

  tasks:

    - name: "确保部署目录和临时目录以及备份存在"
      file:
        path: "{{ item }}"
        state: directory
        mode: '0755'
      loop:
        - "{{ dest_path }}"
        - "{{ upload_path }}"
        - "{{ backup_path }}"


    - name: "[后端] 停止应用服务"
      # 这里使用 shell 执行停止命令,具体取决于你的启动方式
      # 假设你是用 nohup 启动的,这里通过 ps/grep/kill 停止
      shell: |
        ps -ef | grep {{ server_name  }} | grep -v grep | awk '{print $2}' | xargs kill -15
      ignore_errors: yes # 如果进程不存在,忽略错误继续

    - name: "[后端] 检查旧版本 Jar 包是否存在"
      stat:
        path: "{{ dest_path }}/{{ server_name }}"
      register: old_app

    - name: "[后端] 备份旧版本应用 (重命名)"
      command: mv "{{ dest_path }}/{{ server_name }}" "{{ backup_file }}"
      when: old_app.stat.exists

    - name: "[后端] 推送新版本应用包"
      get_url:
       url: "{{ nexus_download_url }}"
       dest: "{{ upload_path }}/{{ server_name }}"
       username: "{{ nexus_user }}"
       password: "{{ nexus_password }}"
       mode: '0755'
       timeout: 60
       force: yes  # 强制覆盖

    - name: "将后端代码上传到工作目录"
      command: mv "{{ upload_path }}/{{ server_name }}" "{{ dest_path }}/{{ server_name }}"

    - name: "[后端] 启动新版本服务"
      shell: |
        source /etc/profile && nohup java -Xms{{ heap_size }} -Xmx{{ heap_size }} -jar "{{ dest_path }}/{{ server_name }}" --server.port={{ server_port }} > "{{ dest_path }}/app.log" 2>&1 &
      async: 45   # 异步运行,最长等待45秒
      poll: 0     # 不阻塞,立即返回

    - name: "[后端] 等待服务启动完成"
      wait_for:
        port: "{{ server_port }}"
        host: "{{ ansible_default_ipv4.address }}" #获取主机ip,通过ip测试
        delay: 5
        timeout: 60
        state: started
      register: health_check
      ignore_errors: yes # 允许失败,以便进入回滚逻辑

    - name: "[后端] 启动失败回滚"
      block:
        - name: "恢复旧版本包"
          shell: mv "{{ backup_file }}" "{{ dest_path }}/{{ server_name }}"
          when: old_app.stat.exists
        
        - name: "重启旧版本服务"
          shell: |
           nohup java -Xms{{ heap_size }} -Xmx{{ heap_size }} -jar "{{ dest_path }}/{{ server_name }}" --server.port={{ server_port }} > "{{ dest_path }}/app.log" 2>&1 &
      
        - name: "报错终止"
          fail:
           msg: "新版本启动失败,已回滚到旧版本!"
      when: health_check is failed

# ==========================================
# Play 2: 前端服务并发更新
# ==========================================
- name: 更新前端静态资源
  hosts: frontend-servers
  remote_user: deploy
  become: yes
  become_method: sudo       #使用 sudo 方式提权
  gather_facts: yes


  vars:
    # 拼接前端 Nexus 链接
    frontend_nexus_url: "{{ nexus_url }}/repository/{{ nexus_repo }}/{{ repo_path }}/{{ dist_package }}"

  tasks:

    - name: "确保部署目录和临时目录以及备份存在"
      file:
        path: "{{ item }}"
        state: directory
        mode: '0755'
      loop:
        - "{{ dist_dest_path }}"
        - "{{ upload_path }}"
        - "{{ backup_path }}"


    - name: "备份现有前端文件 (如果目录非空)"
      # 打包整个目录作为备份,文件名带时间戳
      archive:
       path: "{{ dist_dest_path }}/*"
       dest: "{{ backup_path }}/frontend_bak_{{ ansible_date_time.date }}_{{ ansible_date_time.hour }}{{ ansible_date_time.minute }}{{ ansible_date_time.second }}.tar.gz"
       format: gz
      ignore_errors: yes # 如果目录为空或不存在,忽略错误继续

    - name: "清理旧的前端文件"
      file:
        path: "{{ dist_dest_path }}/*"
        state: absent

    - name: "[前端] 推送前端压缩包"
      get_url:
       url: "{{ frontend_nexus_url }}"
       dest: "{{ upload_path }}/{{ dist_package }}"
       username: "{{ nexus_user }}"
       password: "{{ nexus_password }}"
       mode: '0644'
       force: yes

    - name: "[前端] 解压覆盖静态文件"
      unarchive:
        src: "{{ upload_path }}/{{ dist_package }}"
        dest: "{{ upload_path }}/"  # 先解压到上传目录,此时会生成 {{ upload_path }}/dist
        remote_src: yes # 在远程服务器上解压

    - name: "[前端] 移动内容并移除 dist 目录"
      shell: |
        # 将 dist 目录下的所有内容(包括隐藏文件)移动到目标目录
        mv {{ upload_path }}/dist/* {{ dist_dest_path }}/ && mv {{ upload_path }}/dist/.* {{ dist_dest_path }}/ 2>/dev/null || true
        # 删除空的 dist 目录
        rm -rf {{ upload_path }}/dist

    - name: "[前端] 清理临时压缩包"
      file:
        path: "{{ upload_path }}/{{ dist_package }}"
        state: absent

    - name: "[前端] 重载 Nginx"
      shell: "{{ nginx_reload_cmd }}"
      #ignore_errors: yes

3.编写pipeline流水线

ini 复制代码
pipeline {
    // 使用 Jenkins Slave 节点
    agent { 
        node { 
            label 'jenkins-slave' 
            customWorkspace '/home/jenkins/backend-pipeline' 
        } 
    }

    // --- 参数化构建定义 ---
    parameters {
        choice(
            name: 'DEPLOY_ENV', 
            choices: ['production', 'staging'], 
            description: '选择部署环境:production为生产环境,staging为灰度环境'
        )
        // 新增:部署策略选择
        choice(
            name: 'DEPLOY_STRATEGY', 
            choices: ['灰度发布', '全量发布'], 
            description: '选择部署策略:灰度仅部署到 backend-test 组,全量部署到 backend-servers 组'
        )
        booleanParam(
            name: 'SKIP_TESTS', 
            defaultValue: true, 
            description: '是否跳过 Maven 测试?'
        )
    }

    // --- 全局选项 (Optimize)---
    options {
        // 1. ⏱️ 超时控制:防止任务挂起
        timeout(time: 1, unit: 'HOURS')
        
        // 2. 🧹 清理策略:保留最近 20 次构建,防止磁盘爆满
        buildDiscarder(logRotator(numToKeepStr: '20'))
        
        // 3. 🚫 禁止并发构建
        disableConcurrentBuilds()
        
        // 4. 📜 控制台输出时间戳
        timestamps()
    }

    // 环境变量
    environment {
        // --- Nexus 配置 ---
        NEXUS_CREDENTIALS_ID = 'nexus-deploy-creds'
        NEXUS_TOOL_URL = '172.20.10.6:8081'
        NEXUS_PROTOCOL = 'http'
        NEXUS_MAVEN_REPO = 'app-backend'

        // --- Git 配置 ---
        BACKEND_GIT_URL = 'git@gitee.com:testpm/backend-docker.git'
        DEPLOY_GIT_URL = 'git@gitee.com:testpm/ops-ansible-playbooks.git'
        GIT_CREDENTIALS_ID = 'jenkins-slave-git'

        // --- 版本与镜像 ---
        BUILD_VERSION = "${BUILD_NUMBER}"
        MAVEN_IMAGE = 'maven:3.9.9-eclipse-temurin-21'
        BACKEND_JAR_GLOB = 'backend/target/*.jar'
    }

    stages {
        // ================== 阶段 1: 拉取后端代码 ==================
        stage('📥 拉取后端代码') {
            steps {
                script {
                    echo "🚀 开始拉取后端代码..."
                    dir('backend') {
                        deleteDir()
                        git branch: 'master', 
                             credentialsId: "${GIT_CREDENTIALS_ID}", 
                             url: "${BACKEND_GIT_URL}"
                    }
                }
            }
        }

        // ================== 阶段 2: 后端构建 (Maven) ==================
        stage('🔨 后端构建') {
            steps {
                script {
                    retry(3) {
                        docker.image("${MAVEN_IMAGE}").inside("-u root:root -w ${WORKSPACE} -v /data/maven-cache:/root/.m2 --rm --entrypoint=''") {
                            dir('backend') {
                                def testOpt = params.SKIP_TESTS ? '-DskipTests' : ''
                                sh """
                                    echo "🚀 开始 Maven 编译..."
                                    mvn clean package ${testOpt} -B -U
                                """
                            }
                        }
                    }
                    // 验证产物
                    def jars = findFiles(glob: "${BACKEND_JAR_GLOB}")
                    if (!jars) error "❌ 构建失败:未找到 Jar 包"
                }
            }
        }

        // ================== 阶段 3: 上传到 Nexus ==================
        stage('☁️ 上传后端制品') {
            steps {
                script {
                    echo "🚀 开始上传 Jar 到 Nexus..."
                    def pom = readMavenPom file: 'backend/pom.xml'
                    def jarFile = findFiles(glob: "${BACKEND_JAR_GLOB}")[0]

                    nexusArtifactUploader(
                        nexusVersion: 'nexus3',
                        protocol: "${NEXUS_PROTOCOL}",
                        nexusUrl: "${NEXUS_TOOL_URL}",
                        groupId: "${pom.groupId}",
                        version: "${pom.version}",
                        repository: "${NEXUS_MAVEN_REPO}",
                        credentialsId: "${NEXUS_CREDENTIALS_ID}",
                        artifacts: [[
                            artifactId: "${pom.artifactId}",
                            classifier: '',
                            file: "${jarFile.path}",
                            type: 'jar'
                        ]]
                    )
                }
            }
        }

        // ================== 阶段 4: 获取 Ansible 剧本 ==================
        stage('📂 获取 Ansible 剧本') {
            steps {
                script {
                    echo "🚀 拉取公共运维剧本..."
                    dir('ansible-playbooks') {
                        deleteDir()
                        git branch: 'master',
                             credentialsId: "${GIT_CREDENTIALS_ID}", 
                             url: "${DEPLOY_GIT_URL}"
                    }
                }
            }
        }

        // ================== 阶段 5: 执行部署 ==================
        stage('🚀 部署后端') {
            steps {
                script {
                    echo "🚀 开始执行后端部署..."
                    
                    // 👇 这里增加了人工确认和超时控制
                    timeout(time: 15, unit: 'MINUTES') {
                        input message: "确认要【${params.DEPLOY_STRATEGY}】到 ${params.DEPLOY_ENV} 环境吗?", ok: '确认部署'
                    }

                    dir('ansible-playbooks') {
                        // 👇 核心逻辑:根据参数DEPLOY_STRATEGY动态决定是"灰度发布"还是"全量发布",并传递给 Ansible `--extra-vars`
                        def target_group = ""
                        if (params.DEPLOY_STRATEGY == '灰度发布') {
                            target_group = "backend-test"
                            echo "⚠️ 当前模式:灰度发布,目标组 -> ${target_group}"
                        } else {
                            target_group = "backend-servers"
                            echo "⚠️ 当前模式:全量发布,目标组 -> ${target_group}"
                            }
                        // 传递参数给 Ansible,告诉剧本只做后端
                        def ansibleCmd = """ 
                            ansible-playbook \
                            -i inventory/${params.DEPLOY_ENV}/hosts.ini \
                            deploy.yml \
                            --extra-vars "target_group=${target_group}" \
                            -vvv 
                        """
                        
                        sshagent (credentials: ['deploy-ssh-key']) {
                            sh "${ansibleCmd}"
                        }
                    }
                }
            }
        }
    }

    // --- 统一清理 ---
    post {
        always {
            // 无论成功失败,都清理工作空间
            cleanWs()
        }
    }
}

这份 Jenkins Pipeline 属于典型的企业级 CI/CD 流水线雏形。它不仅涵盖了从代码拉取到部署的全流程,还融入了很多生产环境必需的"防错"和"优化"机制。

相关推荐
身如柳絮随风扬12 小时前
Docker 化部署 Spring Boot + Vue 全栈应用:从打包到容器化上线
vue.js·spring boot·docker
会编程的土豆12 小时前
Docker 里面的镜像(Image)和容器(Container)到底是什么
运维·docker·容器
r-t-H12 小时前
KVM虚拟化与Docker基础实践-第三章
linux·运维·nginx·docker·容器
运维瓦工13 小时前
DevOps 生态介绍(四):Sonarqube&jacoco 与jenkins集成使用
运维·jenkins·devops
Niliuershangba1 天前
Docker Desktop 部署 ChestnutCMS 全流程:从零搭建企业级 CMS 开发环境
运维·docker·容器
我是坑货1 天前
Jenkins 构建失败排查记录:mvn -U 把新版依赖被远程旧版覆盖
运维·jenkins
darkdragonking1 天前
Docker(五)OpenEuler22.03 安装docker ce、排坑
运维·docker·容器
song5011 天前
多卡训练加速:HCCL 集合通信实战
分布式·python·flutter·ci/cd·分类
Waay1 天前
图文详解|K8s Pod内部结构
docker·云原生·kubernetes