从零搭建CI/CD:3天省下300小时重复劳动

最开始那个月,我每天手动ssh上去git pull、npm build、pm2 restart,重复机械劳动到怀疑人生。直到有天凌晨3点部署把生产环境搞崩了,才决心搞CI/CD。刚开始以为这玩意儿很简单,结果连踩三个坑:权限配置搞了2天、流水线跑了6次全红、部署脚本把数据库干瘫痪了。

先说说为什么你需要CI/CD

0,.08);”
loading=”lazy” width=”800″ height=”500″>

假设你每天部署1次,每次手动操作15分钟。一年就是15*365=5475分钟≈91小时。这还不算半夜爬起来修bug、回滚失败、环境不一致导致的排查时间。我团队去年统计的数据:手动部署平均耗时3.2小时(包含环境准备+测试+回滚预案),而CI/CD自动部署平均0.8分钟——从3.2小时降到0.8分钟,效率提升240倍。

另一个坑:你以为”就我一个人写代码,用不着自动化”。但当你第5次忘记执行测试就push代码,或者第10次把开发环境的配置带到生产环境时,就会明白——CI/CD不是给团队用的,是给未来的自己用的

核心概念:流水线到底长什么样

先看一个最简单的CI/CD流水线骨架:

yaml

.gitlab-ci.yml 最小可行配置

stages:
- build
- test
- deploy

before_script:
- npm ci --prefer-offline # 用ci代替install,速度提升40%

build-job:
stage: build
script:
- npm run build
artifacts:
paths:
- dist/ # 把构建产物传给后续阶段
expire_in: 1 hour

test-job:
stage: test
script:
- npm run lint
- npm run test:unit
needs: ["build-job"] # 显式依赖,比默认顺序更快

