话题缘由

最近在写代码之余又前前后后花了不少时间给团队搭建了一套基于 GitOps 的运维体系,整个过程使我对 GitOps 又产生了一些新的不成熟的看法和观点,借着当前的新鲜劲记录一下,纯当是一个对 GitOps 的探索和总结(后续的实践也极有可能会推翻当前的经验)。

为什么我们需要 GitOps

关于 GitOps 这个话题,我感觉相关文章早已汗牛充栋,但真正将其彻底应用和实践的公司其实并不算太多,其本质原因我觉得还是每个公司在演进运维体系时通常都带有极大的惯性,想要变革一个公司的运维体系,其背后不仅仅是技术问题,还有很多其他非技术上的考量。不过,一个相对明确的是:GitOps 正逐渐成为新的技术趋势,值得每一个 DevOps 团队投入时间研究。

在谈及 GitOps 之时,我们不得不先给 GitOps 下一个相对明确的定义,但这并非易事。GitOps 本质上是软件持续交付领域(Continuous Delivery)一种新的理念,而非某项具体的技术或项目。不过我们可以结合 gitops.tech 的描述,并结合如今流行的使用形式,可将 GitOps 简单地理解为以下几大特点:

  1. 代码化描述基础设施和应用的部署状态

    任何正在线上运行的基础设施资源或者应用,都可以用代码化的方式来描述其当前状态。描述方式最好采用声明式,这样代码化的部署状态可维护性将会更强。只有将其代码化,我们才能使用代码的方式来管理基础设施和应用的部署。这类代码通常不像应用代码有着复杂的逻辑,更多地是描述各种部署状态,大多数情况下是逻辑相对简单的配置代码

    正因如此,GitOps 与 IaC(Infrastructure as Code)是密不可分的,或者说,IaC 就包含于 GitOps 中。无法进行 IaC,GitOps 也就无从谈起。IaC 同样是目前非常流行的技术名词,大有 “X as Code” 的趋势。IaC 是后续 GitOps 规模化运维最关键的基础。

  2. 使用 Git 的语义来管理代码化后的配置代码

    正如 1 所描述,代码化后的部署配置代码将面临着管理问题,而 GitOps 顾名思义,采用了 Git 的语义来管理代码,主要有:

    • 代码存储于 Git 仓库中(或者使用任何 Git-like 的版本管理工具);

    • 基于分支模式来管理代码版本;

    • 使用 Pull Request 来提交代码变更和 Code Review,对应的变更可方便进行 Revert;

  3. 具备将配置代码进行自动化部署的能力

    这项能力尤为重要。我们已经将基础设施和应用的配置代码化并使用 Git 来进行管理,那么接下来就必须要有相应的能力将配置代码自动化部署于各种真实的线上环境,使其当前环境与配置代码所描述的状态一致

