Kubernetes---gitlab的ci/cd发布基于k8s的项目示例参考

一、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 项目流程:
  1. 代码质量检查:black、isort、flake8
  2. 单元测试:pytest 带覆盖率
  3. 安全扫描:Trivy 容器扫描
  4. 镜像构建:多阶段 Docker 构建
  5. 镜像推送:推送到私有仓库
  6. 部署测试环境:手动触发
  7. 部署生产环境:手动触发(可配置蓝绿/金丝雀)
  8. 通知:成功/失败通知
Java 项目流程:
  1. 编译:Maven 编译
  2. 测试:单元测试、集成测试
  3. 代码质量:SonarQube 分析
  4. 打包:构建 JAR 包
  5. 安全扫描:Trivy 镜像扫描
  6. 镜像构建和推送
  7. 部署测试环境:带冒烟测试
  8. 部署生产环境:带回滚机制
  9. 性能测试:可选阶段
  10. 通知和清理

3.2 关键特性

安全特性:
  • 使用非 root 用户运行容器
  • 镜像安全扫描
  • 密钥管理(通过 Kubernetes Secrets)
  • 网络策略限制
高可用特性:
  • 多副本部署
  • 反亲和性配置
  • 健康检查(就绪、存活、启动探针)
  • 自动扩缩容(HPA)
监控和可观测性:
  • Prometheus 指标暴露
  • 结构化日志
  • 分布式追踪
  • 应用性能监控

3.3 最佳实践建议

  1. 环境分离:严格区分开发、测试、生产环境
  2. 权限控制:使用最小权限原则
  3. 密钥管理:不要将密钥硬编码在代码或配置文件中
  4. 回滚策略:总是准备好回滚方案
  5. 监控告警:设置关键指标的告警
  6. 文档维护:保持 CI/CD 流程文档的更新
  7. 定期审计:定期审计 CI/CD 流程和配置

这个完整的 CI/CD 流程提供了从代码提交到生产部署的全套自动化方案,可以根据实际项目需求进行调整和优化。

相关推荐
VermiliEiz2 小时前
使用二进制方式部署k8s(6)
云原生·容器·kubernetes
汪碧康3 小时前
一文讲解kubernetes的gateway Api的功能、架构、部署、管理及使用
云原生·容器·架构·kubernetes·gateway·kubelet·xkube
Wpa.wk17 小时前
容器编排 - 了解K8s(pod, deployment,service,lable等概念)
经验分享·测试工具·docker·云原生·容器·kubernetes
xuefuhe19 小时前
Kubernetes基础入门4 应用的扩展与收缩
云原生·容器·kubernetes
Wpa.wk20 小时前
容器编排 - K8s - 配置文件参数说明和基础命令
经验分享·测试工具·docker·云原生·容器·kubernetes
一体化运维管理平台1 天前
容器监控难题破解:美信监控易全面支持K8s、Docker
云原生·容器·kubernetes
qiubinwei1 天前
kubeadm部署K8S集群(踩坑实录)
云原生·容器·kubernetes
张小凡vip1 天前
Kubernetes---存储方案:Rook自动结合Ceph
ceph·容器·kubernetes
Cyber4K1 天前
【Kubernetes专项】K8s 控制器 StatefulSet 从入门到企业实战应用
云原生·容器·kubernetes