简历怎么写
负责搭建公司前端 CI/CD 自动化体系,从零到一建设 Jenkins 流水线,实现代码提交到生产部署的全流程自动化。部署效率提升 85%,从手动 30 分钟缩短至自动化 4.5 分钟,日均部署次数从 2 次提升到 50+ 次,部署失败率从 15% 降至 2%。
- 设计多环境发布策略,实现 dev/test/pre/prod 四套环境的自动化部署和回滚
- 使用 Docker 多阶段构建优化镜像,体积从 800MB 压缩至 80MB,部署速度提升 70%
- 实施蓝绿发布和灰度发布策略,生产环境零停机部署,问题发现时间从 2 小时缩短至 5 分钟
- 建立完善的回滚机制,支持一键回退,平均回滚时间从 15 分钟降至 1 分钟
面试怎么说
面试官: 讲讲你们的 CI/CD 是怎么做的?
我的回答:
我们项目从手动部署升级到全自动化 CI/CD,整个过程经历了几个阶段。
最开始的痛点是这样的:每次上线需要人工打包、上传服务器、重启服务,一套流程下来至少 30 分钟,而且经常出错。有一次因为配置文件没改对,导致生产环境报错,回滚又花了半小时,影响了线上用户。
所以我就主导搭建了自动化流水线,核心思路是代码提交自动触发,一键部署到生产。
具体做了这几件事:
第一步,Jenkins 流水线。我配置了完整的构建流程:代码检出 → 安装依赖 → 跑 lint 和测试 → 构建打包 → 构建 Docker 镜像 → 推送到私有仓库 → 部署到 K8s。整个流程完全自动化,开发人员只需要 git push,剩下的交给流水线。
第二步,Docker 容器化。之前部署要在服务器上装 Node、配置 Nginx,环境不一致经常出问题。我用 Docker 把应用打包成镜像,多阶段构建把体积从 800MB 优化到 80MB,这样部署快,而且开发、测试、生产环境完全一致。
第三步,多环境管理。我们有四套环境:dev、test、pre、prod。每个环境对应不同分支,开发分支自动部署到 dev,发布分支走审批流程部署到 pre 和 prod。用 Kubernetes 的 namespace 做隔离,配置用 ConfigMap 管理,敏感信息用 Secret。
第四步,灰度发布。生产环境我不敢直接全量发布,所以做了灰度策略。先把新版本部署 1 个 Pod,只给 10% 流量,观察 10 分钟没问题再放到 30%、50%,最后全量。期间一旦发现错误率上升,自动回滚。
效果很明显:部署时间从 30 分钟降到 4.5 分钟,一天能部署 50 多次,快速迭代。部署成功率从 85% 提升到 98%,回滚时间只要 1 分钟。最关键是,生产环境出问题能快速定位是哪个版本,一键回退。
面试官: 你们的回滚机制具体怎么做的?
遇到问题回滚分三层:
第一层是应用回滚。Kubernetes 有版本历史,执行 kubectl rollout undo 就能回到上一个稳定版本,30 秒搞定。
第二层是配置回滚。配置文件都在 Git 管理,有问题直接 revert 提交,触发重新部署。
第三层是数据回滚。数据库迁移我们用的是前向兼容策略,新增字段但保留旧字段,代码先部署新版本,数据迁移完成后再删除旧字段。万一要回滚,不会因为字段删了导致旧代码跑不起来。
另外还做了降级开关,紧急情况下可以通过配置中心关闭有问题的新功能,让流量走旧逻辑。
完整技术实现
一、Jenkins 流水线配置
项目结构
project/
├── src/ # 源代码
├── Dockerfile # Docker 配置
├── Jenkinsfile # Jenkins 配置
├── nginx.conf # Nginx 配置
├── k8s/ # K8s 配置
│ ├── deployment.yaml
│ ├── service.yaml
│ └── ingress.yaml
└── scripts/ # 部署脚本
└── deploy.sh
Jenkinsfile(完整版)
groovy
pipeline {
agent any
environment {
PROJECT_NAME = 'my-frontend'
DOCKER_REGISTRY = 'registry.example.com'
K8S_NAMESPACE = 'production'
DINGTALK_WEBHOOK = credentials('dingtalk-webhook')
}
parameters {
choice(name: 'ENV', choices: ['dev', 'test', 'pre', 'prod'], description: '部署环境')
booleanParam(name: 'SKIP_TEST', defaultValue: false, description: '跳过测试')
}
stages {
stage('代码检出') {
steps {
checkout scm
script {
env.GIT_COMMIT_MSG = sh(returnStdout: true, script: 'git log -1 --pretty=%B').trim()
env.GIT_COMMIT_ID = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
env.BUILD_VERSION = "${env.BUILD_NUMBER}-${env.GIT_COMMIT_ID}"
}
}
}
stage('安装依赖') {
steps {
sh '''
if [ -d "node_modules" ]; then
echo "使用缓存依赖"
else
npm ci --prefer-offline
fi
'''
}
}
stage('代码检查') {
steps {
sh 'npm run lint'
}
}
stage('单元测试') {
when {
expression { !params.SKIP_TEST }
}
steps {
sh 'npm run test:ci'
publishHTML([
reportDir: 'coverage',
reportFiles: 'index.html',
reportName: '测试覆盖率'
])
}
}
stage('构建打包') {
steps {
sh """
npm run build:${params.ENV}
du -sh dist
"""
}
}
stage('构建镜像') {
steps {
script {
def imageTag = "${DOCKER_REGISTRY}/${PROJECT_NAME}:${BUILD_VERSION}"
sh """
docker build -t ${imageTag} .
docker push ${imageTag}
"""
}
}
}
stage('部署') {
steps {
script {
if (params.ENV == 'prod') {
input message: '确认部署到生产环境?', submitter: 'admin'
}
sh """
kubectl set image deployment/${PROJECT_NAME} \
${PROJECT_NAME}=${DOCKER_REGISTRY}/${PROJECT_NAME}:${BUILD_VERSION} \
-n ${params.ENV}
kubectl rollout status deployment/${PROJECT_NAME} -n ${params.ENV}
"""
}
}
}
stage('健康检查') {
steps {
sh """
sleep 10
curl -f http://${params.ENV}.example.com/health || exit 1
"""
}
}
}
post {
success {
sh """
curl -X POST ${DINGTALK_WEBHOOK} \
-H 'Content-Type: application/json' \
-d '{
"msgtype": "markdown",
"markdown": {
"title": "部署成功",
"text": "## ✅ 部署成功\\n\\n**项目**: ${PROJECT_NAME}\\n**环境**: ${params.ENV}\\n**版本**: ${BUILD_VERSION}\\n**提交**: ${GIT_COMMIT_MSG}"
}
}'
"""
}
failure {
sh """
curl -X POST ${DINGTALK_WEBHOOK} \
-H 'Content-Type: application/json' \
-d '{
"msgtype": "markdown",
"markdown": {
"title": "部署失败",
"text": "## ❌ 部署失败\\n\\n**项目**: ${PROJECT_NAME}\\n**环境**: ${params.ENV}\\n**版本**: ${BUILD_VERSION}\\n**查看日志**: ${BUILD_URL}"
}
}'
"""
}
}
}
二、Docker 容器化
Dockerfile(优化版)
dockerfile
# ========== 构建阶段 ==========
FROM node:18-alpine AS builder
WORKDIR /app
# 只复制依赖文件,利用缓存
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
# 复制源代码
COPY . .
# 构建
ARG ENV=production
RUN npm run build:${ENV}
# ========== 运行阶段 ==========
FROM nginx:alpine
# 安装 curl(健康检查用)
RUN apk add --no-cache curl
# 复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html
# 复制 Nginx 配置
COPY nginx.conf /etc/nginx/nginx.conf
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost/health || exit 1
# 暴露端口
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
nginx.conf
nginx
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# 健康检查
location /health {
access_log off;
return 200 "healthy\n";
}
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# API 代理
location /api/ {
proxy_pass http://backend:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# SPA 路由
location / {
try_files $uri $uri/ /index.html;
}
}
构建脚本
bash
#!/bin/bash
# build-docker.sh
ENV=${1:-prod}
VERSION=$(git rev-parse --short HEAD)
IMAGE="registry.example.com/my-frontend:${VERSION}"
echo "构建镜像: ${IMAGE}"
docker build \
--build-arg ENV=${ENV} \
-t ${IMAGE} \
-t registry.example.com/my-frontend:${ENV}-latest \
.
docker push ${IMAGE}
docker push registry.example.com/my-frontend:${ENV}-latest
echo "镜像已推送: ${IMAGE}"
三、Kubernetes 多环境部署
deployment.yaml
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-frontend
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: my-frontend
template:
metadata:
labels:
app: my-frontend
version: v1
spec:
containers:
- name: my-frontend
image: registry.example.com/my-frontend:latest
ports:
- containerPort: 80
# 环境变量
envFrom:
- configMapRef:
name: my-frontend-config
# 资源限制
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
# 健康检查
livenessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 5
periodSeconds: 5
configmap.yaml
yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: my-frontend-config
namespace: production
data:
API_URL: "https://api.example.com"
NODE_ENV: "production"
LOG_LEVEL: "info"
service.yaml
yaml
apiVersion: v1
kind: Service
metadata:
name: my-frontend
namespace: production
spec:
selector:
app: my-frontend
ports:
- port: 80
targetPort: 80
type: ClusterIP
ingress.yaml
yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-frontend
namespace: production
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
ingressClassName: nginx
tls:
- hosts:
- www.example.com
secretName: example-tls
rules:
- host: www.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-frontend
port:
number: 80
部署脚本
bash
#!/bin/bash
# deploy-k8s.sh
ENV=$1
VERSION=$2
if [ -z "$ENV" ] || [ -z "$VERSION" ]; then
echo "用法: ./deploy-k8s.sh <env> <version>"
exit 1
fi
echo "部署到 ${ENV} 环境,版本 ${VERSION}"
# 更新镜像
kubectl set image deployment/my-frontend \
my-frontend=registry.example.com/my-frontend:${VERSION} \
-n ${ENV}
# 等待部署完成
kubectl rollout status deployment/my-frontend -n ${ENV}
# 验证
kubectl get pods -n ${ENV} -l app=my-frontend
echo "部署完成"
四、蓝绿发布
蓝绿发布架构
┌─────────────┐
│ Ingress │
└──────┬──────┘
│
┌───┴────┐
│ Service │ ──► selector: version=blue
└────────┘
│
┌───┴─────┬────────┐
│ │ │
┌─▼──┐ ┌─▼──┐ ┌─▼──┐
│Blue│ │Blue│ │Blue│ (v1 - 旧版本)
└────┘ └────┘ └────┘
┌────┐ ┌────┐ ┌────┐
│Green│ │Green│ │Green│ (v2 - 新版本,待切换)
└────┘ └────┘ └────┘
blue-green-deploy.sh
bash
#!/bin/bash
set -e
APP="my-frontend"
NAMESPACE="production"
NEW_VERSION=$1
CURRENT_COLOR=$(kubectl get service ${APP} -n ${NAMESPACE} -o jsonpath='{.spec.selector.color}')
if [ -z "$NEW_VERSION" ]; then
echo "用法: ./blue-green-deploy.sh <version>"
exit 1
fi
# 确定新颜色
if [ "$CURRENT_COLOR" == "blue" ]; then
NEW_COLOR="green"
else
NEW_COLOR="blue"
fi
echo "当前运行: ${CURRENT_COLOR}"
echo "准备部署: ${NEW_COLOR} (${NEW_VERSION})"
# 1. 部署新版本
echo "Step 1: 部署新版本到 ${NEW_COLOR} 环境"
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${APP}-${NEW_COLOR}
namespace: ${NAMESPACE}
spec:
replicas: 3
selector:
matchLabels:
app: ${APP}
color: ${NEW_COLOR}
template:
metadata:
labels:
app: ${APP}
color: ${NEW_COLOR}
spec:
containers:
- name: ${APP}
image: registry.example.com/${APP}:${NEW_VERSION}
ports:
- containerPort: 80
EOF
# 2. 等待就绪
echo "Step 2: 等待新版本就绪"
kubectl rollout status deployment/${APP}-${NEW_COLOR} -n ${NAMESPACE}
# 3. 健康检查
echo "Step 3: 健康检查"
sleep 10
POD=$(kubectl get pod -l app=${APP},color=${NEW_COLOR} -n ${NAMESPACE} -o jsonpath='{.items[0].metadata.name}')
kubectl exec ${POD} -n ${NAMESPACE} -- curl -f http://localhost/health
# 4. 切换流量
echo "Step 4: 切换流量"
read -p "确认切换到 ${NEW_COLOR}?(y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
kubectl patch service ${APP} -n ${NAMESPACE} -p "{\"spec\":{\"selector\":{\"color\":\"${NEW_COLOR}\"}}}"
echo "流量已切换到 ${NEW_COLOR}"
# 5. 观察期
echo "Step 5: 观察 30 秒"
sleep 30
# 6. 清理旧版本
read -p "删除旧版本 ${CURRENT_COLOR}?(y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
kubectl delete deployment ${APP}-${CURRENT_COLOR} -n ${NAMESPACE}
echo "已删除 ${CURRENT_COLOR} 环境"
fi
else
echo "取消部署"
kubectl delete deployment ${APP}-${NEW_COLOR} -n ${NAMESPACE}
fi
五、灰度发布
canary-deploy.sh
bash
#!/bin/bash
set -e
APP="my-frontend"
NAMESPACE="production"
NEW_VERSION=$1
STAGES=(10 30 50 100) # 灰度阶段
if [ -z "$NEW_VERSION" ]; then
echo "用法: ./canary-deploy.sh <version>"
exit 1
fi
echo "开始灰度发布: ${NEW_VERSION}"
# 1. 部署 Canary 版本(1个副本)
echo "Step 1: 部署 Canary 版本"
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${APP}-canary
namespace: ${NAMESPACE}
spec:
replicas: 1
selector:
matchLabels:
app: ${APP}
version: canary
template:
metadata:
labels:
app: ${APP}
version: canary
spec:
containers:
- name: ${APP}
image: registry.example.com/${APP}:${NEW_VERSION}
ports:
- containerPort: 80
EOF
kubectl rollout status deployment/${APP}-canary -n ${NAMESPACE}
# 2. 逐步放量
for PERCENTAGE in "${STAGES[@]}"; do
echo ""
echo "Step 2: 放量到 ${PERCENTAGE}%"
# 计算副本数(假设 Stable 有 9 个副本)
CANARY_REPLICAS=$((9 * PERCENTAGE / 100))
if [ $CANARY_REPLICAS -lt 1 ]; then
CANARY_REPLICAS=1
fi
echo "Canary 副本数: ${CANARY_REPLICAS}"
kubectl scale deployment/${APP}-canary --replicas=${CANARY_REPLICAS} -n ${NAMESPACE}
# 观察
echo "观察 5 分钟..."
for i in {1..5}; do
echo " 第 ${i} 分钟..."
sleep 60
# 检查错误率(这里简化处理,实际应该对接监控系统)
ERROR_COUNT=$(kubectl logs -l app=${APP},version=canary -n ${NAMESPACE} --tail=100 | grep -c "ERROR" || true)
echo " 错误数: ${ERROR_COUNT}"
if [ $ERROR_COUNT -gt 10 ]; then
echo "❌ 错误率过高,回滚"
kubectl delete deployment/${APP}-canary -n ${NAMESPACE}
exit 1
fi
done
echo "✅ ${PERCENTAGE}% 阶段通过"
done
# 3. 全量切换
echo ""
echo "Step 3: 全量切换"
kubectl delete deployment/${APP}-stable -n ${NAMESPACE}
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${APP}-stable
namespace: ${NAMESPACE}
spec:
replicas: 9
selector:
matchLabels:
app: ${APP}
version: stable
template:
metadata:
labels:
app: ${APP}
version: stable
spec:
containers:
- name: ${APP}
image: registry.example.com/${APP}:${NEW_VERSION}
ports:
- containerPort: 80
EOF
kubectl delete deployment/${APP}-canary -n ${NAMESPACE}
echo "灰度发布完成"
六、回滚机制
rollback.sh
bash
#!/bin/bash
APP="my-frontend"
NAMESPACE=$1
REVISION=${2:-0} # 默认回滚到上一个版本
if [ -z "$NAMESPACE" ]; then
echo "用法: ./rollback.sh <namespace> [revision]"
exit 1
fi
echo "回滚 ${APP} 在 ${NAMESPACE} 环境"
# 查看历史版本
echo "历史版本:"
kubectl rollout history deployment/${APP} -n ${NAMESPACE}
# 回滚
if [ "$REVISION" == "0" ]; then
echo "回滚到上一个版本"
kubectl rollout undo deployment/${APP} -n ${NAMESPACE}
else
echo "回滚到版本 ${REVISION}"
kubectl rollout undo deployment/${APP} --to-revision=${REVISION} -n ${NAMESPACE}
fi
# 等待完成
kubectl rollout status deployment/${APP} -n ${NAMESPACE}
echo "回滚完成"
# 发送通知
curl -X POST ${DINGTALK_WEBHOOK} \
-H 'Content-Type: application/json' \
-d '{
"msgtype": "text",
"text": {
"content": "⚠️ 生产环境已回滚\n项目: '${APP}'\n环境: '${NAMESPACE}'"
}
}'
自动回滚(基于监控)
bash
#!/bin/bash
# auto-rollback.sh
APP="my-frontend"
NAMESPACE="production"
ERROR_THRESHOLD=5 # 错误率阈值 5%
while true; do
# 获取最近 5 分钟的错误率
ERROR_RATE=$(curl -s "http://prometheus:9090/api/v1/query?query=rate(http_requests_total{status=~\"5..\",app=\"${APP}\"}[5m])" | jq -r '.data.result[0].value[1]')
if [ -z "$ERROR_RATE" ]; then
ERROR_RATE=0
fi
ERROR_RATE_PERCENT=$(echo "$ERROR_RATE * 100" | bc)
echo "当前错误率: ${ERROR_RATE_PERCENT}%"
# 如果错误率超过阈值,自动回滚
if (( $(echo "$ERROR_RATE_PERCENT > $ERROR_THRESHOLD" | bc -l) )); then
echo "⚠️ 错误率超过阈值,触发自动回滚"
./rollback.sh ${NAMESPACE}
# 发送告警
curl -X POST ${DINGTALK_WEBHOOK} \
-H 'Content-Type: application/json' \
-d '{
"msgtype": "text",
"text": {
"content": "🚨 自动回滚触发\n错误率: '${ERROR_RATE_PERCENT}'%\n已回滚到上一个稳定版本"
}
}'
break
fi
sleep 60
done
实际遇到的问题和解决方案
问题 1:镜像构建慢
现象: Docker 构建每次都要 8 分钟
原因:
- 没有利用缓存
- 依赖全量安装
解决:
dockerfile
# 优化前
COPY . .
RUN npm install
RUN npm run build
# 优化后
COPY package*.json ./
RUN npm ci # 使用 ci 而不是 install
COPY . .
RUN npm run build
问题 2:部署后偶尔 502
现象: 新版本部署后,偶尔会出现 502 错误
原因: Pod 还没完全启动,K8s 就把流量导入了
解决:
yaml
readinessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 10 # 增加初始延迟
periodSeconds: 5
问题 3:回滚后配置不对
现象: 代码回滚了,但是配置还是新的
原因: 配置和代码没有绑定
解决: 配置版本化
bash
# 部署时同时打标签
git tag -a v1.2.3 -m "Release 1.2.3"
git push origin v1.2.3
# ConfigMap 也打版本
kubectl create configmap my-config-v1.2.3 --from-file=config.json
问题 4:灰度发布流量不准
现象: 设置 10% 灰度,实际流量 20%+
原因: 副本数计算问题
解决: 使用 Istio VirtualService
yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: my-frontend
spec:
http:
- route:
- destination:
host: my-frontend
subset: stable
weight: 90
- destination:
host: my-frontend
subset: canary
weight: 10
核心数据
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 部署时间 | 30min | 4.5min |
| 部署频率 | 2次/周 | 50次/天 |
| 部署成功率 | 85% | 98% |
| 回滚时间 | 15min | 1min |
| 镜像大小 | 800MB | 80MB |
| 构建时间 | 8min | 1.5min |
总结
搭建 CI/CD 的核心是自动化 + 标准化 + 可回滚。代码提交后自动构建部署,每个环境配置标准化,出问题能快速回退。
技术选型上,Jenkins 适合复杂流程,GitLab CI 适合简单场景。Docker 保证环境一致,K8s 管理容器。蓝绿发布适合预发,灰度发布适合生产。
最重要的是要有监控和告警,发现问题能自动回滚,这样才敢频繁发布。