TencentHub技术架构与DevOps实践揭秘(下)

2019 年 10 月 29 日

TencentHub技术架构与DevOps实践揭秘(下)

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 的设计和一些实现细节,希望对大家会有一些帮助。


本文转载自公众号云加社区(ID:QcloudCommunity)。


原文链接:


https://mp.weixin.qq.com/s/Fcp2Z7afM5DHnTov31w6TA


2019 年 10 月 29 日 19:131335

评论

发布
暂无评论
发现更多内容

写文档太麻烦,试试这款 IDEA 插件吧!

程序员小航

Java markdown IDEA idea插件 文档

EDA最强攻略,如何为EDA选择存储?

焱融科技

分布式 高性能 存储 半导体 EDA

一个研发团队是如何坚持7年技术分享的?

PingCode

团队管理 敏捷开发 研发管理 技术分享 程序员节

TensorFlow 篇 | TensorFlow 数据输入的最佳实践

Alex

tensorflow keras input pipeline dataset

TypeScript魔法堂:函数类型声明其实很复杂

肥仔John

Java typescript

直播预告 | CloudQuery初体验——安装及多数据源连接

CloudQuery社区

数据库 sql 安全 工具软件 dba

C++ primer -- 第16章 string类和标准模版库

Dreamer

c++

当下工作流管理系统的发展趋势

Marilyn

敏捷开发 快速开发 软件架构 企业开发

独立显卡市场又一巨头跻入,英特尔锐炬® Xe MAX 独立显卡来了!

intel001

MySQL中事务的持久性实现原理

X先生

MySQL 数据库 sql 数据库事务 事务

C++ primer -- 第17章 输入,输出和文件

Dreamer

c++

专利申请其实并不难?四步教你玩转专利申请!

华为云开发者社区

专利 保护

数据结构与算法系列之链表操作全集(二)(GO)

书旅

go 数据结构 算法

快速掌握并发编程---深入学习ThreadLocal

田维常

面试时说Redis是单线程的,被喷惨了!

云流

redis 编程 程序员 计算机

在2020年更受关注和追捧的JS框架

Geek_Willie

react.js Vue js Svelte

基于React+Koa实现一个h5编辑器

徐小夕

Java nodejs H5 React koa

架构师训练营第 1 期 第 5 周作业

李循律(祥龙)

极客大学架构师训练营

SpringBoot-技术专题-war包部署读取外部配置Yml

李浩宇/Alex

永续合约系统开发源码,合约跟单软件搭建app

WX13823153201

接口测试人员需要掌握的知识技能

测试人生路

接口测试

华为云的销售凭什么说“赢”了罗振宇?

ToB行业头条

tob

Docker底层技术

混沌畅想

Docker 容器 DevOps 底层技术

Caffe 安装踩坑记录

Dreamer

caffe

目标检测综述

Dreamer

架构师训练营第 1 期 第 3 周作业

李循律(祥龙)

极客大学架构师训练营

架构师训练营 1 期第 6 周作业

木头发芽

C++ primer -- 第18章 探讨C++新标准

Dreamer

c++

SpringBoot-技术专题-war包部署读取外部配置Properties

李浩宇/Alex

试用阿里网盘内测版-不限速、无广告、隐私安全我全都要

郭旭东

阿里云网盘

Forsage矩阵系统开发,智能合约搭建

薇電13242772558

TencentHub技术架构与DevOps实践揭秘(下)-InfoQ