Jenkins CI/CD流水线从零搭建:代码提交到自动部署全流程
以前每次上线都是:打包→上传→部署→测试,一套流程下来半小时。现在代码一推,自动构建、自动测试、自动部署,喝杯咖啡的功夫就上线了。
一、为什么要搞CI/CD?
先说说我们之前的"人肉部署"流程:
- 开发写完代码,提交Git
- 运维拉代码到本地
- mvn clean package 打包
- scp上传到服务器
- 停服务、备份、替换jar包
- 启动服务、查看日志
- 测试人员验证
问题:
- 耗时长:一次部署30分钟起步
- 容易出错:手抖删错文件、忘记备份
- 不可追溯:谁什么时候部署的?部署了什么版本?
- 效率低下:一天最多部署3-4次
搞CI/CD之后:
- 代码push后自动触发
- 构建、测试、部署全自动
- 每次部署有记录可查
- 出问题一键回滚
二、整体架构
开发者 → GitLab → Jenkins → Docker镜像 → 目标服务器
│ │ │ │
│ webhook触发 构建+测试 推送到Harbor
│ │
│ 部署到K8s/Docker
│ │
└───────── 钉钉/企微通知 ←───────┘
组件说明:
- GitLab:代码仓库(也可以用GitHub、Gitee)
- Jenkins:CI/CD引擎
- Harbor:Docker镜像仓库
- 目标环境:K8s集群或Docker服务器
三、Jenkins安装部署
3.1 Docker方式安装(推荐)
# 创建数据目录
mkdir -p /data/jenkins
chown -R 1000:1000 /data/jenkins
# 启动Jenkins
docker run -d \
--name jenkins \
--restart=always \
-p 8080:8080 \
-p 50000:50000 \
-v /data/jenkins:/var/jenkins_home \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /usr/bin/docker:/usr/bin/docker \
jenkins/jenkins:lts
# 查看初始密码
docker logs jenkins 2>&1 | grep -A 5 "initial"
# 或者
cat /data/jenkins/secrets/initialAdminPassword
3.2 访问配置
- 浏览器打开
http://你的IP:8080 - 输入初始密码
- 选择"安装推荐的插件"
- 创建管理员账号
3.3 必装插件
进入 Manage Jenkins → Plugins → Available plugins:
必装:
- Git
- Pipeline
- Docker Pipeline
- SSH Agent
- Publish Over SSH
- GitLab / GitHub Integration
- Blue Ocean(可视化界面)
- DingTalk(钉钉通知)
推荐:
- Credentials Binding
- Build Timeout
- Timestamper
- AnsiColor(彩色日志)
四、配置凭据
4.1 Git凭据
Manage Jenkins → Credentials → System → Global credentials
添加GitLab/GitHub的SSH私钥或用户名密码:
Kind: SSH Username with private key
ID: gitlab-ssh
Username: git
Private Key: (粘贴私钥内容)
4.2 服务器SSH凭据
Kind: SSH Username with private key
ID: deploy-server
Username: root
Private Key: (部署服务器的私钥)
4.3 Harbor凭据
Kind: Username with password
ID: harbor-auth
Username: admin
Password: Harbor密码
五、第一个Pipeline
5.1 创建Pipeline项目
New Item → Pipeline → 输入项目名
5.2 简单的Jenkinsfile
在项目根目录创建 Jenkinsfile:
pipeline {
agent any
environment {
APP_NAME = 'my-app'
GIT_REPO = 'git@gitlab.example.com:team/my-app.git'
}
stages {
stage('拉取代码') {
steps {
git branch: 'main',
credentialsId: 'gitlab-ssh',
url: "${GIT_REPO}"
}
}
stage('编译构建') {
steps {
sh 'mvn clean package -DskipTests'
}
}
stage('单元测试') {
steps {
sh 'mvn test'
}
post {
always {
junit '**/target/surefire-reports/*.xml'
}
}
}
stage('部署') {
steps {
sshPublisher(
publishers: [
sshPublisherDesc(
configName: 'deploy-server',
transfers: [
sshTransfer(
sourceFiles: 'target/*.jar',
remoteDirectory: '/opt/app',
execCommand: '''
cd /opt/app
./restart.sh
'''
)
]
)
]
)
}
}
}
post {
success {
echo '部署成功!'
}
failure {
echo '部署失败!'
}
}
}
5.3 配置GitLab Webhook
让代码push后自动触发构建:
- Jenkins项目 → Configure → Build Triggers
- 勾选 "Build when a change is pushed to GitLab"
- 复制Webhook URL
在GitLab项目设置:
- Settings → Webhooks
- URL填Jenkins的Webhook地址
- Trigger选择Push events
六、完整的Docker化部署Pipeline
这是我实际在用的生产级Pipeline:
6.1 项目结构
my-app/
├── src/
├── Dockerfile
├── Jenkinsfile
├── deploy/
│ ├── docker-compose.yml
│ └── k8s/
│ ├── deployment.yaml
│ └── service.yaml
└── pom.xml
6.2 Dockerfile
FROM openjdk:11-jre-slim
WORKDIR /app
COPY target/*.jar app.jar
ENV JAVA_OPTS="-Xms512m -Xmx512m"
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
6.3 完整Jenkinsfile
pipeline {
agent any
environment {
// 基础配置
APP_NAME = 'my-app'
GIT_REPO = 'git@gitlab.example.com:team/my-app.git'
// Docker配置
DOCKER_REGISTRY = 'harbor.example.com'
DOCKER_IMAGE = "${DOCKER_REGISTRY}/myteam/${APP_NAME}"
// 部署配置
DEPLOY_HOST = '192.168.1.100'
DEPLOY_PATH = '/opt/apps/${APP_NAME}'
}
options {
// 构建超时时间
timeout(time: 30, unit: 'MINUTES')
// 保留构建记录
buildDiscarder(logRotator(numToKeepStr: '20'))
// 时间戳
timestamps()
}
stages {
stage('检出代码') {
steps {
checkout([
$class: 'GitSCM',
branches: [[name: '*/main']],
userRemoteConfigs: [[
url: "${GIT_REPO}",
credentialsId: 'gitlab-ssh'
]]
])
script {
// 获取commit信息
env.GIT_COMMIT_SHORT = sh(
script: 'git rev-parse --short HEAD',
returnStdout: true
).trim()
env.GIT_COMMIT_MSG = sh(
script: 'git log -1 --pretty=%B',
returnStdout: true
).trim()
// 镜像tag:时间戳+commit
env.IMAGE_TAG = sh(
script: 'date +%Y%m%d%H%M%S',
returnStdout: true
).trim() + "-${env.GIT_COMMIT_SHORT}"
}
echo "构建版本: ${env.IMAGE_TAG}"
echo "提交信息: ${env.GIT_COMMIT_MSG}"
}
}
stage('编译构建') {
steps {
sh '''
mvn clean package -DskipTests -U
'''
}
}
stage('单元测试') {
steps {
sh 'mvn test'
}
post {
always {
junit allowEmptyResults: true,
testResults: '**/target/surefire-reports/*.xml'
}
}
}
stage('代码扫描') {
steps {
// SonarQube代码质量扫描(可选)
sh '''
mvn sonar:sonar \
-Dsonar.host.url=http://sonar.example.com \
-Dsonar.login=${SONAR_TOKEN}
'''
}
}
stage('构建Docker镜像') {
steps {
script {
docker.build("${DOCKER_IMAGE}:${env.IMAGE_TAG}")
docker.build("${DOCKER_IMAGE}:latest")
}
}
}
stage('推送到Harbor') {
steps {
script {
docker.withRegistry("https://${DOCKER_REGISTRY}", 'harbor-auth') {
docker.image("${DOCKER_IMAGE}:${env.IMAGE_TAG}").push()
docker.image("${DOCKER_IMAGE}:latest").push()
}
}
}
}
stage('部署到测试环境') {
when {
branch 'develop'
}
steps {
deployToServer('test', '192.168.1.200')
}
}
stage('部署到生产环境') {
when {
branch 'main'
}
steps {
// 生产环境需要手动确认
input message: '确认部署到生产环境?',
ok: '确认部署'
deployToServer('prod', '192.168.1.100')
}
}
}
post {
success {
script {
sendDingTalkNotify('success')
}
}
failure {
script {
sendDingTalkNotify('failure')
}
}
always {
// 清理工作空间
cleanWs()
// 清理本地Docker镜像
sh "docker rmi ${DOCKER_IMAGE}:${env.IMAGE_TAG} || true"
}
}
}
// 部署函数
def deployToServer(String env, String host) {
sshagent(['deploy-server']) {
sh """
ssh -o StrictHostKeyChecking=no root@${host} '
docker pull ${DOCKER_IMAGE}:${IMAGE_TAG}
docker stop ${APP_NAME} || true
docker rm ${APP_NAME} || true
docker run -d \\
--name ${APP_NAME} \\
--restart=always \\
-p 8080:8080 \\
-e SPRING_PROFILES_ACTIVE=${env} \\
-v /data/logs/${APP_NAME}:/app/logs \\
${DOCKER_IMAGE}:${IMAGE_TAG}
'
"""
}
}
// 钉钉通知
def sendDingTalkNotify(String status) {
def color = status == 'success' ? '#00FF00' : '#FF0000'
def statusText = status == 'success' ? '✅ 成功' : '❌ 失败'
dingtalk (
robot: 'dingding-robot',
type: 'MARKDOWN',
title: "Jenkins构建通知",
text: [
"### Jenkins构建${statusText}",
"- 项目:${APP_NAME}",
"- 分支:${env.BRANCH_NAME}",
"- 版本:${env.IMAGE_TAG}",
"- 提交:${env.GIT_COMMIT_MSG}",
"- 耗时:${currentBuild.durationString}",
"- [查看详情](${env.BUILD_URL})"
]
)
}
七、多环境部署策略
7.1 分支策略
main分支 → 生产环境
develop分支 → 测试环境
feature/* → 开发环境(可选)
7.2 环境变量管理
stage('部署') {
steps {
script {
def envConfig = [
'dev': [
host: '192.168.1.201',
profile: 'dev',
jvmOpts: '-Xms256m -Xmx256m'
],
'test': [
host: '192.168.1.202',
profile: 'test',
jvmOpts: '-Xms512m -Xmx512m'
],
'prod': [
host: '192.168.1.100',
profile: 'prod',
jvmOpts: '-Xms2g -Xmx2g'
]
]
def config = envConfig[env.DEPLOY_ENV]
// 使用config部署...
}
}
}
八、跨网络部署:异地环境怎么办?
我们公司测试环境在办公室、生产环境在阿里云,网络不通。
方案1:公网暴露Jenkins(不推荐)
把Jenkins暴露到公网,风险太大。
方案2:VPN(传统方案)
缺点:
- 经常断
- 速度慢
- 配置复杂
方案3:SD-WAN组网(我在用的)
用星空组网把Jenkins和各环境服务器组到一个虚拟网络:
办公室Jenkins (192.168.188.10)
│
├── 办公室测试服务器 (192.168.188.20)
├── 阿里云生产服务器 (192.168.188.30)
└── 腾讯云灾备服务器 (192.168.188.40)
配置超简单:
# 在Jenkins服务器
curl -sSL https://down.starvpn.cn/linux.sh | bash
xkcli login your_token && xkcli up
# 在各环境服务器执行同样操作
组网后,Jenkins直接用虚拟IP连接所有服务器:
environment {
TEST_HOST = '192.168.188.20'
PROD_HOST = '192.168.188.30'
}
效果:
- 办公室到阿里云延迟:35ms(以前VPN要100ms+)
- 构建产物上传速度:50MB/s(以前20MB/s)
- 稳定性:3个月没断过
九、常见问题排查
9.1 构建超时
options {
timeout(time: 30, unit: 'MINUTES')
}
// 或者单个stage超时
stage('构建') {
options {
timeout(time: 10, unit: 'MINUTES')
}
steps {
sh 'mvn package'
}
}
9.2 Docker权限问题
# Jenkins容器需要访问宿主机Docker
docker run -v /var/run/docker.sock:/var/run/docker.sock ...
# 或者把jenkins用户加到docker组
usermod -aG docker jenkins
9.3 SSH连接失败
// 跳过SSH主机密钥检查
sh "ssh -o StrictHostKeyChecking=no root@${host} '...'"
9.4 Maven下载慢
配置阿里云镜像,在Jenkins服务器的 /data/jenkins/.m2/settings.xml:
<mirrors>
<mirror>
<id>aliyun</id>
<mirrorOf>central</mirrorOf>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
</mirrors>
十、最佳实践
10.1 Pipeline即代码
把Jenkinsfile放在代码仓库,不要在Jenkins界面写Pipeline。
10.2 敏感信息用凭据
// 不要这样
sh "docker login -u admin -p 123456 harbor.example.com"
// 要这样
withCredentials([usernamePassword(
credentialsId: 'harbor-auth',
usernameVariable: 'USER',
passwordVariable: 'PASS'
)]) {
sh "docker login -u $USER -p $PASS harbor.example.com"
}
10.3 保留构建记录
options {
buildDiscarder(logRotator(
numToKeepStr: '30', // 保留30次构建
artifactNumToKeepStr: '10' // 保留10次制品
))
}
10.4 构建通知必须有
不管成功失败,都要通知到人:
post {
success {
dingtalk(robot: 'dingding', type: 'TEXT', text: ['构建成功'])
}
failure {
dingtalk(robot: 'dingding', type: 'TEXT', text: ['构建失败,请检查'])
}
}
十一、效果对比
| 指标 | 人肉部署 | CI/CD |
|---|---|---|
| 单次部署耗时 | 30分钟 | 5分钟 |
| 日部署次数 | 3-4次 | 不限 |
| 出错率 | 高 | 低 |
| 可追溯性 | 无 | 完整记录 |
| 回滚速度 | 30分钟 | 1分钟 |
实际收益:
- 运维从"部署机器人"变成"平台建设者"
- 开发专注写代码,不用管部署
- 出问题能快速定位是哪次提交引入的
总结
搞CI/CD这事儿,前期投入时间是值得的:
- 先搭个最简单的Pipeline跑起来
- 逐步加入测试、扫描、通知
- 多环境用组网工具打通
- 把Pipeline当代码管理
一旦跑顺了,后面省的时间是前期投入的N倍。
有问题评论区交流~

浙公网安备 33010602011771号