Git,作为开发过程中的核心工具,提供了强大的版本控制功能。即便在写代码的时候稍微手抖一下,我们也能通过 Git 的差异对比(diff)轻松追踪到庞大工程中的问题,确保代码的准确与可靠。这种无与伦比的自省能力,不仅提高了工作效率,也带来了极大的安全感,让一切都有迹可循。
如果上面的这些能力在运维上也能实现,是不是稳定性会特别好,变更时候想出故障都难?任何非预期的变化都能通过diff对比出来?是的。GitOps就是基于这些触发点而产生的。2017年Weaveworks的CEO Alexis Richardson 提出了这个概念:通过一个模型抽象使得整个系统的操作变得自动化,使用Git来承载这个模型。
不过这么多年过去了,GitOps相关的产品和平台层出不穷,除了WeaveWorks,似乎大家对GitOps各有各的理解和实践:似乎解决了一些问题,但似乎又没完全解决。那么,问题到底出在哪里?GitOps到底能为我们带来些什么?本文尝试展开讲讲这个问题。
首先,我们先从日常的工作开始思考,我们到底希望GitOps帮我们实现哪些目标?我先列了三个目标,大家看看是不是也有类似的想法:
带着这三个目标,我们先去找找业界对于GitOps的定义,看看是否能找到一些线索:
云原生基金会(CNCF)在2021年成立了一个OpenGitOps工作组,旨在推广GitOps的最佳实践。在这个工作组的官网首页放着四条GitOps的原则:
这四条原则基本是WeaveWorks的GitOps定义的一个深化,而且较多地推荐使用Flux进行GitOps实践,有关这块介绍,我们会在第三章展开。鉴于这个工作组中能得到的有效信息实在太少,我们继续寻找业界有关GitOps的介绍。
GitLab出版过一本电子书叫《A beginners guide to GitOps》(下载地址在参考材料中),在书中提出了一个概念公式:GitOps = IaC + MRs + CI/CD。这个概念就比前面的GitOps四原则更具体一些了:
在第四章,我们会展开说说如何基于这个公式去进行GitOps实践。
本来我的觉得GitOps差不多概念也就这样了,基本上几大厂商的观点都看过了。没想到又在亚马逊上看到了一本书《Repeatability, Reliability, and Scalability through GitOps》(购买链接在参考材料中),阅读之后对于GitOps的认知又拓宽了不少,作者在书中提出了三种类型的GitOps:
说实话,第三种类型的GitOps让我豁然开朗,这种GitOps确实更贴近实际落地:IaC的改造推广常常会有较大的成本,而在没有改造完之前,就不能享受GitOps带来的好处了吗?作者告诉我们一种选择,这种以验证为目的GitOps就行。
作者为了让我们更能深入地理解这个验证型GitOps,还画了一张流程示意图:红色部分为人工,绿色部分为自动。从图中可以看到,自动的流程在右侧自动流转,左侧分别是development、devops、sre三种角色在根据git diff进行验证审计。
通过前面一些概念,我们能看到声明式&IaC在gitops中占据非常重要的部分。可以这样说,如果IaC无法承载所有的运维编排能力,运维编排需求可以会外溢到其他系统或平台上,那么这部分外溢的逻辑就无法使用Git来追踪了:所以这个IaC的声明式的编排能力会变得尤为重要。
在分析IaC方案之前,我们先来看看声明式编程(Declarative)和命令式编程(Imperative)的区别:
# 使用SQL查询数据库中的年龄超过30岁的用户
SELECT name, age FROM users WHERE age > 30;
users = [{"name": "Alice", "age": 25}, {"name": "Bob", "age": 35}, {"name": "Carol", "age": 32}]
users_over_30 = []
for user in users:
if user["age"] > 30:
users_over_30.append(user)
通过这两个例子大家可以明显地感觉到声明式编程的可读性更强,描述结果并且只关注结果;与之相对应的是,过程式编程需要理解整个过程,才能知道这段代码在干什么。
我们来列一下市面上比较流行的IaC方案:
光这样讲会有些抽象,我们围绕着一个例子来比较一下这几种IaC方案,如何来创建一个阿里云ECS:
从上面的例子可以看到,Terraform作为早期的IaC方案,这样声明确实大大简化基础设施的使用成本,只要几行代码就能申请出一台虚拟机。Crossplane是扩展k8s的能力,所以每种基础设施都是一个CRD。但标准的YAML也带来一个问题就是遇到一些复杂的条件渲染,就无能为力了,它不像Terraform的HCL能够在语言中增加一些模板类的控制语句,它需要通过自定义控制器来解决,这无疑增加了IaC交付的难度。
Pulumi明显吸收了Terraform的经验:既然IaC中的模板等控制流无法避免,与其想方设法设计包含过程函数的声明式编程语言,不如我干脆就还是用普通编程语言,只是在SDK中引导用户声明式编程,保留用户使用控制语句的权利。
虽然各种IaC提供了非常方便的基础设施交付方案,但事实上如果真正进行工程化的使用,就会遇到一个状态存储的问题。每个IaC都必须将状态存储下来,才是一份完整的幂等声明,如下图所示:
如果没有这份状态文件,反复执行同一份IaC声明,就会不断地创建资源。对于GitOps而言,所有的变化都必须可以被git commit追踪:这份状态文件同样至关重要,已经创建过的资源不能再创建,否则可能就会产生生产故障。
由于状态文件维护的复杂性,但有些用户又垂涎IaC声明式交付的便利,就会衍生出这样的半吊子的使用场景:只用IaC进行资源创建,后续维护依然使用原有的模式,这样就不用管理创建时产生的状态文件。 事物存在即合理,也不能说这样的方案有什么问题,但确实从另外一个侧面反映出一个问题:IaC的声明代码无法表达全部终态,必须要加上状态文件才构成幂等完整的终态。这个问题也带给我们了一个提醒:GitOps需要将状态也纳入进去,否则无法达成前面提到的“变更透明化”这个目标。
分析完了IaC方案,我们继续来看GitOps相关产品,我们来分析一下:由于GitOps是在k8s社区中发展起来,所以现在市面上的GitOps基本都是围绕着k8s容器编排展开,如我们第一章所提到 The Original GitOps(原始GitOps)。
我们首先来看看最老牌的CI/CD工具Jenkins在k8s下的演进:Jenkins X。
通过这张架构图我们可以看出Jenkins X在k8s场景下,放弃了原有的JenkinsFile的构建编排能力,转而使用Tekton来进行构建流水线的编排。Jenkins整体的侧重CI的,在Jenkins X中依然如此,流水线主要是为CI服务,在流水线的末端会有几个CD节点将实例部署到目标环境。这种CI/CD串联的方案适合小型工程,对于大一些的工程,其CD能力则会有些捉襟见肘,因为毕竟只是几个shell命令的组合。
从整体上看,Jenkins X能够实现“变更自动化”,但其他方面则稍显不足。
然后我们来看一下ArgoCD,这是在GitOps领域的当红炸子鸡,它通过一个Application的CRD实现了k8s的YAML的控制权的转移。需要操作k8s集群的需求,转变成了在git中提交git commit。下面是ArgoCD的GitOps架构图:
ArgoCD和Jenkins X这类方案比起来少了很多功能,但架不住它简单好用:只需要把Helm包推到git上,他就会被自动部署到k8s环境之上。ArgoCD的这种纯CD的方案有个自动化的短板就是:在CI环节构建完的容器镜像,需要手工把他们拷贝到Helm包中,然后提交git。虽然说这种镜像地址的人肉拷贝也是个常见的行为,但是毕竟我们当前是在调研完整的GitOps方案,如果从源头上就不支持,那只能说他在“变更自动化”这块存在不足。
最后,我们来看一下FluxCD,这是GitOps概念提出者WeaveWorks公司的开源产品。这个FluxCD确实属于对GitOps的践行产物,在ArgoCD中仅用Application盖住的概念,在这里被分成了Source、Helm、Image Automation 等,分别有对应的控制器来负责干活:
不得不说,FluxCD中的概念还是非常完整的,同时它也有ImageUpdateAutomation这样一个对象,专门用来进行镜像的自动更新、如果在镜像仓库中出现新镜像,它就会自动提交一个git commit把对应的镜像字段更新,这个机制完美地解决了ArgoCD中人手工拷贝的问题:不过度介入CI环节,但对于CI环节的制品能自动感知,自动部署。
通过前三章节的介绍,我们对GitOps的构成基本清晰了,复用一下GitLab中的那个公式基本就是 GitOps = IaC + MRs + CI/CD,不过从IaC开始做存在较大挑战,毕竟需要从源头改变整个链路,我们可以先从CI/CD开始。
我们先尝试解决第一个问题“变更透明化”:增强对于构建环节的制品采集能力,将每个制品的sha256、git commit以及名称全部采集到数据库。尽量不改变原有的CI流程,在CI最终出制品的节点后将信息采回。
有了制品信息之后,我们就能对每次复杂的变更进行源码下探,遇到镜像变更,就制品仓库中找到对应的源码进一步下探(inspect),使得原本两眼一抹黑的镜像变化,也变得清晰透明。
事实上,从The Verified GitOps(验证型GitOps)的角度看,基于制品做变更透视方案已经将GitOps达成了。不过我们一开始定的目标还有两个没有完全达成:变更自动化、变更代码化。自动化这块其实反倒是简单,在CI的最后一个节点接上变更平台的接口即可。变更代码化这块则需要我们设计一套声明式的方案来编排整个变更链路:这里最大的挑战是对于已有流程和平台的整合。
IaC的语法设计是个苦差事,这么多大牛前仆后继,市面的IaC语言似乎总无法完全用声明的方式,解决基础设施编排的问题。Pulumi这套方案给了我们很大的启发,既然这样,为何不直接用现有的编程语言?
于是我们借鉴Pulumi设计了一套SRE Stack方案:
有人可能就会问,如果有人不在sre_stack.py中维护终态,转而在外部系统中维护数据是不是也是可以的。是的,本身这个方案就考虑到了对原有系统的整合,所以这种情况是允许的,但我们能结构化地检查和审计出来:如果sre_stack.py文件没有变化,而status文件变化了,意味着这是一个非IaC驱动的终态变化。如果我们能接受这次变化(毕竟某些低频的复杂操作可能确实没那么适合在IaC中表达),只要审批过了,下次就不会再出现。整个流程如下图所示:
针对开分支,开发者提交了MergeRequest之后,会自动触发SRE Stack的状态渲染,将status文件补全进代码中,因此在审批环节就能完整地看到变化,在本章的开头部分,我们已经对于制品管理以及透视机制有了介绍,因此在通过IaC下的产生的变更透视树就变成了下面的样子:
于是,通过SRE Stack+制品管理,我们基本实现了这三个目标:
当前GitOps体系已经在内部逐步使用推广,有相应需求的公司可以联系SREWorks进行开源共建,我们可以将其作为一款SREWorks运维应用逐步对外推出。
参考材料