CI/CD核心概念:
┌─────────────────────────────────────────┐
│ CI/CD流程 │
├─────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────┐ │
│ │ CI (持续集成) │ │
│ │ ├─ 代码提交 │ │
│ │ ├─ 自动构建 │ │
│ │ ├─ 自动测试 │ │
│ │ └─ 代码质量检查 │ │
│ └─────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────┐ │
│ │ CD (持续交付) │ │
│ │ ├─ 自动部署到测试环境 │ │
│ │ ├─ 自动部署到预发布环境 │ │
│ │ └─ 手动部署到生产环境 │ │
│ └─────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────┐ │
│ │ CD (持续部署) │ │
│ │ └─ 自动部署到生产环境 │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
东巴文理解:
CI/CD对比:
┌──────────────────┬──────────────┬──────────────┐
│ 阶段 │ CI │ CD │
├──────────────────┼──────────────┼──────────────┤
│ 持续集成 │ 自动构建测试 │ - │
│ 持续交付 │ - │ 手动部署生产 │
│ 持续部署 │ - │ 自动部署生产 │
└──────────────────┴──────────────┴──────────────┘
CI/CD优势:
├─ 提高开发效率
├─ 减少人为错误
├─ 快速反馈问题
├─ 加快交付速度
├─ 提高代码质量
└─ 降低发布风险
常见CI/CD工具:
CI/CD工具对比:
┌──────────────────┬──────────────┬──────────────┐
│ 工具 │ 特点 │ 适用场景 │
├──────────────────┼──────────────┼──────────────┤
│ Jenkins │ 插件丰富 │ 企业级应用 │
│ GitLab CI/CD │ 集成GitLab │ GitLab用户 │
│ GitHub Actions │ 集成GitHub │ GitHub用户 │
│ Travis CI │ 配置简单 │ 开源项目 │
│ CircleCI │ 云原生 │ 云原生应用 │
│ TeamCity │ JetBrains │ JetBrains用户│
└──────────────────┴──────────────┴──────────────┘
工具选择建议:
✅ 已有GitLab: 使用GitLab CI/CD
✅ 已有GitHub: 使用GitHub Actions
✅ 企业环境: 使用Jenkins
✅ 开源项目: 使用Travis CI
✅ 云原生应用: 使用CircleCI
Docker在CI/CD中的作用:
Docker在CI/CD中的应用:
┌──────────────────┬──────────────┬──────────────┐
│ 阶段 │ Docker作用 │ 优势 │
├──────────────────┼──────────────┼──────────────┤
│ 构建阶段 │ 构建环境标准化│ 环境一致 │
│ 测试阶段 │ 测试环境隔离 │ 并行测试 │
│ 部署阶段 │ 部署包标准化 │ 快速部署 │
│ 运行阶段 │ 运行环境一致 │ 环境统一 │
└──────────────────┴──────────────┴──────────────┘
Docker CI/CD流程:
1. 代码提交 → 触发CI
2. 拉取代码 → 构建Docker镜像
3. 运行测试 → 在Docker容器中测试
4. 推送镜像 → 推送到镜像仓库
5. 部署应用 → 使用Docker部署
使用Docker安装Jenkins:
# 创建Jenkins数据目录
mkdir -p /var/jenkins_home
# 启动Jenkins容器
docker run -d \
--name jenkins \
-p 8080:8080 \
-p 50000:50000 \
-v /var/jenkins_home:/var/jenkins_home \
-v /var/run/docker.sock:/var/run/docker.sock \
jenkins/jenkins:lts
# 查看初始密码
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
# 输出
a1b2c3d4e5f6g7h8i9j0
# 访问Jenkins
# http://localhost:8080
Jenkins Docker配置:
# 给jenkins用户docker权限
docker exec -it -u root jenkins bash
# 在容器内执行
groupadd -g 999 docker
usermod -aG docker jenkins
# 退出容器
exit
# 重启Jenkins
docker restart jenkins
安装必要插件:
Jenkins插件安装:
1. 登录Jenkins
2. 系统管理 → 插件管理
3. 安装以下插件:
├─ Docker Pipeline
├─ Docker
├─ Git
├─ Pipeline
└─ Blue Ocean
插件配置:
1. 系统管理 → 系统配置
2. 配置Docker:
├─ Docker URL: unix:///var/run/docker.sock
└─ 测试连接
Pipeline结构:
// Jenkinsfile
pipeline {
agent any
environment {
IMAGE_NAME = 'myapp'
IMAGE_TAG = "${env.BUILD_ID}"
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
script {
docker.build("${IMAGE_NAME}:${IMAGE_TAG}")
}
}
}
stage('Test') {
steps {
script {
docker.image("${IMAGE_NAME}:${IMAGE_TAG}").inside {
sh 'npm test'
}
}
}
}
stage('Push') {
steps {
script {
docker.withRegistry('https://registry.example.com', 'docker-registry-credentials') {
docker.image("${IMAGE_NAME}:${IMAGE_TAG}").push()
}
}
}
}
stage('Deploy') {
steps {
sh 'docker-compose -f docker-compose.prod.yml up -d'
}
}
}
post {
always {
cleanWs()
}
success {
echo 'Pipeline succeeded!'
}
failure {
echo 'Pipeline failed!'
}
}
}
Pipeline语法说明:
Pipeline语法:
┌──────────────────┬──────────────┬──────────────┐
│ 部分 │ 说明 │ 示例 │
├──────────────────┼──────────────┼──────────────┤
│ agent │ 执行节点 │ any/docker │
│ environment │ 环境变量 │ IMAGE_NAME │
│ stages │ 阶段集合 │ Build/Test │
│ stage │ 单个阶段 │ Build │
│ steps │ 执行步骤 │ sh/script │
│ post │ 后置处理 │ always │
└──────────────────┴──────────────┴──────────────┘
Agent类型:
├─ any: 任意可用节点
├─ none: 不指定节点
├─ label: 指定标签节点
├─ docker: 使用Docker容器
└─ kubernetes: 使用Kubernetes Pod
完整的Docker Pipeline:
// Jenkinsfile
pipeline {
agent {
docker {
image 'node:18-alpine'
args '-v /var/run/docker.sock:/var/run/docker.sock'
}
}
environment {
REGISTRY = 'registry.example.com'
IMAGE_NAME = 'myapp'
IMAGE_TAG = "${env.BRANCH_NAME}-${env.BUILD_NUMBER}"
}
stages {
stage('Checkout') {
steps {
checkout scm
sh 'git rev-parse --short HEAD > commit'
script {
env.COMMIT_ID = readFile('commit').trim()
}
}
}
stage('Install Dependencies') {
steps {
sh 'npm ci'
}
}
stage('Run Tests') {
steps {
sh 'npm test'
}
post {
always {
junit 'test-results/*.xml'
}
}
}
stage('Build Image') {
steps {
script {
docker.build("${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}", "--build-arg COMMIT_ID=${env.COMMIT_ID} .")
}
}
}
stage('Scan Image') {
steps {
script {
sh "docker scan ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}"
}
}
}
stage('Push Image') {
steps {
script {
docker.withRegistry("https://${REGISTRY}", 'docker-registry-credentials') {
docker.image("${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}").push()
docker.image("${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}").push('latest')
}
}
}
}
stage('Deploy to Dev') {
when {
branch 'develop'
}
steps {
sh """
docker-compose -f docker-compose.dev.yml pull
docker-compose -f docker-compose.dev.yml up -d
"""
}
}
stage('Deploy to Prod') {
when {
branch 'main'
}
steps {
input 'Deploy to production?'
sh """
docker-compose -f docker-compose.prod.yml pull
docker-compose -f docker-compose.prod.yml up -d
"""
}
}
}
post {
always {
cleanWs()
sh 'docker image prune -f'
}
success {
mail to: 'team@example.com',
subject: "Pipeline Success: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: "Pipeline succeeded!\n\nBuild URL: ${env.BUILD_URL}"
}
failure {
mail to: 'team@example.com',
subject: "Pipeline Failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: "Pipeline failed!\n\nBuild URL: ${env.BUILD_URL}"
}
}
}
多环境Pipeline:
// Jenkinsfile
pipeline {
agent any
environment {
REGISTRY = 'registry.example.com'
IMAGE_NAME = 'myapp'
}
stages {
stage('Build') {
steps {
script {
docker.build("${REGISTRY}/${IMAGE_NAME}:${env.BRANCH_NAME}-${env.BUILD_NUMBER}")
}
}
}
stage('Test') {
steps {
script {
docker.image("${REGISTRY}/${IMAGE_NAME}:${env.BRANCH_NAME}-${env.BUILD_NUMBER}").inside {
sh 'npm test'
}
}
}
}
stage('Deploy to Dev') {
when {
branch 'develop'
}
steps {
script {
deployEnvironment('dev')
}
}
}
stage('Deploy to Test') {
when {
branch 'release/*'
}
steps {
script {
deployEnvironment('test')
}
}
}
stage('Deploy to Prod') {
when {
branch 'main'
}
steps {
input 'Deploy to production?'
script {
deployEnvironment('prod')
}
}
}
}
}
def deployEnvironment(String env) {
def composeFile = "docker-compose.${env}.yml"
def imageTag = "${env.BRANCH_NAME}-${env.BUILD_NUMBER}"
sh """
docker-compose -f ${composeFile} pull
docker-compose -f ${composeFile} up -d
docker-compose -f ${composeFile} ps
"""
}
多环境Docker Compose:
# docker-compose.dev.yml
version: '3.8'
services:
app:
image: registry.example.com/myapp:${BRANCH_NAME}-${BUILD_NUMBER}
environment:
- NODE_ENV=development
- DB_HOST=db
depends_on:
- db
ports:
- "3000:3000"
db:
image: postgres:15-alpine
environment:
- POSTGRES_DB=myapp_dev
- POSTGRES_PASSWORD=dev123
volumes:
- db_dev_data:/var/lib/postgresql/data
volumes:
db_dev_data:
---
# docker-compose.prod.yml
version: '3.8'
services:
app:
image: registry.example.com/myapp:${BRANCH_NAME}-${BUILD_NUMBER}
environment:
- NODE_ENV=production
- DB_HOST=db
depends_on:
- db
ports:
- "80:3000"
restart: always
db:
image: postgres:15-alpine
environment:
- POSTGRES_DB=myapp_prod
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
volumes:
- db_prod_data:/var/lib/postgresql/data
restart: always
volumes:
db_prod_data:
安装GitLab Runner:
# 使用Docker安装GitLab Runner
docker run -d \
--name gitlab-runner \
--restart always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /srv/gitlab-runner/config:/etc/gitlab-runner \
gitlab/gitlab-runner:latest
# 注册Runner
docker exec -it gitlab-runner gitlab-runner register
# 输入信息
Enter the GitLab instance URL:
https://gitlab.com
Enter the registration token:
<your-registration-token>
Enter a description for the runner:
my-runner
Enter tags for the runner (comma-separated):
docker,production
Enter optional maintenance note for the runner:
Whether to run untagged jobs [true/false]:
true
Whether to lock Runner to current project [true/false]:
false
Enter the executor:
docker
Enter the default Docker image:
alpine:latest
# 查看Runner状态
docker exec gitlab-runner gitlab-runner list
Runner配置:
# /etc/gitlab-runner/config.toml
concurrent = 4
[[runners]]
name = "my-runner"
url = "https://gitlab.com"
token = "your-token"
executor = "docker"
[runners.docker]
image = "alpine:latest"
privileged = true
volumes = ["/cache", "/var/run/docker.sock:/var/run/docker.sock"]
基本配置:
# .gitlab-ci.yml
stages:
- build
- test
- deploy
variables:
IMAGE_NAME: registry.example.com/myapp
IMAGE_TAG: $CI_COMMIT_SHORT_SHA
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
build:
stage: build
script:
- docker build -t $IMAGE_NAME:$IMAGE_TAG .
- docker push $IMAGE_NAME:$IMAGE_TAG
tags:
- docker
test:
stage: test
script:
- docker pull $IMAGE_NAME:$IMAGE_TAG
- docker run --rm $IMAGE_NAME:$IMAGE_TAG npm test
tags:
- docker
deploy_dev:
stage: deploy
script:
- docker-compose -f docker-compose.dev.yml pull
- docker-compose -f docker-compose.dev.yml up -d
environment:
name: development
url: https://dev.example.com
only:
- develop
tags:
- docker
deploy_prod:
stage: deploy
script:
- docker-compose -f docker-compose.prod.yml pull
- docker-compose -f docker-compose.prod.yml up -d
environment:
name: production
url: https://www.example.com
only:
- main
when: manual
tags:
- docker
多环境配置:
# .gitlab-ci.yml
stages:
- build
- test
- security
- deploy
variables:
IMAGE_NAME: registry.example.com/myapp
DOCKER_DRIVER: overlay2
# 构建阶段
build:
stage: build
script:
- docker build --cache-from $IMAGE_NAME:latest -t $IMAGE_NAME:$CI_COMMIT_SHA -t $IMAGE_NAME:latest .
- docker push $IMAGE_NAME:$CI_COMMIT_SHA
- docker push $IMAGE_NAME:latest
tags:
- docker
# 测试阶段
test:
stage: test
script:
- docker pull $IMAGE_NAME:$CI_COMMIT_SHA
- docker run --rm $IMAGE_NAME:$CI_COMMIT_SHA npm test
coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'
artifacts:
reports:
junit: test-results/*.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
tags:
- docker
# 安全扫描
security_scan:
stage: security
script:
- docker pull $IMAGE_NAME:$CI_COMMIT_SHA
- docker scan $IMAGE_NAME:$CI_COMMIT_SHA
allow_failure: true
tags:
- docker
# 部署到开发环境
deploy_dev:
stage: deploy
script:
- |
cat <<EOF > .env
IMAGE_TAG=$CI_COMMIT_SHA
ENVIRONMENT=development
EOF
- docker-compose -f docker-compose.dev.yml pull
- docker-compose -f docker-compose.dev.yml up -d
environment:
name: development
url: https://dev.example.com
on_stop: stop_dev
only:
- develop
tags:
- docker
stop_dev:
stage: deploy
script:
- docker-compose -f docker-compose.dev.yml down
when: manual
environment:
name: development
action: stop
only:
- develop
tags:
- docker
# 部署到测试环境
deploy_test:
stage: deploy
script:
- |
cat <<EOF > .env
IMAGE_TAG=$CI_COMMIT_SHA
ENVIRONMENT=test
EOF
- docker-compose -f docker-compose.test.yml pull
- docker-compose -f docker-compose.test.yml up -d
environment:
name: test
url: https://test.example.com
only:
- /^release\/.*$/
tags:
- docker
# 部署到生产环境
deploy_prod:
stage: deploy
script:
- |
cat <<EOF > .env
IMAGE_TAG=$CI_COMMIT_SHA
ENVIRONMENT=production
EOF
- docker-compose -f docker-compose.prod.yml pull
- docker-compose -f docker-compose.prod.yml up -d
environment:
name: production
url: https://www.example.com
only:
- main
when: manual
tags:
- docker
配置缓存和制品:
# .gitlab-ci.yml
stages:
- install
- build
- test
- deploy
# 缓存配置
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- .npm/
variables:
npm_config_cache: "$CI_PROJECT_DIR/.npm"
# 安装依赖
install:
stage: install
image: node:18-alpine
script:
- npm ci --cache .npm --prefer-offline
artifacts:
paths:
- node_modules/
expire_in: 1 hour
# 构建
build:
stage: build
image: node:18-alpine
script:
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 week
# 测试
test:
stage: test
image: node:18-alpine
script:
- npm test
coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'
artifacts:
reports:
junit: test-results/*.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
# Docker构建
docker_build:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
only:
- main
- develop
滚动更新配置:
# docker-compose.yml
version: '3.8'
services:
app:
image: registry.example.com/myapp:latest
deploy:
replicas: 3
update_config:
parallelism: 1
delay: 10s
failure_action: rollback
rollback_config:
parallelism: 1
delay: 10s
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 10s
timeout: 3s
retries: 3
start_period: 30s
滚动更新脚本:
#!/bin/bash
# rolling-update.sh
IMAGE_NAME="registry.example.com/myapp"
NEW_TAG=$1
REPLICAS=3
# 拉取新镜像
docker pull ${IMAGE_NAME}:${NEW_TAG}
# 更新服务
docker service update \
--image ${IMAGE_NAME}:${NEW_TAG} \
--update-parallelism 1 \
--update-delay 10s \
--update-failure-action rollback \
myapp_service
# 检查服务状态
docker service ps myapp_service
# 等待更新完成
while [ $(docker service ps myapp_service --filter "desired-state=running" | grep -c "Running") -lt $REPLICAS ]; do
echo "Waiting for all replicas to be running..."
sleep 5
done
echo "Rolling update completed!"
蓝绿部署配置:
# docker-compose.blue.yml
version: '3.8'
services:
app_blue:
image: registry.example.com/myapp:${IMAGE_TAG}
environment:
- NODE_ENV=production
ports:
- "3001:3000"
labels:
- "deployment=blue"
---
# docker-compose.green.yml
version: '3.8'
services:
app_green:
image: registry.example.com/myapp:${IMAGE_TAG}
environment:
- NODE_ENV=production
ports:
- "3002:3000"
labels:
- "deployment=green"
蓝绿部署脚本:
#!/bin/bash
# blue-green-deploy.sh
IMAGE_TAG=$1
CURRENT=$(cat /tmp/current_deployment 2>/dev/null || echo "blue")
if [ "$CURRENT" == "blue" ]; then
NEW="green"
NEW_PORT=3002
OLD_PORT=3001
else
NEW="blue"
NEW_PORT=3001
OLD_PORT=3002
fi
echo "Current deployment: $CURRENT"
echo "New deployment: $NEW"
# 启动新版本
docker-compose -f docker-compose.${NEW}.yml pull
docker-compose -f docker-compose.${NEW}.yml up -d
# 等待新版本启动
sleep 10
# 健康检查
if curl -f http://localhost:${NEW_PORT}/health; then
echo "Health check passed!"
# 切换流量
# 更新负载均衡器配置
# 停止旧版本
docker-compose -f docker-compose.${CURRENT}.yml down
# 记录当前部署
echo $NEW > /tmp/current_deployment
echo "Blue-green deployment completed!"
else
echo "Health check failed! Rolling back..."
docker-compose -f docker-compose.${NEW}.yml down
exit 1
fi
部署监控脚本:
#!/bin/bash
# deploy-monitor.sh
CONTAINER_NAME=$1
TIMEOUT=300
INTERVAL=5
ELAPSED=0
echo "Monitoring deployment: $CONTAINER_NAME"
while [ $ELAPSED -lt $TIMEOUT ]; do
# 检查容器状态
STATUS=$(docker inspect --format='{{.State.Status}}' $CONTAINER_NAME 2>/dev/null)
if [ "$STATUS" == "running" ]; then
# 检查健康状态
HEALTH=$(docker inspect --format='{{.State.Health.Status}}' $CONTAINER_NAME 2>/dev/null)
if [ "$HEALTH" == "healthy" ]; then
echo "Container is healthy!"
exit 0
elif [ "$HEALTH" == "unhealthy" ]; then
echo "Container is unhealthy!"
exit 1
fi
elif [ "$STATUS" == "exited" ]; then
echo "Container exited unexpectedly!"
exit 1
fi
echo "Waiting for container to be healthy... ($ELAPSED/$TIMEOUT)"
sleep $INTERVAL
ELAPSED=$((ELAPSED + INTERVAL))
done
echo "Timeout reached!"
exit 1
自动回滚配置:
// Jenkinsfile
pipeline {
agent any
environment {
IMAGE_NAME = 'registry.example.com/myapp'
PREVIOUS_TAG = ''
}
stages {
stage('Get Previous Version') {
steps {
script {
PREVIOUS_TAG = sh(
script: "docker images ${IMAGE_NAME} --format '{{.Tag}}' | head -n 2 | tail -n 1",
returnStdout: true
).trim()
}
}
}
stage('Deploy') {
steps {
script {
sh """
docker-compose pull
docker-compose up -d
"""
}
}
}
stage('Health Check') {
steps {
script {
timeout(time: 5, unit: 'MINUTES') {
waitUntil {
def r = sh script: 'curl -f http://localhost:3000/health', returnStatus: true
return (r == 0);
}
}
}
}
}
}
post {
failure {
script {
echo "Deployment failed! Rolling back..."
sh """
docker-compose down
docker tag ${IMAGE_NAME}:${PREVIOUS_TAG} ${IMAGE_NAME}:latest
docker-compose pull
docker-compose up -d
"""
}
}
}
}
CI/CD核心概念:
├─ CI(持续集成): 自动构建和测试
├─ CD(持续交付): 自动部署到测试环境
└─ CD(持续部署): 自动部署到生产环境
Jenkins集成Docker:
├─ 安装Docker插件
├─ 配置Docker环境
├─ 使用Pipeline构建镜像
└─ 自动化部署流程
GitLab CI/CD:
├─ 安装GitLab Runner
├─ 配置.gitlab-ci.yml
├─ 多环境部署
└─ 缓存和制品管理
自动化部署:
├─ 滚动更新: 逐步替换
├─ 蓝绿部署: 快速切换
├─ 监控: 实时监控
└─ 回滚: 快速回滚
标准CI/CD流程:
1. 代码提交 → 触发CI
2. 拉取代码 → 构建Docker镜像
3. 运行测试 → 在Docker容器中测试
4. 安全扫描 → 扫描镜像漏洞
5. 推送镜像 → 推送到镜像仓库
6. 部署应用 → 使用Docker部署
7. 监控应用 → 监控应用状态
8. 自动回滚 → 失败时回滚
下一章: Docker实战案例
将学习:
Jenkins安装: 使用Docker安装Jenkins并配置Docker环境。
GitLab Runner: 安装GitLab Runner并注册到GitLab。
简单Pipeline: 编写一个简单的Jenkins Pipeline,构建Docker镜像。
多环境部署: 配置Jenkins Pipeline实现多环境部署。
GitLab CI/CD: 配置GitLab CI/CD实现自动化部署。
蓝绿部署: 实现蓝绿部署策略。
完整CI/CD流程: 搭建完整的CI/CD流程,要求:
CI/CD文档: 编写一份CI/CD文档,包含: