一、先说说我遇到的一个头疼场景

之前我所在的公司,产品线很多,大概三十多个微服务,每个微服务都有自己的Git仓库,由不同的小团队维护。一开始还好,各管各的。可是随着版本迭代,问题就来了:每次上线前,我们需要把十几个服务的镜像版本、配置项全部核对一遍,然后在部署平台手拼一个“发布清单”。这个过程非常痛苦,经常出现服务A的v1.2配了服务B的v1.3后,发现B的v1.3依赖C的v1.4,但C的v1.4还没合并,最后只能临时改配置。更惨的是,一旦线上出问题要回滚,你根本说不清哪个版本组合是“刚才好的那一组”。

后来我们引入了Flux,GitOps拿到了入场券。可是又发现新问题:Flux本身可以监视单个Git仓库,然后自动把仓库里的Kubernetes资源部署到集群。当我们把三十多个仓库全部接到Flux里时,Flux确实是能每个仓库都同步,但这些仓库彼此之间是“各自为政”的,就像一群工人各自干活,但没人告诉他们“你干活之前要等隔壁老王干完”。这不叫联动,这叫“分别部署”。我们需要的是:跨Git仓库统一协调版本、配置和部署顺序,而且这套协调机制还要简单、可维护、能回滚。于是我开始研究怎么用Flux实现“多仓库联动架构”。

二、Flux 到底是什么,能吃吗

用最生活的话说,Flux就是一个住在Kubernetes集群里的“搬运工”。你告诉它:帮我盯着某几个Git仓库,只要仓库里的YAML变了,你就把它们拉到集群里应用。它不像市场里乱吹捧的“AI”,它很实在:拉代码、比较差异、应用资源、报告结果。

Flux有两个最核心的概念,你只要先理解这两个,后面就容易多了。

第一个叫 GitRepository。你可以把它理解为“取件指令”,它告诉Flux:去哪个Git服务器、哪个仓库、哪个分支或哪个tag把代码拉下来。

第二个叫 Kustomization。它告诉Flux:把刚才拉下来的代码,在哪个目录下进行“加工”,然后应用到集群。它还可以配置 prune(清理垃圾资源)、dependsOn(依赖谁先执行)等。

简单说吧,GitRepository 负责“拿菜”,Kustomization 负责“炒菜上桌”。当你有多个仓库时,自然就会有多个“拿菜”指令和多个“炒菜”指令。那么问题来了,怎么让这些“炒菜”的活按合适的顺序做,甚至让不同仓库的菜拼到一桌?这就回到了我们这篇文章要聊的话题。

三、多仓库联动到底难在哪

咱们把痛点掰开揉碎,无非以下几点:

第一,版本组合依赖。服务A的镜像版本是 v1.2.0,它需要服务B的配置字段 store.mode=redis,而服务B的镜像版本必须是 v2.1.0。如果B升到 v2.2.0,配置格式变了,A就崩了。所以我们要能锁定“一个发布组里的整体版本快照”。

第二,共享配置依赖。很多配置不是某个服务独有的,比如数据库连接串、开关标志、公共的告警阈值。这些配置如果散落在各个应用仓库里,很难统一改。但如果单独放一个配置仓库,又怎么让其他仓库的应用依赖它?

第三,部署顺序依赖。比如先要起一个 shared-configmap,再起 user-service,最后起 order-service。Flux默认是并发同步的,你得显式告诉它“谁先谁后”。

第四,回滚麻烦。假设现在有A、B、C三个服务分别来自三个仓库,它们的版本组合是 (A=1.2, B=2.1, C=1.0)。你发现这个组合有问题,想回到 (A=1.1, B=2.0, C=0.9)。如果没有统一协调,你得去三个仓库分别改引用版本,然后一个个触发同步,又慢又容易出错。

正是因为这四个问题,我们才需要一套“多仓库联动架构”,而不是简单地在Flux里创建一堆 GitRepository 对象。

四、一个简单可行的联动架构

我自己在实践中摸索出一个比较顺手的方案,核心思想是:每个应用仓库只关心自己的部署清单,单独的配置仓库放共享配置,然后再搞一个“编排仓库”作为唯一的事实源。

具体来说,就是有三个角色:

  • 应用仓库:例如 order-service、user-service,里面除了代码,还要在 deploy/ 目录下写好自己的Kubernetes资源文件,比如Deployment、Service、HPA等。
  • 配置仓库:例如 shared-configs,里面放各个环境(dev、prod)的ConfigMap、Secret模板,以及一些公共的Kustomize patch。
  • 编排仓库:例如 gitops-cluster,这个仓库不写业务代码,只保存“发布配方”。它里面有一个 versions.json 文件,明确写清楚每个服务要从哪个仓库的哪个tag拉取,以及依赖顺序。

Flux只需要监视编排仓库这一个入口。当编排仓库中 versions.json 变了,或生成的文件变了,Flux就会按照新的“配方”去更新所有的 GitRepository 和 Kustomization,从而实现多仓库联动。

那么,谁来把 versions.json 变成一堆YAML文件呢?答案是一个小脚本。它最好用团队都会的编程语言来写,我这里选用Node.js来演示,因为好读、容易改,而且生态里有 js-yaml 库可以方便地输出YAML。

五、落地细节和示例

下面拿出一个完整示例。场景是这样:我们有 user-service 和 order-service 两个应用仓库,以及一个 shared-configs 配置仓库。它们都放在一个假想的Git服务器上。我们还要创建一个编排仓库,通过 versions.json 统一管控。

5.1 编排仓库的目录结构

gitops-cluster/
├── generated/               # 存放自动生成的YAML文件
├── scripts/
│   └── generate.js         # 生成脚本
└── versions.json           # 版本清单

5.2 版本清单文件 versions.json

我们来定义这么一份清单,注意看注释。

{
  "services": {
    "shared-configs": {
      "repo": "git@github.com:myteam/shared-configs.git",
      "version": "v1.0.2",
      "path": "./k8s"
    },
    "user-service": {
      "repo": "git@github.com:myteam/user-service.git",
      "version": "v2.3.1",
      "path": "./deploy",
      "dependsOn": ["shared-configs"]
    },
    "order-service": {
      "repo": "git@github.com:myteam/order-service.git",
      "version": "v1.4.0",
      "path": "./deploy",
      "dependsOn": ["user-service", "shared-configs"]
    }
  }
}

这份清单表达了三层意思:第一,shared-configs 仓库的 v1.0.2 是基础配置,谁都要先等它;第二,user-service 的 v2.3.1 依赖配置仓库;第三,order-service 的 v1.4.0 在启动之前,必须等 user-service 和 shared-configs 都已经同步成功。

5.3 用 Node.js 脚本自动生成 Flux 对象

接下来是核心示例。我们写一个脚本,读取上面的 versions.json,然后为每个服务生成一个 GitRepository 和一个 Kustomization,统一写到 generated/ 目录。这样每次改版本号,不用手改成堆YAML。

// 示例技术栈:Node.js 18 + js-yaml
// 运行前请执行:npm install js-yaml

const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');

// 这份版本清单其实是从 versions.json 读进来的
// 这里为了展示清晰,直接 require 进来,注意 Node 会缓存,所以实际用的时候建议 fs.readFileSync
const versions = require('./versions.json');

// 生成 GitRepository 对象
function buildGitRepository(serviceName, cfg) {
  return {
    apiVersion: 'source.toolkit.fluxcd.io/v1',
    kind: 'GitRepository',
    metadata: {
      name: serviceName,           // 对象名字,也就是后面 Kustomization 引用它的名字
      namespace: 'flux-system',    // 统一放到 flux-system 命名空间
    },
    spec: {
      interval: '1m',              // 每分钟检查一次远程仓库
      url: cfg.repo,               // 仓库地址
      ref: { tag: cfg.version },  // 关键点:锁定版本号,实现“版本快照”
    },
  };
}

// 生成 Kustomization 对象
function buildKustomization(serviceName, cfg) {
  const ks = {
    apiVersion: 'kustomize.toolkit.fluxcd.io/v1',
    kind: 'Kustomization',
    metadata: {
      name: serviceName,
      namespace: 'flux-system',
    },
    spec: {
      interval: '1m',
      path: cfg.path,                        // 仓库里哪个目录下面有 kustomization.yaml 或资源
      sourceRef: {
        kind: 'GitRepository',
        name: serviceName,                   // 绑定上面那个 GitRepository
      },
      prune: true,                           // 应用停止的资源要自动清理
    },
  };

  // 如果配置了 dependsOn,就把它塞进 spec.dependsOn
  if (Array.isArray(cfg.dependsOn) && cfg.dependsOn.length > 0) {
    ks.spec.dependsOn = cfg.dependsOn.map((depName) => ({ name: depName }));
  }

  return ks;
}

// 清理旧的生成目录,防止删掉的服务残留旧文件
fs.rmSync('./generated', { recursive: true, force: true });
fs.mkdirSync('./generated');

// 遍历版本清单里的每一个服务
for (const [name, cfg] of Object.entries(versions.services)) {
  const gitRepo = buildGitRepository(name, cfg);
  const kustomization = buildKustomization(name, cfg);

  // 写到 generated 目录,文件名用 <服务名>-repo.yaml 和 <服务名>-kust.yaml
  fs.writeFileSync(
    path.join('./generated', `${name}-repo.yaml`),
    yaml.dump(gitRepo),                     // 对象转成 YAML 字符串
    'utf8'
  );
  fs.writeFileSync(
    path.join('./generated', `${name}-kust.yaml`),
    yaml.dump(kustomization),
    'utf8'
  );
}

console.log('搞定!去 generated 目录看看生成的YAML吧。');

脚本跑完之后,你会看到类似下面的文件。

5.4 生成的配置长什么样

以 shared-configs 为例,生成的 shared-configs-repo.yaml 是这样的:

apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: shared-configs
  namespace: flux-system
spec:
  interval: 1m
  url: git@github.com:myteam/shared-configs.git
  ref:
    tag: v1.0.2

而 shared-configs-kust.yaml 是这样的:

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: shared-configs
  namespace: flux-system
spec:
  interval: 1m
  path: ./k8s
  sourceRef:
    kind: GitRepository
    name: shared-configs
  prune: true

user-service 的Kustomization则多出 dependsOn:

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: user-service
  namespace: flux-system
spec:
  interval: 1m
  path: ./deploy
  sourceRef:
    kind: GitRepository
    name: user-service
  prune: true
  dependsOn:
    - name: shared-configs

order-service 的依赖就丰富一点,因为它在 user-service 和 shared-configs 都同步成功之后才会开始拉起。

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: order-service
  namespace: flux-system
spec:
  interval: 1m
  path: ./deploy
  sourceRef:
    kind: GitRepository
    name: order-service
  prune: true
  dependsOn:
    - name: shared-configs
    - name: user-service

看到没有,你只需要维护一个 versions.json,剩下这些繁琐的YAML都由脚本生成。而且这个脚本完全可以放进CI里,每次版本变更后自动生成并提交到编排仓库。Flux检测到编排仓库的变更,就会按顺序去更新各个子仓库的引用。

5.5 配置依赖怎么真正生效

可能有人问:你把 shared-configs 同步进集群了,但 user-service 怎么用到里面的ConfigMap呢?这里有两种常用做法。

第一种,应用仓库里的Kustomization直接引用集群里已存在的ConfigMap。比如 user-service/deploy/kustomization.yaml 中写一个 configMapGenerator,里面引用 shared-configs 生成的ConfigMap?其实不行,因为ConfigMapGenerator只能在本仓库内生成。更推荐用 configMapRef 的方式,也就是Deployment里通过 envFrom 或 volumes 引用 shared-configs 提供的ConfigMap。只要 shared-configs 这个对象先被Flux创建出来,后面应用的Deployment在调度时就能从集群里找到它。

第二种,也是我比较喜欢的:在 shared-configs 仓库里放一个来自配置文件的Kustomization资源,它把一行行配置变成 ConfigMap。其他应用仓库则在Deployment的 envFrom 里明确引用这个ConfigMap。这样,代码库里只写“我要用哪个配置”,而真正的配置内容存放在集中仓库。改造起来也不复杂,但要把应用仓库里的硬编码配置字段全部替换成引用变量。

举例来说,user-service 的Deployment里可以这样写:

envFrom:
  - configMapRef:
      name: shared-config-map

这样当 shared-configs 仓库里的 shared-config-map 更新后,重启后的服务就会拿到新配置。不过这是另一个话题,核心思想是:跨仓库协调的重点,在于让各自仓库的Kubernetes资源能够“看见”对方创建的资源,而不是直接把内容复制来复制去。

六、应用场景与优缺点

