基于基础语法,补充企业实战中常用的进阶特性,包括参数化构建、条件判断、多环境切换,无需编写脚本式逻辑,纯声明式配置即可实现复杂场景。
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_vars和group_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: 1和max_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),在业内通常被称为 "配置与代码分离" 或 "基础设施与业务解耦" 的最佳实践。
✅ 为什么企业普遍采用这种方式?(核心优势)
- 权限隔离与安全性(最重要)
- 业务开发人员:只拥有前端/后端代码仓库的权限,他们不需要也不应该接触生产环境的服务器 IP、SSH 密钥或 Nexus 密码。
- 运维/SRE人员 :只维护
ops-ansible-playbooks这个部署仓库。 - 这种隔离完美契合了企业级的 RBAC(基于角色的权限控制) 要求。
- 单一事实来源 (Single Source of Truth)
- 所有的部署逻辑、服务器分组、环境变量都集中在一个 Git 仓库里。如果生产环境出了配置问题,运维只需要回滚这个部署仓库的代码即可,完全不会影响业务代码。
- 极强的复用性
- 当公司有 50 个微服务时,你不需要写 50 套部署脚本。只需要维护这一套
deploy.yml和Jenkinsfile,通过传入不同的参数(如APP_NAME,NEXUS_PATH),就可以复用给所有项目。
- 当公司有 50 个微服务时,你不需要写 50 套部署脚本。只需要维护这一套
- 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 调用。
- 进入 Jenkins 界面 -> 凭据 (Credentials) -> 系统 (System) -> 全局凭据 (Global credentials)。
- 点击 添加凭据 (Add Credentials)。
- 类型选择:SSH Username with private key。
- ID:填写
deploy-ssh-key(记下来,代码里要用)。 - Username:填写目标服务器的用户名 (如
root或deploy)。 - 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()
}
}
}
未完待续