以前每次上线都是:打包→上传→部署→测试,一套流程下来半小时。现在代码一推,自动构建、自动测试、自动部署,喝杯咖啡的功夫就上线了。
一、为什么要搞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倍。
有问题评论区交流~
[code][/code]
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |