本文共 18846 字,大约阅读时间需要 62 分钟。
随着自动化运维水平的提高,一个基础的运维人员维护成百上千台节点已经不是太难的事情,当然,这需要依靠于稳定、高效的自动化运维体系。本篇文章即是阐述如何利用 bitbucket pipeline 结合 Amazon S3 存储实现项目的自动构建、自动发布以及异常报警等完整的自动化流程处理。不同于常见的自动化项目一般服务于局域网体系,有很大的并发限制与网络带宽限制,本篇介绍并实现的运维体系不存在网络瓶颈,可以支撑广域网运维自动化,并且支持高并发、升级速度快、架构稳定可扩展性强等优点。
本架构中涉及到的主要应用有:
Bitbucket:一种基于 Git 做版本控制的代码托管平台,其他比较流行的平台有 Github、GitLab、Coding。Bitbucket 支持 pipeline 功能,这也是我们自动化体系CI(持续集成)/CD(持续交付)的基础。Amzaon S3:AWS 官网介绍是:“提供了一个简单 Web 服务接口,可用于随时在 Web 上的任何位置存储和检索任何数量的数据”。简而言之,就是 Amazon S3 提供一个网络存储平台,我们可以利用其提供的 Web API 进行内容的存储与访问。Docker Hub:目前 Docker 官方维护的一个公共 Docker 镜像仓库,而在我们的自动化体系中,也是基于基础的 Docker 镜像所做的二次开发。Ansible:一个基于 SSH 安全认证的批量操作工具,相比较于 saltstack 其操作、部署以及维护比较简单,但是连接速度较慢。Slack:优秀的企业协作通讯平台,我们可以利用其提供的接口进行自动化执行结果的消息通知与展示。我们将一个产品项目(ProjectX)根据逻辑功能不同拆分拆分成若干个独立的模块,每个模块存储在一个独立的仓库中。另外,我们根据仓库的维护者不同将仓库分为源码库(source repository)、发布库(release repository)、整合库(distribution repository)。比如我们的项目 ProjectX 对应的整合仓库为 ProjectX.dist ,该项目中有个功能模块为 ModuleX ,针对这个模块需要有源码仓库(ModuleX.src)以及发布仓库(ModuleX.rel)。 其中源码库(后简称 SRC 仓库)由研发人员维护,即提供某个功能模块的源代码、模块功能介绍与使用说明等信息。开发人员对源码库可读写,运维人员可读;发布库(后简称 REL 仓库)由系统运维人员维护,提供源码(假设源码为编译型语言)编译后的二进制文件安装程序。运维人员对发布库可读写,开发人员无权限;整合库(后简称 DIST 仓库)则是将不同的仓库模块按照一定的逻辑顺序组合起来,形成一个完整的产品项目。运维人员对整合库可读写,开发人员无权限。产品项目的 Git 仓库逻辑分层结构示意图如下:
一个项目由一个 DIST 仓库、N 个 SRC 仓库、N+M 个 REL 仓库组成。只有一个整合仓库,这个很好理解,但是 SRC 与 REL 仓库的关系如何理解呢?一般来说一个 SRC 仓库会对应一个 REL 仓库,即研发人员只负责提供源代码,至于如何安装到系统、安装到系统的哪个目录、以及如何制定安全备份策略、日志回滚策略等问题则不需要关注,这些由系统运维人员将该程序的执行策略以程序或者脚本的形式存在对应的 REL 发布库中,因此一个 SRC 仓库会有一个相应的 REL 仓库与之对应。但是一个 REL 仓库有可能是不需要 SRC 仓库的,比如我们在系统中会引入某个开源程序(如 Nginx),为了项目的稳定性,我们会把 Nginx 指定版本的 RPM 安装包作为一个文件存储至 REL 仓库中,这样这个仓库其实就不需要源码库即 SRC 仓库,但是如果我们要对 Nginx 做二次研发,然后再编译成二进制文件,则需要额外创建 SRC 仓库用于存储源代码。
当开发者将代码推送至 Bitbucket 上的源码仓库之后,会触发 SRC 仓库的 pipeline ,调用指定的 Docker Image (一般需要我们做二次修改、发布才能使用)完成源码自动编译、打包,我们定义打包文件名同二进制文件名加上指定的压缩后缀(比如 ModuleX.src 中提供了一个二进制程序为 modulex-client ,那么该模块打包名称即为 modulex-client.tar.gz),并且存储至 Amazon S3 存储桶中;然后运维人员需要根据程序的配置文档(由研发人员提供相应的 README 文件,指明程序如何安装、如何指定配置文件等)开发相应的安装程序,然后将安装程序推送至 REL 仓库中。这同样会触发 REL 仓库的 pipeline ,通过调用相应的 Docker Image 将安装脚本打包存储至 Amazon S3 中,同样,我们也需要定义发布库库的打包文件名同发布仓库名称(去掉 .rel 后缀)加速指定的压缩后缀(比如 ModuleX.rel 打包之后的名称为 ModuleX.tar.gz)。这样在 S3 中就有了编译后的二进制程序,以及该程序的安装脚本,我们在 DIST 仓库的整合程序中只需要根据两个包的 S3 存储位置将其下载下来,执行安装脚本即完成该模块的安装,对于其他模块也是如此。DIST 仓库只需要一个整合程序将所有的子模块(仓库)按照一定的逻辑顺序组合起来,就可以完成了一个项目的发布。至此,我们已经能够完成一个“半自动”的项目安装,运维人员只需要将 DIST 仓库中的整合程序下载到节点并执行,整合程序会去相应的 Amazon S3 存储上下载各个仓库的安装程序与二进制文件(如果有的话),然后执行各个模块的安装程序,这样当所有的模块安装配置完成后,一个完整的项目就已经正常运行起来了。
接下来,我们可以借助自动化执行工具(如 Ansible、Puppet、Saltstack 等)与 pipeline 完成节点的自动化批量部署。即系统运维人员将整合脚本提交至 DIST 仓库后,会触发仓库的 pipeline ,将整合脚本与其他相关文件(如项目版本信息、子模块引用信息,以及相应的存储信息等)推送至安装了自动化执行工具的中控节点,中控节点将安装或者更新任务分发到子节点上,继而完成全球节点持续集成与持续发布。
当完成节点的发布任务之后,我们需要对发布结果(特别是失败结果)做有效监控与通知反馈。从中控平台发布任务到节点执行,至少包含两个关键节点:一是中控平台能否成功将任务下发到被控节点,二是被控节点能否成功执行下发任务。前一个关键节点可以通过中控平台的分发任务执行结果(或者日志)得知,后一个关键节点可以通过被控节点的任务执行状态(或者日志)得知。架构的执行流程图如下:
我们需要制定 S3 Bucket 存储桶结构规则,以便于后期程序可以按照这样的规则完成自动存储与获取。首先,一个项目对应一个存储桶,即 Amazon S3 Bucket;其次,每个子仓库(模块)是该存储桶下的一个对象(可以简单的理解为一个目录),而上文提到的源码仓库编译后打包的二进制文件,以及发布仓库打包的安装配置等文件皆存储在该目录中。如果我们继续以上文提到的 ProjectX 项目为例,那么当前存储桶的结构如下:
ProjectX: - ModuleX - modulex-client.tar.gz - ModuleX.tar.gz
这样已经可以满足单个模块功能的信息存储,但是只能存储最近一次打包结果。如果我想存储之前发布的某个版本,则不能实现,设置不能够进行该版本的任何测试就会被推上生产系统,这显然不合理。为了解决这个问题,我们给存储桶又加了一层“分支”与“版本控制”的结构概念。对于分支,一般我们在生产开发中至少都会有两个 git 分支,即 master 与 developer 分支,其中 developer 分支代码用于功能测试,当测试通过之后提交到 master 分支,用于正式版本发布。因此我们在 S3 存储桶中模块对象下,又新增了一层目录结构,developer 目录与 master 目录,针对测试代码,我们无需存储之前的版本,只保留当前最新代码即可,因此直接将打包后的文件存储至 developer 目录下即可替换之前的测试程序。但是对于发布版,则需要保留历史版本,以便快速回滚,或者由于某些其他模块引用了当前模块的某个指定版本,而导致该版本必须保留。因此 master 分支与 developer 不同,不能通过简单的创建一个 master 目录来解决问题,基于此,我们对 mater 分支制定了一个规则,即一旦合并了 developer 分支,必须打上相应的 tag 信息(否则无法触发 PIPELINE 完成自动构建),存储桶功能模块下不再以 master 作为主分支打包程序的放置目录,而是以 tag 名称作为放置目录。这样的话,我们就可以通过 tag 信息,获知某个目录下存储的是哪个 tag 的发布代码,这样当前的存储桶目录结构如下:
• ProjectX: • - ModuleX • - developer • - modulex-client.tar.gz • - ModuleX.tar.gz • - 1.0.0 • - modulex-client.tar.gz • - ModuleX.tar.gz • - 2.0.0 • - modulex-client.tar.gz • - ModuleX.tar.gz • - ModuleY • - developer • - moduley-client.tar.gz • - ModuleY.tar.gz • - 3.2.0 • - moduley-client.tar.gz • - ModuleY.tar.gz
源码仓库(source repository):仓库名称以 ‘.src’ 作为后缀,包含源代码、PIPELINE 文件(注意:bitbucket 的 PIPELINE 文件名称必须是 bitbucket-pipelines.yml)、程序配置文件模板(如果需要的话)、CHANGELOG 与 README 等说明文件,以便运维人员开发相应的安装配置程序。
发布仓库(release repository):仓库名称以 ‘.rel’ 作为后缀,包含源码编译后的二进制程序安装与配置程序(脚本)、升级程序、配置文件、源码库版本信息(安装程序就是根据这个版本(或者说是 tag)信息去 S3 上获取相应 tag 名称目录下的打包文件)。 整合仓库(distribution repository):仓库名称以 ‘.dist’ 作为后缀,包含整合程序(安装与升级)、维护一个项目(project)所需的模块列表以及各模块的版本信息,整合程序根据各个模块列表的版本信息从 Amazon S3 的存储上下载相应模块的指定版本,通过执行各模块的安装脚本,完成各个模块的安装。 模块版本信息文件(repo-info):该文件存在于 REL 仓库与 DIST 仓库中,满足 yaml 文件格式。该文件在 rel 仓库中主要用于记录该模块的源码 tag 信息(后面如无特殊数码,同“发布版本信息”同义),发布库分支或版本信息,以及依赖模块的发布版本信息,文件格式如下:• # Repo-Info 文件配置模板,其中 repo_name 可以理解为单个模块功能在 git 中的仓库名称 • rel_branch: [developer|rel_tag_info] # 生产环境只能 developer OR rel_tag_info 二选一 • src_branch: [developer|src_tag_info] # 生产环境只能 developer OR src_tag_info 二选一 • depend_list: [none] # 生产环境只能是 none 或者 下面的 KEY-VALUES 二选一 • repo_name1: 1.2.3 • repo_name2: 1.1.1 • repo_name3: 1.1.2 • repo_name4: 2.1.3 • repo_name5: 3.1.3
通过上面的格式定义, rel 仓库中的安装脚本就可以去 S3 存储上获取源码安装包,以及某个版本的依赖模块安装包。简单说明下配置文件各个参数的含义: rel_branch: 用于指定 REL 仓库的打包文件存放位置,如果是 developer 则表示存储在 S3 上模块名称目录下,developer 目录中,用于生产测试;而如果是 rel_tag_info 则表示存储在 S3 上模块名称目录下,对应 tag 名称目录中,用于生成发布。因为只有在测试通过之后,才会合并到 master 分支,而 master 分支也只有在打上 tag 之后,才会触发 pipeline 完成自动构建,将对应的打包文件存储在以当前 tag 为名称的目录下。所以,S3 存储中 tag 目录下的打包文件,均是生产发布版本。 src_branch: 用于指定 SRC 仓库的打包文件存放位置,与 rel_branch 设计理念相同,测试分支会存储在 developer 目录下,正式发布版本会存储在以 tag 信息作为名称的目录下。 depend_list: 用于指定该模块的依赖模块(或者说是库),如果值为 ‘none’ ,表示该模块没有依赖模块,可以直接安装。如果不是 ‘none’ ,则需要指定依赖模块的名称,以及相应依赖模块的版本信息。需要注意的是,这里的模块版本信息我们规定只能使用 tag 信息,也就是只能指定某个模块的发布版,而不能使用 developer 下的打包文件,即该模块的测试版,因为依赖模块作为一个底层模块,必须保证稳定的前提下才能被其他上层模块所引用,也才能保证上层模块的稳定性。 而在 DIST 仓库中的 repo-info 文件,文件格式同样满足 yaml 语法,文件的内容参数略有调整,以下为模板文件:
rel_branch: [developer|rel_tag_info] # 生产环境只能 developer OR rel_tag_info 二选一 sub_repo: [none] ##如果是 none ,则不会有下面需要安装的仓库列表信息 repo_name1: 1.2.3 repo_name2: 1.1.1 repo_name3: 1.1.2 repo_name4: 2.1.3 repo_name5: 3.1.3 upgrade_list: [none] # 如果是 none ,则不会有下面需要升级的仓库列表信息 repo_name3: 1.1.3 repo_name4: 2.1.4 repo_name5: 4.0.0
其中 rel_branch 与在 REL 仓库中的含义一样,用于表示 DIST 仓库存储在 Amazon S3 上的位置,但是需要注意的是 DIST 仓库是没有 src_branch 的,因为这个仓库就是专门用于各个子模块的整合,不存任何代码,只保留各模块版本(也即是存储)信息。 sub_repo: 表示这个项目(Project)由哪些模块组成,各模块的版本是什么,整合脚本就是根据 sub_repo 的信息进行对应的功能模块获取与安装的,这里需要注意的是,模块安装会存在顺序关系,这里需要区别对待依赖关系。依赖关系是缺少某个模块会导致当前模块无法运行,或者部分功能不能使用;而顺序关系,则是逻辑关系,缺少某个或者顺序不对不会导致另一个无法安装,只是整个业务逻辑上会存在一定的问题。举个例子,我们编译 nginx 需要 gcc 库,必须先安装 gcc 库才能编译安装 nginx,这就是依赖关系。而 nginx 在接收客户端请求后,可能会将请求转给 php 处理,php 可能会去操作数据库,我们一般安装的时候,就会先安装数据库,但是这两者之间的安装就不存在依赖关系,而是一种顺序关系。如果先安装 nginx 然后在安装数据库,可能只会导致业务不能处理动态请求而已,而不会影响两个应用的正常安装。 upgrade_list: 用于指定项目升级(也即 DIST 仓库升级)涉及到哪些模块的更新,‘none’ 表示无升级需求,如果为空,则需要根据下面的仓库名称,下载相应的子模块,执行里面的升级脚本()。
在我们定义完 Amazon S3 存储规则,并且在发布库与整合库中提供了模块信息,各个模块的安装或升级程序就可以利用这些信息到 S3 上获取相应的打包文件,这个文件会包含源码编译后的二进制文件(如果有的话)和针对该二进制文件的安装配置脚本,这样我们只需到让整合仓库根据 repo-info 文件中中的 sub_repo 模块信息到 S3 上下载各个模块的打包文件,执行各自的安装与升级脚本即可。至于某个模块依赖哪些模块,则不用在 DIST 库中处理,只需要在执行单个模块的安部署装()或升级脚本()即可,因为这个模块是最清楚自己依赖哪些模块的,在这里处理也是最清晰、简单的。下图为项目的安装流程,升级流程与安装流程类似,只不过安装流程是下载各个模块,执行各模块的安装脚本,而升级是执行各模块的升级脚本。
Bitbucket 支持 pipeline 功能,在满足条件时触发某种操作,这个操作我们可以简单的理解为调用 docker 镜像完成某种行为,比如编译、打包、上传至 S3 存储等。这里我们所需要阐述的是,如何制定 pipeline 的触发规则?要回答这个问题,我们首先要弄明白利用 pipeline 的目的,即我们希望当开发人员 push 代码时,能够完成代码的自动编译、打包、发布、测试、以及上线等行为。其中代码编译、打包、发布等行为是统一的,而测试与上线是最终不同的两个目的,这样我们就像需要设定不同的规则来触发测试与上线的不同行为。根据 bitbucket 的 中关于 pipeline 的触发条件,可以分为三种(实际上是四种,但是 bookmarks 是针对 Mercurial ,我们暂不讨论),下面简单说下这三种:
结合我们设定的自动化执行逻辑,需要使用 tags 与 branch 作为触发条件,branch 设置为 developer 分支,这样当开发人员将代码提交到 developer 分支时会立刻触发 pipeline ,执行相关的编译、打包、发布、测试工作。tags 则用于触发测试之后的发布,即一旦我们在分支上打上 tag 标签,就会自动触发 pipeline 完成编译、打包、发布、上线工作。这里需要注意的是,bitbucket pipeline 无法区分 tag 是来自于 master 分支或者 developer 等任意分支,只要检测到有 tag 产生,即会触发 pipeline ,所以打 tag 时一定要慎重,我们规定:必须在 developer 分支完成充分测试之后,才能 pull request 到 master 分支,而所有的 tag 也必须是打在 master 分支上的。当然,意外不可避免,我们做了相关的考虑,如果因为误操作或者测试不充分导致当前版本部署到实际生产节点不能正常使用,只需要 rerun 上个 tag 的 pipeline 即会重新打包发布老版本。
在讲述该问题前,我们先来聊一个业务场景,公司研发了一个底层的基础功能模块,如何被两个甚至多个项目所引用,最简单的做法是将这个模块作为代码的一部分直接提供给各个项目使用,这就容易造成生产中经常遇到的多源问题,后期随着由于各项目的发展,可能会对引入的基础模块功能做调整,那么调整后的基础模块就不再能够保证被其他项目所通用,那么修改后的代码提交到哪呢?只能建立新仓库提交了,这样我们从维护一个基础库就变成了维护多个基础库,开发基础模块的目的本来就是抽象功能、代码复用、提高开发效率,这显示事与愿违。
当然针对这种多源问题,比较常见的解决办法是使用 subtree 或者 submodule 的方式将仓库引入到项目中,subtree 会直接将代码引入,这样会导致我很难快速的指定当前项目引入的是哪个版本的基础模块。submodule 可以理解为引入的是一个指针,指向某个版本的基础库,这在一定程度上能够达到我们的目的,但是一旦被多个项目或者多级(特别是多级引用)引用,如果更新基础模块,则引用该模块的所有上级模块需要一级一级重新引入,简直就是噩梦。而我们实现的通过仓库中指定引用基础模块版本的方式就非常简单了,当基础模块更新时,引用模块如果不需要更新,则不用修改自身的依赖仓库版本信息,如果需要使用新版本基础库,只需要将依赖库的版本号修改为想要引用的版本号即可, S3 存储桶中存放了所有的稳定版本,操作非常之简单。这样我们只需要从一个源提供代码,就可以被多个项目引用,而且可以引用不同版本,互相不影响。
我们知道,一个完整的项目需要多个不同的模块,我们是使用存储桶的方式将各个模块打包存放,那么也就是说每个存储桶中都包含了所有的模块打包文件,以及不同版本的打包文件,那么当一个基础模块在触发了 pipeline 完成自动编译打包之后,如果分发到不同的存储桶中呢?这就是我们接下来要讨论的“一源多存问题”。 这里我们需要引入 pipeline 中变量的概念,我们从 bitbucket 关于变量的 中可以得知,pipeline 可以使用 bitbucket 内置的变量,当然我们也可以自己定义变量,如果变量名称相同,会以自定义变量为准(准确的说,会以最后声明的变量为准,但是内置变量声明是在自定义变量声明之前就完成的)。这样我们就可以利用变量的方式,给不同的存储桶设定不同的变量名称,那么打包存储之后再根据不同的变量名称将文件存储至 S3 上即可。这样我们就需要规范某些自定义变量,这些变量专门用于 pipeline ,而不能被其他脚本定义,这些变量有:
上述表格,左边第一列表示需要在 bitbucket 的每个模块仓库中定义为 pipeline 使用的变量,第二列表示每个模块的安装与升级程序中定义的变量,第三列表示该变量的含义。下面我们从上到下解释这几个变量的含义以及用法:
S3 存储桶名称:用于告诉 pipeline 在完成打包之后发往哪个存储桶。对于一源多存的问题,也是在此处解决的。首先由于我们是从一个源仓库打包引入到不同项目中,那么这份代码在源仓库中就是唯一且通用的,因此我们定义了变量“BUCKET_NAME” 作为存储桶名称,有需要获取 S3 存储上的文件时,用变量代替;另外,我们通过在 bitbucket 的不同项目仓库中设置不同的存储桶名称变量,在 pipeline 中将 ${BUCKET_NAME} 替换成 ${PROJECTA_BUCKET_NAME} 或者 ${PROJECTB_BUCKET_NAME},来将源文件中的存储桶名称修改为当前项目的存储桶名称,并且发送至对应的存储桶下,示例代码如下:
\t# 基础模块 REL 仓库中的代码(deploy.sh 与 upgrade.sh 均包含如下代码)截取 \tBUCKET_NAME='' \t \t# 获取源码仓库打包后的文件 aws s3 cp s3://${BUCKET_NAME}/${REPO_NAME}/${BITBUCKET_TAG}/${BIN_NAME}.tar.gz ./
从上面的代码中,我们可以看到,获取 S3 上存储的文件,需要四个变量,即:${BUCKET_NAME}、${REPO_NAME}、${BITBUCKET_TAG}、${BIN_NAME},其中除了 ${BITBUCKET_TAG} 外,其余的三个都是我们在上面自己定义的。稍后会对这几个参数的使用做说明,我们继续讨论 ${BUCKET_NAME} 的使用。这里我们看下 pipeline 中的部分代码,就会明白是如何借助与 bitbucket 的变量定义,完成不同源的多存储问题了。
\t# bitbucket-pipelines.yml 中部分代码 \tscript: \t # 发送存储桶 BUCKET_A 中的打包文件 \t - export AWS_ACCESS_KEY_ID=${BUCKETA_S3KEY} AWS_SECRET_ACCESS_KEY=${BUCKETA_S3SECRET} \t - sed -i 's#\\${REPO_NAME}#'${REPO_NAME}'#g ; s#\\${BUCKET_NAME}#'${PROJECTA_BUCKET_NAME}'#g' deploy.sh upgrade.sh \t - tar czf ${REPO_NAME}.tar.gz etc repo-info deploy.sh upgrade.sh \t - aws s3 cp ${REPO_NAME}.tar.gz s3://${PROJECTA_BUCKET_NAME}/${REPO_NAME}/${BITBUCKET_TAG}/ \t \t # 发送存储桶 BUCKET_B 中的打包文件 \t - export AWS_ACCESS_KEY_ID=${BUCKETB_S3KEY} AWS_SECRET_ACCESS_KEY=${BUCKETB_S3SECRET} \t - sed -i 's#\\${REPO_NAME}#'${REPO_NAME}'#g ; s#\\${BUCKET_NAME}#'${PROJECTB_BUCKET_NAME}'#g' deploy.sh upgrade.sh \t - tar czf ${REPO_NAME}.tar.gz etc repo-info deploy.sh upgrade.sh - aws s3 cp ${REPO_NAME}
首先需要说明的一点是,上面的 pipeline 使用的 docker 镜像是 atlassian/pipelines-awscli ,由 aws 官方提供,其中变量 AWS_ACCESS_KEY_ID 与 AWS_SECRET_ACCESS_KEY 分别表示访问存储通的 key 和 secret ,每个存储桶的 key 和 secret 均不一样,具体请参考 ,此处不再赘述。
我们通过在 bitbucket 上设置变量 ${PROJECTA_BUCKET_NAME} 和 ${PROJECTB_BUCKET_NAME} 来替换脚本中 ${BUCKET_NAME} ,以将存储在某个桶下的项目脚本可以正确的适配该项目。
接下来我们说下其余的几个变量:
仓库名称:即 ${REPO_NAME},需要在 bitbucket 的每个仓库中设定,主要有两个目的,一是确定 S3 上的存储位置;二是确定 REL 仓库打包后的名称。
二进制文件名称:即 ${BIN_NAME},同样需要在 bitbucket 的每个仓库中设定,目的与 ${REPO_NAME} 一样,用于确认 SRC 文件打包后的名称,已经在 S3 上的文件存储位置。
而变量 ${BITBUCKET_TAG} 则是 bitbucket 内置的变量,是用于获取最近一次仓库 tag 名称的,这个不需要我们手动设定,只需要直接在 pipeline 中引用即可。
综上所述,我们通过规定变量名称,以及利用 bitbucket pipeline 内置变量、自定义变量,解决了同一份数据源适配不同项目的问题。这样不同的项目整合程序,在各自的项目中引入的模块就是以及适配好的程序,可以直接用于项目的安装与升级。那么接下来,我们就开始讨论项目的自动化部署。
在我们完成 Amazon S3 存储规则、仓库结构、安装与升级逻辑流程、PIPELINE 等规则内容设定以及一源多存问题解决之后,我们已经可以快速便捷的完成单个模块、部分模块以及整个项目的安装与更新操作,但是这还需要人工参与,接下来我们就讨论如何利用批量化部署工具,完成全球节点的自动化部署。 首先我们对批量化部署工具进行选择,当前比较热门的有 saltstack 、puppet、ansible 等,为了降低维护成本以及快速部署,我们结合当前生产环境现状以及生产节点数量,选择了轻便快捷的 ansible 作为批量控制平台,它是基于 ssh 协议完成任务分发,所以安全性还是比较高的,另外不需要客户端,操作简单、学习成本低等特点,都是我们现下比较需要的。当然,后期随着自动化平台功能的不断完善,以及运维节点数量的增加、对运维时效性的要求提高等因素,可能需要更换中控平台或者自研该平台,这虽是后话,但是我在设计当前的自动化运维体系时已经将该问题考虑进去,将整个体系进行模块化分割,与中控平台之间的耦合性降到最低,这样以后无论使用何种中控平台,都不会对现有体系造成太大波动,以达到降低影响、快速迭代上线的目的。
接下来我们就讨论如何利用 Ansible 完成全球节点的自动构建与更新,核心逻辑处理流程如下: 为了不产生歧义,我们以节点自动构建作为说明对象,自动更新与之类似。当我们决定采用 ansible 作为批量部署工具后,就需要根据业务逻辑设计相应的 ansible 执行脚本即 playbook ,脚本需要具备判断某个节点是否满足安装或者更新条件的能力。为了达到这个目的,我们通过在节点上创建 images.yaml 文件,模板如下:
# 定义 image 版本信息 image: version: 2.1.0 status: building
我们对镜像文件引入“节点状态”与“节点版本”的概念,并且将节点状态分为:节点状态文件不存在、building(节点构建中)、maintain(节点维护中)、failure(失败)、active(可用)。对于这五个状态,其意义如下:
节点状态文件 images.yaml 不存在:即我们任务该节点为新节点,会对该节点执行安装操作(这个状态文件作为自动化执行的重要逻辑判断依据,是绝对不允许被人为干预的,即使是误操作,程序也会自动重建该节点,以保证节点后期的可维护性)。
building(节点构建中):这是当对一个新节点进行项目安装时,会新增 images.yaml 文件,并将状态值修改为 building,其实我们在 DIST 仓库中存储的 imags.yaml 文件初始状态就是 building ,这也是符合我们的执行逻辑,安装时直接将该文件放置到指定位置即可。
maintain(节点维护中):这种状态是由节点的 active 转变,即节点符合更新维护条件,则会将 active 修改为 maintain 。
failure(失败):表示节点安装或者更新失败,需要进行人工干预。
active(可用):解释节点安装或者更新成功,可以正常提供服务。
那么 ansible 是如何根据上面的五种状态,来决定该节点是否能够进行安装或者更新?如何避免对正在维护的节点再次触发维护执行维护程序?Ansible 的判断依据是,节点状态文件不存在,直接执行安装操作,安装操作由 DIST 的安装程序执行,该程序执行完会判断本次执行是否成功,成功则将节点状态由 building 修改为 active ,不成功则修改为 failure 。这样,我们就能得出 failure 状态是由于整合仓库的安装(或者更新)脚本导致,而跟 ansible 无关,也就不应该再让 ansible 继续对 failure 状态的节点下发安装或者更新任务。因此,我们得出结论,ansible 只会对新节点执行安装操作,只应该对 active 状态的节点执行升级操作(当然这是必要条件,而不是充分条件,否则节点会一直处在 maintain 跟 active 间切换)。因此我们通过引入“节点版本”的概念,来作为另外一个判断是否可以进行升级操作的必要条件。我在开发 ansible playbook 脚本时,引入了“可更新节点版本”的变量(该变量通过 playbook 的变量文件存储,因此每次升级项目修改整合仓库时,也需要修改该变量文件中关于“可更新节点”变量的值),即只有当这个变量与镜像文件中的节点版本相匹配时,才会满足升级条件。即,我们判断节点是否应该升级会对节点状态与节点版本判断,只有节点状态为 active 且节点版本与 ansible 中定义的可升级版本所匹配,才会执行升级操作。
接下来,我们继续讨论新版本发布问题。当我们确定要发布新版本时,会根据本次版本中各个模块的调整修改 DIST 仓库的 repo-info 信息、ansible playbook 执行文件、CHANGELOG 、 README 等信息,当然,可能还需要修改 DIST 仓库中的安装脚本。为什么说可能呢?根据前面的设计逻辑, DIST 仓库中的安装脚本只负责根据 repo-info 文件从 S3 上获取各个模块的安装包,然后执行各安装包中的部署脚本即可。这样的话,其实 DIST 仓库的安装脚本只需要执行一个循环,依次读取 repo-info 中模块信息,然后下载文件、安装文件即可。无论是新增、删除、修改功能模块,只需要对 repo-info 中该模块的内容进行相应的新增、删除、修改即可,而不必去修改整合脚本的内部逻辑,这样极大的降低了维护 DIST 仓库的人员水平。也就是说,绝大多数情况下都不需要修改 DIST 仓库的安装脚本,但是有些情况下可能需要单独处理某个模块,例如这个模块的处理逻辑与其他模块不同,那么就需要调整整合脚本,以兼容该模块。举个最简单的例子,假设模块C 在执行部署程序时,需要给该程序传递指定参数,那么这就与其他模块的处理逻辑不同,必须单独设定。
总而言之,我们需要根据将要发布的版本做整合仓库的适配,然后将代码提交至 DIST 仓库 developer 分支,触发 pipeline ,将仓库中指定文件(包括 playbook 以及其变量配置文件、images.yaml、安装与升级脚本、仓库 repo-info 文件等)推送至 ansible 控制节点的某个目录下,ansible 会定期执行该目录下的 playbook 文件,进行全球节点任务分发,对满足安装或者升级的节点进行自动化安装与更新操作。综上所述,ansible 执行自动化安装与升级流程图如下:
整个流程可以概括为:
(1). 适配 DIST 仓库内容,触发 pipeline 推送至 ansible 控制节点。(2). Ansible 执行 DIST 仓库中 playbook 文件,全球节点下发任务。(3). 利用 playbook 逻辑对各节点进行状态判断,决定执行安装、更新程序或者终止任务。(4). 对于执行安装或者更新程序的节点,会根据程序的执行结果修改节点状态为 active 或 failure 。当我们利用 bitbucket pipeline + ansible 完成全球节点的持续集成与持续发布之后,接下来需要解决的问题就是监控。需要注意的是,这里我们提到的监控是指 CI/CD 结果的监控,而不是常规的节点流量、内存、CPU 等性能监控。通过上面的分析我们知道从 ansible 获取最新的 playbook 到将任务推送至全球节点执行,可以分为两个环节,第一是 ansible 将任务推送至全球各节点上,进行安装或更新逻辑判断;第二是 ansible 执行完判断逻辑后,符合安装或更新的节点会执行相应的脚本进行安装或者升级,然后脚本根据自身的执行结果对节点状态进行修改。这样的话,我们的监控也应该触及到这两个环节,第一、Ansible 是否成功将任务推送至全球各节点;第二、节点在接收到 ansible 推送的任务后,是否成功执行了安装或升级脚本。对于第一个点,我们可以通过分析 ansible 的执行日志获取;对于第二点则可以从各节点状态中获取。这样就形成了监控报警的框架,即我们首先对 ansible playbook 日志中 recap 结果进行分析,对其中产生失败(比如 failed、unreachable)的主机节点进行报警;然后通过 ssh 的方式连接远程节点,对状态值为 failure 也进行报警,报警平台我们使用 slack ,其提供了高效的报警接口(我们对该接口又做了二次开发,使其功能更加丰富),调用起来非常简单,如下是发送 slack 的报警信息如下:
时间: 2018-10-25 08:17:46,282 主机: 192.168.10.129 Ansible 执行状态: success 节点状态: failure
解释一下报警信息各条目的含义:
时间:表示 ansible 推送任务到该节点的时间,以 ansible 服务器时间为准。
主机:表示 ansible 正在将任务推送至哪台节点。
Ansible 执行状态:表示从 ansible 对该主机下发任务的状态,状态值有unreachable、 failed、success。 其中 unreachable 表示 ansible 与这台节点连接失败; failed 表示虽然 ansible 可以成功连接到该节点,并且执行 playbook 中的任务,但是因为某些原因导致任务执行失败;success 表示 ansible 成功连接到该节点,并且将 playbook 中的任务依次成功执行完。而根据我们之前的设计,节点的升级或者安装,是由 ansible 调用升级或安装脚本去执行的,因此 ansible 执行状态成功,并不能表示节点被成功安装或者升级了,我们需要继续对节点状态进行判断。
节点状态: 表示各个节点上 images.yaml 文件中记录的节点状态信息,有 active、failure、building、maintain 四个,另外加上节点状态文件不存在与连接远程节点失败,一共有六个状态值,但是 active、building、maintain 为正常状态,因此会产生报警行为的节点状态有 failure、节点状态文件不存在以及连接远程节点失败三种。
综上所述,下面的情况行为会产生报警行为:
(1). Ansible 连接远程节点失败
(2). 节点上执行 playbook 失败(3).节点上执行安装或者升级脚本失败 虽然我们设计的报警逻辑基本上涵盖了大多数可能出现的错误,但并不是很严谨,我们也会继续努力去制定更多维度的监控,以提高监控的有效性。• 权限分配清晰
我们通过对 Git 仓库实行三层逻辑分层,将开发人员与运维人员权限做了严格的区分,使其可以很好的协作而不会互相干扰。
• 自动化程度较高,且运维成本低
通过利用 Bitbucket pipeline + Amazon S3 + Ansible 实现了自动化持续集成与持续部署,且因为各个功能模块打包存放在 S3 中,中控节点仅推送 DIST 仓库中的几个文本文件到各节点上,然后在每个节点上执行安装或升级脚本即可,各个节点会从 S3 上下载相应的模块文件进行安装或升级,这样对中控节点的物理硬件性能要求非常低。纵观这个运维项目,从 bitbucket 代码仓库,到 Amazon S3 存储,再到 ansible 中控节点服务器,整个运维成本大概在 20 - 30 $/Month 。另外,由于自动化流程经过高度抽象与模块划分,对操作人员的技能要求也大大降低,这也减轻了运维成本。
• 支持广域网自动化运维,不存在网络瓶颈,并发能力非常强
传统的自动化运维平台一般是面向局域网的,一般的执行流程是把代码下载至与同一局域网内的中控节点,然后由中控节点将其推送局域网内的所有节点,这就对中控节点的网络带宽带来了极大的考验,因为一个项目上线,所有功能模块的安装包少则几十兆,多则百兆甚至到 GB 级别,这是根本没法实现广域网自动化部署的,因此传统自动化运维平台一般都会有比较严重的网络瓶颈,任务并发处理能力较低。而我们通过架构优化,让中控平台只推送数量很小的文本文件,可以轻松达到几十甚至上百个任务并发,依然不会对中控节点造成太大的压力。各个节点在执行安装或者升级脚本时,会从 Amazon S3 上下载文件,而 S3 所支持的下行带宽是不存在任何下载上的瓶颈的。
• 模块引入非常灵活简单
基础模块的引入一般是比较头疼的问题,我们通过借助于物理存储的形式,以及存储规则设定,可以轻松实现多个项目对某个模块不同版本的引用,且数据源仅一个,不会造成后期的多源问题。
• 版本升级与回滚非常便捷
我们对整个运维流程进行高度抽象化、模块化,项目或者单个模块的安装与升级全部依靠于各自的仓库信息文件,即 repo-info 文件,升级或者回滚操作,仅需要修改配置文件中 S3 存储的指向即可,秒级完成版本升级与回滚。
• 监控报警形成自动化流程闭环,安全性较高
监控是对整个运维框架的逻辑补充,可以帮助我们简单且高效找到问题点,提升整个自动化流程的安全可靠性。
• 基于 ansible 处理,连接速度较慢,节点数在千台以内
由于中控平台采用 ansible 作为批量化操作工具,所以也会受到其性能的影响。Ansible 是基于 ssh 连接对各节点进行操作,这就导致了连接速度比较慢,且维护量级一般在千台以内。
• 监控与分析平台不够强大
目前监控平台是基于两个核心维度进行的,虽然可以发现绝大多数常见的自动化部署产生的问题,但是维度太小,遇到比较复杂的情况无法实现有效监控,而我们也正努力从其他维度去攻克这一监控难题。
当前的自动化运维体系支持多种云平台、支持广域网,能够满足中小企业(批量操作节点在 1000 台以内)的日常运维工作,但是在该体系的构建与应用中,发现了一些问题,另外还有一些新的想法希望能够补充进来,我们将这些放在下个版本中实,这些问题或想法包括但不限于以下这些:
• 提高监控展示能力:
监控维度不够丰富全面,容易造成某些问题的遗漏。下个版本我们会从目标结果的方向出发(比如当前版本号、当前用户连接数、当前流量等),做正向匹配监控,拟引入 elasticsearch 平台,对这些目标数据进行采集,同时也将对安装与升级日志进行收集、以用于分析与结果展示,最终达到深度定位潜在的问题的目的。
• 提高中控节点连接速度
节点连接速度比较慢,ansible playbook 逻辑判断太简单,不太适合复杂模型处理。下个版本应该会调整批量化部署工具,以提高逻辑判断能力与节点部署速度。
• 引入 CMDB 系统,实现节点分组
当前节点尚未分组,人工不干预的情况下,无法做灰度上线。这对生产发布始终是一个隐患,下个版本应该会设计并引入合适的资产管理系统,拟通过引入 tag 的方式解决这一问题。
• 引入堡垒机与审计系统
这一功能虽然与当前版本不存在直接关系,但是确能够解决运维中经常出现的节点权限分配混乱、事故责任纠缠不清、以及人员离职全球节点修改密码等琐碎问题。通过堡垒机机制,可以统一登录入口,增加接入的安全性;另外可以在堡垒机中对人员权限进行严格分组、记录用户操作行为等,这都是标准运维流程需要解决的事情。
作者简介
陈龙飞,北京联宇益通科技发展有限公司(netpas) 系统部负责人,主要负责全球节点系统与应用的自动化运维工作,保证节点稳定、高效运行。通过建立自动化运维体系,完成 DevOps 精准落地,提升运维与开发之间的工作效率,使产品上线与迭代流程更加简便、高效。主要关注领域有:Linux架构与优化、运维自动化、云计算、人工智能。
转载地址:http://yauyx.baihongyu.com/