第05篇:K8s CI/CD 全流程:GitOps × ArgoCD × Harbor——Java SaaS 从代码提交到生产部署一键直达

前言:为什么说没有 GitOps 的 K8s 是"裸奔"?

很多团队刚上 K8s 时,仍然用老方式部署:SSH 登录跳板机,手动 kubectl apply,改完 YAML 直接推集群。这种方式带来的问题很快就会显现:

  • 谁改了什么? 没有审计日志,出了故障无从追溯
  • 现在集群跑的到底是哪个版本? 代码 Git 和集群实际状态可能早已分叉
  • 回滚怎么做? 不记得上一个好的状态是什么样子

GitOps 的核心理念是:Git 仓库是集群状态的唯一真实来源。所有对集群的变更,都通过 PR → 审批 → 合并的方式进行,集群自动与 Git 保持同步。这不只是工具问题,而是一种工程规范的转变。

本文构建一条完整的 Java SaaS CI/CD 流水线:

bash 复制代码
代码提交
  │
  ▼ GitHub Actions / Jenkins(CI 阶段)
单元测试 → 构建 JAR → 构建 Docker 镜像 → 推送到 Harbor
  │
  ▼ 更新 GitOps 仓库中的镜像 tag
ArgoCD 检测到 Git 变更(CD 阶段)
  │
  ▼ 自动同步到 K8s 集群
Deployment 滚动更新 → 健康检查 → 发布完成

一、Harbor:企业级私有镜像仓库

1.1 为什么不用 Docker Hub?

Docker Hub 免费账户有拉取频率限制(匿名 100 次/6小时),生产环境有镜像被公开访问的风险,而且访问速度在国内较慢。Harbor 是 CNCF 毕业项目,提供:

  • 私有镜像存储:完全自托管,数据不出内网
  • 镜像安全扫描:集成 Trivy/Clair,扫描镜像中的 CVE 漏洞
  • 镜像签名:集成 Notary,确保镜像来源可信
  • RBAC 权限控制:按项目隔离镜像访问权限
  • 镜像复制:多数据中心镜像同步

1.2 安装 Harbor

bash 复制代码
# 使用 Helm 安装 Harbor
helm repo add harbor https://helm.goharbor.io
helm repo update

helm install harbor harbor/harbor \
  --namespace harbor --create-namespace \
  --set expose.type=ingress \
  --set expose.ingress.hosts.core=harbor.yoursaas.com \
  --set expose.tls.certSource=secret \
  --set expose.tls.secret.secretName=harbor-tls \
  --set externalURL=https://harbor.yoursaas.com \
  --set harborAdminPassword=YourStrongPassword \
  --set persistence.persistentVolumeClaim.registry.size=200Gi \
  --set persistence.persistentVolumeClaim.database.size=20Gi

# 验证安装
kubectl get pods -n harbor
# 浏览器访问 https://harbor.yoursaas.com,默认账号 admin

1.3 配置 K8s 从 Harbor 拉取镜像

bash 复制代码
# 在 K8s 集群中创建 Harbor 登录凭证 Secret
kubectl create secret docker-registry harbor-registry-secret \
  --docker-server=harbor.yoursaas.com \
  --docker-username=robot$java-saas-deployer \   # 使用 Robot Account,权限最小化
  --docker-password=<robot-account-token> \
  --namespace=production

# 在 Deployment 中引用
yaml 复制代码
# Deployment 中指定 imagePullSecrets
spec:
  template:
    spec:
      imagePullSecrets:
        - name: harbor-registry-secret
      containers:
        - name: spring-boot-app
          image: harbor.yoursaas.com/java-saas/api:1.2.0

⚠️ 踩坑记录 #1:镜像 tag 用 latest 导致部署不一致

这是 CI/CD 最经典的反模式。image: harbor.yoursaas.com/java-saas/api:latest 看起来方便,但 K8s 有本地缓存,如果 Node 上已有 latest 镜像,即使 Harbor 上 latest 已更新,Pod 可能仍运行旧版本。

