Jenkins Pipeline 企业级用法 参数化构建+Ansible发布---上

基于基础语法,补充企业实战中常用的进阶特性,包括参数化构建、条件判断、多环境切换,无需编写脚本式逻辑,纯声明式配置即可实现复杂场景。

3.1 Parameters(参数化构建)

参数化构建允许在流水线运行时动态传入参数,灵活控制构建行为(如选择分支、指定环境、开关功能等),无需修改 Pipeline 脚本即可适配不同场景,大幅提升 CI/CD 流程的复用性和灵活性。参数化让 Pipeline 像函数一样,意味着可重用、更抽象,适合实现通用的 CI/CD 流程

参数定义后通过 params 对象访问,值在构建时传入,不会随分支扫描而丢失。当构建触发时,用户可以通过 Jenkins UI 的 "Build with Parameters" 选项覆盖默认值

3.2 声明式 Pipeline 参数定义

在 Declarative Pipeline 中,使用 parameters 指令定义参数列表,必须放在 pipeline 块下,且只能出现一次。

ini 复制代码
pipeline {
    agent any
    parameters {
        string(name: 'ENVIRONMENT', defaultValue: 'dev', description: 'Specify the deployment environment')
        booleanParam(name: 'RUN_TESTS', defaultValue: true, description: 'Toggle test execution')
        choice(name: 'LOG_LEVEL', choices: ['DEBUG', 'INFO', 'ERROR'], description: 'Logging level')
    }
    stages {
        stage('Test') {
            when { expression { params.RUN_TESTS == true } }
            steps { echo "Testing on ${params.ENVIRONMENT}" }
        }
        stage('Deploy') {
            steps { echo "Deploying to ${params.ENVIRONMENT}" }
        }
    }
}

3.3 参数类型详解

3.1 基础参数类型
参数类型 说明 语法示例 企业级典型用途
String 单行文本输入框,支持任意字符串 string(name: 'BRANCH', defaultValue: 'main', description: '构建分支') 指定构建分支、版本号、镜像 Tag、Git Commit ID
Boolean 复选框,true/false 二选一 booleanParam(name: 'RUN_TESTS', defaultValue: true, description: '是否执行测试') 开关控制:是否执行集成测试、是否发送通知、是否清理环境
Choice 下拉单选,预定义选项列表 choice(name: 'ENV', choices: ['dev', 'staging', 'prod'], description: '部署环境') 部署环境选择、构建模式(debug/release)、数据库类型
Password 密码输入框,输入时隐藏 password(name: 'API_TOKEN', defaultValue: '', description: 'API Token') 接收 API Key、Token、数据库密码等敏感凭证
Text(Multi-line String) 多行文本输入 text(name: 'DESCRIPTION', defaultValue: '', description: '构建描述信息') 提交构建备注、变更日志、配置文件片段
File 文件上传,在构建时可上传本地文件 Jenkins UI 或特定插件支持 上传配置文件、证书、测试数据包

一、核心参数类型详解

1. 基础参数类型

字符串参数 (string)
复制代码
parameters {
    string(
        name: 'VERSION',
        defaultValue: '1.0.0',
        description: '版本号 (遵循语义化版本规范)',
        trim: true
    )
}

企业级配置要点:

  • 启用 trim: true避免空格导致的错误
  • 可添加正则验证:regex: '^\\d+\\.\\d+\\.\\d+(-\\w+)?$'
  • 敏感字符串避免默认值
选择参数 (choice)
复制代码
parameters {
    choice(
        name: 'DEPLOY_TARGET',
        choices: [
            'dev-01       # 开发环境1',
            'dev-02       # 开发环境2',
            'test-01      # 测试环境',
            'staging-01   # 预发布环境',
            'prod-us      # 美国生产环境',
            'prod-eu      # 欧洲生产环境'
        ].join('\n'),
        description: '目标部署环境 (带地区标识)'
    )
}
布尔参数 (booleanParam)
复制代码
parameters {
    booleanParam(
        name: 'DRY_RUN',
        defaultValue: false,
        description: '''
        ✅ 演练模式: 执行所有步骤但不实际部署
        ⚠️  生产环境部署时建议启用'''
    )
    booleanParam(
        name: 'ROLLBACK_ENABLED',
        defaultValue: true,
        description: '失败时自动回滚'
    )
}

