演讲嘉宾:彭磊 | 腾讯云高级工程师
TencentHub是一个集Docker镜像、二进制文件、helmcharts于一体的仓库存储服务。那么这一架构技术是如何基于Kubernetes 快速实现workflow引擎的呢?今天将为大家分享《TencentHub技术架构与DevOps落地实践揭秘》,让我们开始吧!
大家好,这次主要是给大家分享TencentHub技术架构和DevOps相关的一些东西,这两点和大家日常开发结合会比较紧密,比较接地气一点。我今天会主要分享三部分,第一,简单聊一下TencentHub这个产品和DevOps的关系,我们如何去思考建设一个TencentHub镜像仓库+DevOps引擎。第二部分讲讲TencentHub总体的一些东西和Docker镜像存储在腾讯云上是如何实现的,最后是我们的workflow引擎:为了支撑DevOps工作流去设计产品以及一些具体设计/实现细节。
TencentHub与DevOps?
什么是DevOps?
我相信很多人对DevOps已经比较熟悉了,这个概念从2009年大火起来到现在已经过去了接近十年的时间。直击DevOps的核心理念,下面这句话是我最赞同的对DevOps的描述:以业务敏捷为中心,去构造适应快速发布软件的工具和文化。我们做DevOps,要去推崇DevOps,肯定不能忘掉我们的目标是快速交付软件产品。在达到这个目标的过程当中,非常重要的落地就是我们需要工具去支撑DevOps,所以接下来分享TencentHub这样一个以DevOps引擎为指导的工具。
什么是TencentHub?
TencentHub是什么?TencentHub核心有两部分,第一,它是一个多功能的存储仓库,包含了Docker镜像存储功能以及helmcharts这样的存储,还有一些构建产物的存储等等。第二,TencentHub是一个DevOps引擎,通过我们自己设计的workflow引擎对工作流进行编排,去帮助大家建立自己的DevOps流程,比如说构建、测试、审核、部署等等任务。当然,在前置环节,我们也将去对接腾讯云提供的TGit代码托管服务,最终给开发者提供一套完整的云上Devops工具解决方案。
TencentHub技术架构
TencentHub – 总体架构
我简单介绍一下TencentHub的总体架构,TencentHubd 的镜像仓库存储是基于COS实现,因为COS可靠性非常高,这非常方便地给我们的镜像存储带来高可靠保证。TencentHub核心还有一个自研workflow引擎,可以使用YAML定义DevOps流程,让我们的DevOps本身达到Programmable,这对Devops的管理是很重要的。在TencentHub的Devops引擎里面,我们使用容器去实现插件机制,用来封装用户自定义的DevOps任务,后面会详细介绍。使用Kubernetes作为执行引擎,运行DevOps任务,目前是使用TKE来实现的。在Docker存储仓库里,我们也会加入Docker镜像漏洞的安全扫描,后面会做一个的简单介绍。
镜像存储
Registry Token Authentication Specification
左边这个图分为两大部分。上面是Registry,下面是StorageEngine。Registry主要包含了组织、团队、成员的管理,以及一些权限的控制,同时还包括仓库Repository的管理,负责Docker镜像、文件、helmchart的存储。我们所有存储都是使用一个仓库来管理,镜像、文件等都会放在某一个仓库下面。在最上面,我们提供了Webhook,可以帮助用户对接自己的系统,监听仓库或者DevOps流程发生的一些事情。在Registry里面还有Component组件,负责DevOps任务的存储。下面这部分StorageEngine,是我们去解决统一的仓库存储层的问题,它是一个我们提供出来给全球的腾讯云的机房都能使用TencentHub服务的机制。
TencentHub仓库中最重要的是Docker镜像的存储。除了公共的镜像存储之外,TencentHub还支持私有的镜像存储。私有镜像存储需要通过登录才能获取或者上传Docker镜像。如何是想登录认证呢?这是Docker官方的Registry用户认证的规范,它并没有纳入到OCI规范里面,只是在官方有一个文档去说明流程是什么样的,Docker客户端也是按这个流程去实现的,所以我们只需要在TencentHub后端按这个流程去实现自己的授权服务就可以了。
TencentHub pull 授权流程
在我们使用Docker进行push/pull的时候,本地的Docker客户端会调到containerd,检查当前这个操作是否有授权。当我们没有带上一个Token的时候,后端的存储服务会返回401,并且携带一个授权服务的地址,告诉客户端去这个地方拿授权码。客户端请求授权服务获取将要进行操作的授权Token。
获取Token支持两种方式,一种是OAuth2密码模式,你可以通过登录之后,授权服务会给你下发一个key,它和你的密码无关系,后续的通信都是使用Key获取Token。
第二种方式是Basic Authentication方式,很多时候我们采用的都是这种方式。TencentHub实现的也是后者。这种方式会在Token客户端留下帐号密码,并不是太安全,我们后面可能会去支持OAuth2的密码模式。
当客户端发起一个请求到授权服务的时候,授权服务就根据当前的请求去生成一个授权的列表,封装成Token返回客户端,这个Token的格式对客户端是透明的,可用使用任意自己的格式。我们这里继续保持了选择JWT的格式,是因为我们把StorageEngine和Registry做了一个状态的隔离,因为我们把需要的权限都放到了Token里面进行封装,不需要有一个中心化的带状态的权限校验服务去连接两者。最后当Token拿到之后,客户端重新再去请求Registry,重新拉取它的Docker镜像,或者进行数据的上传/下载之类。
这里用一个时序图简单说一下它的流程。首先可以看到,Docker服务端首先会去访问/v2/这样的路径,客户端会根据当前是否有携带Token来返回一个授权的地址,我们目前返回的是hub.tencentyun.com/token这个地址,客户端就会自动访问/token的URL,带上当前的账户以及申请的权限范围。客户端申请Token完成之后,就会进入Docker镜像拉取流程。
OCI Distribution Specification
Docker镜像是分层组织形式,每个Docker镜像包含多个Layer和一个Config文件,每个Layer包含构建一个Docker镜像时文件系统里面出现的差异,Config文件里面会包含当前这个Docker镜像能运行的一些环境要求,例如OS,及一些配置入口命令,导出的一些端口之类。这些文件被一个Manifest文件引用起来,通过它可以找到这个Docker镜像的所有内容。Docker是在V2版本设计了这样一套规范,目前它也已经提交到OCI组织,成为一个Distribution Specification标准。
这样的设计带来很多好处。
首先,它的安全性有比较大的提高。镜像中的所有文件都是content addressable 的设计,让我们只需要拿到最后的Manifest去校验,就可以发现整个数据的传输过程中是否有篡改。这在现在比较流行的区块链中都采用类似的技术,是merkle tree的一种实现。
第二,它可以极大减少冗余。每个构造的Docker镜像都不会从空白文件开始,都是从各个发行版或者是比如CentOS的基础镜像开始,这些基础镜像存在非常多的基础镜像Layer都是相同的。它通过content addressable storage 的组织结构,不仅在客户端减少存储空间,也可以在远端仓库中减少存储所需要的空间。
最后,它对缓存是非常友好的。因为整个Docker镜像里面不管哪一个Layer有改变,都会最终影响到Manifest的改变,所以Docker镜像并不是重新修改一个Layer,而是重新生成的,我们去做缓存的时候就可以非常方便地在不同的环境下地部署我们的缓存,只需要使用content digest 作为Layer的key来引用Layer文件即可,Layer不会修改,只会生成新的或删除老的Layer,缓存的管理就非常简单。
distribution RESTful API
这里再简单补充一下关于distribution 规范的一些API。前面两个不说,主要是第三个,每个Docker镜像的上传都会访问到/v2/仓库名/manifests/tags|digest地址,在这个地址上,它支持去GET、PUT、DELETE,去实现Docker镜像在远端仓库的拉取/上传/删除的功能。刚才所说的Layer、Config文件,包括Menifests文件,也可以通过V2这个路径下面的name+blobs+digest去获取。更详细的规范,可以看一下OCI组织已经成为标准的说明,下面有个URL。
distribution 架构
TencentHub镜像的存储核心还是利用了Docker官方的distribution来实现的,distribution的实现在这个图里面描述得比较清楚,最上面有API route层分发URL的规则。第二层会有一个权限控制,我们在这里做了一些改造,增加了TencentHub的hubtoken的实现。我们为什么要去实现hubtoken,而不是用官方现在自己的GWT格式?
是因为TencentHub可能还会面临着一些私有化部署的需求,或者说一些用户在公有云上面希望有自己独立的存储仓库。但是我们上面的Registry和StorageEngine两者之间并没有中心化的授权服务,所以我们就通过在hubtoken里面包含了一些不同租户的身份识别信息,在上下文进行传输,所以就单独改造了这里。
在权限控制下面会有API协议的函数处理,一些主要的业务逻辑,都在这里实现。最终到下面storage的实现,它提供了一套存储的插件机制,有一个标准的存储接口,规定了文件上传、文件移动等等接口,只要去实现就可以了。我们这里也会实现GOS,是对COS的简单封装,后面会做一个GOS简单介绍。
现在腾讯云容器服务的仓库是CCR,还没有提到TencentHub上面来。CCR有两个问题,第一是不同地域的镜像是不通的,比如说我们在广州上传一个镜像,想在硅谷拉取是拉取不到的,这是我们直接依赖了COS提供的分发能力,没有去对它做封装,COS是无法跨区域访问的,所以会存在这个问题,这会带来用户体验上的不一致,基于仓库的触发器、Docker镜像构建功能,都会受到这个问题的影响,所有基于仓库的服务都需要单独又去部署一份,维护上也带来了很大影响。所以我们就做了下面这一层。
我们在设计global object storage的时候,发现distribution协议的设计没有事务的需求,第二是我们拉取多个镜像,它对延时不太敏感,但是对吞吐量很敏感。因此,实现这个存储,我们只对COS做了很简单的封装,提供中心化的服务,它类似于一个p2p的tracker,会去记录它所有的文件分布在哪个区域的哪个COS里面。比如说我们从广州上传了一个镜像,他需要在硅谷去拉取,我们通过这个global object storage服务,会发现它在本地是没有的。这个时候通过腾讯云内部的专线,直接去广州把它拉取过来。在拉的过程当中,它会同步写到本地的COS和客户端去,这样客户端也不用做很长的等待。当下一次硅谷再访问该镜像的时候,它就不会再跨区域去访问这个数据,给我们减少了很多成本,最终也给用户带来很好的体验。
Docker镜像安全扫描
在Docker镜像的存储完成之后,我们还是提供了一个Docker镜像的静态扫描。它通过对比Docker镜像包里所包含的软件列表版本和漏洞数据库里面漏洞对应的软件版本,得出一个差异,通过这个差异计算出当前Docker镜像里面可能会存在的漏洞风险。
Scanner服务是扫描的中心服务,我们借鉴了CoreOS的Claire来实现的,它会周期性地和几大发行版的漏洞数据库进行同步。Docker镜像上传完成之后,就会提交到扫描中心,逐个分析它的每个Layer,把每个Layer取出来,看里面的软件版本。当漏洞被发现的时候,Scanner发起一个通知,最终通知到前端的Registry服务,用户可以通过webhook拿到这个消息。因为扫描只能获得已知的漏洞,针对一些0day漏洞是没有办法的,当新漏洞被曝光出来之后,可能各大发行商会跟进,然后更新他们的漏洞数据库,已经完成扫描的Docker镜像就需要再去做对比,这个时候就需要我们开发人员比较小心,留意是否有影响到自己的漏洞。
前面基于Docker仓库关于镜像存储方面的分享,也是TencentHub镜像仓库里面最复杂的部分,关于artifact和heml chart这样的文件存储,我们就不做过多的实现,因为它的实现都类似。
workflow引擎设计与实现
如何设计一个通用的DevOps型解决方案?
我们为什么要去做一个TencentHub的DevOps引擎呢?因为我们发现很多客户在上云的时候,平时遇到非常多重复性的操作工作,例如在腾讯云UI上面做很多事情,没办法自动化或者腾讯云的API还不够好用等等各种问题。其实DevOps的自动化要求是很高的,我们不能加入大量的人工去操作,基于这个考虑,同时还要照顾到中长尾客户的一些需求,我们最终决定要做一个DevOps的工具,去帮用户来建立自己的DevOps流程。
这里是我们考虑如何去做这个事情。
第一点,要实现DevOps这样一个工具,首先要把DevOps任务编排起来,所以我们需要做到一个能尽量覆盖到尽多客户DevOps需求的编排逻辑。
第二,DevOps任务是千差万别的,我们没办法去开发完所有的插件,去适配每个客户自己的需求,他们的部署策略,他们的单元测试、编译等等。所以我们需要把这些任务交给客户自己去完成,需要设计一个插件机制,就是Component,后面会介绍。第三点,用户的DevOps流程运行在TencentHub里面,我们需要真正去执行它,但我们不想去发一个执行任务的集群,我们只需要只需要做很简单的任务调度,然后交给成熟的集群管理组件完成。所以我们最终还是选择kubernetes来做这个事情,也就是TKE,它为我们省去了很多运维工作,还有TKE的监控都可以复用。
DevOps任务编排 Workflow/Stage/Job
按业界通用的方法,workflow设计成三级结构,每个workflow包含多个stage,每个stage里面会有很多job,job会有并行和串行的执行方式。stage有一个类型叫pause。为什么会这样一个类型呢?比如金融客户的发布有很严格的审批流程,并不是说我修完代码,提交到git server,代码的提交,触发workflow的执行,然后去线上部署,直接一股脑跑通了。这是不行的,在DevOps流程当中,在合适的时候是需要人工介入的,需要团队中不同角色来决定流程是否继续,因此我们设计可以暂停的stage,就是为了来适应这样的场景。
具体的设计思想是,用户可以去开发一个自己的DevOps Task任务逻辑(component),我们会把stage接下来继续执行的调用接口或者取消接口通过环境变量传到component里面去,在component插件里面,开发者可以通过邮件或者其他形式,把你需要审批的流程发给上级审批部门去察看,他如果通过之后,通过客户自己的系统,点击这个URL,回调到TencentHub里面来,TencentHub就会根据你当前回调的URL是取消任务还是继续执行任务,来决定这条workflow是继续向下走还是终止。
Component的设计我们后面再详细介绍。
Workflow 生命周期
workflow的设计生命周期需要简单介绍一下。首先,workflow可以被触发执行,这里有三种方式。一种是把某一条workflow和代码关联起来,当提交代码的时候,可以触发这条workflow的执行。第二种是也可以和TencentHub的镜像存储关联起来,比如说我们可能不需要代码去触发,我们想通过push一个镜像,因为我们的代码可能是在内部比较重要的地方去存储,不会用到公有的github、gitlab,或者不想暴露git仓库出来,就可以通过push一个镜像去触发。第三种是可以通过调API直接触发某一条workflow。
workflow被触发执行,就会在系统中把它置成一个pending状态。因为每个客户有配额限制,我们的scheduler会去检查这个用户的配置是否足够,例如他如果有些比较长的workflow执行,我们会一直将它放在一个调度的状态,当检查通过之后,就会被投入到TKE当中去执行。但是它也不一定马上会运行起来,因为TKE的资源也有可能会比较紧张,我们这里提供的是公有云服务,所有人都会访问,所以资源可能会临时不够用,也会处于pending。我们会有一个状态的检测服务,不停的轮巡job的状态,如果发现部分它已经脱离了pending,进入了其它状态类型值,我们就会把它标定为running状态。如果一个stage被标记为是一个可暂停的stage,这个stage里所有的job被执行完之后,我们会把它标记为已被暂停的状态,这时候它需要等待外界的反馈,来决定这条workflow继续如何走: 如果外界反馈说它要取消,workflow就会进入到整个流程的结束。
Job feature
首先,每个job都考虑成为一个类似于函数一样,去处理输入,在内部做一些自己的业务逻辑,最后通过我们定义的标准输出,去输出一些它处理完的信息。
第二,job可以从workflow全局环境变量中去读取信息,做一些逻辑。这些环境变量不能修改,因为如果我们提出一个全局可修改的变量存储,它会导致整个component的调试或者开发会非常麻烦。
第三,每个component肯定需要和外界打交道,我们workflow里面会去提供cache和artifact指令,让用户可以非常方便地把container里面构建出或者运行的一些结果保存到我们提供的外部存储里面。
第四,workflow没有办法去循环执行,只是一个DAG构成的关系图,然后一条一条向前执行。这是我们设计workflow的一些考虑,没有去违背这些,才去做接下来的开发。
Job Storage -- Artifacts & Cache
Cache,是用来在不同的job之间共享和传递一些数据,它可以是文件夹,也可以是文件。Artifacts是构建出的一些结果,可以保存在TencentHub仓库里面,在仓库里面有界面,也有API,可以进行查看或者拉取下来。Cache没有去跨多个workflow实例。Cache的具体实现工程是对指定的文件/文件目录进行压缩,上传到TencentHub的对象存储里面。当下面有一个Job依赖它的时候,又会在Component内部把它下载下来。整个实现就是在两个hook中进行完成,两个hook一个是Prestart,一个Poststop,后面会介绍我们如何去实现hook机制。
为什么使用容器去做DevOps?
我们选择了用容器来做DevOps,为什么我们要选择用容器来做这样的插件机制呢?也是考虑了很久,讨论了很久,核心是基于下面四个考虑。
• 第一,我们面对的所有用户是公共服务,隔离性是我们要考虑的第一件事情,用容器可以非常方便的帮我们实现不同用户的任务隔离,有些用户可能对自己任务的安全性要求非常好,我们后期会考虑和CIS做结合,直接用Clear Container来提供更高的隔离性。
• 第二,复用,在不同的部门之外,它们的技术栈可能会有相似的,后台有一些公共的DevOps任务需要去使用,通过容器Docker镜像可以非常方便的去共享这样的逻辑。
• 第三,Docker标准非常固定,也已经非常成熟,通过我们前面对job特性的规范把它确定下来,它可以在本地直接去调试自己的Component,而不需要要建到一个workflow里面去做这个事情。我们知道,如果我们用jenkins去搭一个复杂的workflow,编写、调试脚本是比较麻烦的一件事情。
• 最后,我们考虑到需要平滑过渡客户已有的DevOps流程,因为每个公司会积累很多已有的不管是否标准的DevOps任务,如果让完全重新开发,代价是很高的。当我们用容器封装的时候,只需要在Component封装一个原来的任务,无论是用shell脚本,或者还是用Go写的,只要提供一个环境,用容器封装起来,就可以快速的把之前的逻辑挪过来。当然,这些迁移都不是完全透明的,需要去做开发,用容器封装之后,我们相信这个工作量不会特别大。所以我们最终选择了使用容器来作为DevOps的核心插件机制。
Component像一个函数实现一样,会有Input和Output,每个Component可以从另一个Component的Output去添加需要使用的一些输出值,workflow会把它变成Component执行的容器的环境变量,注入到当前Component里面。在Component容器进程里面可以使用这些环境变量做一些逻辑,比如说克隆指定版本的代码,或者发布上一个环节构建出的版本。Component的输出直接放到标准输出里面,我们规定了每一行只要符合下面红色地方标明的数据格式,我们就把它作为Component的输出。选择这样不够优美,但是非常实用的方式去实现,是考虑到如果使用外部的存储比如redis对input/output 作为存储,会对用户编写Component造成很大的影响:需要在Component里面还要去调用外部存储,保存输出需要存储一个Key,在下一个阶段使用时又要用key到存储里去取出来。虽然可能用一个第三方的存储会带来更丰富的数据结构方面的优势,但是我们认为对客户开发Component侵入性太大,所以我们直接用环境变量和标准输出来做这个事情。
workflow引擎
workflow是在我们的TKE去执行的,选择用DevOps做workflow引擎的执行集群,是基于这些特性去考虑的。
• 首先,Kubernetes的可靠性,TKE非常可靠,我们不用去担心它的运维方面的事情。
• 第二,workflow跑的很多任务可能会占用不同的资源,有可能一个固定的集群资源大小是不够的,结合TKE的自动扩缩容,我们就非常方便的去在上层workflow变得很多时,自动为客户提供构建的能力,不用我们人工又去介入。
• 第三,它的资源分配更灵活,并不是每个客户自己的workflow都是很小的资源占用,有些可能会做一些压力测试之类,会占用更大的资源。如果我们自己去实现一个workflow执行程序,就要考虑这些调度问题,所以我们最终选择用kubernetes去做这个事情。
前面到使用Component作为插件的时候,使用到 Prestart和Poststop这两个hook,在设计的时候,我们调研过是否能复用kubernetes pod 的Poststop。发现是不行的,Poststop是在pod的生命没结束之前由外面的controller去结束一个pod的时候执行(这才会有机会在容器之内去运行指定的hook程序)。但如果像workflow这种Task是一次性执行然后自行退出的,kubernetes没有办法在进程正常前调用调用Poststop里面指定的程序 – kubernetes不知道什么时候Task会退出 。
所以我们最终实现了自己的方式: workflow把Component在kubernetes里面运行变成pod前,把它的CMD替换成另一个程序CommandWrapper,这个程序采用静态链接,保证可以在Linux上运行。CommandWrapper会在组件component任务被执行之前,会完成Cache和Artifact相应的逻辑,然后component会在子进程中运行,子进程退出后, CommandWrapper会继续处理Cache和Artifact的上传逻辑,最后,CommandWrapper程序会以子进程所退出状态码进行退出,如果是返回码是0,workflow引擎就把它标记为是成功运行,然后会调度下一个job的运行。这就是我们去设计的Hook机制。
这里简单介绍如果workflow引擎如何获取Task的Log。开发人员常常需要查看每个运行的workflow job的Log。可能workflow里面会有Bug,或者想看它当前进行到什么地步。Log的获取没有使用Docker driver,因为这在kubernetes里面目前还没有实现,即时kubernetes支持了docker的log driver,通过单独的通道去收集Log也会带来很大的复杂性,所以workflow引擎直接调用了kubernetes的API去获取它的Log,因为kubernetes API的Log只要在调用没退出之前,会一直把pod运行的Log从集群里面读出来,返回给调用方。Workflow引擎把整个Log存起来,放到COS里面,或者最后把它通过websocket去写到前端,让用户可以去实时查看。当然,后者我们还没有实现,开发人员太少,但是我们预留了,可以这样去做。
今天没有给大家分享怎么去搭建自己DevOps流程的例子,因为我相信这是千差万别的,在不同的公司,不同的组织里面,流程都不相同,我们没有办法枚举出来。而是站在公共服务的提供商角度,考虑怎么给用户最大的便利去建立这样一套工具。所以我今天的分享主要是关于DevOps的设计和一些实现细节,希望对大家会有一些帮助。
TencentHub这个产品目前已经在腾讯云官网上开启内测,邀请大家一起来体验这个产品,也欢迎大家的反馈和吐槽。
https://cloud.tencent.com/product/thub
推荐阅读:
小白也能玩转Kubernetes 你与大神只差这几步
互动福利!
为了方便广大开发者更好的学习和提升技术实力,云加社区建立了微信群,一起交流学习包括机器学习、Python、小程序、Java、云计算等知识。
想要入群的朋友可以加 微信(itquan1234,备注入群),我们邀请了腾讯的技术大牛定期在群里分享,不定时组织抽奖,送书等活动,更有各种技术资源干货。
↓↓↓点击阅读原文,你也可以成为技术大牛!