强制规范 :镜像 tag 必须使用 Git commit SHA 或语义化版本号(1.2.01.2.0-20240115-abc1234)。在 CI 流水线中自动生成 tag,绝不手写 latest


二、Java SaaS 的 Dockerfile 最佳实践

在进入 CI/CD 流程前,先把 Java 应用的镜像构建做好。一个低效的 Dockerfile 会让每次构建耗时 5 分钟,优化后可以压缩到 1 分钟内。

dockerfile 复制代码
# Dockerfile(多阶段构建 + 分层优化)

# ── 阶段 1:构建 JAR ────────────────────────────────────────
FROM maven:3.9-eclipse-temurin-21-alpine AS builder

WORKDIR /build

# 关键优化:先只复制 pom.xml,下载依赖(这层会被 Docker 缓存)
# 只要 pom.xml 不变,后续构建无需重新下载依赖,节省 2-3 分钟
COPY pom.xml .
COPY .mvn .mvn
RUN mvn dependency:go-offline -B

# 再复制源码并构建(源码变化只影响这一层)
COPY src ./src
RUN mvn package -DskipTests -B \
    && mv target/*.jar target/app.jar

# ── 阶段 2:提取 Spring Boot 分层 ─────────────────────────
FROM eclipse-temurin:21-jre-alpine AS extractor
WORKDIR /extract
COPY --from=builder /build/target/app.jar .
# Spring Boot 3.x 支持分层 JAR,把依赖和应用代码分开存储
# 这样每次只有应用代码层变化,依赖层可以复用缓存
RUN java -Djarmode=tools -jar app.jar extract --layers --launcher

# ── 阶段 3:最终运行镜像 ────────────────────────────────────
FROM eclipse-temurin:21-jre-alpine AS runner

# 安全加固:不以 root 用户运行
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

WORKDIR /app

# 按变化频率从低到高分层复制(充分利用 Docker 层缓存)
COPY --from=extractor --chown=appuser:appgroup /extract/app/dependencies/ ./
COPY --from=extractor --chown=appuser:appgroup /extract/app/spring-boot-loader/ ./
COPY --from=extractor --chown=appuser:appgroup /extract/app/snapshot-dependencies/ ./
COPY --from=extractor --chown=appuser:appgroup /extract/app/application/ ./

# 创建日志目录
RUN mkdir -p /app/logs /app/config /app/secrets

EXPOSE 8080

# 使用 exec 形式确保信号能传递到 JVM(优雅关闭必须)
ENTRYPOINT ["java", \
    "-XX:+UseContainerSupport", \
    "-XX:MaxRAMPercentage=75.0", \
    "-XX:+UseG1GC", \
    "-Djava.security.egd=file:/dev/./urandom", \
    "org.springframework.boot.loader.launch.JarLauncher"]

三、GitHub Actions CI 流水线

3.1 流水线整体设计

bash 复制代码
触发条件
  ├── push to main → 部署到 staging
  ├── tag v* → 部署到 production
  └── PR → 只跑测试,不部署

Job 1: test         → 单元测试 + 集成测试
Job 2: build-push   → 构建镜像 + 推送 Harbor(依赖 test 通过)
Job 3: update-gitops → 更新 GitOps 仓库镜像 tag(触发 ArgoCD 部署)
yaml 复制代码
# .github/workflows/ci-cd.yml
name: Java SaaS CI/CD

on:
  push:
    branches: [main, develop]
    tags: ['v*.*.*']
  pull_request:
    branches: [main]

env:
  HARBOR_REGISTRY: harbor.yoursaas.com
  IMAGE_NAME: java-saas/api
  JAVA_VERSION: '21'

jobs:
  # ── Job 1:测试 ────────────────────────────────────────────
  test:
    name: 单元测试 & 集成测试
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: testpassword
          MYSQL_DATABASE: testdb
        ports:
          - 3306:3306
        options: --health-cmd="mysqladmin ping" --health-interval=10s
    steps:
      - uses: actions/checkout@v4

      - name: 设置 JDK ${{ env.JAVA_VERSION }}
        uses: actions/setup-java@v4
        with:
          java-version: ${{ env.JAVA_VERSION }}
          distribution: temurin
          cache: maven                # 缓存 Maven 依赖,加速后续构建

      - name: 运行测试
        run: mvn test -B
        env:
          SPRING_PROFILES_ACTIVE: test
          SPRING_DATASOURCE_URL: jdbc:mysql://localhost:3306/testdb
          SPRING_DATASOURCE_PASSWORD: testpassword

      - name: 上传测试报告
        uses: actions/upload-artifact@v4
        if: always()                  # 测试失败也要上传报告
        with:
          name: test-reports
          path: target/surefire-reports/

  # ── Job 2:构建并推送镜像 ──────────────────────────────────
  build-push:
    name: 构建 Docker 镜像
    runs-on: ubuntu-latest
    needs: test                       # 测试通过才构建
    if: github.event_name != 'pull_request'  # PR 不推镜像
    outputs:
      image-tag: ${{ steps.meta.outputs.version }}

    steps:
      - uses: actions/checkout@v4

      # 确定镜像 tag 策略:
      # main 分支 → 使用 commit SHA 短码(如 abc1234)
      # tag v1.2.0 → 使用 1.2.0
      - name: 生成镜像元数据
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.HARBOR_REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch,suffix=-{{sha}}
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}

      - name: 登录 Harbor 镜像仓库
        uses: docker/login-action@v3
        with:
          registry: ${{ env.HARBOR_REGISTRY }}
          username: ${{ secrets.HARBOR_USERNAME }}
          password: ${{ secrets.HARBOR_PASSWORD }}

      - name: 设置 Docker Buildx(支持多平台构建和缓存)
        uses: docker/setup-buildx-action@v3

      - name: 构建并推送镜像
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          # 使用 GitHub Actions 缓存加速构建(首次 4 分钟,后续 <1 分钟)
          cache-from: type=gha
          cache-to: type=gha,mode=max
          platforms: linux/amd64      # 生产只需 amd64

      - name: Harbor 镜像安全扫描
        run: |
          # 触发 Harbor 对刚推送的镜像进行 Trivy 漏洞扫描
          # 若有高危漏洞,阻断部署流程
          IMAGE_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' \
            ${{ env.HARBOR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }})
          echo "镜像摘要:$IMAGE_DIGEST"
          # 实际项目中调用 Harbor API 获取扫描结果并判断是否有 HIGH/CRITICAL 漏洞

  # ── Job 3:更新 GitOps 仓库 ────────────────────────────────
  update-gitops:
    name: 更新 GitOps 仓库触发部署
    runs-on: ubuntu-latest
    needs: build-push
    steps:
      - name: Checkout GitOps 仓库
        uses: actions/checkout@v4
        with:
          repository: your-org/k8s-gitops   # 独立的 GitOps 配置仓库
          token: ${{ secrets.GITOPS_TOKEN }} # 有写权限的 PAT
          path: gitops

      - name: 更新镜像 Tag
        working-directory: gitops
        run: |
          IMAGE_TAG="${{ needs.build-push.outputs.image-tag }}"
          TARGET_ENV=$([[ "${{ github.ref }}" == refs/tags/* ]] && echo "production" || echo "staging")

          echo "更新 ${TARGET_ENV} 环境镜像 tag 为: ${IMAGE_TAG}"

          # 使用 yq 工具更新 YAML 中的镜像 tag(比 sed 更可靠)
          yq e -i ".spec.template.spec.containers[0].image = \"${{ env.HARBOR_REGISTRY }}/${{ env.IMAGE_NAME }}:${IMAGE_TAG}\"" \
            "overlays/${TARGET_ENV}/deployment-patch.yaml"

          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add .
          git commit -m "chore(${TARGET_ENV}): update java-saas image to ${IMAGE_TAG}

          Triggered by: ${{ github.event.head_commit.message }}
          Commit: ${{ github.sha }}"
          git push

四、GitOps 仓库结构设计

GitOps 仓库与应用代码仓库分开存放,这是重要的工程实践:代码仓库变更频繁,GitOps 仓库只存配置,权限和变更历史互不干扰。

bash 复制代码
k8s-gitops/                      # GitOps 配置仓库(独立 Git 仓库)
├── base/                         # 基础配置(各环境共享)
│   ├── namespace.yaml
│   ├── deployment.yaml           # 不含 image tag,由 patch 覆盖
│   ├── service.yaml
│   ├── hpa.yaml
│   └── kustomization.yaml
├── overlays/
│   ├── staging/
│   │   ├── kustomization.yaml
│   │   ├── deployment-patch.yaml # 覆盖:staging 镜像 tag、副本数
│   │   └── configmap-patch.yaml  # 覆盖:staging 数据库地址等
│   └── production/
│       ├── kustomization.yaml
│       ├── deployment-patch.yaml # 覆盖:production 镜像 tag、副本数
│       └── configmap-patch.yaml
└── apps/                         # ArgoCD Application 定义
    ├── java-saas-staging.yaml
    └── java-saas-production.yaml

base/kustomization.yaml(基础层):

yaml 复制代码
# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - namespace.yaml
  - deployment.yaml
  - service.yaml
  - hpa.yaml
  - configmap.yaml

overlays/production/kustomization.yaml(生产覆盖层):

yaml 复制代码
# overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: production
resources:
  - ../../base
patches:
  - path: deployment-patch.yaml    # 覆盖镜像 tag 和副本数
  - path: configmap-patch.yaml     # 覆盖生产环境配置
images:
  - name: harbor.yoursaas.com/java-saas/api
    newTag: "1.2.0-abc1234"        # CI 流水线自动更新这里

overlays/production/deployment-patch.yaml

yaml 复制代码
# CI 自动更新 image tag 的目标文件
apiVersion: apps/v1
kind: Deployment
metadata:
  name: java-saas-api
spec:
  replicas: 5                      # 生产 5 个副本(staging 只有 1 个)
  template:
    spec:
      containers:
        - name: spring-boot-app
          image: harbor.yoursaas.com/java-saas/api:1.2.0-abc1234
          resources:               # 生产环境更大的资源配额
            requests:
              memory: "1Gi"
              cpu: "500m"
            limits:
              memory: "2Gi"
              cpu: "2000m"

五、ArgoCD:自动同步 Git 到集群

5.1 安装 ArgoCD

bash 复制代码
kubectl create namespace argocd
kubectl apply -n argocd \
  -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# 等待所有 Pod 就绪
kubectl wait --for=condition=ready pod \
  -l app.kubernetes.io/name=argocd-server \
  -n argocd --timeout=120s

# 获取初始 admin 密码
kubectl get secret argocd-initial-admin-secret \
  -n argocd \
  -o jsonpath="{.data.password}" | base64 -d

# 通过 Port-Forward 访问 UI(或配置 Ingress 对外暴露)
kubectl port-forward svc/argocd-server -n argocd 8443:443
# 浏览器访问 https://localhost:8443

5.2 创建 ArgoCD Application

ArgoCD Application 定义了"监听哪个 Git 仓库的哪个路径,同步到哪个集群的哪个 Namespace":

yaml 复制代码
# apps/java-saas-production.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: java-saas-production
  namespace: argocd
  # 应用删除时不自动清理 K8s 资源(防止误删生产数据)
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: default
  source:
    repoURL: https://github.com/your-org/k8s-gitops.git
    targetRevision: main             # 监听 main 分支
    path: overlays/production        # 监听此路径下的变更
  destination:
    server: https://kubernetes.default.svc   # 目标集群(当前集群)
    namespace: production
  syncPolicy:
    automated:
      prune: true                    # Git 中删除的资源,K8s 中也删除
      selfHeal: true                 # 有人直接改了 K8s,ArgoCD 自动恢复到 Git 状态
      allowEmpty: false              # 防止意外清空集群
    syncOptions:
      - CreateNamespace=true         # 自动创建 namespace
      - PrunePropagationPolicy=foreground
      - RespectIgnoreDifferences=true
    retry:
      limit: 3
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m
  # 忽略某些字段的差异(如 HPA 动态修改的副本数)
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jsonPointers:
        - /spec/replicas            # 副本数由 HPA 管理,忽略 Git 和集群的差异

5.3 ArgoCD 的典型工作流

bash 复制代码
# 安装 argocd CLI
brew install argocd   # macOS

# 登录
argocd login localhost:8443 --username admin --password <初始密码>

# 查看所有 Application 状态
argocd app list
# 输出:
# NAME                   CLUSTER     NAMESPACE   PROJECT  STATUS  HEALTH
# java-saas-production   in-cluster  production  default  Synced  Healthy
# java-saas-staging      in-cluster  staging     default  Synced  Healthy

# 查看某个 App 的详情(哪些资源、同步状态、最近变更)
argocd app get java-saas-production

# 手动触发同步(通常不需要,automated 模式会自动)
argocd app sync java-saas-production

# 回滚到上一个成功的版本(ArgoCD 保存历史同步记录)
argocd app rollback java-saas-production 3   # 回滚到第 3 次同步的状态

# 查看同步历史
argocd app history java-saas-production

⚠️ 踩坑记录 #2:ArgoCD selfHeal 与 HPA 冲突

开启 selfHeal: true 后,ArgoCD 会把集群状态强制拉回 Git 状态。但 HPA 会动态修改 Deployment 的 spec.replicas,这与 Git 中写死的副本数不一致,ArgoCD 每次检测到差异就把副本数改回去,导致 HPA 完全失效。

解决方案 :在 Application 的 ignoreDifferences 中忽略 /spec/replicas 字段(见上方配置),同时在 base/deployment.yaml 中删除 replicas 字段,让 HPA 完全接管副本数管理。


六、完整发布流程演示

把以上所有内容串联,走一遍完整的"代码提交 → 生产发布"流程:

bash 复制代码
# 1. 开发者提交代码
git add .
git commit -m "feat: 新增 AI 摘要功能,接入通义千问 API"
git push origin main

# 2. GitHub Actions 自动触发(约 3-5 分钟)
#    - 单元测试通过 ✓
#    - Docker 镜像构建完成 ✓
#    - 镜像推送到 Harbor:harbor.yoursaas.com/java-saas/api:main-abc1234 ✓
#    - GitOps 仓库 overlays/staging/deployment-patch.yaml 自动更新 ✓

# 3. ArgoCD 检测到 GitOps 仓库变更(约 3 分钟轮询间隔)
#    - 自动同步 staging 环境
#    - Deployment 滚动更新(新镜像 tag)
#    - 等待 readinessProbe 通过

# 4. 验证 staging 环境
kubectl get pods -n staging
argocd app get java-saas-staging

# 5. staging 验证通过,打生产 tag
git tag v1.3.0
git push origin v1.3.0

# 6. GitHub Actions 检测到 tag,构建生产镜像
#    - 镜像 tag:1.3.0(语义化版本)
#    - 更新 GitOps 仓库 overlays/production/deployment-patch.yaml

# 7. ArgoCD 自动同步生产环境
argocd app get java-saas-production
# STATUS: Synced, HEALTH: Healthy ✓

# 8. 发现问题,需要回滚
argocd app rollback java-saas-production    # 回滚到上一个版本
# 或者回滚 Git tag,推送到 GitOps 仓库,ArgoCD 自动同步

七、常见问题 FAQ

Q1:CI 仓库和 GitOps 仓库为什么要分开?

两者职责不同:CI 仓库存应用代码,变更频繁;GitOps 仓库存集群期望状态,变更应该经过审批。分开后,可以对 GitOps 仓库设置更严格的分支保护规则(如生产环境变更需要两人审批),而不影响开发效率。

Q2:ArgoCD 的 automated sync 会不会误删生产资源?

prune: true 确实会删除 Git 中不存在的 K8s 资源。防护措施:① 生产 Application 可以不开 automated,改为手动触发同步,只有 staging 自动同步;② 配置 ArgoCD 通知,每次同步前发 Slack/钉钉告警。

Q3:如何在 GitOps 中安全管理 Secret?

不要把明文 Secret 提交 Git。方案:① Sealed Secrets(第 02 篇已讲);② External Secrets Operator 对接 Vault/云 KMS,GitOps 仓库只存 ExternalSecret 对象(声明从哪里取 Secret),实际 Secret 值不落 Git。

Q4:多个微服务共用一个 GitOps 仓库还是各自独立?

中小规模(<10 个服务)建议共用一个 GitOps 仓库,用目录区分:services/java-saas/services/ai-gateway/。超过 10 个服务后,可以拆分为多个仓库,用 ArgoCD 的 ApplicationSet 批量管理。

Q5:Harbor 挂了,K8s 还能正常运行吗?

已在各 Node 上缓存的镜像不受影响,现有 Pod 继续运行。但新 Pod 启动(如扩容、重启)会因拉取镜像失败而卡住。Harbor 建议配置多副本 + 持久化存储,或配置镜像复制到云厂商 Registry 作为备份。


总结

环节 工具 职责
镜像仓库 Harbor 私有镜像存储、安全扫描、权限控制
持续集成 GitHub Actions 测试、构建镜像、推送 Harbor
配置管理 Kustomize 多环境配置的 base+overlay 分层管理
持续交付 ArgoCD 监听 Git 变更,自动同步到 K8s 集群
镜像构建 多阶段 Dockerfile 分层缓存优化,最小化镜像体积

💬 一句话总结:GitOps 让 Git 成为集群的"遥控器"------你只需要改 Git,ArgoCD 负责让集群跟上;Harbor 确保每一个部署的镜像都是可信、可追溯的;整条流水线从提交代码到生产上线,全程无需人工介入,也无需登录服务器。


下一篇K8s 弹性伸缩实战:HPA、VPA、KEDA------Java SaaS 应对流量洪峰的秘密武器

下一篇K8s 可观测性三剑客:Prometheus + Grafana + Loki------Java SaaS 生产监控告警实战

📝 系列文章持续更新中,欢迎关注、收藏、点赞三连支持!

相关推荐
IT策士2 小时前
第 37 篇 k8s之调度进阶:亲和性、污点与容忍
云原生·容器·kubernetes
小猿姐2 小时前
三种 MongoDB Operator 实测对比:Community、Percona 与 KubeBlocks,谁更适合团队落地?
运维·mongodb·kubernetes
IT策士3 小时前
第 38 篇 k8s之RBAC 与 ServiceAccount 实战
云原生·容器·kubernetes
IT策士4 小时前
第 36 篇 k8s之资源管理:Requests、Limits 与 QoS
云原生·容器·kubernetes
无心水4 小时前
【Harness:落地实战】23、从CI/CD到AI原生底座:Harness平台全景深度解析——现代软件交付的最终答案?
人工智能·ci/cd·ai-native·openclaw·harness·hermes·honcho
STDD6 小时前
Gitea Actions Runner 搭建指南:为 Gitea 添加 CI/CD 自动化执行器
ci/cd·自动化·gitea
深圳行云创新6 小时前
企业现有的 CI/CD 流程,如何融入 AI 能力?
人工智能·ci/cd
行者-全栈开发6 小时前
SpringBoot CI/CD 流水线实战|Jenkins+GitLab CI,从手动到自动化交付
ci/cd·jenkins·springboot·devops·自动化部署·gitlab ci
Rain5096 小时前
GitLab-Runner + AI 代码审查服务 + 远程大模型 全套部署运维实战
linux·运维·人工智能·python·ci/cd·gitlab·ai编程