使用参数化构建+ansible进行发布

前后端服务器

ini 复制代码
前端
172.20.10.6
172.20.10.5
后端
172.20.10.7
172.20.10.8

在实际企业中,基本都是禁用root用户的,那么我们在部署的时候需要使用普通用户进行连接前后端服务器,所以我们在前后端服务器创建普通用户
[root@localhost ~]# useradd deploy
[root@localhost ~]# passwd deploy
Changing password for user deploy.
New password: 
BAD PASSWORD: The password is shorter than 8 characters
Retype new password: 
passwd: all authentication tokens updated successfully.
[root@localhost ~]#

首先,你需要登录到安装 Jenkins 的那台 Linux 服务器

ini 复制代码
# 1. 安装 EPEL 源 (Ansible 在 EPEL 仓库中)
[root@jenkins-slave ~]# ls /etc/yum.repos.d/
CentOS-Base.repo  docker-ce.repo  epel.repo

# 2. 安装 Ansible
[root@jenkins-slave ~]# yum install -y ansible

#3. 验证安装
[root@jenkins-slave ~]# ansible --version
ansible 2.9.27
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.5 (default, Nov 14 2023, 16:14:06) [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]

第二步:配置 SSH 免密登录 (至关重要)

Ansible 是通过 SSH 协议去控制其他服务器的。Jenkins 服务器(作为控制端)必须能免密码登录到目标服务器(被管理端)。操作位置: 在 Jenkins 服务器 上操作。

ini 复制代码
# 1. 切换到 jenkins 用户
意义:Jenkins 服务是以 jenkins 用户运行的,必须用这个用户生成密钥,否则权限会报错。
[root@jenkins-slave ~]# su - jenkins

# 2. 生成 SSH 密钥对
# 意义:生成公钥(id_rsa.pub)和私钥(id_rsa)。
# 注意:全程直接回车,不要设置密码(passphrase),否则自动化会卡住。(如果已经有了就不用在执行了)
[jenkins@jenkins-slave ~]$ ssh-keygen

# 3. 将公钥复制到目标前后端服务器(实际工作中目标端也是普通用户)
[jenkins@jenkins-slave ~]$ ssh-copy-id deploy@172.20.10.6

# 4. 测试连通性
[jenkins@jenkins-slave ~]$ ssh root@172.20.10.6 "echo ok"
ok

提示: 如果你的目标服务器有多台,需要对每一台都执行一遍 ssh-copy-id

第三步:定制ansible的配置文件。

目录规划

ini 复制代码
ansible/
├── ansible.cfg             # 核心配置文件
├── deploy.yml              # 主 Playbook (入口)
└── inventory/              # 库存目录
    ├── production/         # 【生产环境】
    │   ├── hosts.ini       # 主机清单
    │   └── group_vars/     # 生产环境专用变量
    │       ├── all.yml     # 全局变量 (如环境名、日志级别)
    │       ├── backend.yml # 后端专用变量 (jar包名、路径)
    │       └── frontend.yml# 前端专用变量 (dist包名、路径)
    └── test/               # 【测试环境】
        ├── hosts.ini       # 主机清单
        └── group_vars/     # 测试环境专用变量
            ├── all.yml     # 全局变量
            ├── backend.yml # 后端专用变量
            └── frontend.yml# 前端专用变量

1.在企业环境中,默认的 Ansible 配置往往不够用(例如默认会检查 SSH 主机指纹,导致第一次运行报错)。我们需要在项目代码库中提供一个 ansible.cfg 文件,让 Ansible 按照我们的意愿工作。

