GitOps CD:用 FluxCD + KubeVela 把镜像自动发布到 K8s

GitOps CD:用 FluxCD + KubeVela 把镜像自动发布到 K8s

这是《Gitea Actions 全攻略:把 GitHub Actions 私有化》的续篇。

前篇我们搭起了一条完全内网的 CI:git push tag → runner 构建多架构镜像 → 推到 Zot registry。但镜像推上去之后就静静躺在那儿了——谁来把它部署到 K8s?每次都手动 kubectl apply、手动改 image tag、手动滚到所有集群?那违背了「内网自动化」的初衷。

本篇就补上这一块:让一次 git commit 等于一次发布。读完你能搭出这条链路:

1
改 manifests 里的 image tag → git commit → Flux 拉到集群 → KubeVela 渲染 → Pod Ready

全程不出内网,全程声明式。

一、先问一个为什么:CI 之后还差什么

回顾前篇结尾的「四块拼图」:Gitea server、gitea-runner、Action mirror、私有 registry。拼完它,你拥有了一套构建链路。但完整的 DevOps 还差最后一步:把构建产物部署到集群

传统做法(CI 直接部署)有几个毛病:

  • CI runner 通常拿不到集群凭证(或者为了拿凭证,把 kubeconfig 塞进 CI secret,安全隐患大)。
  • 部署逻辑写在 CI yaml 里,和构建逻辑混在一起,谁部署的、部署了什么、什么时候部署的,审计困难。
  • 多集群时,CI 要 ssh 到每台机器执行,扩展性差。

GitOps 给了另一种范式:集群主动去拉 git,而不是 CI 主动推集群

  • git 仓库是唯一的事实来源(single source of truth)。
  • 集群里有个控制器,定时 clone git,发现 diff 就 apply。
  • 你想发布?改 git 提交就行。想回滚?git revert
  • 谁发布的、发布了什么、什么时候,全部在 git log 里,天然可审计。

这就是本篇要做的事。技术选型是 FluxCD + KubeVela

二、选型:为什么 FluxCD + KubeVela,而不是二选一

GitOps 工具两个主流:Argo CD 和 FluxCD。

Argo CD FluxCD
风格 重 UI、可视化同步状态 纯 CLI、声明式 CR 驱动
心智负担 一堆概念(Application/AppProject/Repository) 就三个 CR:GitRepository / Kustomization / HelmRelease
适合 喜欢点按钮、看图的人 喜欢 git diff、喜欢把一切写进 yaml 的人

我选 FluxCD。它够纯——纯 GitOps,纯声明式,没有 UI 反而强迫你把状态都落到 git 里,这恰恰是 GitOps 的本意。

但纯 Flux 有个痛点:它只负责「把目录里的 yaml 同步到集群」,你得自己写一坨原生 K8s yaml(Deployment + Service + Ingress + Secret…)。对一个简单 web 服务来说太啰嗦。

KubeVela 就是来填这个缺口的。 它是 OAM(Open Application Model)的 K8s 实现,提供一个 Application CRD,让你用「应用中心」的抽象来描述服务:

1
2
3
4
5
6
7
spec:
components:
- name: my-app
type: webservice # ← 这一行顶你十行原生 yaml
properties:
image: my-registry/my-app:v1.0.0
port: 8080

type: webservice 这个组件类型,KubeVela controller 会自动帮你渲染出 Deployment + Service。你只声明「我要一个镜像跑在 8080 端口」,剩下的交给 controller。

所以最终架构是分层的:

1
2
3
你写的:      KubeVela Application   (应用抽象,薄薄一层)
渲染成: Deployment / Service (原生 K8s 资源)
谁触发渲染: Flux 同步 git → apply Application → KubeVela controller 接管

Flux 负责「同步」,KubeVela 负责「抽象」,各司其职。 一句话总结为什么是这俩组合:Flux 让 git 成为事实来源,KubeVela 让你写在 git 里的东西足够简洁。

💡 为什么不用 KubeVela 自带的 addon fluxcd?

我试过。KubeVela 官方的 fluxcd addon(catalog 3.0.1)有个硬伤:它的 HelmRepository 不支持 insecure 字段。而我的 Zot registry 走纯 http、无证书,Flux source-controller 拉 chart 时会因 https 握手失败。

所以我做了个定制版 vela-core chart,把改造后的 FluxCD 控制器直接渲染进 chart 的 templates/,镜像地址全替换成内网。本篇第三节讲 Flux 安装时会用到这个改造的产物。

三、装 FluxCD(内网镜像替换)

FluxCD 的安装哲学是「导出 yaml → 替换镜像 → apply」。一切都在你掌控的 yaml 里,不依赖任何 Helm repo。

3.1 装 flux CLI + 导出 yaml

1
2
3
4
5
# 装 flux CLI
curl -s https://fluxcd.io/install.sh | sudo bash

# 导出整套控制器 yaml(source + kustomize + helm 三个 controller)
flux install --components="source-controller,kustomize-controller,helm-controller" --export > fluxcd.yaml

导出来的 fluxcd.yaml 大约 8700 行,包含:

1
2
3
4
5
8 个 CRD(GitRepository/Kustomization/HelmRepository/HelmRelease 等)
3 个 Deployment(source/kustomize/helm controller)
3 个 ServiceAccount + RBAC
3 个 NetworkPolicy
1 个 Namespace(flux-system

3.2 把镜像拷到 Zot

fluxcd.yaml 里的镜像都是 ghcr.io/fluxcd/...,内网拉不到。先用 skopeo 把三个 controller 镜像拷到 Zot:

1
2
3
4
5
6
7
8
9
10
11
skopeo copy docker://ghcr.io/fluxcd/source-controller:v1.7.4 \
docker://172.29.102.221:5000/fluxcd/source-controller:v1.7.4 \
--dest-tls-verify=false --dest-creds="boer:Admin@123"

skopeo copy docker://ghcr.io/fluxcd/kustomize-controller:v1.7.3 \
docker://172.29.102.221:5000/fluxcd/kustomize-controller:v1.7.3 \
--dest-tls-verify=false --dest-creds="boer:Admin@123"

skopeo copy docker://ghcr.io/fluxcd/helm-controller:v1.4.5 \
docker://172.29.102.221:5000/fluxcd/helm-controller:v1.4.5 \
--dest-tls-verify=false --dest-creds="boer:Admin@123"

3.3 替换 + apply

sed 一把梭,把 fluxcd.yaml 里的 ghcr.io 全换成内网:

1
2
sed -i 's#ghcr.io#172.29.102.221:5000#g' fluxcd.yaml
kubectl apply -f fluxcd.yaml

确认三个 controller 起来了:

1
2
3
4
kubectl -n flux-system get pods
# source-controller-xxx-xxx Running
# kustomize-controller-xxx-xxx Running
# helm-controller-xxx-xxx Running

这套 fluxcd.yaml 我直接存进了仓库的 gitops/fluxcd.yaml,开箱即用,不用再导出。

四、装 KubeVela(定制 chart)

KubeVela 用 helm 装。我用的是自己定制的 chart(基于官方 1.10.6),它额外内嵌了三样东西:

  1. FluxCD 控制器 —— 就是第三节那个 insecure 问题的解决方案,渲染产物直接放进 chart 的 templates/。这样 KubeVela 和 FluxCD 一个 helm install 全装好。
  2. local-path-storage —— rancher 的本地路径 provisioner,给集群一个默认 StorageClass。
  3. 两个 CUE 定义 —— helm-labels.cue / helm-release-def.cue,让 KubeVela 能管理 Flux 的 HelmRelease(本篇用不到,但留了扩展口)。

chart 推到 Zot 后,一行 helm install:

1
2
3
4
5
helm install --create-namespace -n vela-system kubevela \
oci://172.29.102.221:5000/charts/vela-core:1.10.6 \
--set imageRegistry="172.29.102.221:5000/" \
--set multicluster.enabled=false \
--wait
  • --set imageRegistry —— 把 chart 里所有镜像加内网前缀。
  • multicluster.enabled=false —— 关掉多集群,单集群够用,省资源。

装完确认 Application CRD 就位:

1
2
kubectl get crd applications.core.oam.dev
# applications.core.oam.dev 2026-xx-xx

五、核心配置一:delivery.yaml(Flux 同步规则)

这是本篇核心之一。Flux 同步需要三个 CR,我写在一个文件 gitops/delivery.yaml 里,完整代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# gitops/delivery.yaml
apiVersion: v1
kind: Secret
metadata:
name: git-credentials
type: Opaque
stringData:
username: boer
password: a8dbe0667bc58d78191545c407ba9f1ddfcc9d1c # Gitea token
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: actions-demo
spec:
interval: 1m
url: http://172.29.102.221:3000/actions/demo.git
ref:
branch: main
secretRef:
name: git-credentials
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: actions-demo
spec:
interval: 1m
prune: true
timeout: 3m
wait: true
sourceRef:
kind: GitRepository
name: actions-demo
path: ./manifests

逐段拆解:

Secret git-credentials —— Gitea 认证

1
2
3
stringData:
username: boer
password: a8dbe0667bc58d78191545c407ba9f1ddfcc9d1c

password 是一个 access token,不是登录密码。在 Gitea Web UI:头像 → Settings → Applications → Generate New Token,勾选 repository:read(只读就够,Flux 只需要 clone)。生成的 token 长这样一串十六进制。

GitRepository actions-demo —— 告诉 Flux 去哪 clone

1
2
3
4
5
6
7
spec:
interval: 1m # 轮询频率,1 分钟够灵敏
url: http://172.29.102.221:3000/actions/demo.git
ref:
branch: main # 监听 main 分支
secretRef:
name: git-credentials # 引用上面的 Secret 认证

source-controller 每分钟 clone 一次这个仓库,发现新 commit 就更新内部 artifact。

Kustomization actions-demo —— 告诉 Flux 同步哪个目录

1
2
3
4
5
6
7
8
9
spec:
interval: 1m
prune: true # ← 关键:git 里删了,集群里也删
timeout: 3m
wait: true # 等 apply 的资源 Ready 才算成功
sourceRef:
kind: GitRepository
name: actions-demo
path: ./manifests # 同步这个目录
  • prune: true 是 GitOps 的核心承诺:集群状态 = git 状态。git 里删了某个资源,集群里也删掉,不会留垃圾。
  • wait: true + timeout: 3m:apply 完等资源 Ready,3 分钟没 Ready 就报错,避免静默故障。

apply 它

1
kubectl apply -f gitops/delivery.yaml

⚠️ 名字撞车警告:kustomize 有个资源类型叫 Kustomizationkustomize.config.k8s.io/v1beta1),Flux 也有个 CR 叫 Kustomizationkustomize.toolkit.fluxcd.io/v1)。两个完全不同的东西:前者是「目录怎么打包」,后者是「什么时候同步」。本节的 delivery.yaml 用的是后者。

六、核心配置二:manifests/(KubeVela 应用定义)

Flux 现在每分钟去拉 actions/demo.git./manifests 目录。我们得在那个仓库里放点东西——就是 KubeVela 的 Application。两个文件,放在前篇那个 actions 仓库下:

1
2
3
4
5
6
7
actions/
├── .gitea/workflows/actions.yaml ← 前篇的 CI workflow
├── Dockerfile ← 前篇的构建
├── main.go
└── manifests/ ← 本篇新增
├── namespace.yaml
└── application.yaml

6.1 manifests/namespace.yaml

1
2
3
4
5
# actions/manifests/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: actions

Application 要部署到 actions namespace,得先有这个 namespace。

6.2 manifests/application.yaml —— KubeVela 应用定义

完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# actions/manifests/application.yaml
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: actions-demo
namespace: actions
spec:
components:
- name: actions-demo
type: webservice
properties:
image: 172.29.102.221:5000/actions/demo:v1.0.3 # ← 发版时改这一行
imagePullPolicy: IfNotPresent
exposeType: NodePort
ports:
- port: 8080
expose: true
nodePort: 30080

这一小段 yaml 顶下面这一大坨原生 K8s yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 等价的原始写法(KubeVela 帮你省掉的)
apiVersion: apps/v1
kind: Deployment
metadata:
name: actions-demo
namespace: actions
spec:
replicas: 1
selector:
matchLabels: { app.oam.dev/component: actions-demo }
template:
metadata:
labels: { app.oam.dev/component: actions-demo }
spec:
containers:
- name: actions-demo
image: 172.29.102.221:5000/actions/demo:v1.0.3
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: actions-demo
namespace: actions
spec:
type: NodePort # exposeType: NodePort 渲染出来的
selector:
app.oam.dev/component: actions-demo
ports:
- port: 8080
targetPort: 8080
nodePort: 30080 # 集群外访问 :30080

逐字段拆 application.yaml:

字段 作用
type: webservice KubeVela 内置组件类型,自动渲染 Deployment + Service
image 来自前篇 CI 构建出的镜像。发版时只改这一行
imagePullPolicy: IfNotPresent 节点已有就不拉,省内网带宽
exposeType: NodePort Service 类型。生产里可换 ClusterIP + Ingress
ports.port: 8080 容器监听端口(main.go 里 app.Listen(":8080")
ports.expose: true 这个端口对外暴露(生成 Service)
ports.nodePort: 30080 节点端口,集群外 curl <nodeIP>:30080 访问

省心程度立见高下。type: webservice 这一行,KubeVela controller 看到 Application 被 apply,自动把 Deployment + Service 渲染出来。image 来自前篇 CI 构建的 actions/demo:v1.0.3——闭环了。

6.3 不需要 kustomization.yaml

你可能注意到目录里没有 kustomization.yaml。这是有意的——Flux 的 kustomize-controller 遇到没有 kustomization.yaml 的目录,会把目录里所有 .yaml 当作普通资源直接 apply(叫「Kustomize 无构建模式」)。对于几个文件的小项目,省一个文件。如果目录变复杂、需要 patches/transformers,再加 kustomization.yaml

七、跑起来:从 git push 到 Pod Ready

现在把整条链路走一遍。假设前篇已经构建出 actions/demo:v1.0.3

① 首次部署 —— 把 manifests 推到 git:

1
2
3
4
cd actions/
git add manifests/
git commit -m "feat: add kubevela application for gitops"
git push

Flux 最多 1 分钟内拉到,自动 apply。

② 后续发版 —— 改 image tag 提交:

1
2
3
4
5
6
7
8
9
# 前篇流程:打 tag 触发 CI 构建出 v1.0.4
git tag v1.0.4 && git push origin v1.0.4
# CI 跑完,镜像 actions/demo:v1.0.4 进了 Zot

# 本篇流程:改 manifests 里的 tag,提交
vim manifests/application.yaml # image: ...:v1.0.3 → v1.0.4
git add manifests/application.yaml
git commit -m "bump actions-demo to v1.0.4"
git push

Flux 1 分钟内拉到新 commit → apply 新 Application → KubeVela 重新渲染 Deployment → rolling update → 新 Pod Ready。

③ 访问验证

1
2
curl http://172.29.102.221:30080
# Hello, World!

整个发版过程就是一次 git commit。没有 kubectl,没有「部署」按钮,没有人在集群里手敲命令。git commit 等于发布。这就是 GitOps。

八、排查:出问题时看这几条命令

GitOps 链路长,排查要分层。从 git 到 Pod,每层都有对应的 CR / 资源可看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# ① Flux: git 拉到了吗?
kubectl describe gitrepositories.source.toolkit.fluxcd.io actions-demo
# 看 Status.Conditions,artifact 就绪 = 已 clone

# ② Flux: 目录 apply 了吗?
kubectl describe kustomizations.kustomize.toolkit.fluxcd.io actions-demo
# 看 Status.Conditions,Ready=True = 已 apply

# ③ KubeVela: Application 渲染了吗?
kubectl -n actions describe application actions-demo
# 看 Status.Phase,running = 已渲染

# ④ KubeVela: 渲染出啥了?
kubectl -n actions get deploy,svc
# 应该看到 actions-demo 的 Deployment 和 Service

# ⑤ Pod: 起来了吗?
kubectl -n actions get pods

常见卡点对照表:

现象 原因 排查
GitRepository 报 401 token 过期或没勾 repository:read 重新生成 token,更新 Secret
Kustomization 一直 NotReady path 路径错,或 yaml 语法错 看 kustomization 的 Status 里 error 信息
Application 状态 workflowFinished 但没 Deployment namespace 不存在 确认 namespace.yaml 已 apply
Pod ImagePullBackOff 节点没配 insecure registry,或 tag 拼错 crictl pull <image> 手动验证
改了 git 但集群没更新 Flux 还没轮询到(1 分钟周期) 等,或 flux reconcile 手动触发

九、总结:发版闭环

把前篇和本篇拼起来,一条完全私有、声明式、可审计的 CI/CD 链路:

1
2
3
4
5
6
7
开发机:  改代码 → git push tag v1.x
Gitea: 触发 release workflow
runner: docker build → push 镜像到 Zot
发版人: 改 manifests/application.yaml 的 image tag → git commit
Flux: 1 分钟内拉到新 commit → apply Application
KubeVela: 渲染 Deployment → rolling update
集群: 新版本 Pod Ready,:30080 可访问

涉及的文件清单(都在你的仓库里):

文件 作用
gitops/fluxcd.yaml FluxCD 控制器安装 yaml(8700 行,镜像已替换内网)
gitops/delivery.yaml Flux 同步规则(GitRepository + Kustomization + Secret)
actions/manifests/application.yaml KubeVela 应用定义(webservice 抽象)
actions/manifests/namespace.yaml actions namespace
charts/vela-core/ 定制版 KubeVela chart(集成 FluxCD)

一句话记住核心:CI 把代码变成镜像(前篇),CD 把镜像变成集群里跑的服务(本篇),中间的桥梁是一个存了 image tag 的 git 仓库

下一站大概是可观测性(监控 + 日志 + 链路追踪),或者镜像签名准入(Cosign + policy-controller),那又是另一个故事。


合集导航


GitOps CD:用 FluxCD + KubeVela 把镜像自动发布到 K8s
https://www.boer.xyz/posts/gitops-fluxcd-kubevela/
作者
boer
发布于
2026年8月5日
许可协议