项目背景:
各个环境Nginx配置变更不透明,团队内部和其他团队之间协作方式不够高效,因为采用项目迭代的方式进行配置代码化变更。
目录结构:
├──环境
│ ├──开发
│ │ ├──发展管理-mcrio.icbc.trade
│││└──dev-connex-mgt-MC Rio . conf
││dev-ops-am.icbc.com├──
│ │ │ └──开发-网站-ops.conf
││dev-web.icbc.com├──
│ │ │ └── Dev-website-client.conf
│ │ ├──管理发展工银贸易
│ │ │ └── Dev-mgt-mcrio.conf
││summit-dev.icbc.com└──
│ │ └──发展-网站-峰会. conf
│ ├──胖
││fat-mgt-mcrio.icbc.com├──
│ │ │ └── Fat-mgt-mcrio.conf
││fat-web.icbc.com├──
│ │ │ └──胖胖-网站-客户端. conf
││summit-fat.icbc.com└──
│ │ └──胖-网站-summit.conf
│ ├──镜报
│ ├──
│ │ ├── qa-mgt-mcrio.icbc.trade
│ │ │ └── QA-connex-mgt-mcrio.conf
││qa-mgt-mcrio.icbc.com├──
│ │ │ └── QA-mgt-mcrio.conf
││qa-ops-am.icbc.com├──
│ │ │ └── Qa-website-ops.conf
│ │ ├── qa-web.icbc.trade
│ │ │ └──连接-QA-网站-客户端. conf
││qa-web.icbc.com├──
│ │ │ └── Qa-website-client.conf
││qa-web2.icbc.com├──
│ │ │ └── Qa2-website-client.conf
││summit-qa.icbc.com└──
│ │ └── Qa-website-summit.conf
│ ├──坐
│ │ ├── sit-mgt-mcrio.icbc.trade
│││└──sit-connex-mgt-MC Rio . conf
││sit-mgt-mcrio.icbc.com├──
│ │ │ └── Sit-mgt-mcrio.conf
│ │ ├── sit-web.icbc.trade
│││└──connex-sit-website-client . conf
││sit-web.icbc.com├──
│ │ │ └── Sit-website-client.conf
││summit-sit.icbc.com└──
│ │ └── Sit-website-summit.conf
│ └──大学
│summit-uat.icbc.com├──
│ │ └── Uat-website-summit.conf
│ ├── uat-mgt-mcrio.icbc.trade
│ │ └── Uat-connex-mcrio.conf
│uat-mgt-mcrio.icbc.com├──
│ │ └── Uat-mgt-mcrio.conf
│uat-ops-am.icbc.com├──
│ │ └── Uat-website-ops.conf
│ ├── uat-web.icbc.trade
││└──connex-UAT-website-client . conf
│uat.icbc.com└──
│└──UAT-网站-客户端. conf
├──库存. ini
└──自述. md
Github工作流程:
名称:部署Nginx
开启:
推送:
分支:[关]
路径:
-"环境/**"
环境:
PKG _目录:/data/pkg/devops-nginx-config
COMMIT_ID: ${{ github.sha }}
工作:
部署:
运行:[自托管,ltp]
步骤:
-用途:actions/checkout@v3
使用:
提取深度:0
- name:仅部署已更改的文件
运行:|
CHANGED _ FILES = (git diff-diff-filter = AM-name-only { { github . event . before } } { { github . sha } }--' environments/' \| grep ' \\。conf' || true)
回显"更改的文件:"
回显" $CHANGED_FILES "
if[-z " $ CHANGED _ FILES "];然后
回声"不。配置文件已更改。没有要部署的内容。
出口0
船方不负担装货费用
部署失败=假
SERVERS = (echo " CHANGED _ FILES " | awk-F '/' ' { print 2 "/ 3 } ' | sort-u)
对于$SERVERS中的目标;做
ENV = (echo " TARGET " | cut-d '/'-f1)
SERVER = (echo " TARGET " | cut-d '/'-F2)
host = (grep-a1 " \^\\\[env:$server\)" inventory . ini | tail-1 | tr-d '[:space:]')
回显""
echo " = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = "
回声"🚀正在处理:$SERVER "
echo " ENV: $ENV "
回显"主机:$HOST"
echo " = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = "
if[-z " $ HOST "];然后
echo "❌错误:在inventory.ini中找不到[ENV:SERVER]的主机"
回显"跳过此服务器..."
继续
船方不负担装货费用
server _ files = (echo " changed _ files " | grep " ^environments/env/server/ " | | true)
if[-z " $ SERVER _ FILES "];然后
回显"⚠️没有要为server部署的文件,跳过..."
继续
船方不负担装货费用
STAGING _ DIR = "/tmp/nginx-STAGING-$ { { env。COMMIT_ID }} "
BACKUP _ DIR = "/data/pkg/devo PS-nginx-config/ ENV/ SERVER/$ { { ENV。COMMIT_ID }} "
ssh root @ HOST " mkdir-p STAGING _ DIR $ BACKUP _ DIR "
#第一步:上传到临时目录
回声"📤正在上传到暂存中..."
对于$SERVER_FILES中的文件;做
文件名=(基本名称" 文件")
scp " FILE " root @ HOST: STAGING _ DIR/ FILENAME
回显"↳$文件名"
完成的
#第二步:备份当前配置并验证
回声"📦正在备份当前配置..."
BACKUP_OK=true
对于$SERVER_FILES中的文件;做
文件名=(基本名称" 文件")
if[[" $ FILENAME " = = " nginx . conf "]];然后
DEST="/etc/nginx/nginx.conf "
其他
DEST = "/etc/nginx/conf . d/$ FILENAME "
船方不负担装货费用
记录文件是否为新增(服务器上不存在)
如果ssh root @ HOST"test-f DEST";然后
如果!ssh root @ HOST " CP DEST 备份目录/文件名";然后
回显"❌备份失败:$FILENAME"
BACKUP_OK=false
其他
echo"↳备份:$FILENAME"
船方不负担装货费用
其他
新文件,写标记以便回滚时知道要删除
ssh root@HOST "echo '新文件' \> BACKUP_DIR/$FILENAME.new_flag "
echo"↳新文件(不需要备份):$FILENAME"
船方不负担装货费用
完成的
备份失败则跳过该服务器
if[" $ BACKUP _ OK " = false];然后
回显"❌备份失败,跳过server以避免数据丢失"
ssh root @ HOST " RM-RF STAGING _ DIR "
部署失败=真
继续
船方不负担装货费用
#第三步:替换为新配置
回声"📤正在部署新配置..."
对于$SERVER_FILES中的文件;做
文件名=(基本名称" 文件")
if[[" $ FILENAME " = = " nginx . conf "]];然后
ssh root @ HOST " CP STAGING _ DIR/$ FILENAME/etc/nginx/nginx . conf "
其他
ssh root @ HOST " CP STAGING _ DIR/ FILENAME/etc/nginx/conf . d/ FILENAME "
船方不负担装货费用
回显"↳$文件名"
完成的
#第四步:检测语法
回声"🔍正在测试nginx配置..."
如果ssh root @ $ HOST " nginx-t " 2 > & 1;然后
回显"✅语法检查通过"
ssh root@$HOST "nginx -s reload "
回显"✅ Nginx重装成功"
其他
echo"❌语法检查失败!正在回滚..."
回滚失败=假
对于$SERVER_FILES中的文件;做
文件名=(基本名称" 文件")
if[[" $ FILENAME " = = " nginx . conf "]];然后
DEST="/etc/nginx/nginx.conf "
其他
DEST = "/etc/nginx/conf . d/$ FILENAME "
船方不负担装货费用
有备份则恢复,新增文件则删除
如果ssh root @ HOST " test-f BACKUP _ DIR/$ filename . new _ flag ";然后
ssh root @ host " RM-f dest " & & echo "↳已删除(新文件):$ filename " | | roll back _ failed = true
elif ssh root @ HOST " test-f BACKUP _ DIR/$ FILENAME ";然后
ssh root @ host " CP backup _ dir/ filename dest " & & echo "↳恢复:$ filename " | | roll back _ failed = true
船方不负担装货费用
完成的
验证回滚后一款反向代理网页服务器配置是否正常
如果ssh root @ $ HOST " nginx-t " 2 > & 1;然后
回显"✅回滚完成,nginx配置有效"
其他
echo"⚠️警告:回滚已完成,但nginx -t仍然失败!"
回显"备份目录保留在:$BACKUP_DIR"
回显"请手动检查服务器$HOST!"
船方不负担装货费用
if[" $ roll back _ FAILED " = true];然后
echo "⚠️警告:一些文件无法在$HOST上回滚!"
回显"备份目录:$BACKUP_DIR"
船方不负担装货费用
ssh root @ HOST " RM-RF STAGING _ DIR "
部署失败=真
继续
船方不负担装货费用
清理临时目录
ssh root @ HOST " RM-RF STAGING _ DIR "
清理旧备份,保留最近10次
ssh root@HOST "ls -dt {{ env。PKG _目录} }/ ENV/ SERVER/*/2 >/dev/null | tail-n+11 | xargs RM-RF 2 >/dev/null | | true "
回显"完成:$SERVER"
完成的
if[" $ DEPLOY _ FAILED " = true];然后
回声"❌一些服务器部署失败,检查上面的日志。"
1号出口
船方不负担装货费用
詹金斯·皮普林斯:
管道{
任何代理
环境{
ANSIBLE_KEY = '/data/runner.key '
INVENTORY _ FILE = " $ { WORKSPACE }/INVENTORY . ini "
GIT _ REPO _ URL = " GIT @ github . com:ICBC/$ { JOB _ BASE _ NAME }。饭桶"
}
参数{
选择(
名称:'部署环境',
选择:['dev ',' fat ',' sit ',' qa ',' uat ',' mirror'],
'描述: '选择要部署的环境'
)
选择(
名称:'部署模式',
选项:['仅更改','完全同步'],
'描述: '部署模式:仅已更改=仅部署有变更的文件(默认),full_sync=全量同步'
)
字符串(
名称:'基本提交',
默认值: '',
描述:'[仅仅已更改\模式有效]对比基准提交(留空则自动使用HEAD ~ 1)'
)
}
阶段{
阶段('拉最新'){
步骤{
结帐([
$class: 'GitSCM ',
分支机构:[[name:'非盈利']],
扩展:[
$class: 'CleanCheckout'\], \[$class: 'CloneOption ',深度:0,浅度:false
],
用户远程配置:[[
url: env。GIT报告URL
]]
])
脚本{
环境。GIT_AUTHOR = sh(脚本:' git log -1 - pretty=format:%an ',returnStdout: true)。修剪()
环境。GIT_MSG = sh(脚本:' git log -1 - pretty=format:%s ',returnStdout: true)。修剪()
环境。GIT_COMMIT = sh(脚本:' git rev-parse HEAD ',returnStdout: true)。修剪()
currentBuild.description = "环境:{params。部署环境} \|模式:{params。部署模式} |分支:nonprod | {env。GIT_AUTHOR}: {env。GIT_MSG} "
}
}
}
stage('检测文件'){
步骤{
脚本{
def changedFiles = ' '
if (params。DEPLOY_MODE == 'changed_only') {
def baseCommit = params。BASE_COMMIT?。修剪()
如果(!baseCommit) {
baseCommit = sh(脚本:' git rev-parse HEAD~1 ',returnStdout: true)。修剪()
}
def headCommit = sh(脚本:' git rev-parse HEAD ',returnStdout: true)。修剪()
回声"📌 对比范围:{baseCommit} → {headCommit} "
changedFiles = sh(
脚本:""
git diff-diff-filter = AM-name-only { base commit } { head commit } \
- "environments/${params。部署环境}//\
| grep -i '\\。conf\$' || true
""",
returns out:true
).修剪()
如果(!changedFiles) {
回声"ℹ️ [{params。DEPLOY_ENV}\]在{baseCommit.take(8)}..${headCommit.take(8)}范围内没有变更的。会议文件。"
回声"💡 如需部署全部文件,请选择完全同步模式;或在基本提交中指定更早的基准提交,"
currentBuild.result = ' NOT _ BUILT '
返回
}
}否则{
changedFiles = sh(
脚本:" find environments/${params。DEPLOY_ENV}/ -name '*。conf"| sort | | true",
returns out:true
).修剪()
}
如果(!changedFiles) {
error("❌环境[{params。DEPLOY_ENV}\]下未找到任何。会议文件,请检查environments/{params。部署_环境}/目录。")
}
回声"📄 待部署文件列表:\n${changedFiles} "
def servers = changed files . split(' \ n ')。收集{ line-->
line.trim()。拆分('/')[2]
}.唯一()
回声"🖥️涉及服务器:${servers.join(',')} "
环境。CHANGED_FILES = changedFiles
环境。TARGET_SERVERS = servers.join(',')
}
}
}
登台('部署'){
何时{
表达式{ env。已更改_文件?。trim() }
}
步骤{
脚本{
def文件= env。CHANGED_FILES.split('\n ')
定义服务器=环境。TARGET_SERVERS.split(',')
servers . each { server-->
def serverIP = sh(
脚本:""
awk '/^\\[{params.DEPLOY _ ENV }: { server } \ \]/{ found = 1;下一个} \
发现& & /^[0-9]/{print;退出} \
找到& & /^\\[/{exit}' $ { inventory _ file }
""",
returns out:true
).修剪()
如果(!serverIP) {
error("❌库存. ini中未找到[{params。DEPLOY_ENV}:{server}],请检查inventory.ini,")
}
回声"🚀 开始部署到:{server} ({serverIP})"
def server files = files . find all { it . contains("/$ { server }/)}
server files . each { conf file-->
def fileName = confFile.split('/')。最后一个()
def remote path = "/etc/nginx/conf . d/$ { fileName } "
def backup path = "/etc/nginx/conf . d/$ { fileName }。bak "
嘘"""
ansible all -i '${serverIP},' \
-u根私钥${ ansi ble _ KEY } \
-m shell \
-a ' test-f { remote path } \& \& CP { remote path } $ { backup path } | | true '
"""
嘘"""
ansible all -i '${serverIP},' \
-u根私钥${ ansi ble _ KEY } \
-m副本\
-a ' src = { WORKSPACE }/ { conf file } dest = $ { remote path } mode = 0644 '
"""
}
def testResult = sh(
脚本:""
ansible all -i '${serverIP},' \
-u根私钥${ ansi ble _ KEY } \
-m shell \
-a 'nginx -t 2>&1 '
""",
returnStatus: true
)
if (testResult!= 0) {
回声"❌ nginx -t校验失败,回滚{server}({serverIP})上的配置..."
server files . each { conf file-->
def fileName = confFile.split('/')。最后一个()
def remote path = "/etc/nginx/conf . d/$ { fileName } "
def backup path = "/etc/nginx/conf . d/$ { fileName }。bak "
嘘"""
ansible all -i '${serverIP},' \
-u根私钥${ ansi ble _ KEY } \
-m shell \
-a ' test-f { backup path } \& \& mv { backup path } { remote path } \| \| RM-f { remote path } '
"""
}
error("❌ ${server} nginx配置校验失败,已回滚,请检查配置文件。")
}
嘘"""
ansible all -i '${serverIP},' \
-u根私钥${ ansi ble _ KEY } \
-m shell \
-一个"nginx -s重新加载"
"""
echo "✅ {server} ({serverIP})部署完成"
}
}
}
}
}
帖子{
成功{
wrap([$class: 'BuildUser']) {
脚本{
嘘"""
python3 /data/notify_lark.py \
" {env。JOB _ NAME }" { env .BUILD _ NUMBER }"$ { env .BUILD_URL}" \
" {env。GIT_AUTHOR}" "{env。GIT_COMMIT}" \
" {env。GIT_MSG}" "{env。BUILD _ USER }"$ { params。DEPLOY_ENV}" "nonprod " "成功"
"""
}
}
}
失败{
wrap([$class: 'BuildUser']) {
脚本{
嘘"""
python3 /data/notify_lark.py \
" {env。JOB _ NAME }" { env .BUILD _ NUMBER }"$ { env .BUILD_URL}" \
" {env。GIT _作者?:' unknown'}" "{env。GIT_COMMIT?:'未知' }" \
" {env。GIT_MSG?:' unknown'}" "{env。BUILD_USER?:' unknown'}" "${params。DEPLOY_ENV}" "nonprod " "失败"
"""
}
}
}
未建造{
回声"⏭️ 无需部署(没有变更的文件)"
}
总是{
脚本{
echo " = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = "
echo "Nginx配置部署完成"
回声"环境:${params。部署环境} "
回声"模式:${params。部署模式} "
回声"状态:${currentBuild.currentResult} "
回声"分支:非盈利"
echo " = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = "
}
}
}
}


目录结构:
## 快速开始
### 1. 修改配置文件
```bash
vim environments/dev/dev-web.icbc.com/Dev-website-client.conf
```
### 2. 提交并推送
```bash
git add .
git commit -m "update dev-web nginx config"
git push origin nonprod
```
### 3. 自动部署
推送后 GitHub Actions 自动执行:
1. 检测变更的 `.conf` 文件
2. 从 `inventory.ini` 读取目标主机 IP
3. 上传配置到临时目录
4. 备份当前配置
5. 应用新配置并执行 `nginx -t`
6. 测试通过后 `nginx -s reload`
7. 测试失败自动回滚
## 主机映射 (inventory.ini)
`inventory.ini` 定义了每个域名目录对应的服务器 IP 地址。CI/CD 流程通过此文件确定配置文件应该部署到哪台服务器。
### 格式说明
```ini
[环境:域名目录]
服务器IP
```
- **环境**: 必须与 `environments/` 下的目录名一致 (dev/fat/sit/qa/uat)
- **域名目录**: 必须与 `environments/{env}/` 下的目录名完全一致
- **服务器IP**: 目标 Nginx 服务器的 IP 地址,下一行紧跟 `[env:domain]`
### 示例
```ini
# ========== DEV ==========
[dev:mgt-dev.icbc.com]
10.16.137.5
[dev:dev-mgt-mcrio.icbc.com]
10.16.137.5
[dev:dev-web.icbc.com]
10.16.76.31
[dev:dev-ops-am.icbc.com]
10.16.76.31
[dev:summit-dev.icbc.com]
10.16.76.31
# ========== QA ==========
[qa:qa-mgt-mcrio.icbc.com]
10.16.60.103
[qa:qa-web.icbc.com]
10.16.76.31
```
### 添加新域名
1. 创建目录: `environments/{env}/{domain}/`
2. 添加配置文件: `{Name}.conf`
3. **在 `inventory.ini` 中添加映射** (必须):
```ini
[{env}:{domain}]
{server_ip}
```
> 如果 `inventory.ini` 中缺少对应配置,部署时会报错 `No host found for [env:domain]` 并跳过该域名。
## 回滚操作
在 GitHub Actions 页面手动触发 `Rollback Nginx Config` 工作流:
| 参数 | 说明 | 示例 |
|------|------|------|
| env | 环境名称 | dev / fat / sit / qa / uat |
| domain | 域名目录 | dev-web.icbc.com |
| commit_id | 回滚到的版本 (可选) | 留空使用最近备份 |
备份存储位置: `/data/pkg/devops-nginx-config/{env}/{domain}/{commit_id}/`
## 配置文件部署规则
| 文件名 | 部署位置 |
|--------|----------|
| `nginx.conf` | `/etc/nginx/nginx.conf` |
| `*.conf` (其他) | `/etc/nginx/conf.d/` |
## 注意事项
- 只有 `.conf` 文件的变更会触发部署
- 删除的文件不会触发部署操作
- 每次部署前会自动备份当前配置
- `nginx -t` 失败会自动回滚到部署前状态


Github actions:

詹金斯·皮普林斯: