GitOps:这次怎么落地的

这次我以 ArgoCD 为例讲,但概念是通用的。

它建立在几个基本原则上:

  1. 声明式配置:用 YAML 或类似格式描述你想要的状态,而不是用脚本描述怎么达到这个状态
  2. 版本化:所有配置都放在 Git 里,每次变更都有记录
  3. 自动化:有进程自动对比期望状态和实际状态,并做同步
  4. 持续协调:这个同步过程是持续运行的,不是一次性任务

这套思路跟 Kubernetes 的设计哲学很契合。

为什么要折腾 GitOps

说实话,刚开始接触 GitOps 的时候,我持怀疑态度。以前的做法很传统:CI/CD 流水线跑完,最后一步用 kubectl 或者 Helm 命令把新版本部署上去。这套流程跑了好几年,除了偶尔有人手滑敲错命令,倒也没出过大问题。

直到有一次,线上某个服务突然挂了,大家开始排查问题。我们发现环境的实际状态跟任何人记忆里的都不一样:有人直接在服务器上改过配置,有人用 kubectl apply 覆盖过某些资源,还有一份 Helm values 文件在本地却没提交到仓库。最要命的是,没人记得这些操作具体是在哪天做的,也就没法准确定位问题根源。

从那之后,我开始认真考虑 GitOps。它的核心想法其实很简单:用 Git 仓库来存储目标状态,让系统自动把实际状态同步到目标状态。任何变更都必须通过 Git 的 Pull Request 流程,这样就有完整的审计历史,也方便回滚。

GitOps 到底是什么

GitOps 不是一个具体工具,而是一套方法论。它建立在几个基本原则上:

  1. 声明式配置:用 YAML 或类似格式描述你想要的状态,而不是用脚本描述怎么达到这个状态
  2. 版本化:所有配置都放在 Git 里,每次变更都有记录
  3. 自动化:有进程自动对比期望状态和实际状态,并做同步
  4. 持续协调:这个同步过程是持续运行的,不是一次性任务

这套思路跟 Kubernetes 的设计哲学很契合。Kubernetes 本身就是声明式的:你告诉它你想要三个 Pod,它就去弄三个出来,中间挂了就自动拉起新的。GitOps 把这个思想延伸到整个集群的配置管理上。

ArgoCD vs Flux:工具选择

市面上最常用的两个 GitOps 工具是 ArgoCD 和 Flux。我用过两个,简单说下感受。

ArgoCD 有个很友好的 Web UI,能看到集群当前状态、同步历史、资源依赖关系图。这对刚开始上手的人很友好,也能给不直接操作集群的同事一个视图。它的功能比较丰富,支持多集群、应用健康检查、自动回滚等。但功能多也意味着配置选项多,学习曲线陡一些。

Flux 相对更轻量,没有 UI,主要靠 CLI 和 CRD 操作。它的设计更贴近 Kubernetes 原生体验,如果你已经习惯了 kubectl 和 YAML,上手会快一些。Flux 2 之后重构了架构,变得更模块化,可以根据需要只启用部分组件。

如果你有可视化需求或者团队里有人不熟悉 kubectl,ArgoCD 可能更合适。如果追求轻量和简洁,或者已经在重度使用 GitOps 的各种高级功能,Flux 可能更对味。这次我以 ArgoCD 为例讲,但概念是通用的。

实战 ArgoCD

安装 ArgoCD

假设你已经有一个 Kubernetes 集群(本机 Minikube、云厂商托管都行),先安装 ArgoCD:

# 创建 namespace
kubectl create namespace argocd

# 安装 ArgoCD
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# 查看 pod 状态
kubectl get pods -n argocd

安装完成后,可以把 ArgoCD 的 API Server 暴露出来。生产环境建议用 Ingress,本地测试可以直接用 port-forward:

kubectl port-forward svc/argocd-server -n argocd 8080:443

然后访问 https://localhost:8080,默认用户名是 admin,初始密码需要从 secret 里获取:

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

# 登录后记得改密码
argocd account update-password

准备 Git 仓库

GitOps 的第一步是建一个 Git 仓库来存放你的应用配置。仓库结构可以按应用分,也可以按环境分,这个看你习惯。我习惯这样组织:

gitops-repo/
├── apps/
│   ├── app1/
│   │   ├── base/
│   │   │   ├── deployment.yaml
│   │   │   ├── service.yaml
│   │   │   └── kustomization.yaml
│   │   └── overlays/
│   │       ├── dev/
│   │       │   ├── kustomization.yaml
│   │       │   └── patch.yaml
│   │       └── prod/
│   │           ├── kustomization.yaml
│   │           └── patch.yaml
│   └── app2/
│       └── ...
├── clusters/
│   ├── dev/
│   │   └── argocd/
│   │       ├── app1.yaml
│   │       └── app2.yaml
│   └── prod/
│       └── argocd/
│           ├── app1.yaml
│           └── app2.yaml

这个结构用到了 Kustomize 来管理不同环境的差异。base/ 存放通用配置,overlays/ 下按环境放覆盖配置。clusters/ 目录里定义每个集群要部署哪些应用。

创建第一个应用

假设有一个简单的 Web 应用,准备一个 deployment 和 service:

# apps/webapp/base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp
  labels:
    app: webapp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: webapp
  template:
    metadata:
      labels:
        app: webapp
    spec:
      containers:
      - name: webapp
        image: nginx:1.24
        ports:
        - containerPort: 80
        resources:
          requests:
            memory: "64Mi"
            cpu: "50m"
          limits:
            memory: "128Mi"
            cpu: "100m"
# apps/webapp/base/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: webapp
spec:
  selector:
    app: webapp
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
  type: ClusterIP
# apps/webapp/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yaml
  - service.yaml
commonLabels:
  app: webapp

然后创建一个 Application 对象告诉 ArgoCD 去同步这个应用:

# clusters/dev/argocd/webapp.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: webapp
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/your-org/gitops-repo.git
    targetRevision: HEAD
    path: apps/webapp/overlays/dev
  destination:
    server: https://kubernetes.default.svc
    namespace: default
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
    - CreateNamespace=true

把这个文件 apply 到集群:

kubectl apply -f clusters/dev/argocd/webapp.yaml

如果一切正常,ArgoCD 会开始同步你的应用。在 UI 里应该能看到应用状态变成 “Synced”,集群里也能看到对应的 Pod 跑起来了。

实战踩坑

权限问题

刚开始的时候,我遇到过 ArgoCD 报权限错误:它试图创建某些资源但被 RBAC 拒绝。这是因为 ArgoCD 的 ServiceAccount 默认没有足够的权限。

解决办法是给 ArgoCD 的 ServiceAccount 绑定合适的 ClusterRole:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: argocd-application-controller
  namespace: argocd
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: argocd-application-controller
rules:
- apiGroups:
  - '*'
  resources:
  - '*'
  verbs:
  - '*'
- nonResourceURLs:
  - '*'
  verbs:
  - '*'
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: argocd-application-controller
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: argocd-application-controller
subjects:
- kind: ServiceAccount
  name: argocd-application-controller
  namespace: argocd

这个配置给了 ArgoCD 全局权限,生产环境里应该收紧一些,只给它需要的权限。

Git 仓库访问问题

ArgoCD 需要访问你的 Git 仓库。如果是公开仓库,直接用 HTTPS URL 就行。如果是私有仓库,需要配置访问凭据。

最简单的方式是用 SSH 密钥。先生成密钥对:

ssh-keygen -t ed25519 -N '' -f ~/.ssh/argocd -C "argocd"

把公钥加到你的 GitHub/GitLab 账户里,然后在 ArgoCD 里创建 secret:

# 从私钥创建 secret
kubectl create secret generic argocd-repo-creds \
  --from-file=sshPrivateKey=~/.ssh/argocd \
  --type=kubernetes.io/ssh-auth \
  -n argocd

然后在 Application 里把 repoURL 改成 SSH 格式:

spec:
  source:
    repoURL: [email protected]:your-org/gitops-repo.git

如果你用 HTTPS + Personal Access Token,可以这样配置:

kubectl create secret generic argocd-repo-creds \
  --from-literal=username=your-username \
  --from-literal=password=your-token \
  -n argocd

然后在 Application 里引用这个 secret:

spec:
  source:
    repoURL: https://github.com/your-org/gitops-repo.git
    repoCredsSecret:
      name: argocd-repo-creds

同步频率问题

ArgoCD 默认每 3 分钟同步一次状态。有时候你觉得太慢,希望改动更快生效。可以在 Application 里调整:

spec:
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
    - CreateNamespace=true
  # 添加这个字段
  ignoreDifferences:
  - group: apps
    kind: Deployment
    jsonPointers:
    - /spec/replicas

这个例子告诉 ArgoCD 忽略 Deployment 副本数的变化,避免某些自动扩缩容操作导致的同步冲突。如果你只是想加快同步频率,可以在 ArgoCD 的 ConfigMap 里调整:

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
  namespace: argocd
data:
  repo.server: "github.com"
  timeout.reconciliation: "30s"
  resource.exclusion: |
    - apiGroups:
      - cilium.io
      kinds:
      - CiliumIdentity
      clusters:
      - "*"

timeout.reconciliation 控制同步操作的超时时间,默认是 180 秒。这个值调小一些,失败的时候能更快发现。

资源依赖问题

有些资源之间有依赖关系,比如 Deployment 依赖 ConfigMap,Service 依赖 Pod。ArgoCD 默认的同步策略可能会导致顺序不对,先创建了 Deployment 但 ConfigMap 还没准备好,导致 Pod 启动失败。

ArgoCD 提供了几个 syncOptions 来处理这个问题:

spec:
  syncPolicy:
    syncOptions:
    - CreateNamespace=true
    - RespectIgnoreDifferences=true
    - ApplyOutOfSyncOnly=true
    # 按依赖顺序同步
    - ServerSideApply=true
    # 跳过不健康资源
    - SkipDryRunOnMissingResource=true