ini 复制代码
[root@jenkins-slave ~]# mkdir ansible
[root@jenkins-slave ~]# vim ansible/ansible.cfg
[defaults]
# 1. 【关键】关闭主机密钥检查,防止首次连接因指纹确认而卡死
host_key_checking = False

# 2. 设置超时时间,防止网络波动导致部署假死
timeout = 30

# 3. 禁用 cowsay(如果目标机器没装 cowsay 会报错,关闭更清爽)
nocows = 1

# 4. 设置 Inventory 的默认位置(可选,也可以在命令行动态指定)
# inventory = ./inventory/

# 5. 意义:指定 roles 的搜索路径
roles_path = ./roles

# 6. 开启 SSH 连接复用,加快批量执行速度
ssh_args = -o ControlMaster=auto -o ControlPersist=60s

[privilege_escalation]
# 允许自动提权 (sudo)
become = True
become_method = sudo
become_user = root
become_ask_pass = False

2.🛠️ 如何定义 Ansible 清单与配置

使用变量ansible/inventory/${DEPLOY_ENV}路径结构,以及企业级容器化构建的需求,设计了一套标准的 Ansible 项目结构。这套结构的核心在于:配置与代码分离,通过目录隔离环境,利用变量层级实现配置的复用与覆盖。

ini 复制代码
[root@jenkins-slave ~]# mkdir ansible/inventory/{production,staging} -p
查看目录结构
[root@jenkins-slave ~]# tree ansible/
ansible/
├── ansible.cfg
└── inventory
    ├── production #生产环境
    ├── staging    #测试/预发布环境

为了让ansible/inventory/${DEPLOY_ENV}正常工作,需要定义以下关键文件:使用 Jenkins 参数 -i 指向的具体文件。

示例:ansible/inventory/production/hosts.ini

ini 复制代码
[root@jenkins-slave ~]# cd ansible/inventory/production/
[root@jenkins-slave production]# vim hosts.ini
#定义主机组
[backend-servers]
172.20.10.7 ansible_port=22 ansible_user=deploy
172.20.10.8 ansible_port=22 ansible_user=deploy

[frontend-servers]
172.20.10.6 ansible_port=22 ansible_user=deploy
172.20.10.5 ansible_port=22 ansible_user=deploy

#定义组的层级关系 (backend_servers 和 frontend_servers 都属于生产环境)
[production:children]
backend-servers
frontend-servers

# 定义组变量 (可以直接写在 ini 里,也可以引用 vars 文件)
[production:vars]
env_name="production"
log_level="error"

3.定义变量 ansible/inventory/production/group_vars

  • 变量文件 :主要有host_varsgroup_vars两个特殊目录名。Ansible 会在 Playbook 所在目录,以及清单文件所在目录查找它们。加载逻辑如下:
    • group_vars/目录 :存放组变量 ,对所有属于该组的主机生效。
      • group_vars/all.yml:全局变量,对所有主机生效。
      • group_vars/<group_name>.yml:针对特定组的变量。
    • host_vars/目录 :存放主机变量,仅对文件名对应的单个主机生效。例如,host_vars/host1.yml文件中的变量只对名为 host1 的主机可见。

⚖️ 优先级覆盖原则 (Priority Precedence)

这是最容易踩坑的地方。Ansible 定义了严格的优先级,优先级高的变量会覆盖优先级低的同名变量

优先级 来源 说明 常见场景
最高 命令行变量 (-e) ansible-playbook ... -e "http_port=8080" Pipeline 动态传参,用于区分环境(测试/生产)。
⬆️ Host Vars host_vars/web01.yml 特定机器的特殊配置(如:特定 IP 或 硬件参数)。
⬆️ Group Vars group_vars/prod.yml 环境级配置(如:生产环境数据库地址)。
⬆️ Inventory 变量 直接写在清单文件里的变量 较老式的写法,不推荐大量使用。
⬇️ Playbook Vars Playbook 文件中 vars: 段落 任务特定的临时变量。
⬇️ Role Defaults roles/xxx/defaults/main.yml 优先级最低。通常用于提供"兜底"的默认值。

