第330集CI/CD架构实战:Jenkins+GitLab企业级持续集成与持续部署完整解决方案 | 字数总计: 3.5k | 阅读时长: 17分钟 | 阅读量:
CI/CD架构实战:Jenkins+GitLab企业级持续集成与持续部署 一、CI/CD概述 1.1 CI/CD核心概念 CI/CD(Continuous Integration/Continuous Deployment)实现代码到生产的自动化流程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 CI/CD核心流程: Continuous Integration (持续集成): - 代码提交触发构建 - 自动运行测试 - 快速反馈问题 - 保证代码质量 Continuous Deployment (持续部署): - 自动打包应用 - 自动部署到环境 - 自动化验证 - 快速发布 CI/CD工具链: - Jenkins: 构建和部署编排 - GitLab: 代码仓库和触发 - Docker: 容器化打包 - Kubernetes: 容器编排
1.2 Jenkins+GitLab架构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Jenkins + GitLab架构: GitLab: - 代码仓库 - GitHook触发 - Merge Request管理 - Issue跟踪 Jenkins: - 接收Webhook - 执行Pipeline - 运行测试 - 构建镜像 - 部署应用 流程: 1 . 开发者push代码到GitLab 2 . GitLab发送Webhook给Jenkins 3 . Jenkins触发Pipeline 4 . 构建、测试、打包、部署
二、Jenkins安装配置 2.1 Jenkins安装 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 docker run -d \ -p 8080:8080 \ -p 50000:50000 \ -v jenkins_home:/var/jenkins_home \ --name jenkins \ jenkins/jenkins:lts wget -q -O - https://pkg.jenkins.io/debian-stable/jenkins.io.key | sudo apt-key add - sudo sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list' sudo apt-get update sudo apt-get install jenkins sudo systemctl start jenkins sudo systemctl enable jenkins sudo systemctl status jenkins
2.2 初始配置 1 2 3 4 5 6 7 8 9 10 11 12 sudo cat /var/lib/jenkins/secrets/initialAdminPassword
2.3 GitLab集成配置 Jenkins配置GitLab凭据
配置GitLab Connection 1 2 3 4 5 6 Connection Name: GitLab GitLab Host URL: https://gitlab.example.com Credentials: GitLab Token Test Connection: Success
三、GitLab CI配置 3.1 .gitlab-ci.yml基础 完整的GitLab CI配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 stages: - build - test - package - deploy variables: DOCKER_REGISTRY: registry.example.com DOCKER_IMAGE: $CI_PROJECT_NAME DOCKER_TAG: $CI_COMMIT_REF_SLUG build: stage: build image: node:16 script: - echo "构建应用..." - npm install - npm run build artifacts: paths: - dist/ expire_in: 1 week only: - main - develop unit_test: stage: test image: node:16 script: - echo "运行单元测试..." - npm test coverage: '/覆盖率:\s+\d+\.\d+%/' allow_failure: true integration_test: stage: test image: node:16 services: - postgres:13 - redis:6 variables: POSTGRES_DB: testdb REDIS_URL: redis://redis:6379 script: - echo "运行集成测试..." - npm run test:integration only: - main docker_build: stage: package image: docker:20 services: - docker:20-dind before_script: - echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY script: - docker build -t $DOCKER_REGISTRY/$DOCKER_IMAGE:$DOCKER_TAG . - docker push $DOCKER_REGISTRY/$DOCKER_IMAGE:$DOCKER_TAG only: - main - tags deploy_dev: stage: deploy image: alpine:latest script: - echo "部署到开发环境..." - apk add --no-cache curl - curl -X POST "$JENKINS_WEBHOOK_URL?job=deploy-dev&token=$DEPLOY_TOKEN" only: - develop environment: name: development url: https://dev.example.com deploy_prod: stage: deploy image: alpine:latest script: - echo "部署到生产环境..." - apk add --no-cache curl - curl -X POST "$JENKINS_WEBHOOK_URL?job=deploy-prod&token=$DEPLOY_TOKEN" only: - main when: manual environment: name: production url: https://example.com
四、Jenkins Pipeline 4.1 声明式Pipeline 完整的Jenkinsfile 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 pipeline { agent any options { buildDiscarder(logRotator(numToKeepStr: '30' )) disableConcurrentBuilds() timeout(time: 30 , unit: 'MINUTES' ) timestamps() } environment { DOCKER_REGISTRY = 'registry.example.com' IMAGE_NAME = "${env.JOB_NAME}" IMAGE_TAG = "${env.BUILD_NUMBER}" DEPLOY_PATH = '/opt/app' } parameters { choice(name: 'ENV' , choices: ['dev' , 'test' , 'prod' ], description: '部署环境' ) string(name: 'VERSION' , defaultValue: '' , description: '版本号' ) booleanParam(name: 'SKIP_TESTS' , defaultValue: false , description: '跳过测试' ) } stages { stage('Checkout' ) { steps { echo '检出代码...' checkout scm script { env.GIT_COMMIT = sh( script: 'git rev-parse --short HEAD' , returnStdout: true ).trim() env.BRANCH_NAME = env.BRANCH_NAME ?: env.GIT_BRANCH?.replaceAll('origin/' , '' ) } } } stage('Build' ) { steps { echo "构建 ${params.VERSION ?: env.GIT_COMMIT}" sh ''' echo "安装依赖..." npm install echo "构建应用..." npm run build ''' } } stage('Test' ) { when { expression { !params.SKIP_TESTS } } parallel { stage('Unit Tests' ) { steps { script { try { sh 'npm run test:unit' publishTestResults testResultsPattern: 'test-results/**/*.xml' } catch (Exception e) { echo "单元测试失败: ${e}" currentBuild.result = 'UNSTABLE' } } } } stage('Integration Tests' ) { steps { sh 'npm run test:integration' } } } } stage('Code Quality' ) { steps { script { sh 'npm run lint' | ignoreErrors() publishCoverage adapters: [ coberturaAdapter('coverage/cobertura-coverage.xml' ) ] } } } stage('Docker Build' ) { steps { script { def imageName = "${env.DOCKER_REGISTRY}/${env.IMAGE_NAME}:${params.VERSION ?: env.GIT_COMMIT}" sh "docker build -t ${imageName} ." withCredentials([usernamePassword( credentialsId: 'docker-registry' , usernameVariable: 'DOCKER_USER' , passwordVariable: 'DOCKER_PASS' )]) { sh "echo \$DOCKER_PASS | docker login -u \$DOCKER_USER --password-stdin ${env.DOCKER_REGISTRY}" sh "docker push ${imageName}" } env.DOCKER_IMAGE = imageName } } } stage('Deploy' ) { steps { script { def deployScript = """ #!/bin/bash ssh deploy@${params.ENV}-server " # 拉取镜像 docker pull ${env.DOCKER_IMAGE} # 停止旧容器 docker stop ${env.IMAGE_NAME} || true # 启动新容器 docker run -d \\ --name ${env.IMAGE_NAME} \\ --restart unless-stopped \\ -p 8080:8080 \\ ${env.DOCKER_IMAGE} " """ sh deployScript } } } stage('Health Check' ) { steps { script { def healthUrl = getHealthUrl(params.ENV) retry(5 ) { sleep(10 ) def response = sh( script: "curl -s -o /dev/null -w '%{http_code}' ${healthUrl}" , returnStdout: true ).trim() if (response != '200' ) { error "健康检查失败: HTTP ${response}" } echo "健康检查通过: ${healthUrl}" } } } } } post { success { echo '部署成功!' emailext( subject: "构建成功: ${env.JOB_NAME} #${env.BUILD_NUMBER}" , body: "构建成功,已部署到${params.ENV}环境" , to: 'devops@example.com' ) } failure { echo '部署失败!' emailext( subject: "构建失败: ${env.JOB_NAME} #${env.BUILD_NUMBER}" , body: "构建失败,请检查日志" , to: 'devops@example.com' , attachLog: true ) script { sh ''' ssh deploy@${params.ENV}-server " docker stop ${IMAGE_NAME} || true docker run -d --name ${IMAGE_NAME} --restart unless-stopped -p 8080:8080 ${DOCKER_REGISTRY}/${IMAGE_NAME}:previous " ''' } } always { cleanWs() } } } def getHealthUrl(env) { def urls = [ dev: 'http://dev.example.com:8080/health' , test: 'http://test.example.com:8080/health' , prod: 'https://example.com/health' ] return urls[env] ?: urls.dev }
五、多环境部署Pipeline 5.1 分环境Pipeline 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 pipeline { agent any environment { DOCKER_REGISTRY = 'registry.example.com' APP_NAME = 'myapp' } parameters { choice( name: 'DEPLOY_ENV' , choices: ['dev' , 'test' , 'prod' ], description: '选择部署环境' ) } stages { stage('Build' ) { steps { echo "构建应用..." sh 'npm install && npm run build' } } stage('Test' ) { when { anyOf { params.DEPLOY_ENV == 'dev' params.DEPLOY_ENV == 'test' } } steps { echo "运行测试..." sh 'npm test' } } stage('Package' ) { steps { echo "打包Docker镜像..." sh """ docker build -t ${env.DOCKER_REGISTRY}/${env.APP_NAME}:${env.BUILD_NUMBER} . docker push ${env.DOCKER_REGISTRY}/${env.APP_NAME}:${env.BUILD_NUMBER} """ } } stage('Deploy' ) { steps { script { switch (params.DEPLOY_ENV) { case 'dev' : deployToDev() break case 'test' : deployToTest() break case 'prod' : deployToProd() break } } } } } } def deployToDev() { echo '部署到开发环境...' sh ''' kubectl set image deployment/myapp myapp=registry.example.com/myapp:BUILD_NUMBER -n dev kubectl rollout status deployment/myapp -n dev ''' } def deployToTest() { echo '部署到测试环境...' sh ''' kubectl set image deployment/myapp myapp=registry.example.com/myapp:BUILD_NUMBER -n test kubectl rollout status deployment/myapp -n test ''' } def deployToProd() { echo '部署到生产环境...' sh ''' # 蓝绿部署 kubectl apply -f manifests/production/ kubectl rollout status deployment/myapp -n production ''' }
六、自动化测试 6.1 单元测试集成 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 stage('Unit Tests' ) { steps { script { def testResults = [:] testResults['Frontend' ] = { dir('frontend' ) { sh 'npm test' archiveArtifacts artifacts: 'frontend/coverage/**/*' } } testResults['Backend' ] = { dir('backend' ) { sh 'mvn test' archiveArtifacts artifacts: 'backend/target/surefire-reports/**/*' } } parallel testResults } } }
6.2 E2E测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 stage('E2E Tests' ) { agent { docker { image 'cypress/browsers:latest' args '-v /tmp:/tmp' } } steps { echo '运行E2E测试...' sh ''' npm install npm run test:e2e -- --browser chrome ''' archiveArtifacts artifacts: 'cypress/videos/**/*, cypress/screenshots/**/*' } }
6.3 性能测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 stage('Performance Tests' ) { steps { script { def perfResults = [] sh ''' echo "运行压力测试..." wrk -t12 -c1000 -d30s http://test-server:8080/api/test ''' perfTest([ runner: 'Gatling' , simulation: 'MySimulation' , reportFiles: 'build/reports/**/*.html' ]) } } }
七、Docker构建和推送 7.1 Dockerfile最佳实践 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 FROM node:16 AS builderWORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . RUN npm run build FROM node:16 -slimWORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/package*.json ./ RUN groupadd -r appuser && useradd -r -g appuser appuser RUN chown -R appuser:appuser /app USER appuserEXPOSE 8080 HEALTHCHECK --interval=30s --timeout =3s \ CMD node healthcheck.js || exit 1 CMD ["node" , "dist/index.js" ]
7.2 Jenkins构建和推送 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 stage('Docker Build & Push' ) { steps { script { def imageName = "${env.DOCKER_REGISTRY}/${env.IMAGE_NAME}:${env.BUILD_NUMBER}" def latestImage = "${env.DOCKER_REGISTRY}/${env.IMAGE_NAME}:latest" sh """ docker build \ --build-arg NODE_ENV=production \ -t ${imageName} \ -t ${latestImage} \ . """ withCredentials([usernamePassword( credentialsId: 'docker-registry' , usernameVariable: 'DOCKER_USER' , passwordVariable: 'DOCKER_PASS' )]) { sh """ echo \$DOCKER_PASS | docker login -u \$DOCKER_USER --password-stdin ${env.DOCKER_REGISTRY} docker push ${imageName} docker push ${latestImage} """ } sh "docker rmi ${imageName} ${latestImage} || true" } } }
八、Kubernetes部署 8.1 Deployment配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 apiVersion: apps/v1 kind: Deployment metadata: name: myapp namespace: production spec: replicas: 3 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 selector: matchLabels: app: myapp template: metadata: labels: app: myapp spec: containers: - name: myapp image: registry.example.com/myapp:BUILD_NUMBER ports: - containerPort: 8080 env: - name: NODE_ENV value: "production" - name: DATABASE_URL valueFrom: secretKeyRef: name: db-secret key: url resources: requests: memory: "256Mi" cpu: "250m" limits: memory: "512Mi" cpu: "500m" livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /ready port: 8080 initialDelaySeconds: 5 periodSeconds: 5
8.2 Jenkins部署到K8s 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 stage('Deploy to Kubernetes' ) { steps { script { sh """ sed -i 's/BUILD_NUMBER/${env.BUILD_NUMBER}/g' manifests/deployment.yaml """ sh """ kubectl apply -f manifests/deployment.yaml """ sh """ kubectl rollout status deployment/myapp -n production --timeout=300s """ sh """ kubectl get pods -n production -l app=myapp kubectl logs -n production -l app=myapp --tail=100 """ } } }
九、通知和告警 9.1 钉钉通知 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 post { success { script { def message = """ 构建成功! 项目: ${env.JOB_NAME} 构建号: ${env.BUILD_NUMBER} 分支: ${env.BRANCH_NAME} 提交: ${env.GIT_COMMIT} 环境: ${params.ENV} """ sh """ curl -X POST "$DINGTALK_WEBHOOK" \\ -H 'Content-Type: application/json' \\ -d '{ "msgtype": "text", "text": {"content": "${message}"} }' """ } } failure { script { def message = """ 构建失败! 项目: ${env.JOB_NAME} 构建号: ${env.BUILD_NUMBER} 错误: 请查看Jenkins日志 """ sh """ curl -X POST "$DINGTALK_WEBHOOK" \\ -H 'Content-Type: application/json' \\ -d '{ "msgtype": "text", "text": {"content": "${message}"} }' """ } } }
9.2 邮件通知 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 post { success { emailext( subject: "✅ 构建成功: ${env.JOB_NAME} #${env.BUILD_NUMBER}" , body: """ <h2>构建成功</h2> <p><strong>项目:</strong> ${env.JOB_NAME}</p> <p><strong>构建号:</strong> ${env.BUILD_NUMBER}</p> <p><strong>分支:</strong> ${env.BRANCH_NAME}</p> <p><strong>环境:</strong> ${params.ENV}</p> <p><a href="${env.BUILD_URL}">查看详情</a></p> """ , mimeType: 'text/html' , to: 'devops@example.com' ) } }
十、最佳实践 10.1 Pipeline最佳实践 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 CI/CD最佳实践: 1 . 版本控制: - Jenkinsfile提交到Git - 使用GitOps理念 - 配置即代码 2 . 环境隔离: - 开发、测试、生产分离 - 独立Pipeline - 权限控制 3 . 安全性: - 使用凭据管理 - 避免硬编码密码 - 定期轮换密钥 4 . 监控告警: - 构建状态通知 - 部署状态告警 - 性能监控 5 . 回滚机制: - 自动回滚 - 快速恢复 - 版本管理
10.2 GitLab CI最佳实践 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 GitLab CI最佳实践: 1 . 缓存优化: - 缓存依赖包 - 加速构建 - 减少网络开销 2 . 并行执行: - 测试并行化 - 提高效率 - 缩短时间 3 . 条件执行: - 按分支执行 - 按标签执行 - 手动触发 4 . 工件管理: - 保存构建产物 - 设置过期时间 - 版本管理
十一、故障排查 11.1 常见问题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 sudo tail -f /var/log/jenkins/jenkins.log
11.2 调试技巧 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 script { echo "Debug: BUILD_NUMBER = ${env.BUILD_NUMBER}" echo "Debug: DOCKER_IMAGE = ${env.DOCKER_IMAGE}" sh 'env | sort' } try { sh 'npm test' } catch (Exception e) { echo "测试失败: ${e}" currentBuild.result = 'UNSTABLE' }
十二、总结 Jenkins+GitLab CI/CD方案实现了:
核心要点
CI/CD流程 :构建、测试、打包、部署
Pipeline自动化 :Declarative、Scripted
多环境支持 :dev、test、prod
监控告警 :钉钉、邮件通知
技术要点
Jenkins Pipeline :声明式与脚本式
GitLab CI :gitlab-ci.yml 配置
Docker :构建、推送、打包
Kubernetes :部署、回滚
实践建议
Jenkinsfile纳入版本控制
实施严格的测试与质量检查
完善监控、日志与告警
建立自动回滚机制
定期演练与优化流程
通过Jenkins+GitLab,可实现软件交付的自动化与一致性。