Jenkins CI/CD流水线从零搭建:代码提交到自动部署全流程

以前每次上线都是:打包→上传→部署→测试,一套流程下来半小时。现在代码一推,自动构建、自动测试、自动部署,喝杯咖啡的功夫就上线了。

一、为什么要搞CI/CD?

先说说我们之前的"人肉部署"流程:

  1. 开发写完代码,提交Git
  2. 运维拉代码到本地
  3. mvn clean package 打包
  4. scp上传到服务器
  5. 停服务、备份、替换jar包
  6. 启动服务、查看日志
  7. 测试人员验证

问题:

  • 耗时长:一次部署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 访问配置

  1. 浏览器打开 http://你的IP:8080
  2. 输入初始密码
  3. 选择"安装推荐的插件"
  4. 创建管理员账号

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后自动触发构建:

  1. Jenkins项目 → Configure → Build Triggers
  2. 勾选 "Build when a change is pushed to GitLab"
  3. 复制Webhook URL

在GitLab项目设置:

  1. Settings → Webhooks
  2. URL填Jenkins的Webhook地址
  3. 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这事儿,前期投入时间是值得的:

  1. 先搭个最简单的Pipeline跑起来
  2. 逐步加入测试、扫描、通知
  3. 多环境用组网工具打通
  4. 把Pipeline当代码管理

一旦跑顺了,后面省的时间是前期投入的N倍。

有问题评论区交流~


posted @ 2025-12-03 12:48  花宝宝  阅读(32)  评论(0)    收藏  举报