陈曦 分布式实验室
Docker技术应用广泛,可以将软件与其依赖环境打包起来,以镜像方式交付,让软件运行在“标准环境”中。这一特性可以应用到持续集成中,实现原生支持容器云平台持续交付。本文将概述CI&CD基本工作流程,软件整体框架以及实现原理。
背景概述
持续集成是一种软件开发实践,即团队成员经常集成他们的工作,每次集成都需要通过自动化的构建,包括编译、发布、自动化测试来验证,从而尽早的发现集成错误。持续交付是指频繁地将软件新版本交付质量团队或者用户以供评审,如果评审通过,发布到生产环境。
Docker是一款基于LXC的容器引擎,自2013年开源以来,因为其易用性、高可移植性在开源社区非常火热。Docker将软件与其依赖环境打包起来,以镜像方式交付,让软件运行在“标准环境”中,这非常符合云计算的要求。各大IT巨头纷纷跟进,基于Docker容器技术创业公司也如雨后春笋,Docker创造了一个崭新的容器云行业。
Docker技术应用广泛。如,利用其隔离特性,为开发、测试提供一个轻量级独立沙盒环境进行集成测试。加速本地开发和构建流程,使其更加高效轻量化,开发人员可以构建、运行并分享容器,轻松提交到测试环境中,并最终进入生产环境。Caicloud Circle正是基于Docker这一特性打造的一款容器原生持续集成持续交付Saas产品。
CI&CD基本流程
Circle提供丰富的rest API供Web应用调用;
通过API建立VCS与Circle服务关联后,VCS上的commit等动作会触发Circle构建;
VCS Provider组件从VCS拉取代码库中源码;
基于源码中配置文件CI配置启动需要的CI微服务容器进行集成测试;
集成测试通过后,进入Pre Build阶段在指定编译环境容器中编译可执行文件;
Build阶段中将可执行文件拷贝到指定运行环境容器中,打成镜像推送到Registry中;
Post build阶段会做一些镜像发布后的关联操作,比如推送运行依赖的静态资源文件到CDN中;
镜像发布后可以自动部署应用到Caicloud、Kubernetes、Mesos、Swarm等PaaS平台;
构建过程日志可以通过LogServer拉取;
构建结束构建结果可以发邮件通知用户。
整体框架
Circle整体框架如下图所示。Circle运行在容器中,与Kafka-ZooKeeper容器集群通信推送拉取构建过程日志,基础数据信息存储在mongo数据库容器集群中。在Circle内部:API Swagger组件提供在线Circle API说明帮助文档;API Server组件接收用户http请求,生成异步待处理事件记入Async Event Manager组件的事件队列中,并应答用户请求;Async Event Manager组件在检测到新的待处理事件创建后,根据事件具体操作类型调用CI&CD组件提供的工具函数组装拼接成工作流水线,检测到事件完成后更新数据库中文档记录;在容器外运行一组Docker Daemon队列,各工作流水线独立使用一个Docker Daemon,以实现用户和事件隔离;CD&CD组件会从Docker Daemon Queue中调度取出一个空闲Docker Daemon执行流水线任务,过程日志推送到Kafka指定的topic中;Log Server组件向用户提供一个获取日志的Websocket服务器,从Kafka拉取实时日志推送给用户。
Circle也可以多节点分布式部署,部署图如下所示。每一个立方体代表一个节点,使用Haproxy反向代理实现负载均衡和SSL数据加密,分发API请求到各Circle节点中。
实现原理
在Web页面通过OAUTH用户授权拉取用户VCS版本库列表,选择建立与指定版本库相关联的服务。如果VCS使用的Git还能调用Git API建立Webhook,当版本库产生commit、tag、pull request等事件时调用Circle API触发CI&CD。
Circle CI&CD的具体各步骤操作定义在代码库caicloud.yml文件中。分为integration、pre_build、build、post_build、deploy五段。Circle从代码库中拉取文件后,解析caicloud.yml文件,依配置执行具体操作。
integration段执行集成测试,Yaml文件中定义了编译可执行文件使用的语言镜像名,运行使用的环境变量,启动指令(可以运行一些测试脚本、测试应用),以及集成测试依赖的微服务容器配置。首先使用Docker remote API调用分配的Docker Daemon启动依赖的微服务容器;然后启动集成容器编译可以文件,执行命令行,完成后容器退出,返回命令行执行结果码标示集成结果。
#integration段 integration: image: golang:v1.5.3 #镜像名 environment: #环境变量 - key = value commands: #容器运行后依次执行的命令 - ls - pwd services: #依赖的服务 postgres: #服务名 image: postgres:9.4.5 #服务镜像 enviroment: #环境变量 - key = value commands: #服务容器启动后依次执行的命令 - cmd1 - cmd2
prebuild段执行编译工作,可以使用Dockerfile也可以使用Yaml的K-V值。Yaml文件中可以定义构建使用的Dockerfile路径文件名,prebuild容器启动的基础镜像、环境变量、启动命令行以及编辑结束后,需要输出的可执行文件夹或文件名,如果Dockerfile和容器配置同时被定义,优先使用Dockerfile。CI&CD首先解析指定的Dockerfile或者yaml文件内容,获取容器启动配置,依配置调用容器,执行命令行编译可执行文件后退出容器,如果编译成功,使用Docker copy API拷出指定的输出文件。
#prebuild段 #若context_dir和dockerfile_name至少配置一项,则优先以Dockerfile构建镜像 #若context_dir和dockerfile_name均未配置,则以以下容器配置构建镜像 pre_build: context_dir: Prebuild #Dockerfile路径,构建依赖文件不能在该目录之外,默认为repo根目录 dockerfile_name: Dockerfile_prebuild #Dockerfile文件名,默认为Dockerfile image: golang:v1.5.3 #prebuild镜像名 volumes: #挂载数据卷 - .:/root #挂载repo文件到指定目录 environment: #环境变量 - key = value commands: #容器运行后依次执行的命令 - ls - pwd outputs: #输出文件,如果生成的输出文件在工作路径下,可以缺省 - file1 - dir2
在build段中基于指定的Dockerfile构建发布镜像。在该Dockerfile中可以添加prebuild段中输出的可执行文件到发布环境中。Prebuild和build的分步操作实现了软件编译环境与软件运行环境的隔离。构建完成后push镜像到指定的镜像仓库中。
#build段 #context_dir和dockerfile_name至少配置一项 build: context_dir: . #Dockerfile路径,构建依赖文件不能在该目录之外,默认为repo根目录 dockerfile_name: Dockerfile_publish #Dockerfile文件名,默认为Dockerfile
Post build段可以定义一些镜像发布后的关联操作。比如用户可以制作一个包含程序运行依赖的必要静态资源的镜像,在Post build阶段以该镜像启动容器,并通过执行命令行推送到CDN中。
#post_build段 #若context_dir和dockerfile_name至少配置一项,则优先以Dockerfile构建镜像 #若context_dir和dockerfile_name均未配置,则以以下容器配置构建镜像 post_build: context_dir: Prebuild #Dockerfile路径,构建依赖文件不能在该目录之外,默认为repo根目录 dockerfile_name: Dockerfile_postbuild #Dockerfile文件名,默认为Dockerfile image: golang:v1.5.3 #镜像名 environment: #环境变量 - key = value commands: #容器运行后依次执行的命令 - ls - pwd
发布完成后进入部署阶段。用户在Web界面预先配置好部署方案,指定集群分区应用和容器名,Circle会调用集群提供的应用部署API,将构建好的镜像部署应用中,并查询部署状态,如果失败回滚到上一个成功镜像。
一些亮点Feature
多Docker Daemon构建实现用户事件隔离
作为服务器不会在同一时刻仅处理单一请求,在同一节点上同时运行的事件任务可能会相互影响。Circle采用多Docker Daemon隔离实现用户事件隔离。在节点上运行一组Docker Daemon队列,调度分配给单一事件任务使用,使用完成后清理残留容器和镜像,确保构建环境整洁。队列元素个数有限,当没有空闲Docker Daemon时事件任务进入排队等待状态,等待超过2小时,事件任务超时失败。当用户需要取消构建时,仅需kill正在执行当前构建任务的Docker Daemon,然后重启一个新的Docker Daemon加入到空闲队列中。
微服务多模块联合发布
微服务正在博客、社交媒体讨论组和会议演讲中获得越来越多的关注。微服务架构是一种特定的软件应用程序设计方式——将大型软件拆分为多个独立可部署服务组合而成的套件方案。为了管理微服务多模块间的依赖管理,联合集成发布多个模块,Circle实现了联合发布功能。
首先建立多个与单一模块代码库代码关联的服务;然后通过UI拖拽方式设置多个服务间的树形依赖关系,Circle将树形关系转化成线性发布序列存储。当用户点击一键联合发布时,Circle将同时启动多条CI&CD流水线对多个模块分别进行集成测试和构建(Integration+Prebuild+Build+Post Build操作),所有模块构建都完成后依存储线性发布序列依次部署到容器集群应用中(Deploy操作)。
镜像安全扫描
常见的文件分析方法有两种:静态分析和动态分析。我们采用的是静态分析,检阅镜像的文件系统。漏洞是从Linux操作系统的通用漏洞披露(CVE)数据库获取。
今后工作展望
运维
目前Circle实际部署采用的多节点分布式部署方式,各节点上使用Docker compose运行各容器,升级运维都比较麻烦,需要SSH连接到各节点上命令行操作。我们计划近期将Circle部署到Caicloud CLaas技术栈,运用Kubernetes强大的运维功能提高Circle的生产效率。可能会对现有框架做些改造,目前思路如下:
每个立方体表示一个Pod。使用Nignx做反向代理,TLS加密;Circle-Master中API-Server组件向web提供API服务,Log-server组件提供实时日志服务,当调用API构建,会到新建一个构建任务发送给Worker manager组件,Worker manager记录任务信息到etcd中,并新建一个Circle-Worker pod执行任务;Circle-Worker中有之前的CI&CD组件,依次启动容器执行Integration、Prebuild、Build、Post Build和Deploy,中间过程日志推送到Kafka,任务状态同步到etcd,任务结束后pod退出;各组件需要持久化的信息写入mongo。
并发
CI&CD任务需要占用大量的系统资源,Circle服务器资源有限,如何才能支持大量并发的构建任务?我们的思路是可以让用户添加自有工作节点到Circle集群中,由Circle来调度CI&CD任务管理逻辑,由用户自有工作节点来承载执行任务的运算负荷。将CI&CD流水线部分拆分出来,基于Docker in Docker镜像打包制作成Circle-Worker镜像。用户在自有机器上安装运行Docker后,将Docker remote API地址以及机器资源配置告知Circle,Circle验证机器有效性后将该机器拉入集群中。当该用户有需要执行的CI&CD任务时,调度用户节点运行Circle-Worker容器从Circle获取任务信息并执行,完成后返回结果。