6.1 到底适合什么场景

第一,微服务数量多、团队多的公司。如果只有三五个仓库,手动改配置也还凑合,根本不需要这套复杂机制。可一旦超过十个,尤其是跨团队发布版本时要一起上线,就非常值得用。

第二,需要环境隔离和灰度发布。我们的一个客户场景里,开发环境同意用大版本号倒数第二新的组合,生产环境则用最新的稳定组合。不同环境对应不同的 versions-env.json,通过脚本生成到不同目录,Flux针对不同环境指定不同的Kustomization,就能做到多环境独立联动。

第三,频繁需要“一键回滚到某个时间点”。因为编排仓库里记录了每次提交对应的 versions.json 快照,一旦出了事故,直接把编排仓库回退到上一个提交,所有GitRepository 的tag引文都会跟着回退,Flux会自动把集群恢复到那个状态。

6.2 优点有哪些

这套架构最明显的好处,是把“每个仓库各自的版本”变成了“一个编排仓库里的清单”,所有决定都集中在一个可审查、可追溯的地方。另外,脚本自动生成YAML,减少手写错误,也省掉了大量重复工作。因为每个服务都是一个独立的 GitRepository 对象,Flux只会在那个仓库真正变化时才拉取,所以性能上不会成为瓶颈。配合 dependsOn,还能优雅地控制部署顺序,避免“数据库配置还没就绪就拉起服务”的尴尬。

6.3 缺点也不能不提

首先,引入了一个“生成的中间层”,也就是 versions.json 和 generate.js。团队成员需要理解这个脚本的工作方式,否则容易产生“我改了版本但没跑脚本”的低级错误。其次,dependsOn 只是一个粗粒度的先后顺序,它并不能代表服务已经“就绪”(比如Deployment副本数达到目标)。如果你等的是服务探针检查通过,Flux的Kustomization本身不提供这种能力,你得搭配 Flux notifications 和外部健康检查,这会更复杂。第三,每个服务都需要单独建一个 GitRepository 和 Kustomization 资源,当服务数量暴涨到几百个时,Flux管理的资源对象数量会很多,需要适当增加内存限制。

七、注意事项

实践中有几个小坑,记下来可以帮你少走弯路。

注意一,确保你用的Flux版本足够新。dependsOn 字段在Flux v2的早期版本中是有的,但要小心不同版本的YAML结构略有差异。建议使用当前稳定版。

注意二,每个子仓库里必须有 kustomization.yaml 文件,否则Flux的Kustomization会构建失败。你可以自己在子仓库里创建,也不难,关键是别漏了。

注意三,版本清单中的 dependsOn 不能形成循环。比如A依赖B,B依赖C,结果C又依赖A,Flux会对这个循环直接报错,而且不好排查。写脚本时最好加一个拓扑检查,当然我上面示例里没有加,实际生产建议加上。

注意四,对于私有仓库,GitRepository 需要配置认证Secret。Flux bootstrap时创建的Secret可能只覆盖编排仓库,子仓库如果是私有的,你要在 GitRepository 的 spec.secretRef 里指定一个能访问子仓库的Secret。

注意五,不要把所有环境的版本放在一个 versions.json 里。按环境拆分,比如 versions.dev.json、versions.prod.json,然后用脚本参数指定要生成哪个环境。这样能降低改错环境的概率。

注意六,生成的YAML应该连同 versions.json 一起提交到编排仓库,而不是只在CI服务器上保留。因为GitOps的黄金法则是:Git仓库是唯一事实源,你只有把生成的YAML也提交上去,集群里才能看到。

八、总结

多仓库联动本质上不是Flux特有的难题,而是一个“如何把多组外部输入组合成一组可协调部署”的工程问题。Flux本身已经能高效地监视单个仓库,我们只要在外面套一层“编排仓库 + 版本清单 + 自动生成脚本”,就能像指挥家一样,让不同Git仓库里的应用按照同一个乐谱演奏。这样既能保留各个团队独立的仓库所有权,又能获得一个集中、可审计、可回滚的发布入口。虽然这套方案不是“银弹”,它需要你维护额外的脚本和配置,但对于几十个微服务、多团队协作的场景来说,一次投入,长期受益。希望我踩过的这些坑和这套思路,能帮你在构建自己的Flux多仓库联动架构时,少走几段弯路。