综上所述,GitOps 整体上看起来并不难懂,说白了就是把持续交付的配置也当成一个代码项目来进行管理和发布。回到我们最初的问题:为什么需要 GitOps ?根据我的体验,我觉得使用 GitOps 能有以下几个比较大的收益:

  1. Single Source of Truth

    一旦我们将基础设施和应用的配置代码化并进行发布部署,我们就全局拥有了一份可以可靠地描述线上环境部署信息的元数据。有了这份元数据,我们就再也不用担心线上环境配置信息会被零散地分布于好几个平台的情况。结合一些工具,我们也能很好地解决配置漂移、雪花实例等这一类常见的问题。配置代码总是可以真实地反应当前线上各种环境实际部署状态

    不过,我们也要看到其局限性。我们只能从配置代码中获得相对静态的数据,无法获得最完整的线上数据。如果我们对一个 Kubernetes 的 Deployment 使用了 HPA(弹性伸缩),那么当前应用真实的实例数量便无法从配置代码中获得,我们只能获得 HPA 的相应规则。因此,配置代码一般只能反应相对静态的数据,更完整的数据集必须结合其他平台能力(比如在线服务监控)才能得到。

  2. 用代码化方式解决持续集成中的各种问题

    配置代码化后,我们也就获得了写代码的不少好处,很多持续集成中遇到的问题可以采用编程的方式来解决。例如:

    • 为配置代码集成各种不同类型的 CI 插件

      比如我们可以为配置代码仓库增加一个安全扫描的 CI 插件,每次变更提交前都会检查配置代码中使用的软件制品(比如 Container Image)是否符合安全要求:是否来自可信的制品源,制品是否已经通过安全扫描等等。正因为代码化,我们可以很容易为其编写各种类型的 CI 插件来做各种不同的检查。这样一来,我们可以很容易对部署配置做各种形式的检查和准入控制。

    • 代码可测试能力

      代码化一个显著能力是具备某种程度的可测试性。但就 GitOps 场景来说,其代码可测试能力依赖于所采用的 IaC 工具的能力。代码具备可测试性,我们就可以为其编写相应的单元测试或集成测试,理论上可在部署服务之前降低出错的概率。

    • 可复用代码能力

      我们可以将某些被验证合理的运维经验以代码化的方式抽象成库或者模版对外提供,从而让团队内的运维经验以代码化的方式进行有效的传承和使用。

    • 可审计能力

      GitOps 天然就具备强大的审计能力,因为每次代码变更都可以很方便地通过 Git History 进行回溯。我们可以通过回溯 Git 记录,很容易地找到对应的修改记录是由谁在何时引入。

    应用 GitOps 之后,我们相当于将持续集成在某种程度变成了一个编程问题,很多时候我们可以用写代码来解决问题。但是这项能力非常依赖于 IaC 的能力,不同 IaC 工具带来的抽象效果是不一样的。

  3. 规模化运维能力

    这项能力我觉得对于中大型团队至关重要。很多时候,运维最大的挑战就在于规模化。个人经验,评价一个公司整体技术能力怎么样,可通过观察其日常的运维方式来管中窥豹。可以说,运维能力是一个公司整体技术能力的综合体现。如果一个公司总是不太敢变更或者每次变更都充满繁琐低效的审批流,那么这公司整体技术能力也很难强到哪里去。在如今云原生的趋势下,一个公司的运维复杂性日益增长。公司规模越大,面临的运维挑战就愈加严峻,如不立刻着手解决,很容易对业务的敏捷性和稳定性造成极大的影响。

    GitOps 最大的好处就是在规模化运维的目标上提供了一种可行的模式:

    • 杜绝人工化的 ClickOps

      ClickOps 通常意味着某项资源的运维依赖于 GUI。从产品层面来说,GUI 一定是对大多数用户最友好的展现形式。设计精良的 GUI 可以让毫无前置知识的用户通过简单的人工操作就能完成对某种资源的复杂控制。但是,GUI 带来的 ClickOps 是极低的可扩展性。假如操作某种资源都需要一种 GUI,那么多种风格迥异的 GUI 所引入的 ClickOps 将带来致命的不可维护性

      之前在某公司工作的时候,公司的技术氛围是:无论你做什么样的业务,哪怕是一个极底层的服务,都需要引入一个 GUI(通常是 WebUI)来作为最终的操作界面,并美其名曰 “产品化”(也许 GUI 更易于让其他人对其业务有产品感知度吧)。在这种 GUI First 理念的驱使下,内部的各种服务衍生出了无数 WebUI。由于设计、产品和前端人力的极度紧缺,很多时候这类 WebUI 的设计和交互都由不太专业的工程师操刀,并交由外包前端工程师进行代码实现。当连外包人力资源都紧缺的时候,迫于项目压力,甚至会让业务提供方的工程师来编写前端代码。可想而之,在这种开发模式下,各种 WebUI 的整体交互模式和 UI 设计都大相径庭,更别提质量良莠不齐的前端代码,以至于每一个新人工程师都要被这一系列奇怪的 WebUI 折磨许久才能将一个极其简单的服务部署上线。WebUI 也为业务提供方带来沉重的维护压力:实现 WebUI 的前端工程师大多不会长期专职维护一个项目的 WebUI。当一个 WebUI 出现问题或者需要改进时,紧俏的开发人力又一次成了严峻的问题。对 GUI 的过分追求还导致大多数业务不关注更高效的 CLI 工具的建设,很多时候也不设计对外扩展的 API,导致不少内部服务形成了单独的技术烟囱,对外冒着熊熊的浓烈呛鼻的黑烟。

      这类 ClickOps 也很难有效地形成复用性强的运维经验,每一次经验的传承大多数时候是通过陈旧的文档、口口相传或者一次次反反复复的鼠标点击来实现。ClickOps 在小团队中或许不是太大的问题,但在中大型团队中,ClickOps 便意味业务部署的低效。繁杂的 ClickOps 让业务部署成为难以逾越的 “最后一公里” 并因此耗费了大量宝贵且无意义的开发时间。

      GitOps 某种程度上可有效解决 ClickOps 所带来的糟糕的可维护性,但其实仍需要对应底层基础设施的支持:底层基础设施要具备将资源代码化的能力。很多时候资源可代码化是因为其服务对外有着设计良好的 API。参考 AWS 的整体产品,几乎每一种产品对外都有统一的 API,因此几乎每一种 AWS 资源都可以以各种不同的 IaC 工具进行代码化。又比如 Kubernetes,其内部的所有资源都做了统一的资源化,并提供给用户可自定义资源的能力(CRD 机制),因此运行在 Kubernetes 上的应用天然就具备声明式代码化的能力。也就是说,我们要从 GUI First 的理念转变成 API First。这样一来,我们就可以相对容易地用某种 IaC 工具将 ClickOps 转变成 GitOps,用户不再需要进行低扩展性的鼠标点击,转而来写配置代码(写代码总比点击无数奇怪的按钮要更符合程序员的天性)。GitOps 的代码具有更强的统一性和可复用性。

    • 为持续交付提供底层标准化运维原语

      这一点可能描述起来有点抽象,但其实是想表达:GitOps 为持续交付提供了一种标准化操作模式,后续的其他服务都可以建立在这种标准化操作模式之上。如上所说,我们不希望有 ClickOps,但是 ClickOps 与 GitOps 并非完全对立,而是可以相辅相成,前提是 ClickOps 必须建立在 GitOps 之上。ClickOps 底层用到的模型必须是 GitOps 抽象的模型,而且关键的 GUI 操作可被映射为 GitOps 的标准操作,比如发布服务进行审批底层就是提交 PR 并由相应的 Owner 进行 Review。对于某些简单的操作,GitOps 要比 ClickOps 复杂啰嗦不少,此时用 ClickOps 进行流程简化是再好不过。我们完全可以为 GitOps 设计一套 GUI 来进一步提升其运维效率。同理,各种其他类型的 Ops,比如 ChatOps、FinOps 等,其实底层都可以尝试基于 GitOps。

    GitOps 为规模化运维提供更好的管理模式,得以用更少的人力投入实现运维更多资源的能力,还可以解放开发和运维团队的时间从而投入去做更多更有价值的事情。对于大公司来说,建立一个庞大的运维团队或许能实现规模化运维,但肯定不是最优雅和经济的模式,尤其是在人力成本日益上涨的今天,我们不能再用简单堆人力的思维去支撑业务的可运维性(短期内也许有效,长期看则是毒药)。我们一定要用可扩展性的方式去思考规模化运维。GitOps 提供的将持续交付转换成代码化编程问题的模式无疑是要更具可扩展性:不同的团队统一用 Git 操作方式来编写代码即可。但是,GitOps 并非规模化运维的钥匙,它只是提供了可行的模式供大家去参考,在已有一定业务规模的公司实际落地的时候还必须充分考虑具体的业务场景和对应的历史技术债务,否则可能会适得其反,我更推崇循序渐进的小步变革。

    对于新团队,不但没有沉重的技术债务,而且业务规模较小,因此最适合从 Day 1 就开始实施 GitOps,并籍此建立起 GitOps 文化,为后续其他类型的 DevOps 提供更好的开发基础。在这个阶段,打好一个坚实的底座尤为关键,这会在后续业务规模化推进中起到至关重要的作用。

如何设计和应用 GitOps

这部分内容会涉及怎么去设计和应用 GitOps。由于目前 GitOps 技术生态工具较多,我很难面面俱到,只能根据个人的一点经验来粗浅地聊一聊。

明确 GitOps 的设计原则

拿我当前场景来举例。由于我们是使用公有云的初创团队,因此我们技术决策的原则比较明确:

  1. GitOps First

    落地为王,我们必须先把 GitOps 最关键的几点落地,此后才渐进地推进其他方面的改进,比如易用性、安全性等。对于大团队来说,做到这一点非常困难,但凡一点不太好用的运维改进都会导致业务团队的怨声载道,为求落地可能要做更多额外的增强工作,推进节奏不可能太快。小团队则相对容易太多了。只要几个人彼此对齐理念,很容易就说干就干。尽管初期易用性可能是个问题,但可以等 GitOps 初步落地后逐步改善。

  2. 优先选择主流的开源工具来构建整体的 GitOps 方案

    受限于有限的人力投入,我们不可能也完全没必要自己造轮子。GitOps 生态好的工具,我们秉承拿来主义,先用先熟悉,后续如果工具不满足我们的业务需求,尽量从上游提出建议或者贡献相应的代码。选主流的工具也是一种相对保守的方式,主流就意味着用的人多,社区和生态丰富,可维护性也强,这对于团队早期尤为重要:少在运维上踩太多不必要的坑,尽量将更多精力投入到产品的快速迭代中。