在 Playbook 中不需要显式指定group_vars。Ansible 的设计哲学是**"基于约定的自动发现"。Playbook 只需要告诉 Ansible "我要对哪组主机执行任务",Ansible 就会根据这个组名**,自动去加载对应的变量文件。

为了实现"企业级"的复用,不要把 IP 地址以外的配置写死在 ini 文件里,而是使用 group_vars

ansible/inventory/production/group_vars/backend-servers.yml (所有部署应用的服务器通用)

ini 复制代码
[root@jenkins-slave ~]# mkdir ansible/inventory/production/group_vars
[root@jenkins-slave ~]# cd ansible/inventory/production/group_vars
[root@jenkins-slave group_vars]# cat backend-servers.yml 
---
# 生产后端专用变量
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"

ansible/inventory/production/group_vars/frontend-servers.yml (所有部署应用的服务器通用)

ini 复制代码
[root@jenkins-slave group_vars]# cat frontend-servers.yml 
---
# 生产前端专用变量
dist_package: "frontend-dist-58.zip"
dist_dest_path: "/www/data"
nginx_reload_cmd: "systemctl reload nginx"
upload_path: "/data/upload"
backup_path: "/data/backup"

nexus_url: "http://172.20.10.6:8081"
nexus_repo: "app-frontend-raw"
repo_path: "com/company/frontend/frontend-dist/58"
nexus_user: "admin"
nexus_password: "admin123"

企业级 Playbook ( deploy.yml)

ini 复制代码
[root@jenkins-slave ~]# cd ansible/
[root@jenkins-slave ansible]# cat deploy.yml 
---
# ==========================================
# Play 1: 后端服务滚动更新 (带备份与回滚)
# ==========================================
- name: 滚动更新后端服务
  hosts: 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: "127.0.0.1"
        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

零停机滚动更新

  • 剧本配置serial: 1max_fail_percentage: 0
  • 效果:如果有 2 台服务器,Ansible 会先停掉第 1 台,更新并启动成功后,再更新第 2 台。如果第 1 台失败,整个流程立即终止,保证不会所有机器都挂掉。

完整的ansible目录

ini 复制代码
[root@jenkins-slave ~]# tree ansible/
ansible/
├── ansible.cfg
├── deploy.yml
└── inventory
    ├── production
    │   ├── group_vars
    │   │   ├── backend-servers.yml
    │   │   └── frontend-servers.yml
    │   └── hosts.ini
    └── staging
        ├── group_vars
        │   ├── all.yml
        │   ├── backend-test.yml
        │ 
        └── hosts.ini

将ansible目录下的所有文件上传到git仓库,方便统一管理,在实际工作中前端一个仓库后端一个仓库,我的环境也是,但是千万不要把同一个 ansible 目录分别上传到两个仓库

如果这样做,会导致严重的维护灾难:

  • 代码不一致 :你在后端仓库改了 deploy.yml,前端仓库就不知道,导致两个环境配置漂移。
  • 难以维护:你需要改一个变量(比如 Nexus 密码),得去两个仓库各改一次。

针对"两个代码仓库 + 一个部署剧本"的场景,业界标准的最佳实践是 "独立部署仓库""剧本归一化" 。那么实际工作中我们在gitlab中创建第三个仓库,专门用于保存ansible。这绝对是目前企业中非常主流且标准的 CI/CD 落地方式。

目前我搭建的这套架构(独立代码仓库 + 独立部署仓库 + 统一的 Jenkins Pipeline),在业内通常被称为 "配置与代码分离""基础设施与业务解耦" 的最佳实践。