ServerSideApply=true 会用 Kubernetes 的 Server-Side Apply 特性,能更好地处理多字段修改冲突。SkipDryRunOnMissingResource=true 会在资源不存在时跳过预检,加快同步速度。

如果你需要明确控制同步顺序,可以在 Application 的 syncPolicy 里设置 syncOptions

spec:
  syncPolicy:
    syncOptions:
    - CreateNamespace=true
    - PrunePropagationPolicy=foreground
    - PruneLast=true

PruneLast=true 会先创建新资源,再删除旧资源,减少服务中断。PrunePropagationPolicy=foreground 会确保被删除资源(如 Pod)先被清理,再删除依赖它们的资源(如 Deployment)。

进阶用法

多集群管理

ArgoCD 可以管理多个 Kubernetes 集群。先添加远程集群的凭据:

# 获取远程集群的 kubeconfig
argocd cluster add <context-name>

# 查看已添加的集群
argocd cluster list

然后在 Application 里指定目标集群:

spec:
  destination:
    server: https://your-production-cluster.example.com
    namespace: default

这样就可以用一个 ArgoCD 实例管理多个集群了。需要注意网络连通性,ArgoCD 所在的集群要能访问到目标集群的 API Server。

Secret 管理

把敏感信息直接放进 Git 仓库是个坏习惯。ArgoCD 支持多种 Secret 管理方案,比如:

  1. Sealed Secrets:用公钥加密 Secret,只有集群里的私钥能解密
  2. External Secrets Operator:从外部系统(AWS Secrets Manager、Azure Key Vault 等)同步 Secret
  3. Vault Agent:从 HashiCorp Vault 读取 Secret

以 Sealed Secrets 为例,先安装:

kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.0/controller.yaml

然后加密你的 secret:

# 创建原始 secret
echo -n "my-password" > password.txt

# 用公钥加密
kubeseal -f password.txt -w sealed-secret.yaml --scope strict

得到的 sealed-secret.yaml 可以安全地放进 Git 仓库。Sealed Secrets Controller 会自动解密并创建真正的 Secret。

CI/CD 集成

GitOps 不是要替代 CI/CD,而是跟它配合。典型的流程是:CI 流水线测试代码、构建镜像、更新 Git 仓库里的镜像版本,然后 GitOps 工具自动把新版本部署上去。

比如用 GitHub Actions:

name: CI/CD

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3

    - name: Build and push Docker image
      run: |
        docker build -t your-registry/webapp:${{ github.sha }} .
        docker push your-registry/webapp:${{ github.sha }}

    - name: Update GitOps repository
      run: |
        git config --global user.name "GitHub Actions"
        git config --global user.email "[email protected]"
        git clone https://${{ secrets.GH_TOKEN }}@github.com/your-org/gitops-repo.git
        cd gitops-repo
        # 用 yq 更新镜像版本
        yq e '.spec.template.spec.containers[0].image = "your-registry/webapp:${{ github.sha }}"' \
          -i apps/webapp/overlays/dev/deployment.yaml
        git add apps/webapp/overlays/dev/deployment.yaml
        git commit -m "Update webapp image to ${{ github.sha }}"
        git push

这个流水线构建新镜像后,自动更新 GitOps 仓库里的镜像标签。ArgoCD 检测到变化后会自动同步新版本。

不是银弹

GitOps 好是好,但不是所有场景都适合。如果你在部署单体应用,或者环境很少、团队不大,传统的 CI/CD 可能更简单。GitOps 的价值在复杂场景下更明显:多环境、多集群、多团队、需要强审计的场景。

还有一个现实问题是学习曲线。团队成员需要理解 GitOps 的工作方式,知道所有变更都要通过 Git,不能随手 kubectl apply。这需要一定的文化改变。

另外,GitOps 工具本身也是个要维护的组件。ArgoCD 挂了怎么办?它的存储是 etcd,要有备份策略。权限管理要规划好,不然给了太大权限又是一把双刃剑。

写在最后

GitOps 不是什么革命性技术,它只是把"配置即代码"这个思想贯彻到底。用 Git 作为单一事实来源,让部署过程可审计、可回滚、可自动化。听起来简单,但真正做起来需要不少调整:工具、流程、甚至团队文化。

从我的实践看,GitOps 最核心的价值不在于它多酷,而在于它减少了手动操作的不确定性。半夜排障时能准确知道环境是什么时候变成现在这个样子的,这种感觉挺踏实。

当然,它解决不了所有问题。代码写得烂、架构设计有问题,GitOps 再高级也救不了。但它至少让部署和配置管理这件事变得可控、可追踪。在复杂系统的维护上,这点确定性挺重要的。

版权声明: 本文首发于 指尖魔法屋-GitOps:这次怎么落地的https://blog.thinkmoon.cn/post/135-gitops-deep-guide-concept-to-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!