使用 FluxCD + GitHub + SOPS + age 构建完整 GitOps 部署流程
使用 FluxCD + GitHub + SOPS + age 构建完整 GitOps 部署流程
最近我将自己的 Kubernetes 部署方式从手工 Helm 部署逐步迁移到了 FluxCD 管理的 GitOps 模式。
最终希望实现的目标是:
1 | GitHub |
其中两个 GitHub Repository 都是 Private Repository。
同时,由于 FluxCD 访问 hoteler-deployment 需要 SSH Deploy Key,因此又引出了一个新的问题:
GitOps 仓库本身应该如何安全地管理 Secret?
最终我选择了:
1 | FluxCD + GitHub + SSH Deploy Key + SOPS + age |
来完成整个部署流程。
本文记录完整配置过程,以及过程中遇到的一个比较典型的 SOPS Bootstrap “鸡生蛋”问题。
一、整体架构
最终的结构大致如下:
1 | GitHub |
这里我把职责进行了拆分。
fleet-infra 负责:
1 | 集群级 GitOps 配置 |
hoteler-deployment 负责:
1 | 应用 Kubernetes manifests |
这样应用部署配置和集群基础设施配置不会完全混在一起。
二、使用 Flux Bootstrap 接入 GitHub
首先准备 GitHub Personal Access Token。
我使用的是 GitHub Fine-grained Personal Access Token。
环境变量:
1 | export GITHUB_TOKEN="github_pat_xxxxxxxxx" |
然后执行:
1 | flux bootstrap github \ |
这里有一个小坑。
一开始我使用:
1 | --owner=$GITHUB_USER |
但是没有正确设置:
1 | GITHUB_USER |
于是出现:
1 | failed to get Git repository "https://github.com//fleet-infra" |
注意错误 URL:
1 | https://github.com//fleet-infra |
这里实际上已经说明 owner 是空的。
如果不想额外维护 GITHUB_USER,直接写:
1 | --owner=damingerdai |
即可。
Bootstrap 成功之后,GitHub 中的:
1 | fleet-infra |
就成为 FluxCD 的 Source of Truth。
目录类似:
1 | fleet-infra/ |
检查:
1 | flux get sources git |
可以看到:
1 | NAME REVISION READY |
三、让 FluxCD 读取另一个 GitHub Private Repository
我的应用 Kubernetes 配置并不在 fleet-infra,而是在另一个私有仓库:
1 | damingerdai/hoteler-deployment |
因此 FluxCD 还需要获得读取这个仓库的权限。
这里没有继续使用 GitHub PAT,而是使用:
1 | SSH Deploy Key |
这样可以把权限严格限制在:
1 | hoteler-deployment |
这一个 Repository。
生成 Deploy Key
1 | mkdir -p ~/.ssh/flux |
得到:
1 | ~/.ssh/flux/hoteler-deployment |
其中:
1 | hoteler-deployment |
是私钥。
1 | hoteler-deployment.pub |
是公钥。
将公钥添加到:
1 | GitHub |
因为 FluxCD 目前只负责读取配置,所以:
1 | Allow write access |
不需要开启。
测试:
1 | ssh \ |
GitHub 返回:
1 | Hi damingerdai/hoteler-deployment! |
说明 Deploy Key 已经正常工作。
四、为什么还需要 SOPS + age
现在出现了另一个问题。
FluxCD 要读取:
1 | hoteler-deployment |
就必须持有:
1 | SSH Private Key |
最简单的做法当然是:
1 | kubectl create secret ... |
但是这样做会导致 GitOps 不完整。
如果未来服务器重装:
1 | K3s 重建 |
还是需要人工重新创建 Secret。
因此我希望:
Secret 也能够由 fleet-infra 管理。
但又绝对不能把:
1 | -----BEGIN OPENSSH PRIVATE KEY----- |
直接提交到 Git。
这就是 SOPS + age 要解决的问题。
五、SOPS 和 age 分别是什么
可以简单理解为:
1 | SOPS |
组合起来:
1 | secret.yaml |
FluxCD 中保存:
1 | age private key |
因此 Flux 可以:
1 | GitHub |
六、安装 SOPS 和 age
我的服务器使用 Debian。
安装 age:
1 | apt install age |
确认:
1 | age --version |
例如:
1 | 1.1.1 |
安装 SOPS:
1 | curl -LO \ |
确认:
1 | sops --version |
七、生成 age Key
创建:
1 | mkdir -p ~/.config/sops/age |
里面包含:
1 | # public key: age1xxxxxxxxxxxx |
其中:
1 | age1xxxx |
是公钥,可以公开。
但是:
1 | AGE-SECRET-KEY-1... |
是私钥,不能提交 Git。
八、让 Flux 获得 age Private Key
创建 Kubernetes Secret:
1 | kubectl create secret generic sops-age \ |
检查:
1 | kubectl get secret sops-age -n flux-system |
例如:
1 | NAME TYPE DATA |
这个 Secret 是整个 GitOps Bootstrap 中少数仍然需要在初始化阶段注入的 Secret。
九、配置 SOPS
在 fleet-infra 根目录创建:
1 | .sops.yaml |
内容:
1 | creation_rules: |
这里的:
1 | age1xxxx |
就是刚才生成的 age Public Key。
以后:
1 | *.secret.yaml |
文件中的:
1 | data: |
或者:
1 | stringData: |
都会被 SOPS 加密。
十、让 Flux Kustomization 支持 SOPS
Flux Bootstrap 生成的:
1 | clusters/107.172.148.131/flux-system/gotk-sync.yaml |
中存在:
1 | apiVersion: kustomize.toolkit.fluxcd.io/v1 |
需要增加:
1 | decryption: |
最终:
1 | spec: |
十一、一个非常重要的坑:SOPS Bootstrap 鸡生蛋问题
这里遇到了整个配置过程中最值得记录的问题。
我第一次配置的时候,在同一个 Git revision 中同时提交了:
1 | ① flux-system 增加 SOPS decryption |
理论上看起来没问题。
但实际上产生了一个循环依赖。
当前 Kubernetes 中正在运行的是:
1 | 旧 flux-system Kustomization |
它还没有:
1 | decryption: |
然后它从 GitHub 拉到了新的 revision。
新的 revision 同时包含:
1 | 新的 SOPS 配置 |
Flux 在 reconcile 时发现:
1 | Secret/flux-system/hoteler-deployment-auth |
但是当前运行中的 Kustomization 还没有 SOPS 解密能力。
于是直接报错:
1 | Secret/flux-system/hoteler-deployment-auth is SOPS encrypted, |
问题就在这里。
Flux 想 apply:
1 | 新的 decryption 配置 |
需要先 reconcile 这个 revision。
但是 reconcile 这个 revision 又需要:
1 | 新的 decryption 配置 |
于是形成:
1 | 需要 SOPS |
这就是一个典型的 Bootstrap 鸡生蛋问题。
十二、临时解决办法
当时直接手动 patch 当前 Kubernetes 中运行的 Kustomization:
1 | kubectl patch kustomization flux-system \ |
检查:
1 | kubectl get kustomization flux-system \ |
看到:
1 | decryption: |
然后:
1 | flux reconcile kustomization flux-system |
这一次成功:
1 | ✔ applied revision main@sha1:ee907279... |
十三、如何从一开始避免这个问题
正确做法其实非常简单:
不要在同一个 commit 中同时启用 SOPS 和添加第一个 SOPS Secret。
应该分成两个阶段。
Commit 1:只启用 SOPS
首先手动创建:
1 | kubectl create secret generic sops-age \ |
然后 Git 中只修改:
1 | gotk-sync.yaml |
增加:
1 | decryption: |
提交:
1 | git commit -m "feat: enable SOPS decryption" |
然后:
1 | flux reconcile kustomization flux-system |
确认:
1 | kubectl get kustomization flux-system \ |
确定 Kubernetes 中实际运行的 Kustomization 已经具有 SOPS 能力。
Commit 2:再添加 SOPS Secret
这时候再提交:
1 | hoteler-deployment-auth.secret.yaml |
Flux 已经具备解密能力,因此不会再产生循环依赖。
所以以后重新 Bootstrap 集群时,我会固定采用:
1 | 1. flux bootstrap github |
十四、使用 SOPS 加密 GitHub Deploy Key
首先生成 Kubernetes Secret YAML:
1 | kubectl create secret generic hoteler-deployment-auth \ |
注意:
此时这个文件仍然包含可以还原的 Secret,不能提交 Git。
然后:
1 | sops --encrypt --in-place \ |
检查:
1 | grep "ENC\[AES256_GCM" \ |
可以看到:
1 | identity: ENC[AES256_GCM,data:...] |
此时才可以提交 Git。
十五、配置 hoteler-deployment GitRepository
创建:
1 | clusters/107.172.148.131/hoteler-source.yaml |
内容:
1 | apiVersion: source.toolkit.fluxcd.io/v1 |
这里我又遇到了一个小问题。
最开始写的是:
1 | ref: |
Flux 报错:
1 | couldn't find remote ref "refs/heads/main" |
原因非常简单:
1 | hoteler-deployment |
默认分支实际是:
1 | master |
修改之后:
1 | flux get sources git |
成功:
1 | NAME REVISION READY |
至此:
1 | SOPS |
整条链路已经打通。
十六、让 Flux 部署 hoteler-api
hoteler-deployment 使用的是比较标准的 Kustomize 目录结构:
1 | hoteler-deployment/ |
其中:
1 | base |
保存通用 Kubernetes 配置。
而:
1 | overlays/107.172.148.131 |
保存当前服务器真正需要部署的配置。
因此 Flux 不应该直接指向:
1 | base |
而应该指向最终 Overlay。
在 fleet-infra 中创建:
1 | clusters/107.172.148.131/hoteler-api.yaml |
内容:
1 | apiVersion: kustomize.toolkit.fluxcd.io/v1 |
于是形成:
1 | hoteler-deployment |
十七、从 Helm 迁移到 FluxCD
这台 K3s 上原本已经通过 Helm 安装过 Hoteler。
检查:
1 | helm list -A |
可以看到:
1 | NAME NAMESPACE CHART |
原来的 Pod:
1 | hoteler-app |
由于新的 Flux Kustomization 将接管 Hoteler,因此不应该继续让:
1 | Helm |
和:
1 | Flux + Kustomize |
同时管理同一套 Kubernetes Resource。
否则会产生:
1 | Helm |
两个控制来源。
因此卸载旧 Helm Release:
1 | helm uninstall hoteler -n hoteler-app |
然后:
1 | flux reconcile kustomization hoteler-api |
Flux 返回:
1 | ✔ applied revision master@sha1:9fdb65cb... |
再次检查:
1 | helm list -A |
Hoteler 已经消失,只剩 K3s 自带的 Traefik:
1 | traefik |
检查 Pod:
1 | kubectl get pods -A |
新的 Hoteler 已经由 Flux 创建:
1 | NAMESPACE NAME |
并且:
1 | READY |
至此迁移完成。
十八、最终 GitOps 架构
整个系统最终形成:
1 | GitHub |
Secret 的链路则是:
1 | SSH Private Key |
十九、最终目录结构
fleet-infra 大致变成:
1 | fleet-infra/ |
而:
1 | hoteler-deployment/ |
负责应用自己的 Kubernetes 配置:
1 | hoteler-deployment/ |
两个仓库的职责比较清晰:
1 | fleet-infra |
二十、一些实践后的经验
这次迁移下来,有几个点值得记录。
1. Private Repository 不等于 Secret Store
即使:
1 | fleet-infra |
本身就是 Private Repository,也不应该直接提交:
1 | SSH Private Key |
Private Repository 解决的是仓库访问控制。
SOPS 解决的是:
1 | Secret at rest |
两者不是同一个安全层次。
2. GitRepository 尽量使用 Deploy Key
对于:
1 | Flux → GitHub Private Repository |
使用 SSH Deploy Key 的好处是权限天然限定在单个 Repository。
而且如果 Flux 只读:
1 | Allow write access = false |
即可。
3. SOPS Bootstrap 一定要分阶段
这是这次最重要的经验:
1 | 先让 Flux 会解密 |
不要反过来。
4. Flux Kustomization 应该指向 Overlay
对于:
1 | base + overlays |
结构:
1 | base |
只是公共模板。
真正的环境入口应该是:
1 | overlays/<environment> |
所以 Flux:
1 | path: ./hoteler-api/overlays/107.172.148.131 |
而不是:
1 | path: ./hoteler-api/base |
5. 一个 Resource 最好只有一个管理者
原来的:
1 | Helm |
和新的:
1 | Flux + Kustomize |
不应该长期同时管理同一个应用。
迁移完成后,应该明确:
1 | Git |
是唯一部署路径。
总结
完成这次迁移之后,我的 Hoteler 部署已经从:
1 | 人工执行 Helm |
变成:
1 | GitHub |
同时通过:
1 | SOPS + age |
解决 GitOps 中 Secret 的存储问题,通过:
1 | SSH Deploy Key |
解决 FluxCD 访问 GitHub Private Repository 的权限问题。
最终实现:
1 | Infrastructure as Code |
以后对 Hoteler 部署配置的修改,不再需要直接登录服务器执行:
1 | kubectl apply |
或者:
1 | helm upgrade |
而是修改 Git:
1 | hoteler-deployment |
这也是这次迁移最大的意义:
Kubernetes 集群不再依赖“我上次在服务器上执行过什么命令”,而是逐渐变成一个可以通过 Git 中的声明状态重新构建和恢复的系统。