deploy-job:
stage: deploy
script:
- scp -r dist/* user@server:/var/www/app/
only:
- main # 只有main分支才触发部署
`

这个设计真的反人类——我第一次写的时候以为stages是顺序执行的,结果发现同一stage的job是并行的。官方文档这段文档不够清晰,后来才搞明白:默认同一个stage的job并行,不同stage串行

为什么要这么写?因为:

  • artifacts让构建产物在job间传递,避免重复构建
  • needs打破默认顺序依赖,比如测试不需要等构建完才下载依赖
  • only限制部署范围,防止feature分支误部署
  • 真实踩坑记录:Jenkins配置血泪史

    我们团队一开始用Jenkins,因为"业界标准"。结果配置第一周就崩溃了——插件装了40个,每次重启要5分钟。后来发现一个真相:Jenkins适合CI/CD专家,不适合想快速上手的团队

    如果你非要用Jenkins,记住这个救命配置:

    `groovy
    // Jenkinsfile 生产可用的管道脚本
    pipeline {
    agent any

    environment {
    DOCKER_IMAGE = 'myapp:latest'
    DEPLOY_SERVER = 'prod-server-01'
    }

    stages {
    stage('Checkout') {
    steps {
    checkout scm
    // 这个参数能节省30%检出时间
    checkout([$class: 'GitSCM',
    branches: [[name: '*/main']],
    extensions: [[$class: 'CloneOption',
    depth: 1,
    shallow: true]]])
    }
    }

    stage('Build') {
    steps {
    sh '''
    # 用缓存目录避免每次都下载全部依赖
    if [ -d /var/cache/node_modules ]; then
    cp -r /var/cache/node_modules ./node_modules
    fi
    npm run build
    '''
    }
    }

    stage('Test') {
    parallel {
    stage('Unit Test') {
    steps { sh 'npm run test:unit' }
    }
    stage('Lint') {
    steps { sh 'npm run lint' }
    }
    }
    }

    stage('Deploy') {
    when {
    branch 'main'
    // 增加手动审批,防止手滑
    expression {
    input message: '确认部署到生产环境?',
    ok: '部署'
    return true
    }
    }
    steps {
    sshagent(['prod-ssh-key']) {
    sh "scp dist/* ${DEPLOY_SERVER}:/var/www/"
    }
    }
    }
    }

    post {
    failure {
    // 失败时自动通知,省得半夜查邮件
    slackSend(
    channel: '#ops-alert',
    message: "部署失败:${env.BUILD_URL}",
    color: 'danger'
    )
    }
    success {
    // 成功时记录版本号,方便回滚
    sh "echo ${env.BUILD_NUMBER} > version.txt"
    }
    }
    }
    `

    这个配置花了我3天调试。最坑的是sshagent插件——官方文档说"自动管理SSH密钥",结果我配置了把私钥明文写在脚本里,被安全审计查到直接扣绩效。正确做法是用Jenkins凭据管理(Credentials)存储密钥,然后在管道里用sshagent([‘凭证ID’])引用。

    另一个坑:环境一致性

    有次开发环境测试全绿,但生产部署后页面白屏。排查了2小时发现:开发机Node 18,生产机Node 16,某个新语法不支持。这就是典型的"在我电脑上能跑"问题。

    解决方案:用Docker容器化构建环境

    `dockerfile

    Dockerfile.ci 确保构建环境一致

    FROM node:18-alpine AS builder
    WORKDIR /app
    COPY package*.json ./
    RUN npm ci --only=production
    COPY . .
    RUN npm run build

    FROM nginx:alpine AS runner

    只拷贝构建产物,减小镜像体积

    COPY --from=builder /app/dist /usr/share/nginx/html
    EXPOSE 80
    `

    然后在CI管道里用这个镜像:
    `yaml
    build:
    stage: build
    image: node:18-alpine # 锁定Node版本
    script:
    - npm ci
    - npm run build
    `

    这样一来,无论开发机是什么版本,构建环境永远一致。这个改动让我们的"环境不一致"bug从每月3次降到0次。

    还有个技巧:增量部署

    全量部署每次上传几百兆文件,慢得要死。后来我们改成增量部署——只上传变更的文件。

    `yaml

    在GitLab CI中实现增量部署

    variables:
    DEPLOY_DIR: /var/www/app

    deploy-incremental:
    stage: deploy
    script:
    - |
    # 获取上次部署的commit
    LAST_DEPLOY=$(cat $DEPLOY_DIR/LAST_COMMIT 2>/dev/null || echo "none")

    if [ "$LAST_DEPLOY" == "none" ]; then
    # 首次部署,全量
    rsync -avz --delete dist/ user@server:$DEPLOY_DIR
    else
    # 增量:只传输变更文件
    CHANGED_FILES=$(git diff --name-only $LAST_DEPLOY HEAD -- dist/)
    if [ -n "$CHANGED_FILES" ]; then
    rsync -avz $CHANGED_FILES user@server:$DEPLOY_DIR
    fi
    fi

    # 记录当前commit
    echo $CI_COMMIT_SHA > $DEPLOY_DIR/LAST_COMMIT
    `

    这个技巧把部署时间从平均2分钟降到8秒。但有个坑:如果文件被删除,增量更新不会自动删除远程文件。需要在rsync加上–delete参数,或者定期做全量同步。

    生产环境必须加的防护

    最后一次踩坑:同事不小心把feature分支合并到main,自动触发部署,结果数据库DDL没写回滚脚本,导致线上数据丢失2小时。从此我们加了这些防护:

  • 部署前自动运行完整测试(包含集成测试)
  • 生产环境手动审批
  • 自动创建回滚点(部署前备份数据库)
  • 金丝雀部署(先部署10%的服务器)
  • `yaml
    canary-deploy:
    stage: deploy
    script:
    - |
    # 金丝雀部署:先更新1台服务器
    for server in canary-server-01; do
    ssh $server "cd /app && git pull && npm ci && pm2 restart app"
    done

    # 等待5分钟观察
    sleep 300

    # 如果健康检查通过,更新剩余服务器
    if curl -f http://canary-server-01/health; then
    for server in prod-server-{01..10}; do
    ssh $server "cd /app && git pull && npm ci && pm2 restart app"
    done
    else
    # 自动回滚
    echo "金丝雀失败,开始回滚"
    exit 1
    fi

    总结一下,你可以立刻用的三个点

  • 最小可行配置:先跑通最简单的流水线(build→test→deploy),再慢慢加花哨功能。我见过太多人第一周就把管道搞成俄罗斯套娃,结果自己都看不懂。
  • 环境容器化:用Docker锁定构建环境,或者至少锁定Node/Python版本。这个改动值得你花一天时间配置——因为它能消除80%的”在我电脑上能跑”问题。
  • 增量部署+回滚机制:只传输变更文件,部署前备份数据库。这两行脚本能让你从”部署提心吊胆”变成”部署就像喝水”。
  • 最后说个真实数据:我们团队实施CI/CD后,部署失败率从25%降到3%,部署时间从3.2小时降到0.8分钟,更重要的是——再也没有半夜被叫起来修bug了。如果你还在手动部署,找个周末花2小时搭个最小流水线,你会回来感谢自己的。

    滚动至顶部