✅ 为什么企业普遍采用这种方式?(核心优势)
  1. 权限隔离与安全性(最重要)
    • 业务开发人员:只拥有前端/后端代码仓库的权限,他们不需要也不应该接触生产环境的服务器 IP、SSH 密钥或 Nexus 密码。
    • 运维/SRE人员 :只维护 ops-ansible-playbooks 这个部署仓库。
    • 这种隔离完美契合了企业级的 RBAC(基于角色的权限控制) 要求。
  2. 单一事实来源 (Single Source of Truth)
    • 所有的部署逻辑、服务器分组、环境变量都集中在一个 Git 仓库里。如果生产环境出了配置问题,运维只需要回滚这个部署仓库的代码即可,完全不会影响业务代码。
  3. 极强的复用性
    • 当公司有 50 个微服务时,你不需要写 50 套部署脚本。只需要维护这一套 deploy.ymlJenkinsfile,通过传入不同的参数(如 APP_NAME, NEXUS_PATH),就可以复用给所有项目。
  4. GitOps 的雏形
    • 你的部署脚本也是通过 Git 管理的,这完全符合 GitOps 的理念------"一切皆代码,一切皆版本控制"。

我的环境使用的是gitee,所以我在gitee上面创建了一个仓库叫ops-ansible-playbooks,然后将ansible目录下的文件上传到仓库

ini 复制代码
[root@jenkins-slave ~]# mkdir ops-ansible-playbooks
[root@jenkins-slave ~]# cd ops-ansible-playbooks/
[root@jenkins-slave ops-ansible-playbooks]# git init 
Initialized empty Git repository in /root/ops-ansible-playbooks/.git/
[root@jenkins-slave ops-ansible-playbooks]# git remote add origin https://gitee.com/testpm/ops-ansible-playbooks.git
[root@jenkins-slave ops-ansible-playbooks]# cp -r /root/ansible/* .
[root@jenkins-slave ops-ansible-playbooks]# ls
ansible.cfg  deploy.yml  inventory
[root@jenkins-slave ops-ansible-playbooks]# git add -A 
[root@jenkins-slave ops-ansible-playbooks]# git commit -m "add 1"
[master (root-commit) bc5896d] add 1
 10 files changed, 246 insertions(+)
 create mode 100644 ansible.cfg
 create mode 100644 deploy.yml
 create mode 100644 inventory/production/group_vars/.frontend_servers.yml.swp
 create mode 100644 inventory/production/group_vars/backend-servers.yml
 create mode 100644 inventory/production/group_vars/frontend-servers.yml
 create mode 100644 inventory/production/hosts.ini
 create mode 100644 inventory/staging/group_vars/all.yml
 create mode 100644 inventory/staging/group_vars/backend-servers.yml
 create mode 100644 inventory/staging/group_vars/frontend-servers.yml
 create mode 100644 inventory/staging/hosts.ini
[root@jenkins-slave ops-ansible-playbooks]# git push -u origin master 
Username for 'https://gitee.com': xxxxxxxxx
Password for 'https://xxxxxxxxxx@gitee.com': 
Counting objects: 17, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (16/16), done.
Writing objects: 100% (17/17), 4.63 KiB | 0 bytes/s, done.
Total 17 (delta 0), reused 0 (delta 0)
remote: Powered by GITEE.COM [1.1.23]
remote: Set trace flag 094f67bd
To https://gitee.com/testpm/ops-ansible-playbooks.git
 * [new branch]      master -> master
Branch master set up to track remote branch master from origin.

配置ssh连接远程主机的凭据

在 Jenkins 界面录入凭据

虽然我们不用插件调用 Ansible,但为了安全,我们在 Jenkins 里存一下这个私钥,以便在 Pipeline 中通过 sshagent 调用。

  1. 进入 Jenkins 界面 -> 凭据 (Credentials) -> 系统 (System) -> 全局凭据 (Global credentials)
  2. 点击 添加凭据 (Add Credentials)
  3. 类型选择:SSH Username with private key
  4. ID:填写 deploy-ssh-key (记下来,代码里要用)。
  5. Username:填写目标服务器的用户名 (如 rootdeploy)。
  6. Private Key:选择 "Enter directly",把刚才生成的私钥 (cat ~/.ssh/id_rsa) 内容粘贴进去。