选型 IaC 工具

正如前文所说,GitOps 最核心的关键就是要先找到合适的 IaC 工具。对于业务构建于公有云之上的公司,找到合适 IaC 工具应该不是一件难事,因为很多时候不少公有云已经提供了良好的 API 和相应的 IaC 生态。但对于采用私有云的公司(通常都是自建机房的大厂),可能确实要先花点时间实现底层基础设施的 IaC 能力,整体推进要更复杂一些。

将云上资源 IaC 化,其实我们的技术方案选项并不算多,根据我的经验,我们可以从以下几种方案中进行选择:

  1. Terraform

    最老牌的多云 IaC 工具,可以说是 IaC 概念最早期的奠基项目。生态最为完善,社区也非常活跃,背后也有非常成熟的商业上市公司 HashiCorp 进行支持。Terraform 抽象了 HCL 这门相对简单易学的 DSL 作为资源描述语言。

  2. Pulumi

    后起之秀,近几年发展极为迅速。与 Terraform 相比,Pulumi 并没有设计专门的 DSL,转而通过提供不同语言的 SDK。用户可以用不同语言(比如 Go/JavaScript/Python 等)来描述相应的 Infrastructure。Pulumi 也针对 Terraform 的一些短板专门做了改进,可以看 Pulumi 做的这个对比

  3. 公有云厂商提供的 SDK

    比如用 AWS 的 CDK,同样可实现对 AWS 资源的 IaC 化。

  4. 传统的面向过程的配置管理工具,比如 Ansible

    本质上是将一系列面向过程的脚本进行更好更安全的封装,形成相对更抽象的执行单元,比如 Ansible 的 Playbook。

选型的过程很简单,3 和 4 首先排除。我们未来是多云服务,3 很容易导致 vendor lock-in。4 的话很明显不太符合现在云原生下声明式资源表达的模式,用起来很容易出错。在 1 和 2 的选择中,我毫不犹豫选择了 1,主要是受设计原则 2 的驱使。既然 Terraform 是最主流的 IaC 工具,而且现阶段业务模型也极度简单,那么就没必要太纠结,直接使用 Terraform 几乎是最稳妥的选择。不过,我们在实践中发现,直接应用 Terraform 很容易写出极其啰嗦的代码,此时配合 Terragrunt 这个工具(底层基于 Terraform 进行封装)能更好地写出相对紧凑简洁的代码。最近我也正在研究 Crossplane 这个项目,感觉基于 Kubernetes 并通过封装好的形形色色的 CRDs 来操作多云资源也是一个很有意思的想法,而且说不定会比 Terraform 更好用,但由于目前经验不多,就暂且不说。

