一、Python 项目示例
1.1 Python 项目结构
python-app/
├── .gitlab-ci.yml # CI/CD 配置文件
├── Dockerfile # Docker 镜像构建文件
├── requirements.txt # Python 依赖
├── app/
│ ├── __init__.py
│ ├── main.py
│ └── config.py
├── k8s/
│ ├── deployment.yaml # Kubernetes 部署文件
│ ├── service.yaml # 服务暴露
│ ├── ingress.yaml # 入口配置
│ └── hpa.yaml # 自动扩缩容
└── tests/
└── test_main.py
1.2 Dockerfile
dockerfile
# Python Dockerfile
# 使用多阶段构建
# 第一阶段:构建环境
FROM python:3.9-slim as builder
WORKDIR /app
# 安装构建依赖
RUN apt-get update && apt-get install -y \
gcc \
g++ \
&& rm -rf /var/lib/apt/lists/*
# 安装 Python 依赖
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt
# 第二阶段:运行环境
FROM python:3.9-slim
# 设置环境变量
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
APP_HOME=/app \
USER=appuser
# 创建非 root 用户
RUN groupadd -r $USER && useradd -r -g $USER $USER
# 安装运行时依赖
RUN apt-get update && apt-get install -y \
curl \
&& rm -rf /var/lib/apt/lists/*
# 从构建阶段复制已安装的包
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH
WORKDIR $APP_HOME
# 复制应用代码
COPY . .
# 更改文件所有权
RUN chown -R $USER:$USER $APP_HOME
# 切换到非 root 用户
USER $USER
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# 暴露端口
EXPOSE 8000
# 运行应用
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "app.main:app"]
1.3 Kubernetes 部署文件
1.3.1 deployment.yaml
yaml
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: python-app
namespace: production
labels:
app: python-app
version: v1.0.0
spec:
replicas: 3
revisionHistoryLimit: 3
selector:
matchLabels:
app: python-app
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: python-app
version: v1.0.0
spec:
serviceAccountName: python-app-sa
securityContext:
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containers:
- name: python-app
image: registry.example.com/group/python-app:latest
imagePullPolicy: Always
ports:
- containerPort: 8000
name: http
protocol: TCP
env:
- name: ENVIRONMENT
value: "production"
- name: LOG_LEVEL
value: "INFO"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database-url
- name: REDIS_HOST
value: "redis-service"
- name: REDIS_PORT
value: "6379"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
successThreshold: 1
failureThreshold: 3
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 3
volumeMounts:
- name: config-volume
mountPath: /app/config
volumes:
- name: config-volume
configMap:
name: app-config
- name: secrets-volume
secret:
secretName: app-secrets
imagePullSecrets:
- name: registry-secret
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- python-app
topologyKey: kubernetes.io/hostname
1.3.2 service.yaml
yaml
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: python-app-service
namespace: production
labels:
app: python-app
spec:
selector:
app: python-app
ports:
- name: http
port: 80
targetPort: 8000
protocol: TCP
type: ClusterIP
1.3.3 ingress.yaml
yaml
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: python-app-ingress
namespace: production
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
spec:
tls:
- hosts:
- app.example.com
secretName: python-app-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: python-app-service
port:
number: 80
1.3.4 hpa.yaml
yaml
# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: python-app-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: python-app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Pods
value: 1
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Pods
value: 2
periodSeconds: 60
1.4 .gitlab-ci.yml
yaml
# Python 项目的 .gitlab-ci.yml
stages:
- lint
- test
- security
- build
- push
- deploy-staging
- deploy-production
- notify
variables:
# 镜像相关变量
IMAGE_NAME: $CI_REGISTRY_IMAGE
IMAGE_TAG: $CI_COMMIT_SHORT_SHA
LATEST_TAG: latest
# Kubernetes 相关变量
K8S_NAMESPACE_STAGING: staging
K8S_NAMESPACE_PRODUCTION: production
KUBECONFIG: /etc/deploy/config
# 应用相关变量
APP_NAME: python-app
DOCKER_HOST: tcp://docker:2375
# 只在特定分支或标签运行
workflow:
rules:
- if: $CI_COMMIT_BRANCH == "develop"
variables:
DEPLOY_ENV: "staging"
- if: $CI_COMMIT_BRANCH == "main"
variables:
DEPLOY_ENV: "production"
- if: $CI_COMMIT_TAG
variables:
DEPLOY_ENV: "production"
- when: never
# 缓存配置
cache:
key: "${CI_COMMIT_REF_SLUG}"
paths:
- .pip-cache/
policy: pull
# 所有作业共享的 before_script
.before_script: &before_script
- echo "Starting job: $CI_JOB_NAME"
- python --version
- pip --version
# 共享的 after_script
.after_script: &after_script
- echo "Job $CI_JOB_NAME completed"
# 作业 1: 代码质量检查
code_quality:
stage: lint
image: python:3.9-slim
script:
- *before_script
- pip install --cache-dir .pip-cache flake8 black isort
- echo "Running code quality checks..."
- black --check --diff .
- isort --check-only --diff .
- flake8 --max-line-length=88 --extend-ignore=E203,W503 .
- *after_script
artifacts:
when: always
paths:
- flake8_report.txt
reports:
codequality: gl-code-quality-report.json
only:
refs:
- branches
- tags
# 作业 2: 单元测试
unit_tests:
stage: test
image: python:3.9-slim
script:
- *before_script
- pip install --cache-dir .pip-cache -r requirements.txt
- pip install pytest pytest-cov pytest-mock
- echo "Running unit tests..."
- python -m pytest tests/ --cov=app --cov-report=xml --cov-report=html --junitxml=test-report.xml -v
- *after_script
artifacts:
when: always
paths:
- coverage/
- test-report.xml
- coverage.xml
reports:
junit: test-report.xml
coverage_report:
coverage_format: cobertura
path: coverage.xml
coverage: '/TOTAL.*\s+(\d+\.\d+%)/'
only:
refs:
- branches
- tags
# 作业 3: 安全扫描
security_scan:
stage: security
image:
name: aquasec/trivy:latest
entrypoint: [""]
variables:
TRIVY_NO_PROGRESS: "true"
TRIVY_TIMEOUT: "5m"
script:
- echo "Running security scan..."
- trivy filesystem --exit-code 1 --format table --severity HIGH,CRITICAL .
- trivy config --exit-code 1 --format table .
artifacts:
when: always
paths:
- trivy-report.json
reports:
container_scanning: gl-container-scanning-report.json
secret_detection: gl-secret-detection-report.json
only:
refs:
- develop
- main
- tags
# 作业 4: Docker 镜像构建
build_image:
stage: build
image: docker:20.10.16
services:
- docker:20.10.16-dind
variables:
DOCKER_TLS_CERTDIR: ""
script:
- echo "Building Docker image..."
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build --pull -t $IMAGE_NAME:$IMAGE_TAG -t $IMAGE_NAME:$LATEST_TAG .
- docker images
- *after_script
artifacts:
paths:
- docker_build.log
expire_in: 1 week
only:
refs:
- develop
- main
- tags
# 作业 5: 推送镜像到仓库
push_image:
stage: push
image: docker:20.10.16
services:
- docker:20.10.16-dind
needs:
- build_image
script:
- echo "Pushing Docker image to registry..."
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker push $IMAGE_NAME:$IMAGE_TAG
- docker push $IMAGE_NAME:$LATEST_TAG
- echo "Image pushed successfully: $IMAGE_NAME:$IMAGE_TAG"
- *after_script
only:
refs:
- develop
- main
- tags
# 作业 6: 部署到测试环境
deploy_staging:
stage: deploy-staging
image:
name: bitnami/kubectl:latest
entrypoint: [""]
needs:
- push_image
environment:
name: staging
url: https://staging-app.example.com
script:
- echo "Deploying to staging environment..."
- kubectl config use-context staging-cluster
- |
# 更新 deployment 镜像
kubectl set image deployment/$APP_NAME \
$APP_NAME=$IMAGE_NAME:$IMAGE_TAG \
-n $K8S_NAMESPACE_STAGING
# 等待部署完成
kubectl rollout status deployment/$APP_NAME \
-n $K8S_NAMESPACE_STAGING \
--timeout=300s
# 运行健康检查
kubectl get pods -n $K8S_NAMESPACE_STAGING -l app=$APP_NAME
echo "Deployment to staging completed successfully!"
- *after_script
only:
refs:
- develop
when: manual # 手动触发部署
# 作业 7: 部署到生产环境
deploy_production:
stage: deploy-production
image:
name: bitnami/kubectl:latest
entrypoint: [""]
needs:
- push_image
environment:
name: production
url: https://app.example.com
before_script:
- echo "Starting production deployment..."
- kubectl config use-context production-cluster
script:
- |
# 使用 Kustomize 或直接应用 yaml 文件
# 方法1: 直接应用
kubectl apply -f k8s/ -n $K8S_NAMESPACE_PRODUCTION
# 方法2: 使用 Kustomize(推荐)
kubectl kustomize k8s/overlays/production/ | kubectl apply -f -
# 等待部署完成
kubectl rollout status deployment/$APP_NAME \
-n $K8S_NAMESPACE_PRODUCTION \
--timeout=600s
# 验证部署
kubectl get deployments,services,ingress,pods -n $K8S_NAMESPACE_PRODUCTION
# 运行冒烟测试
echo "Running smoke tests..."
# 这里可以添加实际的冒烟测试命令
echo "Production deployment completed successfully!"
- *after_script
after_script:
- echo "Cleanup..."
# 清理临时文件等
rules:
- if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push"
when: manual # 生产环境需要手动确认
- if: $CI_COMMIT_TAG
when: manual
# 作业 8: 蓝绿部署(高级部署策略)
blue_green_deployment:
stage: deploy-production
image:
name: bitnami/kubectl:latest
entrypoint: [""]
needs:
- push_image
script:
- |
echo "Starting blue-green deployment..."
# 获取当前 active 部署
CURRENT_COLOR=$(kubectl get svc $APP_NAME -n $K8S_NAMESPACE_PRODUCTION -o jsonpath='{.spec.selector.color}')
# 确定新部署的颜色
if [ "$CURRENT_COLOR" = "blue" ]; then
NEW_COLOR="green"
else
NEW_COLOR="blue"
fi
# 创建新部署
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: $APP_NAME-$NEW_COLOR
namespace: $K8S_NAMESPACE_PRODUCTION
spec:
replicas: 3
selector:
matchLabels:
app: $APP_NAME
color: $NEW_COLOR
template:
metadata:
labels:
app: $APP_NAME
color: $NEW_COLOR
spec:
containers:
- name: $APP_NAME
image: $IMAGE_NAME:$IMAGE_TAG
ports:
- containerPort: 8000
EOF
# 等待新部署就绪
kubectl rollout status deployment/$APP_NAME-$NEW_COLOR -n $K8S_NAMESPACE_PRODUCTION --timeout=300s
# 切换流量
kubectl patch service $APP_NAME -n $K8S_NAMESPACE_PRODUCTION -p "{\"spec\":{\"selector\":{\"color\":\"$NEW_COLOR\"}}}"
# 清理旧部署
OLD_COLOR=$([ "$NEW_COLOR" = "blue" ] && echo "green" || echo "blue")
kubectl delete deployment $APP_NAME-$OLD_COLOR -n $K8S_NAMESPACE_PRODUCTION --ignore-not-found=true
echo "Blue-green deployment completed successfully!"
- *after_script
only:
- main
when: manual
# 作业 9: 通知
notify_success:
stage: notify
image: curlimages/curl:latest
needs:
- deploy_production
script:
- |
echo "Sending deployment notifications..."
# 发送到 Slack
curl -X POST $SLACK_WEBHOOK_URL \
-H 'Content-type: application/json' \
--data '{
"text": " Deployment Successful!\n• Project: $CI_PROJECT_TITLE\n• Environment: Production\n• Version: $IMAGE_TAG\n• Pipeline: $CI_PIPELINE_URL\n• Commit: $CI_COMMIT_MESSAGE"
}'
# 发送到企业微信/钉钉等
# 根据实际需求添加
echo "Notifications sent!"
- *after_script
rules:
- when: on_success
if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_TAG
notify_failure:
stage: notify
image: curlimages/curl:latest
script:
- |
echo "Sending failure notifications..."
curl -X POST $SLACK_WEBHOOK_URL \
-H 'Content-type: application/json' \
--data '{
"text": " Deployment Failed!\n• Project: $CI_PROJECT_TITLE\n• Job: $CI_JOB_NAME\n• Pipeline: $CI_PIPELINE_URL\n• Error: Check GitLab CI logs"
}'
- *after_script
rules:
- when: on_failure
# 作业 10: 数据库迁移(如果使用数据库)
database_migration:
stage: deploy-production
image: python:3.9-slim
needs:
- push_image
script:
- |
echo "Running database migrations..."
# 安装依赖
pip install -r requirements.txt
# 运行迁移
python manage.py migrate --noinput
# 如果有的话,运行数据种子
python manage.py loaddata initial_data.json
echo "Database migrations completed!"
- *after_script
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
二、Java 项目示例
2.1 Java 项目结构
java-app/
├── .gitlab-ci.yml # CI/CD 配置文件
├── Dockerfile # Docker 镜像构建文件
├── pom.xml # Maven 配置文件
├── src/
│ ├── main/
│ │ ├── java/
│ │ └── resources/
│ └── test/
│ └── java/
├── k8s/
│ ├── deployment.yaml # Kubernetes 部署文件
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── configmap.yaml
│ └── secret.yaml
└── .m2/
2.2 Java Dockerfile
dockerfile
# Java Dockerfile(多阶段构建)
# 第一阶段:构建
FROM maven:3.8.4-openjdk-11-slim AS builder
WORKDIR /app
# 复制 pom.xml 并下载依赖
COPY pom.xml .
RUN mvn dependency:go-offline -B
# 复制源代码并构建
COPY src ./src
RUN mvn clean package -DskipTests
# 第二阶段:运行
FROM openjdk:11-jre-slim
# 设置时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 创建非 root 用户
RUN groupadd -r appuser && useradd -r -g appuser appuser
# 设置工作目录
WORKDIR /app
# 从构建阶段复制 jar 文件
COPY --from=builder /app/target/*.jar app.jar
# 创建必要的目录并设置权限
RUN mkdir -p /app/logs && chown -R appuser:appuser /app
# 切换到非 root 用户
USER appuser
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
# 暴露端口
EXPOSE 8080
# JVM 参数
ENV JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/logs"
# 启动命令
ENTRYPOINT exec java $JAVA_OPTS -jar app.jar
2.3 Java Kubernetes 部署文件
2.3.1 deployment.yaml
yaml
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: java-app
namespace: production
labels:
app: java-app
version: v1.0.0
spec:
replicas: 3
revisionHistoryLimit: 5
selector:
matchLabels:
app: java-app
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: java-app
version: v1.0.0
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/actuator/prometheus"
spec:
serviceAccountName: java-app-sa
securityContext:
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containers:
- name: java-app
image: registry.example.com/group/java-app:latest
imagePullPolicy: Always
ports:
- containerPort: 8080
name: http
protocol: TCP
env:
- name: SPRING_PROFILES_ACTIVE
value: "production"
- name: JAVA_OPTS
value: "-Xms512m -Xmx1024m -XX:+UseG1GC -Dspring.profiles.active=production"
- name: SPRING_DATASOURCE_URL
valueFrom:
secretKeyRef:
name: java-app-secrets
key: spring-datasource-url
- name: SPRING_REDIS_HOST
value: "redis-service"
- name: LOG_LEVEL
value: "INFO"
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 3
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 15
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 3
startupProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 30
volumeMounts:
- name: logs-volume
mountPath: /app/logs
- name: config-volume
mountPath: /app/config
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 30"]
volumes:
- name: logs-volume
emptyDir: {}
- name: config-volume
configMap:
name: java-app-config
imagePullSecrets:
- name: registry-secret
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- java-app
topologyKey: kubernetes.io/hostname
tolerations:
- key: "dedicated"
operator: "Equal"
value: "java-app"
effect: "NoSchedule"
2.3.2 service.yaml
yaml
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: java-app-service
namespace: production
labels:
app: java-app
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
spec:
selector:
app: java-app
ports:
- name: http
port: 80
targetPort: 8080
protocol: TCP
- name: metrics
port: 8081
targetPort: 8081
protocol: TCP
type: ClusterIP
2.4 Java 项目的 .gitlab-ci.yml
yaml
# Java 项目的 .gitlab-ci.yml
stages:
- compile
- test
- sonarqube
- build
- push
- deploy-staging
- deploy-production
- notify
variables:
# Maven 配置
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
MAVEN_CLI_OPTS: "-s .m2/settings.xml --batch-mode --errors --fail-at-end --show-version"
# 镜像配置
IMAGE_NAME: $CI_REGISTRY_IMAGE
IMAGE_TAG: $CI_COMMIT_SHORT_SHA
LATEST_TAG: latest
# Kubernetes 配置
K8S_NAMESPACE_STAGING: staging
K8S_NAMESPACE_PRODUCTION: production
# SonarQube 配置
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
GIT_DEPTH: "0"
# 工作流规则
workflow:
rules:
- if: $CI_COMMIT_BRANCH == "develop"
variables:
DEPLOY_ENV: "staging"
- if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "master"
variables:
DEPLOY_ENV: "production"
- if: $CI_COMMIT_TAG
variables:
DEPLOY_ENV: "production"
- when: never
# 缓存配置
cache:
key: "${CI_COMMIT_REF_SLUG}"
paths:
- .m2/repository/
- target/
policy: pull
# 共享的 before_script
.before_script: &before_script
- echo "Java version:"
- java -version
- echo "Maven version:"
- mvn --version
- echo "Starting job: $CI_JOB_NAME"
# 作业 1: 编译
compile:
stage: compile
image: maven:3.8.4-openjdk-11-slim
script:
- *before_script
- echo "Compiling Java application..."
- mvn $MAVEN_CLI_OPTS clean compile
- echo "Compilation completed successfully!"
artifacts:
paths:
- target/classes/
expire_in: 1 hour
cache:
key: "${CI_COMMIT_REF_SLUG}"
paths:
- .m2/repository/
- target/
policy: pull-push
only:
refs:
- branches
- tags
# 作业 2: 单元测试
unit_tests:
stage: test
image: maven:3.8.4-openjdk-11-slim
script:
- *before_script
- echo "Running unit tests..."
- mvn $MAVEN_CLI_OPTS test
- echo "Generating test reports..."
- mvn $MAVEN_CLI_OPTS surefire-report:report-only
- echo "Unit tests completed!"
artifacts:
when: always
paths:
- target/surefire-reports/
- target/site/surefire-report.html
reports:
junit: target/surefire-reports/TEST-*.xml
coverage: '/Total.*?([0-9]{1,3})%/'
cache:
key: "${CI_COMMIT_REF_SLUG}"
paths:
- .m2/repository/
- target/
policy: pull-push
only:
refs:
- branches
- tags
# 作业 3: 集成测试
integration_tests:
stage: test
image: maven:3.8.4-openjdk-11-slim
services:
- postgres:13-alpine
- redis:6-alpine
variables:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
REDIS_HOST: redis
script:
- *before_script
- echo "Running integration tests..."
- mvn $MAVEN_CLI_OPTS verify -DskipUnitTests
- echo "Integration tests completed!"
artifacts:
when: always
paths:
- target/failsafe-reports/
reports:
junit: target/failsafe-reports/TEST-*.xml
only:
refs:
- develop
- main
- master
# 作业 4: SonarQube 代码质量分析
sonarqube_check:
stage: sonarqube
image:
name: sonarsource/sonar-scanner-cli:latest
entrypoint: [""]
variables:
SONAR_TOKEN: $SONAR_TOKEN
SONAR_HOST_URL: $SONAR_HOST_URL
script:
- echo "Running SonarQube analysis..."
- sonar-scanner \
-Dsonar.projectKey=$CI_PROJECT_NAME \
-Dsonar.projectName="$CI_PROJECT_TITLE" \
-Dsonar.projectVersion=$CI_COMMIT_SHORT_SHA \
-Dsonar.sources=src \
-Dsonar.sourceEncoding=UTF-8 \
-Dsonar.java.binaries=target/classes \
-Dsonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml \
-Dsonar.login=$SONAR_TOKEN \
-Dsonar.host.url=$SONAR_HOST_URL
- echo "SonarQube analysis completed!"
artifacts:
when: always
reports:
codequality: gl-sonar-report.json
only:
refs:
- develop
- main
- master
- tags
# 作业 5: 构建 JAR 包
package:
stage: build
image: maven:3.8.4-openjdk-11-slim
needs:
- compile
- unit_tests
script:
- *before_script
- echo "Packaging application..."
- mvn $MAVEN_CLI_OPTS clean package -DskipTests
- echo "Package created:"
- ls -la target/*.jar
artifacts:
paths:
- target/*.jar
expire_in: 1 week
cache:
key: "${CI_COMMIT_REF_SLUG}"
paths:
- .m2/repository/
- target/
policy: pull-push
only:
refs:
- develop
- main
- master
- tags
# 作业 6: 构建 Docker 镜像
build_image:
stage: build
image: docker:20.10.16
services:
- docker:20.10.16-dind
variables:
DOCKER_TLS_CERTDIR: ""
needs:
- package
script:
- echo "Building Docker image..."
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build --pull -t $IMAGE_NAME:$IMAGE_TAG -t $IMAGE_NAME:$LATEST_TAG .
- docker images | grep $IMAGE_NAME
- echo "Docker image built successfully!"
artifacts:
paths:
- docker_build.log
only:
refs:
- develop
- main
- master
- tags
# 作业 7: 安全扫描
security_scan:
stage: build
image:
name: aquasec/trivy:latest
entrypoint: [""]
needs:
- build_image
variables:
TRIVY_NO_PROGRESS: "true"
script:
- echo "Scanning Docker image for vulnerabilities..."
- trivy image --exit-code 1 --severity HIGH,CRITICAL $IMAGE_NAME:$IMAGE_TAG
- echo "Security scan completed!"
artifacts:
reports:
container_scanning: gl-container-scanning-report.json
only:
refs:
- main
- master
- tags
# 作业 8: 推送镜像
push_image:
stage: push
image: docker:20.10.16
services:
- docker:20.10.16-dind
needs:
- build_image
- security_scan
script:
- echo "Pushing Docker image to registry..."
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker push $IMAGE_NAME:$IMAGE_TAG
- docker push $IMAGE_NAME:$LATEST_TAG
- echo "Image pushed: $IMAGE_NAME:$IMAGE_TAG"
only:
refs:
- develop
- main
- master
- tags
# 作业 9: 部署到测试环境
deploy_staging:
stage: deploy-staging
image:
name: bitnami/kubectl:latest
entrypoint: [""]
needs:
- push_image
environment:
name: staging
url: https://staging-java.example.com
before_script:
- echo "Configuring kubectl for staging..."
- kubectl config use-context staging-cluster
script:
- |
echo "Deploying to staging environment..."
# 使用 Kustomize 进行部署
kubectl apply -k k8s/overlays/staging/
# 等待部署完成
kubectl rollout status deployment/java-app \
-n $K8S_NAMESPACE_STAGING \
--timeout=300s
# 验证部署
kubectl get pods -n $K8S_NAMESPACE_STAGING -l app=java-app
# 运行冒烟测试
SMOKE_TEST_URL=$(kubectl get ingress java-app-ingress -n $K8S_NAMESPACE_STAGING -o jsonpath='{.spec.rules[0].host}')
echo "Running smoke test on $SMOKE_TEST_URL..."
curl -f http://$SMOKE_TEST_URL/actuator/health || exit 1
echo "Staging deployment completed successfully!"
- *after_script
only:
refs:
- develop
when: manual
# 作业 10: 部署到生产环境
deploy_production:
stage: deploy-production
image:
name: bitnami/kubectl:latest
entrypoint: [""]
needs:
- push_image
environment:
name: production
url: https://java-app.example.com
before_script:
- echo "Configuring kubectl for production..."
- kubectl config use-context production-cluster
script:
- |
echo "Starting production deployment..."
# 备份当前部署(用于回滚)
kubectl get deployment java-app -n $K8S_NAMESPACE_PRODUCTION -o yaml > deployment-backup.yaml
# 应用新的部署
kubectl apply -k k8s/overlays/production/
# 等待部署完成(设置较长的超时时间)
kubectl rollout status deployment/java-app \
-n $K8S_NAMESPACE_PRODUCTION \
--timeout=600s
if [ $? -eq 0 ]; then
echo " Production deployment successful!"
# 验证部署
kubectl get pods -n $K8S_NAMESPACE_PRODUCTION -l app=java-app
# 验证服务
kubectl get svc java-app-service -n $K8S_NAMESPACE_PRODUCTION
# 运行端到端测试
echo "Running end-to-end tests..."
# 这里可以添加实际的端到端测试脚本
else
echo " Deployment failed! Rolling back..."
kubectl rollout undo deployment/java-app -n $K8S_NAMESPACE_PRODUCTION
exit 1
fi
- *after_script
rules:
- if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "master"
when: manual
- if: $CI_COMMIT_TAG
when: manual
# 作业 11: 金丝雀部署
canary_deployment:
stage: deploy-production
image:
name: bitnami/kubectl:latest
entrypoint: [""]
needs:
- push_image
script:
- |
echo "Starting canary deployment..."
# 创建金丝雀部署(10%流量)
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: java-app-canary
namespace: $K8S_NAMESPACE_PRODUCTION
labels:
app: java-app
track: canary
spec:
replicas: 1
selector:
matchLabels:
app: java-app
track: canary
template:
metadata:
labels:
app: java-app
track: canary
spec:
containers:
- name: java-app
image: $IMAGE_NAME:$IMAGE_TAG
ports:
- containerPort: 8080
EOF
# 等待金丝雀部署就绪
kubectl rollout status deployment/java-app-canary -n $K8S_NAMESPACE_PRODUCTION --timeout=300s
echo "Canary deployment created. Monitor metrics for 10 minutes..."
sleep 600 # 等待10分钟进行监控
# 如果金丝雀部署成功,逐步扩大流量
echo "Canary successful! Scaling up..."
kubectl scale deployment java-app --replicas=2 -n $K8S_NAMESPACE_PRODUCTION
sleep 300
kubectl scale deployment java-app --replicas=3 -n $K8S_NAMESPACE_PRODUCTION
# 删除金丝雀部署
kubectl delete deployment java-app-canary -n $K8S_NAMESPACE_PRODUCTION
echo "Canary deployment completed successfully!"
- *after_script
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
# 作业 12: 数据库迁移(针对 Spring Boot)
database_migration:
stage: deploy-production
image: curlimages/curl:latest
needs:
- deploy_production
script:
- |
echo "Checking if database migration is needed..."
# 获取应用的健康端点
APP_URL=$(kubectl get ingress java-app-ingress -n $K8S_NAMESPACE_PRODUCTION -o jsonpath='{.spec.rules[0].host}')
# 检查 Flyway/Liquibase 迁移状态
MIGRATION_STATUS=$(curl -s http://$APP_URL/actuator/flyway || curl -s http://$APP_URL/actuator/liquibase)
if echo "$MIGRATION_STATUS" | grep -q "SUCCESS"; then
echo " Database migrations are up to date"
else
echo " Database migration check failed"
exit 1
fi
- *after_script
only:
- main
- master
# 作业 13: 性能测试
performance_test:
stage: test
image: loadimpact/k6:latest
needs:
- deploy_staging
script:
- |
echo "Running performance tests..."
# 编写 k6 性能测试脚本
cat > performance-test.js <<EOF
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 20 },
{ duration: '1m', target: 50 },
{ duration: '30s', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.01'],
},
};
export default function () {
const res = http.get('http://staging-java.example.com/actuator/health');
check(res, {
'status is 200': (r) => r.status === 200,
});
sleep(1);
}
EOF
# 运行性能测试
k6 run --out json=performance-test-result.json performance-test.js
echo "Performance test completed!"
- *after_script
artifacts:
when: always
paths:
- performance-test-result.json
reports:
performance: performance-test-result.json
only:
refs:
- develop
when: manual
# 作业 14: 通知
notify_team:
stage: notify
image: curlimages/curl:latest
needs:
- deploy_production
script:
- |
echo "Sending deployment notifications..."
# 发送到 Slack
curl -X POST $SLACK_WEBHOOK_URL \
-H 'Content-type: application/json' \
--data "{
\"attachments\": [{
\"color\": \"#36a64f\",
\"title\": \" Production Deployment Successful\",
\"fields\": [
{
\"title\": \"Project\",
\"value\": \"$CI_PROJECT_TITLE\",
\"short\": true
},
{
\"title\": \"Environment\",
\"value\": \"Production\",
\"short\": true
},
{
\"title\": \"Version\",
\"value\": \"$IMAGE_TAG\",
\"short\": true
},
{
\"title\": \"Commit\",
\"value\": \"$CI_COMMIT_MESSAGE\",
\"short\": false
}
],
\"footer\": \"GitLab CI/CD\",
\"ts\": $(date +%s)
}]
}"
# 发送邮件通知
# 这里可以添加邮件发送逻辑
echo "Notifications sent successfully!"
- *after_script
rules:
- when: on_success
if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "master"
# 作业 15: 清理(定期清理旧的 Docker 镜像和资源)
cleanup:
stage: notify
image: docker:20.10.16
services:
- docker:20.10.16-dind
variables:
DOCKER_TLS_CERTDIR: ""
script:
- |
echo "Cleaning up old Docker images..."
# 登录到镜像仓库
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
# 删除超过30天的镜像标签(保留最新的10个)
# 注意:这需要仓库支持API删除功能
# 这里只是一个示例,实际实现可能不同
echo "Cleanup would run here..."
echo "Cleanup completed!"
- *after_script
rules:
- if: $CI_PIPELINE_SOURCE == "schedule" # 只在计划任务中运行
when: always
- when: never
三、关键配置说明
3.1 CI/CD 流程总结
Python 项目流程:
- 代码质量检查:black、isort、flake8
- 单元测试:pytest 带覆盖率
- 安全扫描:Trivy 容器扫描
- 镜像构建:多阶段 Docker 构建
- 镜像推送:推送到私有仓库
- 部署测试环境:手动触发
- 部署生产环境:手动触发(可配置蓝绿/金丝雀)
- 通知:成功/失败通知
Java 项目流程:
- 编译:Maven 编译
- 测试:单元测试、集成测试
- 代码质量:SonarQube 分析
- 打包:构建 JAR 包
- 安全扫描:Trivy 镜像扫描
- 镜像构建和推送
- 部署测试环境:带冒烟测试
- 部署生产环境:带回滚机制
- 性能测试:可选阶段
- 通知和清理
3.2 关键特性
安全特性:
- 使用非 root 用户运行容器
- 镜像安全扫描
- 密钥管理(通过 Kubernetes Secrets)
- 网络策略限制
高可用特性:
- 多副本部署
- 反亲和性配置
- 健康检查(就绪、存活、启动探针)
- 自动扩缩容(HPA)
监控和可观测性:
- Prometheus 指标暴露
- 结构化日志
- 分布式追踪
- 应用性能监控
3.3 最佳实践建议
- 环境分离:严格区分开发、测试、生产环境
- 权限控制:使用最小权限原则
- 密钥管理:不要将密钥硬编码在代码或配置文件中
- 回滚策略:总是准备好回滚方案
- 监控告警:设置关键指标的告警
- 文档维护:保持 CI/CD 流程文档的更新
- 定期审计:定期审计 CI/CD 流程和配置
这个完整的 CI/CD 流程提供了从代码提交到生产部署的全套自动化方案,可以根据实际项目需求进行调整和优化。