所有远程主机设置visudo

ini 复制代码
[root@localhost ~]# visudo
实战编写流水线

这个流水线将前后端都放到了一个,成了个超耦合的状态,在实际工作中不会这么做,如果我只想发布前端,这个时候前后端代码都会拉去打包一遍,很不符合常理,所以,正常情况下应该都是独立的流水线共用一个ansible剧本。

ini 复制代码
pipeline {
    // 全局代理:使用 Jenkins Slave 节点
    agent {
        node {
            label 'jenkins-slave'
            customWorkspace '/home/jenkins/docker-all-nexus'
        }
    }
    
    // --- 新增:参数化构建定义 ---
    parameters {
        choice(
            name: 'DEPLOY_ENV', 
            choices: ['production', 'staging'], 
            description: '选择部署环境'
        )
        booleanParam(
            name: 'DEPLOY_BACKEND', 
            defaultValue: true, 
            description: '是否部署后端服务?'
        )
        booleanParam(
            name: 'DEPLOY_FRONTEND', 
            defaultValue: true, 
            description: '是否部署前端服务?'
        )
    }

    // 全局选项
    options {
        // 在控制台输出时间戳,方便排查耗时
        timestamps()
        // 构建超时时间
        timeout(time: 1, unit: 'HOURS')
        // 保留最近 10 次构建记录
        buildDiscarder(logRotator(numToKeepStr: '10'))
        // 禁止并发构建,防止资源冲突
        disableConcurrentBuilds()
    }

    // 环境变量
    environment {
        // --- Nexus 配置 ---
        NEXUS_CREDENTIALS_ID = 'nexus-deploy-creds'
        // 注意:这里只写 IP:PORT,协议单独配置
        NEXUS_TOOL_URL = '172.20.10.6:8081'
        NEXUS_PROTOCOL = 'http'
        NEXUS_MAVEN_REPO = 'app-backend'
        NEXUS_RAW_REPO = 'app-frontend-raw'

        // --- Git 配置 ---
        FRONTEND_GIT_URL = 'git@gitee.com:testpm/frontend-docker.git'
        BACKEND_GIT_URL  = 'git@gitee.com:testpm/backend-docker.git'
        // 新增:定义 Ansible 剧本仓库地址 (第三个仓库)
        DEPLOY_GIT_URL = 'git@gitee.com:testpm/ops-ansible-playbooks.git'
        
        GIT_CREDENTIALS_ID = 'jenkins-slave-git'

        // --- 版本控制 ---
        // 使用构建号作为版本后缀,确保唯一性
        BUILD_VERSION = "${BUILD_NUMBER}"
        
        // --- 镜像配置 ---
        NODE_IMAGE = 'node:20'
        // 使用具体的 Maven 版本
        MAVEN_IMAGE = 'maven:3.9.9-eclipse-temurin-21'

        // 明确定义产物名称,避免路径拼接错误
        BACKEND_JAR_GLOB = 'backend/target/*.jar'
    }

    stages {

        // ================== 阶段 1: 代码拉取 (手动控制目录) ==================
        stage('📥 代码拉取') {
            steps {
                script {
                    echo "🚀 开始拉取代码..."

                    // 1. 拉取后端代码到 backend 目录
                    dir('backend') {
                        // 确保目录干净
                        deleteDir()
                        echo "正在拉取后端代码..."
                        git branch: 'master',
                            credentialsId: "${GIT_CREDENTIALS_ID}",
                            url: "${BACKEND_GIT_URL}"
                    }

                    // 2. 拉取前端代码到 frontend 目录
                    dir('frontend') {
                        deleteDir()
                        echo "正在拉取前端代码..."
                        git branch: 'master',
                            credentialsId: "${GIT_CREDENTIALS_ID}",
                            url: "${FRONTEND_GIT_URL}"
                    }
                }
            }
        }

        // ================== 阶段 2: 后端构建 (Maven) ==================
        stage('🔨 后端构建') {
            steps {
                script {
                    // 针对 Maven 的网络波动,单独加重试
                    retry(3) {
                    // 使用 inside 语法,强制指定工作目录
                    docker.image("${MAVEN_IMAGE}").inside("-u root:root -w ${WORKSPACE} -v /data/maven-cache:/root/.m2 --rm --entrypoint=''") {
                        // 进入 backend 目录执行构建
                        dir('backend') {
                                sh '''
                                    echo "🚀 当前容器内路径: $(pwd)"
                                    echo "🚀 开始 Maven 编译..."
                                    mvn clean package -DskipTests -B -U
                                '''
                            }
                        }
                    }
                
                // 验证产物
                def jars = findFiles(glob: "${BACKEND_JAR_GLOB}")
                if (!jars) error "❌ 后端构建失败:未找到 Jar 包"
                
                // 构建完成后,将产物存入暂存区 (加入构建号防止冲突)
                stash name: "backend-artifact-${BUILD_NUMBER}", includes: 'backend/target/*.jar, backend/pom.xml'
            }
        }
    }

        // ================== 阶段 3: 前端构建 (Node.js) ==================
        stage('🎨 前端构建') {
            steps {
                script {
                    // 使用 inside 语法,强制指定工作目录
                    docker.image("${NODE_IMAGE}").inside("-u root:root -w ${WORKSPACE} -v /data/npm-cache:/root/.npm --rm --entrypoint=''") {
                        dir('frontend') {
                            // 1. 下载依赖(网络不稳定,单独重试)
                            retry(3) {
                                sh '''
                                    echo "📦 安装依赖..."
                                    npm config set registry https://registry.npmmirror.com
                                    export NODE_OPTIONS=--max_old_space_size=1536
                                    npm install --legacy-peer-deps
                                '''
                            }
                            
                            // 2. 编译
                            sh '''
                                echo "🚀 开始编译..."
                                npm run build
                            '''
                            
                            // 3. 打包
                            sh '''
                                echo "📦 正在压缩前端产物..."
                                # 更新源并安装 zip
                                apt-get update || echo "警告: apt-get update 失败,尝试继续..."
                                apt-get install -y zip || echo "警告: zip 安装失败"
                                
                                # 打包
                                zip -r frontend-dist-${BUILD_VERSION}.zip dist/
                            '''
                        }
                    }
                }
                
                // 构建完成后,将产物存入暂存区
                stash name: "frontend-artifact-${BUILD_VERSION}", includes: "frontend/frontend-dist-${BUILD_VERSION}.zip"
            }
        }

        // ================== 阶段 4: 上传到 Nexus ==================
        stage('☁️ 上传制品到 Nexus') {
            // 回到主节点执行上传
            agent {
                node {
                    label 'jenkins-slave'
                }
            }
            steps {
                script {
                    echo "🚀 开始上传制品..."

                    // --- 1. 处理并上传后端 ---
                    echo "📦 上传后端 Jar..."
                    // 恢复后端制品 (注意名字要对应)
                    unstash "backend-artifact-${BUILD_NUMBER}"

                    // 读取 pom.xml 获取 GroupId 和 ArtifactId
                    def pom = readMavenPom file: 'backend/pom.xml'
                    // 找到生成的 jar 文件
                    def jarFile = findFiles(glob: 'backend/target/*.jar')[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'
                            ]
                        ]
                    )

                    // --- 2. 处理并上传前端 ---
                    echo "📦 上传前端 Zip..."
                    // 恢复前端制品
                    unstash "frontend-artifact-${BUILD_VERSION}"

                    def zipFile = findFiles(glob: "frontend/frontend-dist-${BUILD_VERSION}.zip")[0]

                    nexusArtifactUploader(
                        nexusVersion: 'nexus3',
                        protocol: "${NEXUS_PROTOCOL}",
                        nexusUrl: "${NEXUS_TOOL_URL}",
                        // 前端通常作为 Raw 类型上传,这里手动指定 GroupId
                        groupId: 'com.company.frontend',
                        version: "${BUILD_VERSION}",
                        repository: "${NEXUS_RAW_REPO}",
                        credentialsId: "${NEXUS_CREDENTIALS_ID}",
                        artifacts: [
                            [
                                artifactId: 'frontend-dist',
                                classifier: '',
                                file: "${zipFile.path}",
                                type: 'zip'
                            ]
                        ]
                    )
                }
            }
        }

        // ================== 阶段 5: 获取 Ansible 剧本 (第三个仓库) ==================
        stage('📂 获取 Ansible 剧本') {
            steps {
                script {
                    echo "🚀 开始拉取 Ansible 运维剧本..."
                    
                    // 创建专门的目录存放剧本
                    dir('ansible-playbooks') {
                        deleteDir() // 确保干净
                        git branch: 'master', // 请根据实际情况修改分支名
                            credentialsId: "${GIT_CREDENTIALS_ID}",
                            url: "${DEPLOY_GIT_URL}"
                    }
                    
                    // 验证剧本拉取成功
                    def ymlFiles = findFiles(glob: 'ansible-playbooks/*.yml')
                    if (!ymlFiles) {
                        error "❌ 致命错误:未在 ansible-playbooks 目录下找到剧本文件,请检查仓库地址或分支配置。"
                    }
                }
            }
        }

        // ================== 阶段 6: 执行部署 ==================
        stage('🚀 执行 Ansible 部署') {
            steps {
                script {
                    echo "🚀 开始执行部署任务..."
                    echo "🎯 部署环境: ${params.DEPLOY_ENV}"
                    
                    // 进入剧本目录
                    dir('ansible-playbooks') {
                        // 动态构建 Ansible 命令
                        def ansibleCmd = """
                            ansible-playbook \
                            -i inventory/${params.DEPLOY_ENV}/hosts.ini deploy.yml -vvv
                        """
                        
                        echo "执行命令: ${ansibleCmd}"
                        
                        // 👇 使用 sshagent 插件,注入你定义好的 SSH 凭据
                        sshagent (credentials: ['deploy-ssh-key']) {
                            sh """
                                ${ansibleCmd}
                            """
                       }
                   }
               }
           }
       }
    }

    // 后置处理
    post {
        success {
            echo "✅ 构建及上传成功!"
        }
        failure {
            echo "❌ 构建失败,请检查日志。"
        }
        always {
            // 清理工作空间,防止磁盘爆满
            cleanWs()
        }
    }
}

未完待续

相关推荐
亚空间仓鼠14 小时前
Docker容器化高可用架构部署方案(十三)
docker·容器·架构
米高梅狮子15 小时前
01.mysql的备份与恢复
运维·数据库·mysql·docker·容器·kubernetes·github
console.log('npc')15 小时前
Windows 11 → WSL2 → Ubuntu → Docker → Codex → Sub2API
windows·ubuntu·docker
认真的薛薛16 小时前
Linux运维:Jenkins部署
linux·运维·jenkins
终端行者16 小时前
jenkins Pipeline 企业级别的流水线构建 Docker 负责打包 最后上传到Nexus
ci/cd·docker·jenkins
淼淼爱喝水16 小时前
【Ansible 入门实战】三种变量详解
java·linux·数据库·ansible·playbook
酷道17 小时前
CentOS 7 安装 Docker
linux·docker·centos
最后一个bug17 小时前
ubuntu24.04在docker下迁移gitlab16
linux·运维·docker