选择了 Terraform 之后,我们理论上就可以用代码来管理多云资源,接下来就是要选型应用层的 IaC 工具。在云原生的大趋势下,很多公司选择了 Kubernetes 作为 PaaS 的基座,因此应用最终都是容器化运行于 Kubernetes 之上。正如前文所说,运行于 Kubernetes 之上的所有资源天然就已经被代码化了,其形式就是 YAML(JSON 和 YAML 本质上可相互等价)。YAML 是一种比较简单的配置语言,很适合用来描述声明式的资源对象。但也因为它的简单,局限性非常大。如果说 Kubernetes 是云原生时代的操作系统,那么 YAML 就是这个操作系统的汇编语言。此时,我们也有几种方案可供选择:

  1. 最原始的 YAML

    这是最简单直接的方案,同样也是可维护性相对较差的一种方案。由于 Kubernetes API 的特点,一个应用极有可能产生大量的 YAML 代码。直接编写和 Review YAML 代码,对于复杂应用或者复杂场景,难度有点大。

  2. Kustomize 或者 Helm

    这两个工具本质上就是客户端 YAML 渲染引擎,用以更好的管理 YAML。从易用性的角度来看,Kustomize 更容易;而从功能性和生态来看,Helm 无疑是现在 Kubernetes 上的事实标准。但是,Helm 实在太难用了,有一定的学习门槛。Helm 的难用是由于其采用的渲染模版机制的难用。Helm 虽然没有提供任何 DSL,但是各种渲染模版的使用本质上就构成了一种 DSL,而且是一种相对不直观的 DSL。尽管如此,Helm 的功能性非常的完善,基本可以满足绝大多数的 YAML 生成需求。而且,Helm 还有相应的包管理机制 Helm Chart,几乎每一个流行的 Kubernetes 应用都会提供相应的 Helm Chart 供用户安装。

  3. 其他更抽象的部署模型

    观察 Kustomize 和 Helm,其实我们不难发现,这几种工具并没有对 Kubernetes 原生的资源模型做任何形式的模型抽象,用户最终还是必须理解 Kubernetes 所暴露的相应资源的概念。因此,更易用的工具一定是需要基于 Kubernetes 原生资源模型做进一步的模型抽象,这一过程还可以用更易用的 DSL 来描述抽象后的模型(可参考 KubeVela 项目)。当然,更抽象的模型背后意味着运维后端复杂度的上升。

我们最终选择了 Helm,但也并没有完全摒弃裸 YAML 和 Kustomize。理由和选择 Terraform 时的理由类似。方案 1 太过于原始,明显不适用。方案 3 对我们来说有点过度设计而且维护成本和复杂度也相对较高,且易用性目前还不是我们首要关注的问题,因此也放弃。剩下的方案 2 中的 Kustomize 和 Helm,选一个最流行的方案,那便是 Helm。尽管 Helm 不算太好用,但对于小团队来说,这完全不是太大的问题,反而能让我们用更标准的方式来应用 Helm。

在 Kubernetes 上,我们还需要选择一个 CD 工具,就目前来看是从 Argo CDFlux CD 二者中进行选择,而这两个项目近期也相继从 CNCF 毕业。这类 CD 工具有个显著的特点:可以 Watch 代码仓库的某个分支,并拉取代码仓库中的配置代码渲染成 YAML 并将其应用于目标 Kuberntes 集群中,而且会不断地进行 Sync,从而总能保证当前 Kubernetes 所运行的应用与对应代码仓库所描述的配置代码一致。我们最后选择了 Argo CD,主要是它更主流,功能上也非常满足我们的需求。 实际应用的时候,我们会专门部署一个小规模的专门用于运行 Argo CD 的部署元集群。该集群的 Argo CD 将全局负责所有地域 Kubernetes 集群的业务部署。我们编写 Helm 代码的时候,只需要选择不同的集群名,服务便可以自动部署于不同的集群中。

综上,Terraform + Helm + Argo CD 就基本满足了我们小团队 GitOps 的大部分需求。

使用 Monorepo 和 main 分支发布

GitOps 还有一个易被忽视的问题:如何组织代码仓库 ?关于这个问题,我们的选择是:

  1. 使用 Monorepo

    相比于分散代码仓库的方式,大一统的 Monorepo 无疑要更好维护,也更切合 Single Source of Truth 的理念。所有开发者和维护者只需要关注和维护一个代码仓库即可。当然,Monorepo 也有其固有的问题,比如:

    • 细粒度权限管理:如果对某些代码有细粒度权限管理需求的话,Monorepo 不太好直接支持。但我们可以从代码设计初期来尽量避免这类需求,即让 Monorepo 完全 “民主化” ,里面的所有代码都可以对外公开;

    • Secrets 管理:既然 Monorepo 完全 “民主化” ,那就意味着它可以对公司内部绝大多数开发者公开,这就必须要求我们的代码仓库内部不能明文包含任何敏感的 Secrets,比如 AccessKey Secret、证书密钥等这一类 Credentials。此时我们可以选择将 Secrets 加密存储(比如使用 sealed-secrets 方案)或者采用在应用层使用某些 Secrets Manager 服务(比如 AWS Secrets Manager);

    • 规模化应用场景下的性能瓶颈:如果是一个大规模团队,那么使用 Monorepo 很容易便遇到原生 Git 的性能瓶颈,因为此时 Monorepo 很容易就膨胀得非常巨大。管理超大型 Monorepo 并非一件很容易的事情(此时可以参考 Facebook 近期开源的 Sapling 项目和 Google 公开的一些经验);

    总而言之,Monorepo 整体上更容易把控,而且我个人认为是更优雅的解决方案。

  2. 只发布 main 分支

    关于这个其实可以参考 Stop Using Branches for Deploying to Different GitOps Environments 这篇文章。倘若使用分支发布,那么不同分支间的代码将很难统一和管理。所以,只发布 main 分支,总是让 main 分支真实地反应线上环境,这是心智负担相对较小的一种方式。但是,实现这一点也要求基础设施和应用要能做到充分的配置化,不同的环境只需要更改相应的配置文件即可。

基于 GitOps 的商业化产品的遐想

在研究 GitOps 的时候,我突然发现关于 GitOps 的平台化商业产品其实不算太多,营销相对较多的有 WeaveworksCodeFresh 等,但其形态感觉都不合我意。我总觉得 GitOps 可以结合很多东西来形成不少极有意思的玩法。

我设想的基于 GitOps 的商业化产品最好是:

  1. SaaS 化的开箱即用

    该产品一定是一个开箱即用的 SaaS 化产品,整体设计思维可借鉴 Vercel。普通用户拥有免费的使用额度。

  2. 轻量简洁易用

    基于 GitOps 做极简轻量的 GUI,不试图成为一个锁定用户的笨重的平台。产品试图解决落地 GitOps 中遇到的典型的重复性问题,并提供 GitOps 的最佳实践。产品最终将产生用户的代码,并基于这些代码做持续集成。用户对代码拥有所有权,并可以基于代码自行部署,平台不会以任何方式锁定用户。

  3. 开放

    对已有的 GitOps 生态做极大程度的兼容。比如用户可以直接 Import 一个已有的代码仓库就可以初步实现 GitOps。产品会集成各种已有的技术生态,试图给用户提供一个熟悉的操作界面,但不会将用户绑定于某种已有的云上。产品的底层基座必须也是多云的。

  4. GitOps + XOps

    这是我自己随便编的一个名词,意思是指 GitOps 可以很方便与各种其他运维理念进行融合,比如:

    • GitOps + DevSecOps:GitOps 可以与软件安全供应链、零信任等产品结合,将软件安全应用于持续集成中;

    • GitOps + ChatOps:使用 GitOps 的能力,用户可基于一些聊天机器人(比如 Slack)做相应命令式的自动化持续集成;

    • GitOps + FinOps:通过分析 GitOps 的代码,FinOps 可提供一些费用优化的智能建议;

    类似的结合其实可以有很多想象空间,而这部分则可作为产品设计的收费点。

其实整体梳理下来,这个产品技术上的难度还行,不算过于复杂,真正的挑战在于产品设计,这也许会是一个很有意思的产品吧。

总结

虽然 GitOps 的出现时间不算太长,但我觉得它的内核理念非常适合云原生时代的运维模式。实际应用 GitOps 仍需要每一个开发者根据实际业务特点进行慎重灵活的设计,这样才能更好地落地 GitOps。

祝大家阅读愉快