CI

CI

GitLab CI/CD 在 Node.js 项目中的实践

尼古拉斯 发表了文章 • 0 个评论 • 179 次浏览 • 2019-06-03 09:58 • 来自相关话题

【编者的话】近期在按照业务划分项目时,我们组被分了好多的项目过来,大量的是基于 Node.js 的,也是我们组持续在使用的语言。 #现有流程中的一些问题 在维护多个项目的时候,会暴露出一些问题: 如何有效的使用 ...查看全部
【编者的话】近期在按照业务划分项目时,我们组被分了好多的项目过来,大量的是基于 Node.js 的,也是我们组持续在使用的语言。
#现有流程中的一些问题
在维护多个项目的时候,会暴露出一些问题:

  1. 如何有效的使用 测试用例
  2. 如何有效的使用 ESLint
  3. 部署上线还能再快一些吗

* 使用了 TypeScript 以后带来的额外成本

##测试用例
首先是测试用例,最初我们设计在了 git hooks 里边,在执行 git commit 之前会进行检查,在本地运行测试用例。 如果你想和更多 GitLab 技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态。

这会带来一个时间上的问题,如果是日常开发,这么操作还是没什么问题的,但如果是线上 bug 修复,执行测试用例的时间依据项目大小可能会持续几分钟。

而为了修复 bug,可能会采用 commit 的时候添加 -n 选项来跳过 hooks,在修复 bug 时这么做无可厚非,但是即使大家在日常开发中都采用commit -n 的方式来跳过繁琐的测试过程,这个也是没有办法管控的,毕竟是在本地做的这个校验,是否遵循这个规则,全靠大家自觉。

所以一段时间后发现,通过这种方式执行测试用例来规避一些风险的作用可能并不是很有效。
##ESLint
然后就是 ESLint,我们团队基于airbnb的 ESLint 规则自定义了一套更符合团队习惯的规则,我们会在编辑器中引入插件用来帮助高亮一些错误,以及进行一些自动格式化的操作。

同时我们也在 git hooks 中添加了对应的处理,也是在 git commit 的时候进行检查,如果不符合规范则不允许提交。

不过这个与测试用例是相同的问题:

  1. 编辑器是否安装 ESLint 插件无从得知,即使安装插件、是否人肉忽略错误提示也无从得知。
  2. git hooks 可以被绕过

##部署上线的方式
之前团队的部署上线是使用shipit周边套件进行部署的。

部署环境强依赖本地,因为需要在本地建立仓库的临时目录,并经过多次ssh XXX "command"的方式完成部署 + 上线的操作。

shipit提供了一个有效的回滚方案,就是在部署后的路径添加多个历史部署版本的记录,回滚时将当前运行的项目目录指向之前的某个版本即可。(不过有一点儿坑的是,很难去选择我要回滚到那个节点,以及保存历史记录需要占用额外的磁盘空间)

不过正因为如此,shipit在部署多台服务器时会遇到一些令人不太舒服的地方。

如果是多台新增的服务器,那么可以通过在shipit配置文件中传入多个目标服务器地址来进行批量部署。

但是假设某天需要上线一些小流量(比如四台机器中的一台),因为前边提到的shipit回滚策略,这会导致单台机器与其他三台机器的历史版本时间戳不一致(因为这几台机器不是同一时间上线的)

了这个时间戳就另外提一嘴,这个时间戳的生成是基于执行上线操作的那台机器的本地时间,之前有遇到过同事在本地测试代码,将时间调整为了几天前的时间,后时间没有改回正确的时间时进行了一次部署操作,代码出现问题后却发现回滚失败了,原因是该同事部署的版本时间戳太小,shipit 找不到之前的版本(shipit 可以设置保留历史版本的数量,当时最早的一次时间戳也是大于本次出问题的时间戳的)



也就是说,哪怕有一次进行过小流量上线,那么以后就用不了批量上线的功能了 (没有去仔细研究shipit官方文档,不知道会不会有类似--force之类的忽略历史版本的操作)

基于上述的情况,我们的部署上线耗时变为了: (__机器数量__)X(__基于本地网速的仓库克隆、多次 ssh 操作的耗时总和__)。 P.S. 为了保证仓库的有效性,每次执行 shipit 部署,它都会删除之前的副本,重新克隆

尤其是服务端项目,有时紧急的 bug 修复可能是在非工作时间,这意味着可能当时你所处的网络环境并不是很稳定。

我曾经晚上接到过同事的微信,让我帮他上线项目,他家的 Wi-Fi 是某博士的,下载项目依赖的时候出了些问题。

还有过使用移动设备开热点的方式进行上线操作,有一次非前后分离的项目上线后,直接就收到了联通的短信:「您本月流量已超出XXX」(当时还在用合约套餐,一月就800M流量)。
##TypeScript
在去年下半年开始,我们团队就一直在推动 TypeScript 的应用,因为在大型项目中,拥有明确类型的 TypeScript 显然维护性会更高一些。

但是大家都知道的, TypeScript 最终需要编译转换为 JavaScript(也有 tsc 那种的不生成 JS 文件,直接运行,不过这个更多的是在本地开发时使用,线上代码的运行我们还是希望变量越少越好)。

所以之前的上线流程还需要额外的增加一步,编译 TS。

而且因为shipit是在本地克隆的仓库并完成部署的,所以这就意味着我们必须要把生成后的 JS 文件也放入到仓库中,最直观的,从仓库的概览上看着就很丑(50% TS、50% JS),同时这进一步增加了上线的成本。

总结来说,现有的部署上线流程过于依赖本地环境,因为每个人的环境不同,这相当于给部署流程增加了很多不可控因素。
#如何解决这些问题
上边我们所遇到的一些问题,其实可以分为两块:

  1. 有效的约束代码质量
  2. 快速的部署上线

所以我们就开始寻找解决方案,因为我们的源码是使用自建的 GitLab 仓库来进行管理的,首先就找到了 GitLab CI/CD。

在研究了一番文档以后发现,它能够很好的解决我们现在遇到的这些问题。

要使用 GitLab CI/CD 是非常简单的,只需要额外的使用一台服务器安装 gitlab-runner,并将要使用 CI/CD 的项目注册到该服务上就可以了。

GitLab 官方文档中有非常详细的安装注册流程:

install | runner
register | runner
group register | repo 注册 Group 项目时的一些操作

上边的注册选择的是注册 group ,也就是整个 GitLab 某个分组下所有的项目。

主要目的是因为我们这边项目数量太多,单个注册太过繁琐(还要登录到 runner 服务器去执行命令才能够注册)
##安装时需要注意的地方
官网的流程已经很详细了,不过还是有一些地方可以做一些小提示,避免踩坑。
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner

这是 Linux 版本的安装命令,安装需要 root (管理员) 权限,后边跟的两个参数:

* --user 是 CI/CD 执行 job (后续所有的流程都是基于 job 的)时所使用的用户名
* --working-directory 是 CI/CD 执行时的根目录路径 个人的踩坑经验是将目录设置为一个空间大的磁盘上,因为 CI/CD 会生成大量的文件,尤其是如果使用 CI/CD 进行编译 TS 文件并且将其生成后的 JS 文件缓存;这样的操作会导致 innode 不足产生一些问题

--user 的意思就是 CI/CD 执行使用该用户进行执行,所以如果要编写脚本之类的,建议在该用户登录的状态下编写,避免出现无权限执行 sudo su gitlab-runner



##注册时需要注意的地方
在按照官网的流程执行时,我们的 tag 是留空的,暂时没有找到什么用途。

以及 executor 这个比较重要了,因为我们是从手动部署上线还是往这边靠拢的,所以稳妥的方式是一步步来,也就是说我们选择的是 shell ,最常规的一种执行方式,对项目的影响也是比较小的(官网示例给的是 Docker)。
##.gitlab-ci.yml 配置文件
上边的环境已经全部装好了,接下来就是需要让 CI/CD 真正的跑起来
runner 以哪种方式运行,就靠这个配置文件来描述了,按照约定需要将文件放置到 repo 仓库的根路径下。

当该文件存在于仓库中,执行 git push 命令后就会自动按照配置文件中所描述的动作进行执行了。

* quick start
* configuration

上边的两个链接里边信息非常完整,包含各种可以配置的选项。

一般来讲,配置文件的结构是这样的:
stages:
- stage1
- stage2
- stage3

job 1:
stage: stage1
script: echo job1

job 2:
stage: stage2
script: echo job2

job 3:
stage: stage2
script:
- echo job3-1
- echo job3-2

job 4:
stage: stage3
script: echo job4

stages 用来声明有效的可被执行的 stage,按照声明的顺序执行。

下边的那些 job XXX 名字不重要,这个名字是在 GitLab CI/CD Pipeline 界面上展示时使用的,重要的是那个 stage 属性,他用来指定当前的这一块 job 隶属于哪个 stage。

script 则是具体执行的脚本内容,如果要执行多行命令,就像job 3那种写法就好了。

如果我们将上述的 stage、job 之类的换成我们项目中的一些操作install_dependencies、test、eslint之类的,然后将script字段中的值换成类似npx eslint之类的,当你把这个文件推送到远端服务器后,你的项目就已经开始自动运行这些脚本了。

并且可以在Pipelines界面看到每一步执行的状态。

P.S. 默认情况下,上一个 stage 没有执行完时不会执行下一个 stage 的,不过也可以通过额外的配置来修改:

* allow failure
* when

设置仅在特定的情况下触发 CI/CD

上边的配置文件存在一个问题,因为在配置文件中并没有指定哪些分支的提交会触发 CI/CD 流程,所以默认的所有分支上的提交都会触发,这必然不是我们想要的结果。

CI/CD 的执行会占用系统的资源,如果因为一些开发分支的执行影响到了主干分支的执行,这是一件得不偿失的事情。

所以我们需要限定哪些分支才会触发这些流程,也就是要用到了配置中的 only 属性。

使用only可以用来设置哪些情况才会触发 CI/CD,一般我们这边常用的就是用来指定分支,这个是要写在具体的 job 上的,也就是大致是这样的操作:

具体的配置文档
job 1:
stage: stage1
script: echo job1
only:
- master
- dev

单个的配置是可以这样写的,不过如果 job 的数量变多,这么写就意味着我们需要在配置文件中大量的重复这几行代码,也不是一个很好看的事情。

所以这里可能会用到一个yaml的语法:

这是一步可选的操作,只是想在配置文件中减少一些重复代码的出现



.access_branch_template: &access_branch
only:
- master
- dev

job 1:
<<: *access_branch
stage: stage1
script: echo job1

job 2:
<<: *access_branch
stage: stage2
script: echo job2

一个类似模版继承的操作,官方文档中也没有提到,这个只是一个减少冗余代码的方式,可有可无。
##缓存必要的文件
因为默认情况下,CI/CD在执行每一步(job)时都会清理一下当前的工作目录,保证工作目录是干净的、不包含一些之前任务留下的数据、文件。

不过这在我们的 Node.js 项目中就会带来一个问题。

因为我们的 ESLint、单元测试 都是基于 node_modules 下边的各种依赖来执行的。

而目前的情况就相当于我们每一步都需要执行npm install,这显然是一个不必要的浪费。

所以就提到了另一个配置文件中的选项:cache

用来指定某些文件、文件夹是需要被缓存的,而不能清除:
cache:
key: ${CI_BUILD_REF_NAME}
paths:
- node_modules/

大致是这样的一个操作,CI_BUILD_REF_NAME是一个 CI/CD 提供的环境变量,该变量的内容为执行 CI/CD 时所使用的分支名,通过这种方式让两个分支之间的缓存互不影响。
##部署项目
如果基于上边的一些配置,我们将 单元测试、ESLint 对应的脚本放进去,他就已经能够完成我们想要的结果了,如果某一步执行出错,那么任务就会停在那里不会继续向后执行。

不过目前来看,后边已经没有多余的任务供我们执行了,所以是时候将 部署 这一步操作接过来了。

部署的话,我们目前选择的是通过 rsync 来进行同步多台服务器上的数据,一个比较简单高效的部署方式。

P.S. 部署需要额外的做一件事情,就是建立从gitlab runner所在机器gitlab-runner用户到目标部署服务器对应用户下的机器信任关系。

有 N 多种方法可以实现,最简单的就是在runner机器上执行 ssh-copy-id 将公钥写入到目标机器。

或者可以像我一样,提前将 runner 机器的公钥拿出来,需要与机器建立信任关系时就将这个字符串写入到目标机器的配置文件中。

类似这样的操作:ssh 10.0.0.1 "echo \"XXX\" >> ~/.ssh/authorized_keys"
大致的配置如下:
variables:
DEPLOY_TO: /home/XXX/repo # 要部署的目标服务器项目路径
deploy:
stage: deploy
script:
- rsync -e "ssh -o StrictHostKeyChecking=no" -arc --exclude-from="./exclude.list" --delete . 10.0.0.1:$DEPLOY_TO
- ssh 10.0.0.1 "cd $DEPLOY_TO; npm i --only=production"
- ssh 10.0.0.1 "pm2 start $DEPLOY_TO/pm2/$CI_ENVIRONMENT_NAME.json;"

同时用到的还有variables,用来提出一些变量,在下边使用。

`ssh 10.0.0.1 "pm2 start $DEPLOY_TO/pm2/$CI_ENVIRONMENT_NAME.json;"`,这行脚本的用途就是重启服务了,我们使用pm2来管理进程,默认的约定项目路径下的pm2文件夹存放着个个环境启动时所需的参数。

当然了,目前我们在用的没有这么简单,下边会统一提到。

并且在部署的这一步,我们会有一些额外的处理。

这是比较重要的一点,因为我们可能会更想要对上线的时机有主动权,所以 deploy 的任务并不是自动执行的,我们会将其修改为手动操作还会触发,这用到了另一个配置参数:
deploy:
stage: deploy
script: XXX
when: manual # 设置该任务只能通过手动触发的方式运行

当然了,如果不需要,这个移除就好了,比如说我们在测试环境就没有配置这个选项,仅在线上环境使用了这样的操作。
##更方便的管理 CI/CD 流程
如果按照上述的配置文件进行编写,实际上已经有了一个可用的、包含完整流程的 CI/CD 操作了。

不过它的维护性并不是很高,尤其是如果 CI/CD 被应用在多个项目中,想做出某项改动则意味着所有的项目都需要重新修改配置文件并上传到仓库中才能生效。

所以我们选择了一个更灵活的方式,最终我们的 CI/CD 配置文件是大致这样子的(省略了部分不相干的配置):
variables:
SCRIPTS_STORAGE: /home/gitlab-runner/runner-scripts
DEPLOY_TO: /home/XXX/repo # 要部署的目标服务器项目路径

stages:
- install
- test
- build
- deploy_development
- deploy_production

install_dependencies:
stage: install
script: bash $SCRIPTS_STORAGE/install.sh

unit_test:
stage: test
script: bash $SCRIPTS_STORAGE/test.sh

eslint:
stage: test
script: bash $SCRIPTS_STORAGE/eslint.sh

# 编译 TS 文件
build:
stage: build
script: bash $SCRIPTS_STORAGE/build.sh

deploy_development:
stage: deploy_development
script: bash $SCRIPTS_STORAGE/deploy.sh 10.0.0.1
only: dev # 单独指定生效分支

deploy_production:
stage: deploy_production
script: bash $SCRIPTS_STORAGE/deploy.sh 10.0.0.2
only: master # 单独指定生效分支

我们将每一步 CI/CD 所需要执行的脚本都放到了 runner 那台服务器上,在配置文件中只是执行了那个脚本文件。

这样当我们有什么策略上的调整,比如说 ESLint 规则的变更、部署方式之类的。

这些都完全与项目之间进行解耦,后续的操作基本都不会让正在使用 CI/CD 的项目重新修改才能够支持(部分需要新增环境变量的导入之类的确实需要项目的支持)。
##接入钉钉通知
实际上,当 CI/CD 执行成功或者失败,我们可以在 Pipeline 页面中看到,也可以设置一些邮件通知,但这些都不是时效性很强的。

鉴于我们目前在使用钉钉进行工作沟通,所以就研究了一波钉钉机器人。

发现有支持 GitLab 机器人,不过功能并不适用,只能处理一些 issues 之类的, CI/CD 的一些通知是缺失的,所以只好自己基于钉钉的消息模版实现一下了。

因为上边我们已经将各个步骤的操作封装了起来,所以这个修改对同事们是无感知的,我们只需要修改对应的脚本文件,添加钉钉的相关操作即可完成,封装了一个简单的函数:
function sendDingText() {
local text="$1"

curl -X POST "$DINGTALK_HOOKS_URL" \
-H 'Content-Type: application/json' \
-d '{
"msgtype": "text",
"text": {
"content": "'"$text"'"
}
}'
}

# 具体发送时传入的参数
sendDingText "proj: $CI_PROJECT_NAME[$CI_JOB_NAME]\nenv: $CI_ENVIRONMENT_NAME\ndeploy success\n$CI_PIPELINE_URL\ncreated by: $GITLAB_USER_NAME\nmessage: $CI_COMMIT_MESSAGE"

# 某些 case 失败的情况下 是否需要更多的信息就看自己自定义咯
sendDingText "error: $CI_PROJECT_NAME[$CI_JOB_NAME]\nenv: $CI_ENVIRONMENT_NAME"

上述用到的环境变量,除了DINGTALK_HOOKS_URL是我们自定义的机器人通知地址以外,其他的变量都是有 GitLab runenr所提供的。

各种变量可以从这里找到:predefined variables
##回滚处理
聊完了正常的流程,那么也该提一下出问题时候的操作了。

人非圣贤孰能无过,很有可能某次上线一些没有考虑到的地方就会导致服务出现异常,这时候首要任务就是让用户还可以照常访问,所以我们会选择回滚到上一个有效的版本去。

在项目中的 Pipeline 页面 或者 Enviroment 页面(这个需要在配置文件中某些 job 中手动添加这个属性,一般会写在 deploy 的那一步去),可以在页面上选择想要回滚的节点,然后重新执行 CI/CD 任务,即可完成回滚。

不过这在 TypeScript 项目中会有一些问题,因为我们回滚一般来讲是重新执行上一个版本 CI/CD 中的 deploy 任务,在 TS 项目中,我们在 runner 中缓存了 TS 转换 JS 之后的 dist 文件夹,并且部署的时候也是直接将该文件夹推送到服务器的(TS项目的源码就没有再往服务器上推过了)。

而如果我们直接点击 retry 就会带来一个问题,因为我们的 dist 文件夹是缓存的,而 deploy 并不会管这种事儿,他只会把对应的要推送的文件发送到服务器上,并重启服务。

而实际上 dist 还是最后一次(也就是出错的那次)编译出来的 JS 文件,所以解决这个问题有两种方法:

  1. 在 deploy 之前执行一下 build
  2. 在 deploy 的时候进行判断

第一个方案肯定是不可行的,因为严重依赖于操作上线的人是否知道有这个流程。

所以我们主要是通过第二种方案来解决这个问题。

我们需要让脚本在执行的时候知道,dist 文件夹里边的内容是不是自己想要的。

所以就需要有一个 __标识__,而做这个标识最简单有效唾手可得的就是,git commit id。

每一个 commit 都会有一个唯一的标识符号,而且我们的 CI/CD 执行也是依靠于新代码的提交(也就意味着一定有 commit)。

所以我们在 build 环节将当前的commit id也缓存了下来:
git rev-parse --short HEAD > git_version

同时在 deploy 脚本中添加额外的判断逻辑:
currentVersion=`git rev-parse --short HEAD`
tagVersion=`touch git_version; cat git_version`

if [ "$currentVersion" = "$tagVersion" ]
then
echo "git version match"
else
echo "git version not match, rebuild dist"
bash ~/runner-scripts/build.sh # 额外的执行 build 脚本
fi

这样一来,就避免了回滚时还是部署了错误代码的风险。

关于为什么不将 build 这一步操作与 deploy 合并的原因是这样的:

因为我们会有很多台机器,同时 job 会写很多个,类似 deploy_1、deploy_2、deploy_all,如果我们将 build 的这一步放到 deploy 中。

那就意味着我们每次 deploy,即使是一次部署,但因为我们选择一台台机器单独操作,它也会重新生成多次,这也会带来额外的时间成本。
##hot fix 的处理
在 CI/CD 运行了一段时间后,我们发现偶尔解决线上 bug 还是会比较慢,因为我们提交代码后要等待完整的 CI/CD 流程走完。

所以在研究后我们决定,针对某些特定情况hot fix,我们需要跳过ESLint、单元测试这些流程,快速的修复代码并完成上线。

CI/CD 提供了针对某些 Tag 可以进行不同的操作,不过我并不想这么搞了,原因有两点:

  1. 这需要修改配置文件(所有项目)
  2. 这需要开发人员熟悉对应的规则(打 Tag)

所以我们采用了另一种取巧的方式来实现,因为我们的分支都是只接收Merge Request那种方式上线的,所以他们的commit title实际上是固定的:Merge branch 'XXX'。

同时 CI/CD 会有环境变量告诉我们当前执行 CI/CD 的 commit message。
我们通过匹配这个字符串来检查是否符合某种规则来决定是否跳过这些job:
function checkHotFix() {
local count=`echo $CI_COMMIT_TITLE | grep -E "^Merge branch '(hot)?fix/\w+" | wc -l`

if [ $count -eq 0 ]
then
return 0
else
return 1
fi
}

# 使用方法

checkHotFix

if [ $? -eq 0 ]
then
echo "start eslint"
npx eslint --ext .js,.ts .
else
# 跳过该步骤
echo "match hotfix, ignore eslint"
fi

这样能够保证如果我们的分支名为 hotfix/XXX 或者 fix/XXX 在进行代码合并时, CI/CD 会跳过多余的代码检查,直接进行部署上线。 没有跳过安装依赖的那一步,因为 TS 编译还是需要这些工具的。
#小结
目前团队已经有超过一半的项目接入了 CI/CD 流程,为了方便同事接入(主要是编辑 .gitlab-ci.yml 文件),我们还提供了一个脚手架用于快速生成配置文件(包括自动建立机器之间的信任关系)。

相较之前,部署的速度明显的有提升,并且不再对本地网络有各种依赖,只要是能够将代码 push 到远程仓库中,后续的事情就和自己没有什么关系了,并且可以方便的进行小流量上线(部署单台验证有效性)。

以及在回滚方面则是更灵活了一些,可在多个版本之间快速切换,并且通过界面的方式,操作起来也更加直观。

最终可以说,如果没有 CI/CD,实际上开发模式也是可以忍受的,不过当使用了 CI/CD 以后,再去使用之前的部署方式,则会明显的感觉到不舒适。(没有对比,就没有伤害

如何实现“持续集成”?闲鱼把研发效率翻了个翻

Andy_Lee 发表了文章 • 0 个评论 • 266 次浏览 • 2019-05-25 09:03 • 来自相关话题

从前端开发人员到DevOps:CI / CD 简介

tiny2017 发表了文章 • 0 个评论 • 279 次浏览 • 2019-05-23 17:45 • 来自相关话题

【编者的话】如果你是一名前端开发者,且认为作为一名开发人员只要管好前端这摊子就算大功告成了,那你就out了。原文作者从一个前端开发者的角度,阐述了DevOps的理念,并结合一些示例和实践,告诉我们了解并运用持续集成和持续部署的诸多好处。但我认为,原文作者从始至 ...查看全部
【编者的话】如果你是一名前端开发者,且认为作为一名开发人员只要管好前端这摊子就算大功告成了,那你就out了。原文作者从一个前端开发者的角度,阐述了DevOps的理念,并结合一些示例和实践,告诉我们了解并运用持续集成和持续部署的诸多好处。但我认为,原文作者从始至终传递给我们一种持续学习的精神,这一点同样值得我们学习。
# 介绍
对于有抱负的前端开发者而言,2019年是充满憧憬的一年。

有不计其数的教材、课件和教程,每天还有无数的博客和文章,像雨后春笋般层出不穷。任何想成为专业人士的人都可以获得他们需要的一切——通常还是免费的。

许多人抓住这个机会自学成才,而且他们当中很多人有机会参与完整的项目,可以迅速上手写功能模块,修改Bug并以适当的方式组建代码。

经过一段时间,有些比较幸运的前端开发者就会在互联网的某个角落看到他们自己实现的功能模块,作为一个web app、门户或者一个website——对于初级开发者而言,这真的是一个荣耀的时刻。但令人惊讶的是,他们中很少有人会问一个非常重要的问题:我们开发的应用程序,是怎么放到互联网上的?
1.png

大家普遍认为这是由开发人员完成的,只不过是更“高级别”的开发人员而已。可能一些人听说过DevOps,运营商,云管理员,系统管理员以及其他更具神秘气息的东西。

嗯,这是真的——在某种程度上,编码和测试成功后发生的一切通常都和脚本,Linux命令和容器之类的黑科技有关。还有一条不成文的规定是,只有组织中最有经验和最值得信赖的开发人员/管理员才有资格完成最后的交付工作。

有必要这样吗?这确实是有一定道理的——毕竟,这是一项复杂且极其重要的任务。但是否就意味着这些技能只属于某些精英?当然不是。

作为前端开发者,我们可以选择忽略这一切并相信其他人会完成剩下的所有事情——但我们不应该这样。IT世界的竞争瞬息万变,无论是前端还是后端,对于技术栈的点滴积累将会使你成为一名更具竞争力的开发者。

如果你想在开发方面进步的更快或者在同龄人中脱颖而出,你迟早会需要这些知识。下面就让我告诉你为什么你会需要这些知识。
# 为什么开发人员都应该尝试自动化处理
正如我们前面提到的那样,写代码只是软件开发这个浩大的工程的一部分。我们先来看看任何产品交付所需的基本步骤——不一定是软件:
2.png

严格来说,我们在这里讨论的不一定都是编码。我们关注的是主要的开发阶段完成之后会发生什么?为什么它会如此重要?因为它有可能会很复杂 - 解决方案越严谨,这部分就越复杂。

假设有一个具有一些特定功能的Web应用。我们假定该应用的版本会按照一个一个的功能定期发布,前提是发布到生产环境之前,每一个功能都会进行测试。
3.png

问题来了,我们一般不会只招一名程序员来完成这项工作, 即会有一个团队来负责这些功能。这些假设意味着——除了每个开发人员本地的编码环境和最终稳定的生产环境之外——最好还有一个“staging”环境来验证这些功能。在这个环境中,测试人员/客户可以在实际投入生产环境之前评估它们的质量。

现在我们越来越接近这样的架构:
4.png

正如你所看到的,事情变得越来越复杂(相信我,我们在这里谈论的真的是一个非常简单的例子),但我们在这里不会涉及产品生命周期管理,我们只关注技术。

假设前端开发人员需要几分钟来构建一个应用程序。如果关心代码质量,他们需要运行linting,单元测试,集成测试或者用其他的方式确认之后才能提交。这个过程很耗时。

最后,将打包好的程序放到服务器额外还需要几分钟时间。如果我们给一个程序员分配了以上所有这些任务,请记住我们还没有考虑其切换上下文所需的时间(例如,更改代码分支,重新聚焦到他们的工作上等等)。

现在,谁想要手动部署每个功能?如果每天都测试了三个新功能怎么办?如果有15个呢?依据不同的部署规模,很有可能需要一个以上的全职人员来处理上述任务。

这就是为什么我们需要在这里运用计算机诞生之初的思想:我们应该用一台机器来为我们做这件事。
# 持续集成和持续部署的好处
在我们讨论用于构建,测试和部署代码的特定软件解决方案之前,我们先来熟悉一下描述该过程的两个术语。你可能已经听说过它们了:
5.png

注意,通常CD部分代表持续交付,这个概念略有不同,我们将不会在这篇文章中讨论。这种容易引起混淆的缩写通常是许多学术讨论的基础。Atlassian有一篇很棒的文章解释了它们之间的差异。

为什么有两个单独的短语,它们到底是什么意思?不用担心——为了避免混淆,让我们先弄清楚一点,然后描述两者背后的普遍意义。

CI / CD的持续集成部分涵盖了应用程序完整性的重复测试。从技术角度来看,这意味着我们需要不断执行linting,运行unit / E2E测试,检查源代码质量等。通过持续的方式,意味着必须在push新代码之前完成 - 即它应该自动完成。

例如,CI流程中可以定义一系列单元测试,这些单元测试将在拉取代码请求时一起运行。这种情形下,每次更新代码时,例如对于开发分支,一些机器会检查它是否符合标准且没有错误。

CI / CD的持续部署通常涵盖了构建和将应用程序部署到可用环境的一系列过程——这也是自动完成的。例如,它可以从指定的分支(例如:`master`)获取我们的应用程序代码,使用适当的工具(例如webpack)构建,并将其部署到正确的环境(例如,托管服务)。

它并不只限于生产环境;例如,我们可以配置一个Pipeline(管道)来构建应用程序的“staging”版本,并将其部署到适当的主机用于测试。

这两个术语是软件生命周期管理理论中完全不同源的独立概念,但在实践过程中,它们通常以互补的方式共存于一个大型Pipeline(管道)。为什么它们如此密不可分?因为通常CI和CD存在部分重叠。

例如,我们可能有一个项目,E2E测试和部署都需要用webpack构建前端代码。同时,在大多数“严苛”的生产级项目中,还有许多流程既有CI又有CD。

现在让我们想象在一个功能众多的项目中,CI / CD可以做些什么呢?
6.png

我知道越深入这个主题,流程图就越复杂 ——但是,这样一来在项目会议中用白板表示时,就显得很酷!

现在试想一下我们可以从上面的流程中得到些什么呢?我们从因与果的角度来分析,可以通过抽象特定的场景形成假想的工作流程。例如:

一名开发人员尝试push代码到公共代码库,然后需要执行一组单元测试。

通过这种方式,我们可以清晰得知道什么时候开始行动 - 我们可以通过使用脚本或其他机制实现自动化。在将来使用CI / CD的过程中,你可以为这些场景化的Pipeline命名。

注意上面的粗体字:当和然后,每个阶段都需要一个动作触发。为了运行特定的Pipeline,我们需要某种kickstart或触发器。这些可能是:

  • 计时类触发器(“每天下午6点构建staging版本的应用程序”)
  • 代码库触发器(“每次发布新的拉取请求时运行单元测试。”)
  • 手动触发器(“项目经理启动应用程序构建过程并部署到生产环境。”)
当然也可以通过其他的触发器触发特定的Pipeline,尤其是当我们需要通过许多单独构建的部分集成一个复杂应用程序的时候。好吧,理论说的差不多了,现在来说说为我们完成这些工作的软件。# CI / CD中用到的软件基本上,每个CI / CD软件说到底只是某种类型的任务执行工具,当触发某些操作时会运行Job(任务)。我们的主要工作是通过配置的方式提示要完成哪些Job以及何时完成等。基于这些基本描述,CI / CD软件有许多类型,规格和偏好 - 其中一些软件非常复杂以至于手册都有数百页。但也不用害怕:在本文结束之前,你将熟悉其中一个。对新手而言,我们可以把CI / CD软件分为两类:
  • 可安装软件:可以在你的电脑上或某些远程机器上安装的应用程序或服务(例如,Jenkins,TeamCity)
  • SaaS:由一个外部公司通过Web界面的方式提供的应用程序或服务(例如,CircleCI,Azure DevOps)
真的很难说哪一个更具优势,就像本文的主题一样,它取决于应用程序的要求,组织的预算和政策以及其他因素。值得一提的是,一些受欢迎的源代码托管商(例如,BitBucket)会维护自己的CI / CD Web服务,这些服务和源代码管理系统紧密联系,旨在简化配置过程。此外,一些云托管的CI / CD服务是免费且对公众开放的 - 只要该应用程序是开源的就可以。一个广受欢迎的例子就是CircleCI。我们将充分利用它的优势,简单几步就可以为我们的前端应用程序示例配置一个功能齐全的CI / CD的Pipeline。# 前提和计划CircleCI是一个云上的CI / CD服务,它能够与GitHub集成从而轻松获取源代码。该服务有一个有趣的规则:即pipeline在源代码内部定义。这意味着所有的操作和连锁反应都通过在源代码中配置一个特殊文件来实现,在CircleCI中是通过配置`.circleci`文件夹的`config.yml`文件实现的。本文为了实现教学目的,将执行以下操作:
  • 写一个简单的前端应用程序并将源代码公开放在GitHub上
  • 创建并push包含Pipeline的配置文件`config.yml`
  • 创建一个CircleCI帐户并关联GitHub帐户
  • 找一个地方部署应用程序(这里,我们使用Amazon S3的主机托管服务)
  • 最后,运行创建的Pipeline
整个过程不应超过30分钟。接下来,我们来看看准备工作的清单。你需要:# 第一步:环境设置首先,从上述代码库签一个分支并克隆到本地计算机。如果你是新人,可以看下这波操作都做了什么。上述操作执行成功后,跳转到目标目录并执行以下命令:
npm installnpm start
现在打开浏览器输入到http://localhost:8080,你应该看到这样的画面:
7.png
这是一个非常简单的前端应用程序,表明成功加载了`.js`和`.css`文件。你可以查看源代码,它用了一个非常简单的实现机制。当然,你也可以用该教程中使用你自己的应用,只需要适当的修改创建脚本的命令就可以。只要程序是基于npm之类的工具创建的标准应用,应该都没有问题。在使用自动化流程进行持续集成、持续部署之前,需要先构建应用程序并手动存到S3。这样,我们才能保证目标环境的配置没有问题。首先,我们在本地构建应用程序包。如果你使用我们提供的示例程序,就可以用`npm run build`命令来创建,然后在项目的根目录底下得到一个名为`dist`的文件夹:
8.png
好了,我们的应用程序已经构建并打包完毕。你可以在测试服务器上执行`npx serve -s dist`命令查看它的运行情况。这个例子会运行一个`serve`包,它是一个轻量的HTTP服务器,可用来分发`dist`目录下的内容。运行完命令以后可以通过http://localhost:5000查看,你会发现和它在开发环境服务器中的运行结果一致。OK,我们把应用部署到互联网上,这要从S3开始。作为AWS生态圈的一部分,Amazon S3的概念非常简单:它提供了一个存储,可以上传任何类型的文件(包括静态HTML,CSS和JavaScript等)并启用简单的HTTP服务器来分发这些内容。最棒的是(在某些情况下)它是免费的!首先,登录:
9.png
AWS登录步骤1:提供登录用的电子邮箱
10.png
AWS登录步骤2:输入密码接下来,点击服务按钮并在存储项中选择S3,跳转到S3控制面板。
11.png
现在,我们将创建一个新的存储桶来存储我们的Web应用程序。首先,输入名称,名称只支持字母数字字符和连字符。然后,为存储桶选择适当的域。注意要记录这两个值,稍后会用到。
12.png
13.png
有一点非常重要就是设置适当的权限,这样文件才是公开的类型。因此,单击下一步直到出现设置权限选项,然后点击取消前三个框以启用文件的公开托管方式:
14.png
这样,HTTP服务器就能将上传的文件作为网站公开。在设置完存储桶之后,你可以访问并查看文件列表:
15.png
点击上传按钮,系统将提示你选择要上传的文件,你可以从`dist`文件夹中选择三个包文件上传。这里的操作和之前的一样,一定要到设置权限处选择管理公共权限框下的对该对象赋予公共读取权限,这点非常重要。瞧!文件已经在了,最后一步是要在S3上启用托管服务。通过存储桶视图找到属性按钮,打开静态网站托管选项:
16.png
你需要添加`index.html`作为索引文档,它将作为应用程序的入口。现在,一切准备就绪。在对话框的开头会生成一个新站点的链接,点击就可以查看刚才部署的应用:
17.png
太棒了,我们有了一个网站——可惜这不是这次的目标,因为到此为止什么都没自动化。你肯定不希望用这种方式登录S3控制台并在每次更新时上传一堆文件,那是机器要做的事。那么,我们来建一个持续部署的流程吧!# 第二步:准备CircleCI配置如果仔细查看代码库中的示例程序,你会看见一个CD的定义文件,打开`.circleci/config.yml`文件。
version: 2.1orbs:  aws-s3: circleci/aws-s3@1.0.4jobs:  build:    docker:      - image: circleci/python:2.7-node    environment:      AWS_REGION: us-east-1    steps:      - checkout      - run: npm install      - run: npm run build      - aws-s3/sync:          from: dist          to: 's3://demo-ci-cd-article/'          arguments: |            --acl public-read \            --cache-control "max-age=86400" \          overwrite: trueworkflows:  version: 2.1  build:    jobs:      - build:          filters:            branches:              only: master
如上所述,`config.yml`是CircleCI可识别的文件,它包含了CD过程中定义好的pipeline信息。本文的例子中,26行代码包含了以下的内容:
  • 构建应用程序需要哪些工具
  • 构建应用程序需要哪些命令
  • 应用程序在哪里以及如何部署
如果你不熟悉YAML文件,你会注意到它大量使用制表格式。这就是这类文件的组织结构:每一部分都有子节点,而层次结构由一个有双空格组成的tab标志。现在,我们来逐层看一下文件结构:
version: 2.1orbs:  aws-s3: circleci/aws-s3@1.0.4
上面几行代码包含了解析器的版本信息,并定义了部署过程中会用到的附属包(CircleCI命名规则中的“orbs”)。在这里,我们需要导入一个名为`aws-s3`的orb ,它包含把文件发送到S3存储桶所需的工具。
jobs:  build:    docker:      - image: circleci/python:2.7-node    environment:      AWS_REGION: us-east-1    steps:      - checkout      - run: npm install      - run: npm run build      - aws-s3/sync:          from: dist          to: 's3://demo-ci-cd-article/'          arguments: |            --acl public-read \            --cache-control "max-age=86400" \          overwrite: true
上面几行代码包含了Job的定义信息,即Pipeline的核心内容。这里要注意的一点是,像上述第二行中所示我们把Job命名为`build`,稍后我们会在CircleCI控制台的报告中看到这个名字。下一行写着`docker`,这部分我们定义创建应用的容器(在哪台虚拟机运行)。如果不熟悉容器或者Docker,这一步你可以想象成选择一台虚拟机然后创建应用。这里,有一台预装了Python和Node.js的Linux虚拟机,我们需要用Python运行AWS S3的工具,用Node创建前端应用。`environment`和`AWS_REGION`是AWS运行用的环境变量,可以不用在意具体的参数。到此,S3就能运行了。下一部分,`steps`更具自描述性。实际上,它是完成Job需要执行的一系列步骤。这部分的示范定义如下:
  • `checkout`:从代码库中获取源代码
  • `run: npm install`:安装依赖包
  • `run: npm run build`:Pipeline的核心,用于构建代码
  • `aws-s3/sync`:另一个重要步骤,它部署(“同步”)S3存储桶中给定的`dist`路径下的内容。注意,这个示例把`demo-ci-cd-article`作为存储桶的名字,你需要修改存储桶名字,使它和示例中的名称一致。

# 解析CircleCI配置
基本上,你可以把它想象成运行在本地的一个包含一组操作的job,这样就是告诉VM一步一步如何完成。当然,你也可以认为它是具备一些特定功能的特殊的shell脚本。

job有一个重要原则那就是每一个步骤都得成功。有任何一个命令失败,就不再执行后续操作,当前的pipeline的状态也会标记为`FAILED`。Job执行失败的信息会在CI / CD控制台中显示,包括相关错误日志,有助于排错。

失败的原因有很多,比方说,对于一个执行自动测试的pipeline,它可能意味着一次单元测试的失败并且某个开发人员需要修复他的代码,或者也有可能是工具的配置有问题,从而导致构建和部署失败。无论是什么原因,CI / CD流程通常会通过电子邮件的方式通知管理员(或本人)执行失败的消息。

这就是为什么要以相对安全的方式定义jobs,以便在执行某一步出错时,确保之前的步骤不会产生任何永久的负面影响。

马上就要结束了,最后一部分是`workflows`:
workflows:
version: 2.1
perform_build:
jobs:
- build:
filters:
branches:
only: master

在CircleCI中“workflow”是一组互相协作的Job。由于之前我们只定义了一个Job(`build`),我们可以暂时不考虑这部分。但是,通过定义工作流的方式可以实现一个很重要的功能:branch filtering(筛选分支)。

你可以看到配置文件中的最后2行定义了`filters`。在这个例子中,它包含了`branches: only: master`,即定义了只有主分支上的代码更新才会执行构建代码Job。

这样就可以通过CI / CD流程筛选出需要“watched(监控)”的分支。例如,可以在不同的分支上调用不同的工作流(包含不同的Job),然后构建单独的版本,或仅在特定情况下测试等。
# 最后一步:CircleCI实践
如果你还没有完成,通过登录GitHub的方式,关联你的GitHub帐户与CircleCI
18.png

登录GitHub并授权给CircleCI后,你会在导航栏看见一个Add Project(添加项目)的选项。点击它可以查看你在GitHub中的代码库列表:
19.png

不管是拷贝的示例还是你自己准备的应用(记住有一个`.circleci/config.yml`文件),先假设你已经有一个代码库了。
然后,在列表中找到该项目后单击旁边的Set Up Project(设置项目)选项。你会看到一个描述CircleCI规则的画面:
20.png

看到底部Start building(开始构建)的按钮了吗?是的,就是它。点击它来启用我们的自动化流程,让机器为我们工作。
点击这个按钮后,你会看到……一个错误提示。
21.png

好吧,我们还需要配置一个地方:让CircleCI API授权给AWS的机制。到目前为止,我们还没有把AWS密码放入代码,GitHub或CircleCI里,所以AWS还不知道我们要把东西放入S3,所以报错。

通过改CircleCI面板中的项目设置来配置。单击右上角的齿轮图标,然后在左边找AWS权限选项卡,你会看到以下画面:
22.png

Access Key ID(访问秘钥ID)Secret Access Key(加密访问秘钥)是AWS的2个鉴权值,用于对CircleCI等第三方服务的鉴权。例如,将文件上传到S3存储桶。最初,这些密钥将具有与分配给它们的用户相同的权限。

你可以通过AWS控制台的IAM生成这些信息。进入IAM,打开访问秘钥Access Key ID(访问秘钥ID)Secret Access Key(加密访问秘钥)】窗口,点击创建新的访问密钥,生成可以复制到CircleCI的密钥对:
23.png

单击Save AWS keys(保存AWS秘钥)就可以了。你可以在CircleCI上尝试重新初始化代码库,也可以用更加快捷的方式:找到失败的报告,然后点击Rerun workflow(重新执行)工作流程按钮。
24.png

现在所有的问题都搞定了,构建应用应该不会再出状况了。
25.png

太棒了!你可以登录S3控制台并检查文件的修改时间,可以证明文件是新上传的。不过还没完事呢,我们来看看“持续”部分是如何工作的。我将返回代码编辑器,说一下应用(`index.html`)的一个小的变动:
26.png

现在,我们可以将代码推送到代码库:
 git add .
git commit -m “A small update!”
git push origin master

神奇的事情发生了。眨眼之间,在成功推送后,CircleCI已经在用更新后的代码构建应用了:
27.png

几秒钟后,会有一条执行`SUCCESS`的消息。现在,你可以刷新一下S3托管的web页面,就可以看到修改后的应用:
28.png

搞定!这一切都是自动执行的:推送完代码后互联网上的一些机器自动构建并部署到生产环境中。
# 进阶练习
当然,这只是一个简单的例子。现在我们来看一个更复杂的例子。例如,部署到多个环境并更改应用。

回到我们的示例代码,你会发现在`package.json`中有两个独立的构建脚本:一个用于`production`环境,一个用于`staging`环境。由于只是一个示例项目,所以不会发生大的变更。这里,它只是用一个不同的JavaScript控制台消息表示而已。

应用在`staging`环境运行之后打开浏览器,你可以在JavaScript控制台看到相应的日志信息:
29.png

现在,我们利用这个机制扩展构建应用的pipelines。请看以下的代码:
version: 2.1
orbs:
aws-s3: circleci/aws-s3@1.0.4
jobs:
build:
docker:
- image: circleci/python:2.7-node
environment:
AWS_REGION: us-east-1
steps:
- checkout
- run: npm install
- run: npm run build
- aws-s3/sync:
from: dist
to: 's3://demo-ci-cd-article/'
arguments: |
--acl public-read \
--cache-control "max-age=86400" \
overwrite: true
build-staging:
docker:
- image: circleci/python:2.7-node
environment:
AWS_REGION: us-east-1
steps:
- checkout
- run: npm install
- run: npm run build:staging
- aws-s3/sync:
from: dist
to: 's3://demo-ci-cd-article/'
arguments: |
--acl public-read \
--cache-control "max-age=86400" \
overwrite: true
workflows:
version: 2.1
build:
jobs:
- build:
filters:
branches:
only: master
build-staging:
jobs:
- build-staging:
filters:
branches:
only: develop

注意,我们添加了一个新的job和一个新的`build-staging`工作流程。有两点不同:新job调用前面提到的
`npm run build:staging`方法,同时用`develop`分支进行筛选。

这意味着所有到`develop`分支的推送都将用“staging”构建,而`master`分支上的所有变更都将保留其原始状态并触发“production”构建。在这里,双方都会在同一个S3存储桶中,但我们可以修改并让它们在相互隔离的目标环境中运行。

可以试一下:基于`master`分支创建一个新的`develop`分支并将代码推送到代码库。在CircleCI控制台,你会看到调用了不同的工作流程:
30.png

相应的变更推送到了S3存储桶,但这次在staging上构建来自`develop`分支的应用,实现了多版本的构建工作。很好,我们马上就能实现之前描述过的工作流程啦!
# 持续集成部分
我们已经完成了持续部署部分的内容,但什么是持续集成呢?正如之前说过的,这部分涉及到定期检查代码的质量,例如:执行测试。

如果仔细看示例的代码库就可以看到有一个单元测试样例,可以用`npm run test`命令执行该测试样例。它通过断言的方式比较了某些模式下虚函数的结果。
function getMessage() {
return 'True!';
}

// ...

module.exports = getMessage;


const getMessage = require('./jsChecker');
const assert = require('assert');

assert.equal(getMessage(), 'True!');

我们可以在管道中加入测试,然后设置成在每个拉取请求时执行就可以。实现方式是在`config.yml`里创建一个新job和一个新的工作流程:

config.yml



version: 2.1
orbs:
aws-s3: circleci/aws-s3@1.0.4
jobs:
build:
# ...
build-staging:
# ...
test:
docker:
- image: circleci/python:2.7-node
steps:
- checkout
- run: npm install
- run: npm run test
workflows:
version: 2.1
build:
# ...
build-staging:
# ...
test:
jobs:
- test

我们已经定义了一个新job和一个名为`test`的工作流程,唯一的目的是触发`npm run test`脚本。然后,将此文件推送到代码库,看一下CircleCI控制台会发生什么:
31.png

一个新的工作流程被自动触发,并完成了一次成功的测试。接下来把它和GitHub的代码库进行对接,这样一来每次拉取特定分支的请求都会触发该job。要实现这一点,只需要打开GitHub页面并到Settings(设置)页面,选择Branches(分支)
32.png

单击Add rule(添加规则),就可以添加一个新的策略。该策略将在合并拉取请求之前强制执行一系列检查,其中一项检查就是调用CircleCI工作流程,如下所示:
33.png

通过勾选Require status checks to pass before merging(合并之前要检查状态)并勾选的`ci/circleci: test`,就可以将规则设置为在拉取前执行该工作流。

该规则可以通过创建一个新的拉取请求来测试,然后打开Checks(检查)面板来查看测试情况:
34.png

当然,也可以测试该规则无效的情况。你可以提交一个会导致测试失败的变更,把它放到一个新分支上并执行一个拉取请求:
35.png

我们模拟了一个失败的测试,输出结果如下:
assert.equal(getMessage(), 'True!');
-->
[quote] node src/modules/jsChecker.test.js
assert.js:42
throw new errors.AssertionError({
^
AssertionError [ERR_ASSERTION]: 'True, but different!' == 'True!'
at Object.

现在这个拉取请求将无法合并,因为它引入了导致测试失败的代码:
36.png

赞!我们的示例项目成功覆盖了连续测试的各种情况,只要测试用例没问题,就不可能把错误的代码引入到生产分支。同样的机制还可用于执行代码linting,静态代码分析,E2E测试和其他自动化检查等。[/quote]

好的,就这样!虽然我们的示例项目非常简单,但它展现了真实且有效的CI / CD流程。无论是集成还是部署都由云上的工具执行,所以开发者可以将所有注意力集中到编码上。

无论涉及多少人,机器都将不知疲倦地工作,并检查一切是否到位。虽然设置这一切也需要花费一些时间,但从长远看,把机械性操作进行自动化处理是非常有意义的一件事。

当然,它不是永远的免税天堂:迟早会产生额外的费用。例如,CircleCI每月提供1,000分钟的免费构建。对于小型团队和简单的开源项目来说足够了,但对大型的企业级项目而言肯定会超过这个配额。
# 延伸阅读
我们学习了许多基础的知识,但这篇文章还有许多重要的内容还没来得及讲解。

有一点就是如何更好的使用环境变量。通常我们都不会直接在源代码中保存密码,API密钥和其他敏感信息。当引入CI / CD自动化流程后,首先需要向机器提供适当的变量,就像我们在示例中使用AWS密码一样。

除此之外,环境变量来可以用来控制构建的过程,例如:应该构建哪个或者应该在特定版本中启用哪些特征之类。你可以通过它们在CircleCI中的使用这篇文章中获得更多的信息。

另一个是:许多CI / CD流程引入了组件管理的概念。组件是对特定构建过程中产生的代码的通称。例如,一个包或具有特定版本的应用程序的容器镜像都可以看做组件。

在特定组织中,由于各种原因导致对组件版本的管理变得格外重要。例如:它们可能会被归类和归档以便用于回滚或其他用途。

另一个重要的部分是角色、权限和安全性。这篇文章涉及到定义Pipelines和工作流的基础操作,但在大型、真实的项目中,有必要将组织的流程和策略等考虑在内。例如,我们希望某个Pipeline只能由公司组织架构中的某个人调用或批准。

另一个例子是对特定管道的设置或VM的配置进行细粒度的控制。但同样,这取决于用什么软件以及特定项目或公司的要求,好的自动化流程没有一个单一的范式,就像好的IT项目没有单一的模式一样。
# 总结
好了,言归正传。

不知道这篇文章会让你有什么样的收获?重要的是,现在你已经对一些“重大”的项目中发生的事情有了一个大致的了解。无论使用何种方法和软件,一些基本的规则总是相似的:有任务、管道和代理执行此工作流程。希望通过这篇文章,会让你对许多概念有一个新的认识。最后,你可以试着创建实际工作中用到的CI / CD Pipeline,并用自动化的方式将应用部署到云上。

接下来你还可以做什么呢?

当然,继续扩充你的知识,努力做的更好。如果你正在为公司开发项目,可以尝试写写代码,创建你自己的测试/部署pipeline。你可以(甚至应该)在你的下一个开源项目中引入自动化测试、打包等。您还可以了解更多的CI / CD软件:例如Travis,Jenkins或Azure DevOps。

此外,你还可以查看我的个人资料中与前端开发相关的其他帖子。祝你好运!

原文链接:From front-end developer to a DevOps: An intro to CI/CD (翻译:Tiny Guo)

GitLab Auto DevOps功能与Kubernetes集成教程

Rancher 发表了文章 • 1 个评论 • 606 次浏览 • 2019-04-23 18:37 • 来自相关话题

介 绍 === 在这篇文章中,我们将介绍如何将GitLab的Auto DevOps功能与Rancher管理的Kubernetes集群连接起来,利用Rancher v2.2.0中引入的授权集群端点 ...查看全部
介 绍
===



在这篇文章中,我们将介绍如何将GitLab的Auto DevOps功能与Rancher管理的Kubernetes集群连接起来,利用Rancher v2.2.0中引入的授权集群端点的功能。通过本文,你将能全面了解GitLab如何与Kubernetes集成,以及Rancher如何使用授权集群端点简化这一集成工作的流程。本文非常适合Kubernetes管理员、DevOps工程师,或任何想将其开发工作流与Kubernetes进行集成的人。




背 景
===



什么是GitLab Auto DevOps?
----------------------



Auto DevOps是在GitLab 10.0中引入的功能,它让用户可以设置自动检测、构建、测试和部署项目的DevOps管道。将GitLab Auto DevOps与Kubernetes集群配合使用,这意味着用户可以无需配置CI / CD资源和其他工具,即可以部署应用程序。



什么是Rancher的授权集群端点?
------------------



从v2.2.0开始,Rancher引入了一项名为Authorized Cluster Endpoint的新功能,用户可以直接访问Kubernetes而无需通过Rancher进行代理。在v2.2.0之前,如果要直接与下游Kubernetes集群通信,用户必须从各个节点手动检索kubeconfig文件以及API服务器地址。这不仅增加了操作的复杂度,而且还没有提供一种机制来控制通过Rancher管理集群时可用的细化权限。



从Rancher v2.2.0开始,部署Rancher管理的集群时,默认情况下会启用授权群集端点(ACE)功能。ACE将部分Rancher身份验证和授权机制推送到下游Kubernetes集群,允许Rancher用户直接连接到这些集群,同时仍遵守安全策略。



如果您已为某些项目中的某个用户明确授予了权限,则当该用户使用授权集群端点进行连接时,这些权限能自动生效应用。现在,无论用户是通过Rancher还是直接连接到Kubernetes集群,安全性都能得到保障。



授权集群端点功能的相关文档对此有更详细的说明:

https://rancher.com/docs/rancher/v2.x/en/cluster-provisioning/rke-clusters/options/#authorized-cluster-endpoint



注意
>
> 目前,授权集群端点功能暂时仅适用于使用Rancher Kubernetes Engine(RKE)启动的下游Kubernetes进群。




前期准备
====



要将GitLab Auto DevOps与Rancher管理的Kubernetes集群进行对接,您需要实现准备好:

- 一个GitLab.com帐户,或一个自托管GitLab实例上的帐户(需已启用Auto
DevOps):GitLab.com帐户需要已经配置好了Auto
DevOps。如果您使用的是自托管GitLab实例,则可以参考这一GitLab文档了解如何启用Auto
DevOps:https://docs.gitlab.com/ee/topics/autodevops/

  • 运行版本v2.2.0或更高版本的Rancher实例:您可以以单节点模式启动Rancher(https://rancher.com/quick-start/),也可以创建HA安装(https://rancher.com/docs/rancher/v2.x/en/installation/ha/)。
  • Rancher管理的Kubernetes集群:您还需要一个通过RKE配置的、Rancher上管理的集群。此外,集群中需要有一个管理员用户,如果您使用的是GitLab.com,则需要通过公共网络访问控制平面节点。

设置Rancher和Kubernetes
====================


首先,我们需要先将Rancher和Kubernetes设置好。该过程的第一部分主要涉及收集信息。



注意
>
> 为简单起见,这些步骤使用的是Rancher中默认的admin帐户。最佳实践要求您使用独立用户执行此类过程,并限制该用户对正在集成GitLab的集群的权限。





登录Rancher并导航到要集成的下游集群。在本演示中,我们将在EC2实例上创建一个名为testing的集群,该集群在Amazon中运行:




在集群的仪表板上,单击顶部的Kubeconfig File按钮。这将打开kubeconfig集群的文件,其中包括授权集群端点的信息。



kubeconfig文件中的第一个条目是通过Rancher服务器的集群端点。向下滚动以标识此集群的授权群集端点,该集群列为单独的集群条目:






在我的示例中,此集群的名称是testing-testing-2,并且端点server是AWS提供的公共IP。



复制server和certificate-authority-data字段的值,不包括引号,并保存它们。



在kubeconfig文件中进一步向下滚动并找到您的用户名和token:




复制token字段(不包括引号)并保存。



接下来解码证书授权机构数据的base64版本,将其转换回原始版本并保存。根据您的工具,一些可行的选项包括:




设置GitLab项目
==========



通过我们从Rancher收集的信息,我们现在可以配置GitLab了。我们将首先在GitLab中创建一个新项目,该项目将使用Auto DevOps功能与我们的Kubernetes集群集成。



首先,登录GitLab,然后选择New Project。



在“新建项目”页面上,选择“从模板创建”选项卡。这将为您提供要使用的模板项目列表。选择NodeJS Express,然后单击“Use template”:




为项目命名,并将“可见性级别”设置为“ 公共”。完成后单击“ 创建项目”。



注意
>
> 在我撰写本文时,可见性级别可以设为“私密”,不过这是GitLab的Auto DevOps实验性功能。





在项目页面左侧的菜单窗格中,选择“设置”>“CI / CD”。展开“ 环境变量”部分,并设置以下变量:




我们这次会禁用下图这些功能,因为我们的简单示例暂时不需要它们,并且它们会延长部署所需的时间。在实际项目中,您可以根据您的实际需求启用其中一些选项:




单击“ 保存变量”以完成GitLab项目配置。




连接GitLab和Rancher
================



现在,我们已准备好将我们的GitLab项目与Rancher管理的Kubernetes集群集成。



在GitLab中,选择新克隆的项目。在左侧菜单中,选择“ 操作”>“Kubernetes”。单击绿色“添加Kubernetes集群”按钮。在下一页上,选择“添加现有集群”选项卡。

按以下信息填写相应字段:







单击“ 添加Kubernetes集群”。GitLab将添加集群,并在其中创建新的命名空间。您可以查看Rancher接口,确认新创建的命名空间已经创建成功。



注意
>
> GitLab连接到集群时所做的第一件事就是为项目创建一个命名空间。如果您在一段时间后没有看到创建名称空间,则说明可能出现了一些问题。





将集群添加到GitLab后,将显示要安装到集群中的应用程序列表。第一个是Helm Tiller。继续,单击“ 安装”将其添加到集群。



接下来,安装Ingress,它将允许GitLab将流量路由到您的应用程序:




根据您配置进群的方式,您的入口端点可能会自动填充,也可能不会。在本教程中,我将使用xip.io主机名来指向单个节点的流量。至于您的用例,您可能需要设置通配符域并将其指向此ingress(或指向您的节点IP等)。



部署好ingress后,滚动到页面顶部并找到“基本域”字段。输入其中一个节点的公共IP地址,然后输入.xip.io。这将创建一个解析为该IP地址的通配符域,这对于我们的示例就足够了:




接下来,在导航栏中,选择“设置”>“CI / CD”。展开“ 自动DevOps”部分,然后选中“默认为自动DevOps管道”框。这不仅意味着Auto DevOps已被设为默认值,还能够触发构建。将“部署策略”设置为“ 继续部署到生产”:




检查Auto DevOps框后,管道运行将开始。导航到GitLab中的CI / CD>管道。您应该看到类似于下图的内容,这表明GitLab正在部署您的应用程序:




验证Rancher中的部署
=============



下面让我们回到Rancher,查看一下我们的部署的情况,看看资源是如何转换为Rancher界面中的Kubernetes对象的。



在Rancher中,导航到您的进群,然后单击顶部导航菜单中的Projects / Namespaces。



GitLab代表您创建了两个命名空间:一个是gitlab-managed-apps,另一个是唯一的应用程序命名空间。gitlab-managed-apps命名空间包含资源,如用于部署应用程序的nginx 和Helm tiller实例。那个应用程序的唯一命名空间,包含着应用程序的部署。



为了将这些进一步可视化,我们可以将这些命名空间移动到我们的Default项目中。您也可以使用任何其他项目。单击“移动”按钮,然后选择所需的项目:




移动命名空间后,导航到他们所属的项目,然后导航到Workloads页面。该页面将在其特定于应用程序的命名空间中显示您的新部署:




请注意部署名称下的443 / https链接。单击该链接,您就可以跳转至您的部署的通配符域的ingress。如果一切顺利,你将可以看到这个象征着成功的页面:





结 语



恭喜!您刚刚成功地使用授权集群端点将GitLab的Auto DevOps与Rancher管理的Kubernetes集群连接,以实现更安全、直接的连接了!



当探索Rancher的其他区域时,你可能会注意到GitLab以你的名义为你创建的其他对象。例如,“负载均衡”选项卡显示已部署的L7 ingress以及创建的主机名。您还可以在“服务发现”选项卡下查看部署的应用程序的内部服务。



GitLab的Auto DevOps功能不仅易于使用,而且可定制且功能强大。在本文的演示中,我们禁用了一些高级功能,如自动测试、依赖项扫描和许可管理。这些功能在后期也可以重新启用,并通过配置GitLab,为您的开发环境提供更多意想不到的便利与价值。除了Auto DevOps之外,GitLab还为CI / CD提供了.gitlab-ci.yml文件,用户可以借此进行更多的扩展定制。在GitLab的文档中您可以了解到更多信息:

https://docs.gitlab.com




在Kubernetes和Rancher上构建CI / CD流水线



Kubernetes的一大价值,就是为企业优化开发操作流程,而CI工作流与Kubernetes的集成,是大多团队极关注的重要部分。



本周三(4月24日)晚20:30,Rancher将举办免费的在线培训《企业如何构建CI/CD流水线》,本次直播中,我们将分享:

- 如何对接GitLab


- 构建镜像


- 发布镜像到内置的镜像仓库


- 发布镜像到远端仓库

- 通过流水线部署应用

- 通过应用商店发布自己的应用

- 如何设置流水线通知

您可以点击链接:http://live.vhall.com/729465809 预约此次课程,周三晚使用同一链接即可观看直播!


DockOne微信分享(二零六):容器环境下的持续集成最佳实践

大卫 发表了文章 • 0 个评论 • 1027 次浏览 • 2019-04-03 11:49 • 来自相关话题

【编者的话】本次分享结合 Drone + GitFlow + Kubernetes 介绍一些容器环境下持续集成、持续发布方面的实践经验。分享将展示从 Hello World 到单人单分支手动发布到团队多分支 GitFlow 工作流再到团队多分支 semanti ...查看全部
【编者的话】本次分享结合 Drone + GitFlow + Kubernetes 介绍一些容器环境下持续集成、持续发布方面的实践经验。分享将展示从 Hello World 到单人单分支手动发布到团队多分支 GitFlow 工作流再到团队多分支 semantic-release 语义化发布最后到通知 Kubernetes 全自动发布,如何从零开始一步一步搭建 CI 将团队开发、测试、发布的流程全部自动化的过程,最终能让开发人员只需要认真提交代码就可以完成日常的所有 DevOps 工作。

云原生(Cloud Native)是伴随的容器技术发展出现的的一个词,最早出自 Pivotal 公司(即开发了 Spring 的公司)的一本技术小册子 Migrating to Cloud-Native Application Architectures, 其中定义了云原生应用应当具备的一些特质,如无状态、可持续交付、微服务化等。随后云原生概念被广为引用,并围绕这一概念由数家大厂牵头,成立了 CNCF 基金会来推进云原生相关技术的发展,主要投资并孵化云原生生态内的若干项目,包括了如 Kubernetes / etcd / CoreDNS 等耳熟能详的重要项目。而这张大大的云原生版图仍然在不断的扩展和完善。

从个人理解来说,传统的应用由于年代久远,更多会考虑单机部署、进程间通信等典型的“单机问题”,虽然也能工作在容器下,但由于历史包袱等原因,架构上已经很难做出大的调整。而“云原生”的应用大都是从一开始就为容器而准备,很少考虑在单机环境下使用,有些甚至无法脱离容器环境工作;考虑的场景少,从而更轻量,迭代更快。比如 etcd 之于 ZooKeeper, Traefik 之于 Nginx 等,相信只要在容器环境下实现一次同样的功能,就能强烈的体会到云原生应用所特有的便捷之处。

在 CNCF 的版图下,持续集成与持续交付(Continuous Integration & Delivery)板块一直缺少一个钦定的主角,虽然也不乏 Travis CI、GitLab、Jenkins 这样的知名项目,但最能给人云原生应用感觉的,应该还是 Drone 这个项目,本文将围绕 Drone 结合 GitFlow 及 Kubernetes 介绍一些容器环境下持续集成、持续发布(CI/CD)方面的实践经验。
#主流 CI/CD 应用对比
之前我也介绍过基于 Travis CI 的一些持续集成实践。后来经过一些比较和调研,最终选择了 Drone 作为主力 CI 工具。截止本文,团队已经使用 Drone 有 2 年多的时间,从 v0.6 一路用到现在即将发布的 v1.0,虽然也踩了不少坑,但总的来说 Drone 还是可以满足大部分需求,并以不错的势头在完善和发展的。

下面这张表总结了主流的几个 CI/CD 应用的特点:
B1.png

Travis CI 和 CircleCI 是目前占有率最高的两个公有云 CI,易用性上相差无几,只是收费方式有差异。由于不支持私有部署,如果并行的任务量一大,按进程收费其实并不划算;而且由于服务器位置的原因,如果推送镜像到国内,速度很不理想。

GitLab CI 虽然好用,但和 GitLab 是深度绑定的,我们的代码托管在 GitHub,整体迁移代码库的成本太大,放弃。

Jenkins 作为老牌劲旅,也是目前市场占有率最高的 CI,几乎可以覆盖所有 CI 的使用场景,由于使用 Java 编写,配置文件使用 Groovy 语法,非常适合 Java 为主语言的团队。Jenkins 显然是可以满足我们需要的,只是团队并非 Java 为主,又已经习惯了使用 YAML 书写 CI 配置,抱着尝鲜的心态,将 Jenkins 作为了保底的选择。

综上,最终选择 Drone 的结论也就不难得出了,Drone 即开源,又可以私有化部署,同时作为云原生应用,官方提供了针对 Docker、Docker Swarm、Kubernetes 等多种容器场景下的部署方案,针对不同容器场景还有特别优化,比如在 Docker Swarm 下 Drone 是以 Agent 方式运行 CI 任务的,而在 Kubernetes 下则通过创建 Kubernetes Job 来实现,显然充分利用了容器的优势所在,这也是 Drone 优于其他 CI 应用之处。个人还觉得 Drone 的语法是所有 CI 中最容易理解和掌握的,由于 Drone 每一个步骤都是运行一个 Docker 容器,本地模拟或调试也非常容易。

一句话概况 Drone,可以将其看做是可以支持私有化部署的开源版 CircleCI,并且目前仍然没有看到有其他主打这个定位的 CI 工具,因此个人认为 Drone 是 CI/CD 方面云原生应用头把交椅的有力竞争者。
#容器环境下一次规范的发布应该包含哪些内容
技术选型完成后,我想首先演示一下最终的成果,希望能直观的体现出 CI 对自动化效率起到的提升,不过这就涉及到一个问题:在容器环境下,一次发布应该包含哪些内容,其中有哪些部分是可以被 CI 自动化完成的。这个问题虽然每家公司各不相同,不过按经验来说,容器环境下一次版本发布通常包含这样一个 Checklist:

* 代码的下载构建及编译
* 运行单元测试,生成单元测试报告及覆盖率报告等
* 在测试环境对当前版本进行测试
* 为待发布的代码打上版本号
* 编写 ChangeLog 说明当前版本所涉及的修改
* 构建 Docker 镜像
* 将 Docker 镜像推送到镜像仓库
* 在预发布环境测试当前版本
* 正式发布到生产环境

看上去很繁琐对吗,如果每次发布都需要人工去处理上述的所有内容,不仅容易出错,而且也无法应对 DevOps 时代一天至少数次的发布频率,那么下面就来使用 CI 来解决所有问题吧。
#CI 流程演示
为了对 CI 流程有最直观的认识,我创建了一个精简版的 GitHub 项目 AlloVince/drone-ci-demo 来演示完整的流程,同时项目对应的 CI 地址是 cloud.drone.io/AlloVince/drone-ci-demo ,项目自动构建的 Docker 镜像会推送到 docker registry 的 allovince/drone-ci-demo。为了方便说明,假设这个项目的核心文件只有 index.html 一个静态页面。
##单人开发模式
目前这个项目背后的 CI 都已经配置部署好,假设我是这个项目的唯一开发人员,如何开发一个新功能并发布新版本呢?

* Clone 项目到本地, 修改项目代码, 如将 Hello World 改为 Hello World V2。
* git add .,然后书写符合约定的 Commit 并提交代码, git commit -m "feature: hello world v2”。
* 推送代码到代码库git push,等待数分钟后,开发人员会看到单元测试结果,GitHub 仓库会产生一次新版本的 Release,Release 内容为当前版本的 ChangeLog, 同时线上已经完成了新功能的发布。

虽然在开发者看来,一次发布简单到只需 3 个指令,但背后经过了如下的若干次交互,这是一次发布实际产生交互的时序图,具体每个环节如何工作将在后文中详细说明。
01.png

##多人开发模式
一个项目一般不止一个开发人员,比如我是新加入这个项目的成员,在这个 Demo 中应该如何上线新功能呢?同样非常简单:

  1. Clone 项目到本地,创建一个分支来完成新功能的开发, git checkout -b feature/hello-world-v3。在这个分支修改一些代码,比如将Hello World V2修改为Hello World V3
  2. git add .,书写符合规范的 Commit 并提交代码, git commit -m "feature: hello world v3”
  3. 将代码推送到代码库的对应分支, git push origin feature/hello-world
  4. 如果功能已经开发完毕,可以向 Master 分支发起一个 Pull Request,并让项目的负责人 Code Review
  5. Review 通过后,项目负责人将分支合并入主干,GitHub 仓库会产生一次新版本的 Release,同时线上已经完成了新功能的发布。

这个流程相比单人开发来多了 2 个环节,很适用于小团队合作,不仅强制加入了 Code Review 把控代码质量,同时也避免新人的不规范行为对发布带来影响。实际项目中,可以在 GitHub 的设置界面对 Master 分支设置写入保护,这样就从根本上杜绝了误操作的可能。当然如果团队中都是熟手,就无需如此谨慎,每个人都可以负责 PR 的合并,从而进一步提升效率。
02.png

##GitFlow 开发模式
在更大的项目中,参与的角色更多,一般会有开发、测试、运维几种角色的划分;还会划分出开发环境、测试环境、预发布环境、生产环境等用于代码的验证和测试;同时还会有多个功能会在同一时间并行开发。可想而知 CI 的流程也会进一步复杂。

能比较好应对这种复杂性的,首选 GitFlow 工作流, 即通过并行两个长期分支的方式规范代码的提交。而如果使用了 GitHub,由于有非常好用的 Pull Request 功能,可以将 GitFlow 进行一定程度的简化,最终有这样的工作流:
03.png


* 以 dev 为主开发分支,Master 为发布分支
* 开发人员始终从 dev 创建自己的分支,如 feature-a
* feature-a 开发完毕后创建 PR 到 dev 分支,并进行 code review
* review 后 feature-a 的新功能被合并入 dev,如有多个并行功能亦然
* 待当前开发周期内所有功能都合并入 dev 后,从 dev 创建 PR 到 master
* dev 合并入 Master,并创建一个新的 Release

上述是从 Git 分支角度看代码仓库发生的变化,实际在开发人员视角里,工作流程是怎样的呢。假设我是项目的一名开发人员,今天开始一期新功能的开发:

  1. Clone 项目到本地,git checkout dev。从 dev 创建一个分支来完成新功能的开发, git checkout -b feature/feature-a。在这个分支修改一些代码,比如将Hello World V3修改为Hello World Feature A
  2. git add .,书写符合规范的 Commit 并提交代码, git commit -m "feature: hello world feature A"
  3. 将代码推送到代码库的对应分支, git push origin feature/feature-a:feature/feature-a
  4. 由于分支是以 feature/ 命名的,因此 CI 会运行单元测试,并自动构建一个当前分支的镜像,发布到测试环境,并自动配置一个当前分支的域名如 test-featue-a.avnpc.com
  5. 联系产品及测试同学在测试环境验证并完善新功能
  6. 功能通过验收后发起 PR 到 dev 分支,由 Leader 进行 code review
  7. Code Review 通过后,Leader 合并当前 PR,此时 CI 会运行单元测试,构建镜像,并发布到测试环境
  8. 此时 dev 分支有可能已经积累了若干个功能,可以访问测试环境对应 dev 分支的域名,如 test.avnpc.com,进行集成测试。
  9. 集成测试完成后,由运维同学从 Dev 发起一个 PR 到 Master 分支,此时会 CI 会运行单元测试,构建镜像,并发布到预发布环境
  10. 测试人员在预发布环境下再次验证功能,团队做上线前的其他准备工作
  11. 运维同学合并 PR,CI 将为本次发布的代码及镜像自动打上版本号并书写 ChangeLog,同时发布到生产环境。

由此就完成了上文中 Checklist 所需的所有工作。虽然描述起来看似冗长,但不难发现实际作为开发人员,并没有任何复杂的操作,流程化的部分全部由 CI 完成,开发人员只需要关注自己的核心任务:按照工作流规范,写好代码,写好 Commit,提交代码即可。

接下来将介绍这个以 CI 为核心的工作流,是如何一步步搭建的。
#Step by Step 构建 CI 工作流
##Step.0:基于 Kubernetes 部署 Drone v1.0.0
以 GitHub 为例,截止本文完成时间(2019 年 3 月 28 日), Drone 刚刚发布了第一个正式版本 v1.0.0。官方文档已经提供了分别基于 Docker、Kubernetes 的 Drone 部署说明,不过比较简略,因此这里给出一个相对完整的配置文件。

首先需要在 GitHub 创建一个 Auth App,用于 repo 的访问授权。应用创建好之后,会得到 Client ID 和 Client Secret 。同时 Authorization callback URL 应填写 Drone 服务对应域名下的 /login,如 https://ci.avnpc.com/login。

Drone 支持 SQLite、MySQL、Postgres、S3 等多种后端存储,主要用于记录 build logs 等文本信息,这些信息并不是特别重要,且我们的 CI 有可能做迁移,因此个人更推荐使用 SQLite。

而在 Kubernetes 环境下,SQLite 更适合用挂载 NAS 的方式供节点使用,因此首先将存储的部分独立为文件 drone-pvc.yml,可以根据实际情况配置 nfs.path 和 nfs.server:
kubectl apply -f drone-pvc.yaml

Drone 的配置主要涉及两个镜像:

* drone/kubernetes-secrets 加密数据服务,用于读取 Kubernetes 的 secrets
* drone/drone:1.0.0-rc.6 就是 Drone 的 server 端,由于在 Kubernetes 下 Drone 利用了 Job 机制,因此不需要部署 Agent。

这部分配置较长,可以直接参考示例 drone.yaml

主要涉及到的配置项包括:

* Drone/kubernetes-secrets 镜像中

* SECRET_KEY:数据加密传输所用的 key,可以使用 openssl rand -hex 16 生成一个

* Drone/Drone镜像中

* DRONE_KUBERNETES_ENABLED:开启 Kubernetes 模式
* DRONE_KUBERNETES_NAMESPACE:Drone 所使用的 Namespace, 这里使用 default
* DRONE_GITHUB_SERVER:GitHub 服务器地址,一般为 https://github.com
* DRONE_GITHUB_CLIENT_ID:上文创建 GitHub Auth App 得到的 Client ID
* DRONE_GITHUB_CLIENT_SECRET:上文创建 GitHub Auth App 得到的 Client Secret
* DRONE_SERVER_HOST:Drone 服务所使用的域名
* DRONE_SERVER_PROTO:http 或 https
* DRONE_DATABASE_DRIVER:Drone 使用的数据库类型,这里为 SQLite3
* DRONE_DATABASE_DATASOURCE:这里为 SQLite 数据库的存放路径
* DRONE_SECRET_SECRET:对应上文的 SECRET_KEY
* DRONE_SECRET_ENDPOINT:加密数据服务的地址,这里通过 Kubernetes Service 暴露,无需修改

最后部署即可:
kubectl apply -f drone.yaml

部署后首次登录 Drone 就会跳转到 GitHub Auth App 进行授权,授权完毕后可以看到所有能读写的 Repo,选择需要开启 CI 的 Repo,点击 ACTIVATE 即可。 如果开启成功,在 GitHub Repo 的 Settings > Webhooks 下可以看到 Drone 的回调地址。
##Step.1:Hello World for Drone
在正式开始搭建工作流之前,首先可以测试一下 Drone 是否可用。Drone 默认的配置文件是 .drone.yml, 在需要 CI 的 repo 根目录下创建.drone.yml, 内容如下,提交并git push到代码仓库即可触发 Drone 执行 CI。
kind: pipeline  
name: deploy

steps:
[list]
[*]name: hello-world[/*]
[/list] image: docker
commands:
- echo "hello world"

Drone v1 的语法主要参考的 Kubernetes 的语法,非常直观,无需阅读文档也可以知道,我们首先定义了一个管道(Pipeline),管道由若干步骤(step)组成,Drone 的每个步骤是都基于容器实现的,因此 Step 的语法就回到了我们熟悉的 Docker,一个 Step 会拉取 image 定义的镜像,然后运行该镜像,并顺序执行 commands 定义的指令。

在上例中,Drone 首先 clone git repo 代码到本地,然后根据 .drone.yml 所定义的,拉取 Docker 的官方镜像,然后运行该进行并挂载 git repo 的代码到 /drone/src 目录。

在 Drone 的界面中,也可以清楚的看到这一过程。
04.png

本阶段对应:

* 代码部分:https://github.com/AlloVince/drone-ci-demo/tree/hello-world
* Drone 构建记录:https://cloud.drone.io/AlloVince/drone-ci-demo/1
* Docker 镜像:无

##Step.2:单人工作流,自动化单元测试与 Docker 镜像构建
有了 Hello World 的基础,接下来我们尝试将这个工作流进行扩充。

为了方便说明,这里假设项目语言为 JavaScript,项目内新增了 test/index.js 文件用于模拟单元测试,一般在 CI 中,只要程序的返回值为 0,即代表运行成功。这个文件中我们仅仅输出一行 Log Unit test passed用于模拟单元测试通过。

我们希望将代码打包成 Docker 镜像,根目录下增加了 Dockerfile 文件,这里直接使用 Nginx 的官方镜像,构建过程只有 1 行 COPY index.html /usr/share/nginx/html/, 这样镜像运行后可以通过 http 请求看到 index.html 的内容。

至此我们可以将工作流改进为:

* 当 Master 分支接收到 push 后,运行单元测试
* 当 GitHub 发布一次 Release, 构建 Docker 镜像,并推送到镜像仓库

对应的 Drone 配置文件如下:
kind: pipeline  
name: deploy

steps:
- name: unit-test
image: node:10
commands:
- node test/index.js
when:
branch: master
event: push
- name: build-image
image: plugins/docker
settings:
repo: allovince/drone-ci-demo
username: allovince
password:
from_secret: DOCKER_PASSWORD
auto_tag: true
when:
event: tag

虽然比 Hello World 复杂了一些,但是可读性仍然很好,配置文件中出现了几个新概念:

Step 运行条件, 即 when 部分,上例中展示了当代码分支为 Master,且收到一个 push;以及当代码被标记 tag 这两种情况。Drone 还支持 repo、运行结果等很多其他条件,可以参考 Drone Conditions 文档。

Plugin 插件,上例中用于构建和推送镜像的是 plugins/docker 这个 Plugin, 一个 Plugin 本质上仍然是一个 Docker 镜像,只是按照 Drone 的规范接受特定的输入,并完成特定的操作。所以完全可以将 Plugin 看做一个无法更改 command 的 Docker 镜像。

Docker 这个 Plugin 由 Drone 官方提供,用于 Docker 镜像的构建和推送,具体的用法可以查看 Docker 插件的文档。例子中演示的是将镜像推送到私有仓库,如果不做特殊配置,镜像将被推送到 Docker 的官方仓库。

此外 Docker 插件还有一个很方便的功能,如果设置 auto_tag: true,将根据代码的版本号自动规划 Docker 镜像的标签,如代码版本为1.0.0,将为 Docker 镜像打三个标签 1, 1.0, 1.0.0。如果代码版本号不能被解析,则镜像标签为 latest。

目前 Drone 的插件已经有很多,可以覆盖主流的云服务商和常见的工作流,并且自己制作插件的成本也不高。

Secret 加密数据,镜像仓库的用户名和密码都属于敏感信息,因此可以使用 from_secret 获取加密数据。一条加密数据就是一个 key / value 对,如上例中的 DOCKER_PASSWORD 就是我们自己定义的加密数据 key。即便加密数据在 log 中被打印,UI 也只能看到 ***。加密数据的 value 需要提前保存好,保存的方式有 3 种:

* 通过 Drone UI 界面中, repo -> Settings -> Secrets 添加,所添加的加密数据将保存在 Drone 的数据库中,仅能在当前 repo 中使用。
* 通过 Drone cli 加密后保存在 .drone.yml 文件中, 使用范围仅限 yaml 文件内
* 通过 Kubernetes 保存为 Kubernetes Secret,称为 External Secrets,所有的 repo 都可以共享。如果是团队使用的话,这种保存方式显然是最方便的,但也要注意安全问题,因此 External Secrets 还支持 repo 级别的权限管理, 可以只让有当前 repo 写入权限的人才能使用对应 secret。

这个阶段对应:

* 代码仓库:https://github.com/AlloVince/drone-ci-demo/tree/single-person
* push 时触发的 Drone CI:https://cloud.drone.io/AlloVince/drone-ci-demo/4
* Release 时触发的 Drone CI:https://cloud.drone.io/AlloVince/drone-ci-demo/5
* Release 后 CI 构建的 Docker 镜像:allovince/drone-ci-demo:latest

##Step.3:GitFlow 多分支团队工作流
上面的工作流已经基本可以应付单人的开发了,而在团队开发时,这个工作流还需要一些扩展。不需要引入 Drone 的新功能,只需要在上文基础上根据分支做一点调整即可。

首先保证单元测试位于 steps 的第一位,并且限定团队工作的分支,在 push 和 pull_request 时,都能触发单元测试。

 - name: unit-test  
image: node:10
commands:
- node test/index.js
when:
branch:
include:
- feature/*
- master
- dev
event:
include:
- push
- pull_request

然后根据 GitFlow 的流程对于不同的分支构建 Docker 镜像并打上特定标签,以 feature 分支为例,下面的配置约定了当分支名满足 feature/*,并收到 push 时,会构建 Docker 镜像并打标签,标签名称为当前分支名去掉 feature/。如分支 feature/readme, 对应 Docker 镜像为 allovince/drone-ci-demo:readme,考虑到 feature 分支一般都出于开发阶段,因此新的镜像会覆盖旧的。配置如下:
- name: build-branch-image  
image: plugins/docker
settings:
repo: allovince/drone-ci-demo
username: allovince
password:
from_secret: DOCKER_PASSWORD
tag:
- ${DRONE_BRANCH[size=16]feature/} [/size]
when:
branch: feature/*
event: push

镜像的 Tag 处不再使用自动方式,其中 DRONE_BRANCH 是 Drone 的内置环境变量(Environment),对应当前的分支名。feature/ 是执行了一个字符串的替换操作(Substitution)。更多的环境变量和字符串操作都可以在文档中找到。

以此类推,可以查看这个阶段的完整 .drone.yml ,此时我们的工作流示例如下:

* 团队成员从 dev 分支 checkout 自己的分支 feature/readme
* 向feature/readme提交代码并 push, CI 运行单元测试,构建镜像allovince/drone-ci-demo:readme
* 功能开发完成后,团队成员向 dev 分支 发起 pull request , CI 运行单元测试
* 团队其他成员 merge pull request, CI 运行单元测试,构建镜像allovince/drone-ci-demo:test
* 运维人员从 dev 向 master 发起 pull request,CI 运行单元测试,并构建镜像allovince/drone-ci-demo:latest
* 运维人员 merge pull request, 并 Release 新版本 pre-0.0.2, CI 构建镜像allovince/drone-ci-demo:pre-0.0.2

可能细心的同学会发现 dev -> Master 的 pull request 时,构建镜像失败了,这是由于 Drone 出于安全考虑限制了在 pull request 时默认无法读取加密数据,因此无法得到 Docker Registry 密码。如果是私有部署的话,可以在 Repo Settings 中勾选 Allow Pull Requests,此处就可以构建成功。
##Step.4:语义化发布
上面基本完成了一个支持团队协作的半自动 CI 工作流,如果不是特别苛刻的话,完全可以用上面的工作流开始干活了。

不过基于这个工作流工作一段时间,会发现仍然存在痛点,那就是每次发布都要想一个版本号,写 ChangeLog,并且人工去 Release。

标记版本号涉及到上线后的回滚,追溯等一系列问题,应该是一项严肃的工作,其实如何标记早已有比较好的方案,即语义化版本。在这个方案中,版本号一共有 3 位,形如 1.0.0,分别代表:

  1. 主版本号:当你做了不兼容的 API 修改,
  2. 次版本号:当你做了向下兼容的功能性新增,
  3. 修订号:当你做了向下兼容的问题修正。

虽然有了这个指导意见,但并没有很方便的解决实际问题,每次发布要搞清楚代码的修改到底是不是向下兼容的,有哪些新的功能等,仍然要花费很多时间。

而语义化发布(Semantic Release)就能很好的解决这些问题。

语义化发布的原理很简单,就是让每一次 Commit 所附带的 Message 格式遵守一定规范,保证每次提交格式一致且都是可以被解析的,那么进行 Release 时,只要统计一下距离上次 Release 所有的提交,就分析出本次提交做了何种程度的改动,并可以自动生成版本号、自动生成 ChangeLog 等。

语义化发布中,Commit 所遵守的规范称为约定式提交(Conventional Commits)。比如 Node.js、 Angular、Electron 等知名项目都在使用这套规范。

语义化发布首先将 Commit 进行分类,常用的分类(Type)有:

* feat:新功能
* fix:BUG 修复
* docs:文档变更
* style:文字格式修改
* refactor:代码重构
* perf:性能改进
* test:测试代码
* chore:工具自动生成

每个 Commit 可以对应一个作用域(Scope),在一个项目中作用域一般可以指不同的模块。

当 Commit 内容较多时,可以追加正文和脚注,如果正文起始为BREAKING CHANGE,代表这是一个破坏性变更。

以下都是符合规范的 Commit:
feat:增加重置密码功能

fix(邮件模块):修复邮件发送延迟BUG

feat(API):API重构

BREAKING CHANGE:API v3上线,API v1停止支持

有了这些规范的 Commit,版本号如何变化就很容易确定了,目前语义化发布默认的规则如下:
Commit	版本号变更
BREAKING CHANGE 主版本号
feat 次版本号
fix / perf 修订号

因此在 CI 部署 semantic-release 之后,作为开发人员只需要按照规范书写 Commit 即可,其他的都由 CI 完成。

具体如何将语义化发布加入 CI 流程中呢, semantic-release 是 js 实现的,如果是 js 的项目,可以直接在 package.json 中增加配置项,而对于任意语言的项目,推荐像 Demo 中一样,在根目录下增加 配置文件release.config.js。这个配置目的是为了禁用默认开启的 npm 发布机制,可以直接套用。

semantic-release 要执行 GitHub Release,因此我们需要在 CI 中配置自己的 Personal access tokens 让 CI 有 GitHub Repo 的读写权限, 可以通过 GitHub 点击自己头像 -> Settings -> Developer settings -> Personal access tokens -> Generate new token 生成一个 Token。 然后在 Drone 的 repo 设置界面新增一个 Secret, key 为 GITHUB_TOKEN, value 填入刚生成的 Token。

最后在 .drone.yml 中增加这样一段就可以了。
- name: semantic-release  
image: gtramontina/semantic-release:15.13.3
environment:
GITHUB_TOKEN:
from_secret: GITHUB_TOKEN
entrypoint:
- semantic-release
when:
branch: master
event: push

来再次模拟一下流程,feature 分支部分与上文相同:

* 从 dev 向 master 发起 pull request,CI 运行单元测试,并构建镜像allovince/drone-ci-demo:latest
* merge pull request,CI 会执行单元测试并运行 semantic-release , 运行成功的话能看到 GitHub 新增 Release v1.0.0
* GitHub Release 再次触发CI 构建生产环境用 Docker 镜像allovince/drone-ci-demo:1.0.0

最终我们能得到这样一个赏心悦目的 Release。
05.png

##Step.5:Kubernetes 自动发布
Docker 镜像推送到仓库后,我们还剩最后一步就可以完成全自动发布的闭环,即通知 Kubernetes 将镜像发布到生产环境。这一步实现比较灵活,因为很多云服务商在容器服务都会提供 Trigger 机制,一般是提供一个 URL,只要请求这个 URL 就可以触发容器服务的发布。Demo 中我们使用更为通用的方法,就是将 kubectl 打包为容器,以客户端调用 Kubernetes 集群 Master 节点 API(kube-apiserver)的形式完成发布。

假设我们在生产环境下 drone-ci-demo 项目的 Kubernetes 发布文件如下:
---  
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: ci-demo-deployment
namespace: default
spec:
replicas: 1
template:
spec:
containers:
- image: allovince/drone-ci-demo
name: ci-demo
restartPolicy: Always

对应 .drone.yml 中增加 step 如下。这里使用的插件是 honestbee/drone-kubernetes, 插件中 kubectl 连接 API 使用的是证书 + Token 的方式鉴权,因此需要先获得证书及 Token, 已经授权的 Token 保存于 Kubernetes Secret,可以通过 kubectl get secret [ your default secret name ] -o yaml | egrep 'ca.crt:|token:' 获得并配置到 Drone 中,注意插件要求 Token 是明文的,需要 base64 解码一下:echo [ your token ] | base64 -d && echo ''。
- name: k8s-deploy  
image: quay.io/honestbee/drone-kubernetes
settings:
kubernetes_server:
from_secret: KUBERNETES_SERVER
kubernetes_cert:
from_secret: KUBERNETES_CERT
kubernetes_token:
from_secret: KUBERNETES_TOKEN
namespace: default
deployment: ci-demo-deployment
repo: allovince/drone-ci-demo
container: ci-demo
tag:
- ${DRONE_TAG}
when:
event: tag

在示例中,可以看到在语义化发布之后 CI 会将新版本的 Docker 镜像自动发布到 Kubernetes,这里为了演示仅打印了指令并未实际运行。相当于运行了如下的指令:
kubectl -n default set image deployment/ci-demo-deployment ci-demo=allovince/drone-ci-demo:v1.0.2

由于自动发布的环节势必要接触到生产服务器,需要格外注意安全问题,首推的方式当然是将 CI 和 Kubernetes 集群放于同一内网中,同时可以使用 Kubernetes 的 RBAC 权限控制,为自动发布单独创建一个用户,并删除不必要的权限。
#后话
总结一下,本文展示了从 Hello World 到单人单分支手动发布再到团队多分支 GitFlow 工作流再到团队多分支 semantic-release 语义化发布最后到 通知 Kubernetes 全自动发布,如何从零开始一步一步搭建 CI 将团队开发、测试、发布的流程全部自动化的过程,最终能让开发人员只需要认真提交代码就可以完成日常的所有 DevOps 工作。

最终 Step 的完成品可以适配之前的所有 Step,如果不太在意实现细节的话,可以在此基础上稍作修改,直接使用。

然而写好每一个 Commit 这个看似简单的要求,其实对于大多数团队来说并不容易做到,在实施过程中,经常会遇到团队成员不理解为什么要重视 Commit 规范,每个 Commit 都要深思熟虑是否过于吹毛求疵等等疑问。

以 Commit 作为 CI 的核心,个人认为主要会带来以下几方面的影响:

  1. 一个好的 Commit,代表着开发人员对当前改动之于整个系统的影响,有非常清楚的认识,代码的修改到底算 feat 还是 fix ,什么时候用 BREAKING CHANGE 等都是要仔细斟酌的,每个 Commit 都会在 ChangeLog 里“留底”,从而约束团队不随意提交未经思考的代码,提高代码质量。
  2. 一个好的 Commit 也代表开发人员有能力对所实现功能进行精细的划分,一个分支做的事情不宜过多,一个提交也应该专注于只解决一个问题,每次提交(至少是每次 push)都应该保持系统可构建、可运行、可测试,如果能坚持做到这些,对于合并代码时的冲突解决,以及集成测试都有很大帮助。
  3. 由于每次发布能清楚的看到所有关联的 Commit 以及 Commit 的重要程度,那么线上事故的回滚也会非常轻松,回滚到哪个版本,回滚后哪些功能会受到影响,只要看 CI 自动生成的 Release 记录就一目了然。如果没有这些,回滚误伤到预期外的功能从而引发连锁反应的惨痛教训,可能很多运维都有过类似经历吧。

因此 CI 自动化其实是锦上添花而非雪中送炭,如果团队原本就无视规范,Commit 全是空白或者没有任何意义的单词,分支管理混乱,发布困难,奢望引入一套自动化 CI 来能解决所有这些问题,无疑是不现实的。而只有原本就重视代码质量,有一定规范意识,再通过自动化 CI 来监督约束,团队在 CI 的帮助下代码质量提高,从而有机会进一步改进 CI 的效率,才能形成良心循环。

愿天下不再有难发布的版本。
#Q&A
Q:Kubernetes 上主流的 CI/CD 方案是啥?
A:其实这无关 Kubernetes,从市场占有率来看,前三名分别是

  1. Jenkins
  2. JetBrains TeamCity
  3. CircleCI

来源:https://www.datanyze.com/market-share/ci

Q:GitLab 自带的 CI 与 Jenkins 和 GitLab 结合的 CI,该如何选择?想知道更深层次的理解。
A:还是要结合自己团队的实际情况做选择。从成熟度来说,肯定是 Jenkins 用户最多,成熟度最高,缺点是侧重 Java,配置相对繁琐。GitLab 自带的 CI 相对简单,可以用 yaml,和 GitLab 结合的最好,但功能肯定没有 Jenkins 全面。如果是小团队新项目,GitLab CI 又已经可以满足需求的话,并不需要上 Jenkins,如果是较大的团队,又是偏 Java 的,个人更偏向 Jenkins。

Q:Jenkins 如果不想运行在 Kubernetes 里面,该怎么和 Kubernetes 集成?
A:从 CI 的流程来说,CI 应用是不是跑在 Kubernetes 的并不重要,CI 只要能访问代码库,有权限在生产环境发布,是不是跑在容器里从效果来说其实没有区别,只是用 Kubernetes 部署 Jenkins 的话,运维的一致性比较好,运维团队不用额外花时间维护一套物理机的部署方案。

Q:Kubernetes 的回滚方案是回滚代码,重做镜像,还是先切流量,后做修改?
A:代码一定是打包到镜像里的,镜像的版本就是代码的版本,所以一定是切镜像。至于回滚操作本身,Kubernetes 已经内置了很多滚动发布(Rolling update)的策略,无论是发新版本还是回滚版本,都可以做到用户无感知。

Q:镜像大到几 G 的话如何更新部署,有什么好的实践呢,以及如何回滚?
A:几个要点:

  1. 镜像仓库部署在内网,镜像推拉走内网,几个 G 的镜像传输起来也只是秒级别的
  2. 镜像构建时将不经常变动的部分划分到 1 层,因为 Docker 镜像是分层的,这样每次拉镜像可以利用到 Docker 的缓存传输的只有镜像发生变化的部分
  3. 选择 alpine 这样尽量小的镜像

回滚如果发生在相邻的版本,一般物理机上是有缓存的,速度都很快。

Q:Drone 开放 API 服务吗?这样方便其他系统集成。
A:可以调整一下思路,直接把需要的功能做成镜像在 Drone 里调用就好了。

Q:如果有 Drone 的 Server怎么做高可用?
A:Drone serve r用 Kubernetes 部署的话本身只起到了一个任务调度的作用,很难会遇到性能瓶颈。真的有性能问题可以尝试水平扩展 Drone server,共享同一数据库。

作者博客:https://avnpc.com/pages/drone-gitflow-kubernetes-for-cloud-native-ci

以上内容根据2019年4月2日晚微信群分享内容整理。分享人徐谦,高级架构师,10 年以上研发经验,2 次创业经历。现从事互联网金融相关研发,关注 Node.js、容器技术、机器学习等。DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiesd,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。

一起探索Kubernetes

新牛哥 发表了文章 • 0 个评论 • 693 次浏览 • 2019-03-31 21:23 • 来自相关话题

【编者的话】本文用图示详细分析了GitLab如何与Kubernetes集群集成,进行CI/CD流水线的配置,从而实现更高效的DevOps流程。 我将介绍使用DigitalOcean创建新的Kubernetes集群(或简称k8s)的经验 ...查看全部
【编者的话】本文用图示详细分析了GitLab如何与Kubernetes集群集成,进行CI/CD流水线的配置,从而实现更高效的DevOps流程。

我将介绍使用DigitalOcean创建新的Kubernetes集群(或简称k8s)的经验,配置我的GitLab项目以使用Kubernetes集群,以及为部署配置CI/CD进程。 如果你想了解现代堆栈的运行是多么简单,请继续阅读。
1.jpg

Fatos Bytyqi拍摄
# 创建一个Kubernetes集群
Kubernetes是一个容器编排平台,由于其简单性而受到广泛欢迎。 Kubernetes很棒,因为你可以使用配置文件定义部署配置,存储和网络,集群将确保你的应用程序始终在该配置中运行。

从源代码构建Kubernetes集群是一项艰巨的任务,但是我们可以通过大型云提供商的几次点击来实现这一目标。 我个人更喜欢DigitalOcean的简单性,我很幸运能够成为他们管理的Kubernetes产品的LTD版本的一部分。

让我们深入了解如何在DigitalOcean上创建集群。

单击“创建Kubernetes”选项后,无论是通过侧面导航还是顶部导航中的下拉菜单,都会显示此屏幕。
2.png

DigitalOceans的Kubernetes集群创建示例

在撰写本文时,Kubernetes版本1.13.1是最新版本,因此我在离我最近的地区选择了该版本。

下一步是配置节点池,标签并选择名称。 我选择用一个低成本节点来保持简单,以便学习。 这可以在将来更改,因此从小规格开始不会限制你的未来容量。

标签是可选的,名称可以是你想要的任何名称。 我发现添加“k8s”标签可以快速识别集群中的droplet。
3.png

配置节点,标签和名字

单击“创建群集”后,该过程大约需要4-5分钟才能完成。 在此期间,我们可以让你的机器设置连接到新的Kubernetes群集。

用于与Kubernetes群集交互的主要的命令行程序是`kubectl`。 对于MacOS用户,可以使用`brew`通过运行以下命令来安装它。
➜ brew install kubernetes-cli

brew完成安装后,你需要从DigitalOcean下载集群配置文件,以使`kubectl`命令知道你的集群所在的位置。 为此,请在DigitalOcean Kubernetes群集安装页面上一直向下滚动到以下部分,然后单击“下载配置文件”按钮
4.png

该文件将保存到`~/Downloads`目录中。 为了简化操作,请将文件复制或移动到`~/.kube/config`文件。 该文件将由`kubectl`命令自动读取。
➜ mkdir -p ~/.kube
➜ mv ~/Downloads/[k8s-cluster].yaml ~/.kube/config

创建集群后,通过运行`kubectl get nodes`测试连接。 这将显示群集中的单个节点。
➜ kubectl get nodes
NAME STATUS ROLES AGE VERSION
tender-einstein-8m4m Ready 21m v1.13.1

在我的例子中,节点(这是一个DigitalOcean Droplet)被命名为“tender-einstein-8m4m”,我们可以在上面看到。 如果看到类似的输出,则表明你的Kubernetes集群已成功创建,并且你可以通过`kubectl`命令行程序与其建立连接。
# 将GitLab连接到Kubernetes
GitLab具有本地集成Kubernetes的功能,我们可以配置任何组或项目来使用它。 你需要在GitLab项目上提升(项目创建者和/或管理员)权限才能设置Kubernetes集成。

首先,选择Operations菜单下的Kubernetes选项卡,然后单击Add Kubernetes Cluster。
5.png

GitLab - 添加 Kubernetes 集群

在下一个屏幕上,单击“添加现有群集”选项卡。 在这里,系统将提示你输入一些不同的项目以允许GitLab连接到你的Kubernetes群集。 GitLab有关于如何添加集群的优秀文档,我建议你阅读这些文档以全面了解集成。我将在此强调所需的步骤。
## 创建帐户
首先,我们需要为GitLab创建一个新的系统级帐户来连接。 此帐户称为ServiceAccount。 为此,我们可以使用`kubectl`命令行程序。 我们将使用YAML语法(在整个Kubernetes中使用)定义帐户,如下所示:
➜ kubectl create -f - <
  apiVersion: v1
kind: ServiceAccount
metadata:
name: gitlab
namespace: default
EOF

此YAML定义将在“default”命名空间中创建名为“gitlab”的ServiceAccount。

下一步是授予GitLab帐户集群管理员权限,以便它可以代表你自由创建和销毁服务。 我们将再次使用`kubectl`和YAML定义。
➜ kubectl create -f - <
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: gitlab-cluster-admin
subjects:
[list]
[*]kind: ServiceAccount[/*]
[/list] name: gitlab
namespace: default
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
EOF

## 连接群集
现在,让我们看一下GitLab连接到Kubernetes集群所需的所有信息。
6.png

GitLab - 添加Kubernetes集群时呈现的表格

第一个字段是Kubernetes集群名称。 这对你来说可以帮助你识别k8s群集。 它并没有真正使用那么多,所以不要花太多时间为它命名。

可以通过运行以下命令获取下一个字段API URL:
➜ kubectl cluster-info | grep 'Kubernetes master' | awk '/http/ {print $NF}'
https://xxxxxx.k8s.ondigitalocean.com

获取命令返回的URL并将其粘贴到API URL字段中。

可以通过从创建GitLab帐户时创建的“secret”中提取数据来获取CA证书和token。 Kubernetes具有`secret`资源的概念,旨在存储敏感信息。 除了设置过程,你还可以创建自己的secret来存储应用程序敏感信息,如数据库凭据,API密钥等。

列出项目运行中的所有secret:
➜ kubectl get secrets                                                                                                                            
NAME TYPE DATA
default-token-xfxg9 kubernetes.io/service-account-token 3
gitlab-token-vxhxq kubernetes.io/service-account-token 3

在这里,我们看到群集中有2个secret对象。 我们感兴趣的是名为`gitlab-token-vxhxq`的名字。 找到以`gitlab-token-*`开头的secret,并在下一个命令中使用它:
➜ kubectl get secret  -o jsonpath="{['data']['ca\.crt']}" | base64 --decode
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----

复制并粘贴从命令返回的所有内容,从`----- BEGIN CERTIFICATE -----`开始,以`----- END CERTIFICATE -----`结尾,进入CA Certificate字段。

可以通过执行以下操作以类似的方式获取token:
➜ kubectl get secret  -o jsonpath="{['data']['token']}" | base64 --decode
WlhsS2FHSkhZMmxQYVVwVFZYc...

再一次,将返回的值粘贴到GitLab中的Token字段中。

至于Project Namespace,我把它留空了。 确保选中RBAC启用的群集复选框。 准备好后,继续并单击Add Kubernetes Cluster按钮。
7.png

GitLab - 集群所有的必填字段均已填入
# 设置CI/CD
在GitLab中设置持续集成和持续部署流水线非常简单。它融入了GitLab产品,只需将`.gitlab-ci`文件添加到项目的根目录即可轻松配置。将代码推送到GitLab仓库时会触发CI/CD流水线。流水线必须在服务器上运行,该服务器称为“Runner”。Runner可以是虚拟专用服务器,公共服务器,也可以是安装GitLab Runner客户端的任何地方。在我们的用例中,我们将在Kubernetes群集上安装一个运行器,以便在Pod中执行Job。此客户端还使其可扩展,因此我们可以并行运行多个Job。

要在群集上安装GitLab Runner客户端,我们首先需要安装另一个名为Helm的工具。 Helm是Kubernetes的软件包管理器,简化了软件的安装。我觉得Helm与Brew for Mac类似,他们都有一个可以安装到系统上的软件回购。
##安装Helm
通过Gitlab安装Helm只需要单击“安装”按钮。假设一切都配置正确,安装只需几秒钟。
8.png

在Kubernetes集群上安装helm tiller

完成之后,让我们看看群集,看看GitLab安装了什么。 使用`kubectl get ns`命令,我们可以看到Gitlab已经创建了自己的命名空间,命名为gitlab-managed-apps。
➜ kubectl get ns 
NAME STATUS AGE
default Active 1d
gitlab-managed-apps Active 23s
kube-public Active 1d
kube-system Active 1d

如果我们运行`kubectl get pods`,当未指定命名空间时,默认使用`default`,我们将看不到任何内容。 要查看GitLab命名空间中的Pod,请运行`kubectl get pods -n gitlab-managed-apps`。
➜ kubectl get pods -n gitlab-managed-apps
NAME READY STATUS RESTARTS AGE
tiller-deploy-7dd47f89cc-27cmt 1/1 Running 0 5m

在这里,我们看到“Helm tiller”已成功创建并正在运行。
##安装GitLab Runner
如前所述,GitLab运行程序允许我们的CI/CD Job在Kubernetes集群中运行。 使用GitLab安装Runner很简单,只需单击“安装”按钮即可。
9.png

GitLab - 在Kubernetes集群上安装运行器

我的群集花了大约1分钟。 完成之后,让我们再看看Pod,你应该看到GitLab Runner的新Pod。
➜ kubectl get pods -n gitlab-managed-apps 
NAME READY STATUS RESTARTS
runner-gitlab-runner-5cffc648d7-xr9rq 1/1 Running 0
tiller-deploy-7dd47f89cc-27cmt 1/1 Running 0

你可以通过查看GitLab中的设置➜CI/CD➜Runner部分来验证Runner是否已连接到你的项目。
10.png

GitLab - Kubernetes运行器已在本项目激活
##运行流水线
很好,所以现在我们有一个功能齐全的GitLab项目,连接到Kubernetes,Runner准备执行我们的CI/CD流水线。 让我们设置一个示例Golang项目,看看如何触发这些流水线。 对于这个项目,我们将运行一个简单的HTTP服务器,返回经典的“Hello World”。

首先,写下Go代码:
# main.go
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc(
"/hello",
func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World!")
},
)
log.Fatal(http.ListenAndServe(":8080", nil))
}

然后是运行的Dockerfile:
# Dockerfile
FROM golang:1.11
WORKDIR /go/src/app
COPY . .
RUN go get -d -v ./...
RUN go install -v ./...
CMD ["app"]

接下来,我们将创建一个.gitlab-ci.yml文件来定义我们的CI/CD流水线。该文件将在每次代码推送时进行评估,如果分支或标签与任何Job匹配,它们将由我们之前配置的GitLab运行程序之一自动执行。

我们流水线的第一步是在我们推送到主分支时创建应用程序的Docker镜像。 我们可以使用以下配置执行此操作:
# Gitlab CI Definition (.gitlab-ci.yml)
stages:
- build
- deploy
services:
- docker:dind
variables:
DOCKER_HOST: tcp://localhost:2375
build_app:
image: docker:latest
stage: build
only:
- master
script:
- docker build -t ${CI_REGISTRY}/${CI_PROJECT_PATH}:${CI_COMMIT_REF_NAME} .
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker push ${CI_REGISTRY}/${CI_PROJECT_PATH}:${CI_COMMIT_REF_NAME}

让我们浏览文件中的每个块。`stages:` 块定义流水线中阶段的顺序。我们只有2个阶段,构建然后部署。

`services:` 块包括官方Docker-in-Docker(或dind)镜像,将在所有Job中链接。我们需要这个,因为我们将成为GitLab CI Docker容器内的应用程序Docker容器。

接下来我们有`build_app:` Job。这个名字由我们的项目组成,可以是你想要的任何名称。`image`表明我们正在使用Docker Hub中的最新Docker镜像。`stage`告诉GitLab这个Job处于什么阶段。要记住的一件好事是,同一阶段的Job将并行运行。`only:`标签表示我们只会在提交到`master`分支时运行此Job。最后,`scrips:`是Job的核心,它将运行`docker build`命令来创建我们的镜像,然后`docker login`到GitLab注册表,然后`docker push`将该镜像推送到我们的注册表。

此时我们可以提交并推送代码,你应该在GitLab注册表中看到一个全新的镜像。

在构建镜像并将其保存在注册表上之后,下一步是部署它。我们需要定义部署配置,告诉Kubernetes我们想要如何运行应用程序。以下yaml文件正是如此:
# Deployment Configuration (deployment-template.yaml)
apiVersion: apps/v1
kind: Deployment
metadata:
name: example-deployment
labels:
app: example
spec:
replicas: 1
selector:
matchLabels:
app: example
template:
metadata:
labels:
app: example
spec:
containers:
- name: example
image: registry.gitlab.com/thisiskj/example:latest
ports:
- containerPort: 8080

此文件定义了一个部署,其中包含一个将从项目运行镜像的单个副本(registry.gitlab.com/thisiskj/example:latest)。

为了触发部署,我已经配置了.gitlab-ci.yml文件,以便在标记代码repo时执行此操作。 这是Job定义:
deploy_app:
image: thisiskj/kubectl-envsubst
stage: deploy
environment: production
only:
- tags
script:
- envsubst \$CI_COMMIT_TAG < deployment-template.yaml > deployment.yaml
- kubectl apply -f deployment.yaml

此Job将运行envsubst命令,以使用触发构建的Git标签的名称替换deployment-template.yaml中的$ CI_COMMIT_TAG变量。 环境变量$CI_COMMIT_TAG由GitLab运行器设置,我们告诉envsubst基本上搜索并替换文件中的变量。
##查看应用程序
此时所有内容都已连线,我们的部署将在每个新标签上运行。

我们可以看到正在运行的Pod:
➜  kubectl -n example-10311640 get pods
NAME READY STATUS RESTARTS
example-deployment-756c8f6dc5-jk85w 1/1 Running 0

现在,Pod正在运行,但我们无法从外部访问Golang HTTP服务。 为了允许外部访问,我们可以创建LoadBalancer类型的服务。 将以下规范添加到部署yaml以在DigitalOcean上创建LoadBalancer。
---
kind: Service
apiVersion: v1
metadata:
name: example-loadbalancer-service
spec:
selector:
app: example
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: LoadBalancer

在下一次部署中,我们可以监视LoadBalancer的创建。 外部IP可能需要几分钟才能显示。
➜  kubectl -n example-10311640 get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
example-loadbalancer-service LoadBalancer 10.245.40.9 80:30897/TCP 4s
...
➜ kubectl -n example-10311640 get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
example-loadbalancer-service LoadBalancer 10.245.40.9 157.230.64.204 80:30897/TCP 2m9s

我们还可以在DigitialOcean控制台中监控负载均衡器的创建:
11.png

DigitalOcean — 网络控制台

最后,我们可以通过导航到浏览器中的IP地址来查看我们的应用程序:
12.png

服务开始运行
#总结一下
我希望你发现此演练有助于你设置自己的群集和现代DevOps工作流程。

你可以在https://gitlab.com/thisiskj/example上查看与该项目相关的所有代码。

如果你有任何其他方法可以将GitLab CI/CD与Kubernetes部署集成,请随时分享评论。

原文链接:Lets Explore Kubernetes(翻译:池剑锋)

知乎部署系统演进

JetLee 发表了文章 • 0 个评论 • 1268 次浏览 • 2019-03-28 15:44 • 来自相关话题

应用部署是软件开发中重要的一环,保持快速迭代、持续部署,减少变更和试错成本,对于互联网公司尤为重要。本文将从部署系统的角度,介绍知乎应用平台从无到有的演进过程,希望可以对大家有所参考和帮助。 知乎部署系统由知乎工程效率团队打造,服务于 ...查看全部
应用部署是软件开发中重要的一环,保持快速迭代、持续部署,减少变更和试错成本,对于互联网公司尤为重要。本文将从部署系统的角度,介绍知乎应用平台从无到有的演进过程,希望可以对大家有所参考和帮助。

知乎部署系统由知乎工程效率团队打造,服务于公司几乎所有业务,每日部署次数在 2000 次左右,在启用蓝绿部署的情况下,大部分业务的生产环境上线时间可以在 10 秒以下(不包含金丝雀灰度验证过程)。

目前知乎部署系统主要实现了以下功能:

* 支持容器、物理机部署,支持在线、离线服务、定时任务以及静态文件的部署
* 支持办公网络预上线
* 支持金丝雀灰度验证,期间支持故障检测以及自动回滚
* 支持蓝绿部署,在蓝绿部署情况下,上线和回滚时间均在秒级
* 支持部署 Merge Request 阶段的代码,用于调试

下文将按时间顺序,对部署系统的功能演进进行介绍。
#技术背景
在介绍部署系统之前,首先需要对知乎的相关基础设施和网络情况进行简单的介绍。
##知乎网络情况
知乎的网络如图所示:
1.png

知乎网络环境简图

主要划分为三个部分:

* 生产环境网络:即知乎对外的在线服务器网络,基于安全性考虑,与其他网络环境完全隔离。
* 测试环境网络:应用在部署到生产环境之前,首先会部署在测试环境,测试环境网络上与生产环境完全隔离。
* 办公室网络:即知乎员工内部网络,可以直接访问测试环境,也可以通过跳板机访问生产环境服务器。

##流量管理
知乎采用 Nginx + HAProxy 的方式管理应用的流量走向:
2.png

知乎在线业务流量架构

应用开发者在 Nginx 平台上配置好 Location 和 HAProxy 的对应关系,再由 HAProxy 将流量分发到 Real Server 上去,同时 HAProxy 承担了负载均衡、限速、熔断等功能。
##持续集成
知乎采用 Jenkins + Docker 进行持续集成,详见《知乎容器化构建系统设计和实践》,持续集成完成后,会生成 Artifact,供部署系统以及其他系统使用。
#物理机部署
像大多数公司一样,知乎最开始是以物理机部署为主,业务自行编写脚本进行部署,部署时间长、风险大、难以回滚。在这种情况下,大约在 2015 年,初版的部署系统 nami (取名自《海贼王》娜美)诞生了。

最初的部署系统采用 Fabric 作为基础,将 CI 产生的 Artifact 上传到物理机上解压,并使用 Supervisor 进行进程管理,将服务启动起来:
3.png

物理机部署

初版的部署系统虽然简单,但是为了之后的改进奠定了基础,很多基础的概念,一直到现在还在使用。
##应用(App)与服务(Unit)
与 CI 相同,每个应用对应一个 GitLab Repo,这个很好理解。

但是在实际使用过程中,我们发现,同一套代码,往往对应着多个运行时的服务,比如以部署系统 nami 本身为例,虽然是同一套代码,但是在启动的时候,又要分为:

* API 服务
* 定时任务
* Celery 离线队列

这些运行单元的启动命令、参数等各不相同,我们称之为服务(Unit)。用户需要在部署系统的前端界面上,为每个 Unit 进行启动参数、环境变量等设置,整个应用才能正常启动起来。
##候选版本(Candidate)
所有的部署都是以 CI 产生 Artifact 作为基础,由于 Artifact 的不可变性,每次部署该 Artifact 的结果都是可预期的。也就是说,每个 Artifact 都是代码的一次快照,我们称之为部署的候选版本( Candidate)。

由于每次 CI 都是由 GitLab 的 Merge Request 产生,候选版本,其实就是一次 MR 的结果,也就是一次代码变更。通常情况下,一个候选版本对应一个 Merge Request:
4.png

每个候选版本对应一个 Merge Request

如图所示是某个应用的候选版本列表,每个候选版本,用户都可以将其部署到多个部署阶段(Stage)。
##部署阶段(Stage)
上文提到,知乎服务器网络分为测试环境和生产环境,网络之间完全隔离。应用总是先部署测试环境,成功后再部署生产环境。

在部署系统上,我们的做法是,对每个候选版本的部署,拆分成多个阶段(Stage):
5.png

构建/部署阶段

图中该应用有 6 个阶段:

* (B)构建阶段:即 CI 生成 Artifact 的过程。
* (T)测试环境:网络、数据都与生产环境相隔离。
* (O)办公室阶段:一个独立的容器,只有办公室网络可以访问,其他与线上环境相同,数据与生产环境共享。
* (C)金丝雀1:生产环境 1% 的容器,外网可访问。
* (C)金丝雀2:生产环境 20% 的容器,外网可访问。
* (P)生产环境:生产环境 100% 容器,外网可访问。

部署阶段从前到后依次进行,每个 Stage 的部署逻辑大致相同。

对于每个部署阶段,用户可以单独设置,是否进行自动部署。如果所有部署阶段都选择自动部署,那么应用就处于一个持续部署(Continuous Deployment)的过程。
##基于 Consul 和 HAProxy 的服务注册与发现
每次部署物理机时,都会先将机器从 Consul 上摘除,当部署完成后,重新注册到 Consul 上。

上文提到,我们通过 HAProxy 连接到 Real Server,原理就是基于 Consul Template 对 HAProxy 的配置进行更新,使其总是指向所有 RS 列表。

另外,在迁移到微服务架构之后,我们编写了一个称为 diplomat 的基础库,从 Consul 上拉取 RS 列表,用于 RPC 以及其他场景的服务发现。
#容器部署
##旧版容器系统 Bay
2015 年末,随着容器大潮的袭来,知乎也进入容器时代,我们基于 Mesos 做了初版的容器编排系统(称为 Bay),部署系统也很快支持了容器的部署。

Bay 的部署很简单,每个 Unit 对应一个容器组,用户可以手动设置容器组的数量和其他参数。每次部署的时候,滚动地上线新版本容器,下线旧版本容器,部署完成后所有旧版本容器就都已回收。对于一些拥有数百容器的大容器组,每次部署时间最长最长可以达到 18 分钟。
##各项功能完善
在迁移到容器部署的过程中,我们对部署系统也进行了其他方面的完善。

首先是健康检查,所有 HTTP、RPC 服务,都需要实现一个 /check_health 接口,在部署完成后会对其进行检查,当其 HTTP Code 为 200 时,部署才算成功,否则就会报错。

其次是在线/离线服务的拆分,对于 HTTP、RPC 等在线业务,采用滚动部署;对于其他业务,则是先启动全量新版本容器,再下线旧版本容器。
#预上线与灰度发布
基于容器,我们可以更灵活地增删 Real Server,这使得我们可以更简单地将流量拆分到不同候选版本的容器组中去,利用这一点,我们实现了办公室网络预上线和金丝雀灰度发布。
##办公室网络预上线
为了验证知乎主站的变更,我们决定在办公室网络,提前访问已经合并到主干分支、但还没有上线的代码。我们在 Nginx 层面做了流量拆分,当访问源是办公室网络的时候,流量流向办公室专属的 HAProxy:
6.png

办公室流量拆分

对于部署系统来说,所需要做的就是在「生产环境」这个 Stage 之前,加入一个「办公室」Stage,对于这个 Stage,只部署一个容器,并将这个容器注册到办公室专属的 HAProxy,从外网无法访问此容器。
##金丝雀灰度发布
在 2016 年以前,知乎部署都是全量上线,新的变更直接全量上线到外网,一旦出现问题,很可能导致整个网站宕机。

为了解决这个问题,我们在「办公室」和「生产环境」Stage 之间,加入了「金丝雀1」和「金丝雀2」两个 Stage,用于灰度验证。

原理是,部署一定量额外的新版本容器,通过 HAProxy,随机分发流量到这些新版本容器上,这样如果新版本代码存在问题,可以在指标系统上明显看出问题:
7.png

Nginx 指标大盘

其中,「金丝雀1」阶段只启动相当于「生产环境」阶段 1% 的容器,「金丝雀2」阶段则启动 20% 数量的容器。

为了避免每次部署到金丝雀后,都依赖人工去观察指标系统,我们在部署系统上,又开发了「金丝雀自动回滚」功能。主要原理是:

* 将金丝雀阶段的指标与生产环境的指标分离
* 金丝雀部署完成后,对指标进行检测,与生产环境进行对比,如果发现异常,则销毁金丝雀容器,并通知用户
* 如果在 6 分钟内没有发现指标异常,则认为代码没有明显问题,才允许用户部署「生产环境」Stage

8.png

金丝雀出现异常,回滚时会自动通知开发者

金丝雀阶段自动监测的指标包括该应用的错误数、响应时间、数据库慢查询数量、Sentry 报错数量、移动端 App 崩溃数量等等。
#新版容器部署
针对旧版容器系统 Bay 部署速度慢、稳定性差等问题,我们将容器编排从 Mesos 切换到 Kubernetes,在此基础上开发出新一代的容器系统 NewBay。

相应地,部署系统也针对 NewBay 进行了一番改造,使得其在功能、速度上均有明显提升。
##蓝绿部署
在旧版 Bay 中,每个 Unit 对应唯一的容器组,新版本容器会覆盖旧版本容器,这会导致:

* 一旦部署失败,服务将处于中间状态,新旧版本会同时在线
* 回滚旧版本代码速度较慢,而且有可能会失败

我们设计了一套新的部署逻辑,实现了蓝绿部署,即新旧版本容器组同时存在,使用 HAProxy 做流量切换:
9.png

蓝绿部署可以有效减少回滚时间

这使得:

* 流量的切换原子化,即使部署失败也不会存在新旧版本同时在线的情况
* 由于旧版本容器组会保留一段时间,这期间回滚代码仅需要将流量切回旧版本,回滚时间可以达到秒级

##预部署
使用 NewBay 之后,大型项目的部署时间由原来的 18 分钟降至 3 分钟左右,但这其中仍有优化的空间。

为了加快部署速度,我们会在金丝雀阶段,提前将「生产环境」Stage 所需要的全量容器异步地启动起来,这样在部署「生产环境」Stage 时,仅需要将流量切换为全量即可:
10.png

预部署可以有效减少上线时间

通过这方面的优化,在全量上线到生产环境时,上线时间同样可以达到秒级。
#分支部署
以上部署均是针对代码合并到主干分支后进行的部署操作,或者可以称之为「上线流程」。

但是实际上很多情况下,我们的代码在 Merge Request 阶段就需要进行部署,以方便开发者进行自测,或者交由 QA 团队测试。

我们在 CI/CD 层面对这种情况进行了支持,主要原理是在 MR 提交或者变更的时候就触发 CI/CD,将其部署到单独的容器上,方便开发者进行访问。
11.png

多个 Merge Request 同时部署和调试

分支部署实现细节较多,篇幅所限,在此不进行展开。
#部署系统平台化
为了方便用户使用 CI/CD,管理应用资源,处理排查故障等,我们将整套知乎的开发流程进行了平台化,最终实现了 ZAE(Zhihu App Engine):
12.png

ZAE 是一套完整的开发者平台

用户可以方便地查看部署进度和日志,并进行一些常规操作:
13.png

在 ZAE 上查看部署进度
#尾声
知乎部署系统从 2015 年开始开发,到目前为止,功能上已经比较成熟。其实,部署系统所承担的责任不仅仅是上线这么简单,而是关系到应用从开发、上线到维护的方方面面。良好的部署系统,可以加快业务迭代速度、规避故障发生,进而影响到一家公司的产品发布节奏。

原文链接:https://zhuanlan.zhihu.com/p/60627311

谷歌开源Kubernetes原生CI/CD构建框架Tekton

尼古拉斯 发表了文章 • 0 个评论 • 535 次浏览 • 2019-03-25 16:54 • 来自相关话题

Tekton 是一个功能强大且灵活的 Kubernetes 原生框架,用于创建 CI/CD 系统。通过抽象出底层实现细节,允许开发者跨多云环境或本地系统进行构建、测试与部署。 特性包括: * 工件管理:存储、 ...查看全部
Tekton 是一个功能强大且灵活的 Kubernetes 原生框架,用于创建 CI/CD 系统。通过抽象出底层实现细节,允许开发者跨多云环境或本地系统进行构建、测试与部署。

特性包括:

* 工件管理:存储、管理和保护工件,同时 Tetkon 管道可以很好地与其它第三方工具相配合。
* 部署管道:部署管道旨在支持复杂的工作流程,包括跨多个环境的部署以及金丝雀部署和蓝/绿部署。
* 结果:作为内置结果存储 API 的一部分,通过日志可以深入了解测试与构建结果。

详情查看 https://cloud.google.com/tekton

怎样在Kubernetes中实现CI/CD的发布流程,大家都贡献出来一下吧!

Twilight 回复了问题 • 2 人关注 • 1 个回复 • 894 次浏览 • 2019-03-25 11:50 • 来自相关话题

21 个好用的持续集成工具

YiGagyeong 发表了文章 • 0 个评论 • 1734 次浏览 • 2019-03-10 18:20 • 来自相关话题

【编者的话】好用的集成工具都在这儿了,总有一款适合你。 市场上持续集成工具众多,找到一个合适的工具并非易事,下面介绍了 21 个比较受欢迎的 CI 工具,并附上了下载链接。 # 1. Buddy 对 Web 开 ...查看全部
【编者的话】好用的集成工具都在这儿了,总有一款适合你。

市场上持续集成工具众多,找到一个合适的工具并非易事,下面介绍了 21 个比较受欢迎的 CI 工具,并附上了下载链接。
# 1. Buddy
对 Web 开发者来说,Buddy 是一个智能的 CI/CD 工具,降低了 DevOps 的入门门槛。Buddy 使用 `Delivery Pipeline` 进去软件构建、测试及发布,创建 Pipeline 时,100 多个就绪的操作可随时投入使用,就像砌砖房一样。

特点:

  • 清晰的配置,友好的交互,15分钟快速配置
  • 基于变更集(changeset)的快速部署
  • 构建运行在使用缓存依赖的独立容器中
  • 支持所有流行的语言、框架和任务管理器
  • Docker / Kubernetes 专用操作手册
  • 与 AWS,Google,DigitalOcean,Azure,Shopify,WordPress 等集成
  • 支持并行和 YAML 配置
下载链接:https://buddy.works# 2. JenkinsJenkins 是一个开源的持续集成工具,使用 Java 编程语言编写的。它有助于实时检测和报告较大代码库中的单一更改。该软件可帮助开发人员快速查找和解决代码库中的问题并自动测试其构建。特点:
  • 支持海量节点扩展并在节点中同等分发工作负载
  • 在各版本Linux、Mac OS 或 Windows 等全平台轻松更新
  • 提供了 WAR 格式的简易安装包,执行导入 JEE 容器中即可运行安装
  • 可以通过 Web 界面轻松设置和配置 Jenkins
  • 可轻松跨机器分发
下载链接:https://jenkins.io/download/# 3. TeamCityTeamCity 是一款拥有很多强大功能的持续集成服务器。特点:
  • 可扩展性和自定义
  • 为项目提供更好的代码质量
  • 即使没有运行构建,也能保持 CI 服务器健康稳定
  • 可在 DSL 中配置构建
  • 项目级云配置文件
  • 全面的 VCS 集成
  • 即时构建进度报告
  • 远程运行和预先测试的提交
下载链接:https://www.jetbrains.com/teamcity/download/#section=windows# 4. Travis CITravis 是一款流行的 CI 工具,可免费用于开源项目。在托管时,不必依赖任何平台。此 CI 工具为许多构建配置和语言提供支持,如 Node,PHP,Python,Java,Perl 等。特点:
  • Travis 使用虚拟机构建应用程序
  • 可通过 Slack,HipChat,电子邮件等通知
  • 允许运行并行测试
  • 支持 Linux、Mac 以及 iOS
  • 易于配置,无需安装。
  • 强大的 API 和命令行工具
下载链接:https://github.com/travis-ci/travis-ci# 5. GoCDGoCD 是一个开源的持续集成服务器。它可轻松模拟和可视化复杂的工作流程。此 CI 工具允许持续交付,并为构建 CD Pipeline 提供直观的界面。特点:
  • 支持并行和顺序执行,可以轻松配置依赖
  • 随时部署任何版本
  • 使用 Value Stream Map 实时可视化端到端工作流程
  • 安全地部署到生产环境
  • 支持用户身份验证和授权
  • 保持配置有序
  • 有大量的插件增强功能
  • 活跃的社区帮助和支持
下载链接:https://www.gocd.org/download/# 6. BambooBamboo 是一个持续集成的构建服务器,可以自动构建、测试和发布,并可与 JIRA 和 Bitbucket 无缝协作。Bamboo 支持多语言和平台,如 CodeDeply、Ducker、Git,SVN、Mercurial、AWS 及 Amazon S3 bucket。特点:
  • 可并行运行批量测试
  • 配置简单
  • 分环境权限功能允许开发人员和 QA 部署到他们的环境
  • 可以根据 repository 中检测到的更改触发构建,并从 Bitbucket 推送通知
  • 可托管或内部部署
  • 促进实时协作并与 HipChat 集成
  • 内置 Git 分支和工作流程,并自动合并分支
下载链接:https://www.atlassian.com/software/bamboo# 7. Gitlab CIGitLab CI 是 GitLab 的一部分。它是一个提供 API 的 Web 应用程序,可将其状态存储在数据库中。GitLab CI 可以管理项目并提供友好的用户界面,并充分利用 GitLab 所有功能。特点:
  • GitLab Container Registry 是安全的 Docker 镜像注册表
  • GitLab 提供了一种方便的方法来更改 issue 或 merge request 的元数据,而无需在注释字段中添加斜杠命令
  • 为大多数功能提供 API,允许开发人员进行更深入的集成
  • 通过发现开发过程中的改进领域,帮助开发人员将他们的想法投入生产
  • 可以通过机密问题保护您的信息安全
  • GitLab 中的内部项目允许促进内部存储库的内部 sourcing
下载链接:https://about.gitlab.com/installation/# 8. CircleCICircle CI 是一个灵活的 CI 工具,可在任何环境中运行,如跨平台移动应用程序、Python API 服务器或 Docker 集群,该工具可减少错误并提高应用程序的质量。特点:
  • 允许选择构建环境
  • 支持多语言及平台,如Linux,包括C ++,Javascript,NET,PHP,Python 和 Ruby
  • 支持 Docker,可以配置自定义环境
  • 触发较新的构建时,自动取消排队或正在运行的构建
  • 跨多容器分割和平衡测试,以减少总体构建时间
  • 禁止非管理员修改关键项目配置
  • 通过发送无错误的应用程序提高 Android 和 iOS 商店评级
  • 最佳缓存和并行性能,实现高性能
  • 与 VCS 工具集成
下载链接:https://circleci.com/# 9. CodeshipCodeship 是一个功能强大的 CI 工具,可自动化开发和部署工作流程。Codeship 通过简化到 repository 的 push 来触发自动化工作流程。特点:
  • 可完全控制 CI 和 CD 系统的设计。
  • 集中的团队管理和仪表板
  • 轻松访问调试版本和 SSH,有助于从 CI 环境进行调试
  • 可完全定制和优化 CI 和 CD 工作流程
  • 允许加密外部缓存的 Docker 镜像
  • 允许为您的组织和团队成员设置团队和权限
  • 有两个版本1)Basic 和 2)Pro
下载链接:https://codeship.com/# 10. BuildbotBuildbot 是一个软件开发 CI,可以自动完成编译/测试周期。它被广泛用于许多软件项目,用以验证代码更改。它提供跨平台 Job 的分布式并行执行。特点:
  • 为不同体系结构的多个测试主机提供支持。
  • 报告主机的内核崩溃
  • 维护单源 repository
  • 自动化构建
  • 每个提交都在集成机器上的主线上构建
  • 自动部署
  • 开源
下载链接:https://buildbot.net/# 11. NevercodeNevercode 是一个基于云端的 CI 传送服务器,可以构建、测试和分发应用程序而无需人工交互。此 CI 工具自动为每个提交构建项目,并在模拟器或真实硬件上运行所有单元测试 或 UI 测试。特点:
  • 基于云服务,因此无需维护服务器
  • 易于学习和使用
  • 良好的文档,易于阅读和理解
  • 通过持续集成和交付自动化整个开发过程
  • 与众多工具集成
下载链接:https://nevercode.io/# 12. IntegrityIntegrity 是一个持续集成服务器,仅适用于 GitHub。在此 CI 工具中,只要用户提交代码,它就构建并运行代码。它还会生成报告并向用户提供通知。 特点:
  • 目前仅适用于 Git,但它可以轻松地映射其他 SCM
  • 支持多通知机制,如 AMQP,电子邮件,HTTP,Amazon SES,Flowdock,Shell 和 TCP
  • HTTP 通告功能将以 HTTP POST 请求发送到特定URL
下载链接:http://integrity.github.io/# 13. StriderStrider 是一个开源工具,用 Node.JS / JavaScript 编写。它使用 MongoDB 作为后端存储。因此,MongoDB 和 Node.js 对于安装此 CI 至关重要。该工具为不同的插件提供支持,这些插件可修改数据库 schema 并注册HTTP路由。特点:
  • Strider 可与 GitHub,BitBucket,Gitlab 等集成。
  • 允许添加钩子来执行构建操作
  • 持续构建和测试软件项目
  • 与 GitHub 无缝集成
  • 发布和订阅 socket 事件
  • 支持创建和修改 Striders 用户界面
  • 强大的插件,定制默认功能
  • 支持 Docker
下载链接:https://github.com/Strider-CD/strider# 14. AutoRABITAutoRABIT 是一个端到端的持续交付套件,可以加快开发过程。它简化了完整的发布流程,并可以帮助任何规模的组织实现持续集成。特点:
  • 专门设计用于在 Salesforce Platform 上部署
  • 支持基于 120 多种元数据类型的更改,实现精简和快速部署
  • 从版本控制系统获取更改并自动部署到 Sandbox 中
  • 直接从 Sandbox 自动向版本控制系统提交更改
下载链接:http://www.autorabit.com/tag/autorabit-download/# 15. FinalBuilderFinalBuilder 是 VSoft 的构建工具。使用 FinalBuilder,无需编辑 XML 或编写脚本。在使用 Windows 调度程序调度构建脚本时,可以定义和调试构建脚本,或者与 Jenkins,Continua CI 等集成。特点:
  • 以逻辑结构化的图形界面呈现构建过程
  • 使用 try 和 catch 操作处理本地错误
  • 与 Windows 调度服务紧密集成,支持定时构建
  • 支持十几个版本控制系统
  • 提供脚本支持
  • 构建过程中所有操作的输出都将定向到构建日志
下载链接:https://www.finalbuilder.com/downloads/finalbuilde# 16. WerckerWercker 是一个 CI 工具,可自动构建和部署容器。它可以创建可以通过命令行界面执行的自动化管道。特点:
  • 与 GitHub 和 Bitbucket 完全集成
  • 使用 Wercker CLI 进行更快的本地迭代
  • 同时执行构建以保持团队的机动
  • 运行并行测试以减少团队的等待时间
  • 集成了 100 多种外部工具
  • 通过产品和电子邮件接收系统通知
下载链接:http://www.wercker.com/# 17. BuildkiteBuildkite 代理是一个可靠的跨平台构建工具。此 CI 工具可以在础架构上轻松地运行自动构建。它主要用于运行构建 Job,报告 Job 的状态代码并输出日志。特点:
  • 可在各种操作系统和体系结构上运行
  • 可以从任何版本控制系统运行代码
  • 允许在计算机上运行任意数量的构建代理
  • 可与 Slack,HipChat,Flowdock,Campfire 等工具集成
  • 永远不会读取源代码或密钥
  • 提供稳定的基础设施
下载链接:https://buildkite.com/# 18. SemaphoreSemaphore 是一个持续集成工具,只需按一下按钮即可测试和部署代码。它支持多种语言、框架并可与 GitHub 集成,还可以执行自动测试和部署。特点:
  • 配置简单
  • 允许自动并行测试
  • 市场上最快的 CI 之一
  • 可以轻松覆盖不同大小的项目数量
  • 与 GitHub 和 Bitbucket 无缝集成
下载链接:https://semaphoreci.com# 19. CruiseControlCruiseControl 既是 CI 工具又是一个可扩展的框架。它用于构建自定义连续的构建。它有许多用于各种源代码控制的插件,包括针对电子邮件和即时消息的构建技术。特点:
  • 与许多不同的源代码控制系统集成,如 vss,csv,svn,git,hg,perforce,clearcase,filesystem 等。
  • 允许在单个服务器上构建多个项目
  • 与其他外部工具集成,如 NAnt,NDepend,NUnit,MSBuild,MBUnit 和 Visual Studio
  • 支持远程管理
下载链接:http://cruisecontrol.sourceforge.net/download.html# 20. BitriseBitrise 是一个持续集成和交付 PaaS,它可以为整个团队提供移动持续集成和交付。它允许与 Slack,HipChat,HockeyApp,Crashlytics 等许多流行服务集成。特点:
  • 允许在终端中创建和测试工作流程
  • 无需手动控制即可获得应用程序
  • 每个构建在其自己的虚拟机中单独运行,并且在构建结束时丢弃所有数据
  • 支持第三方 beta 测试和部署服务
  • 支持 GitHub Pull Request
下载链接:https://github.com/bitrise-io/bitrise#install-and-setup# 21. UrbanCodeIBM UrbanCode 是一个 CI 应用程序。它将强大的可见性,可追溯性和审计功能整合到一个软件包中。特点:
  • 通过自动化,可重复的部署流程提高软件交付频率
  • 减少部署失败
  • 简化多渠道应用程序的部署,无论是在本地还是在云中,都可以部署到所有环境
  • 企业级安全性和可扩展性
  • 混合云环境建模
  • 拖放自动化

下载链接:https://www.ibm.com/ms-en/marketplace/application-release-automation

原文链接:20 Best Continuous Integration(CI) Tools in 2019 (翻译:李加庆

GitLab CI/CD 在 Node.js 项目中的实践

尼古拉斯 发表了文章 • 0 个评论 • 179 次浏览 • 2019-06-03 09:58 • 来自相关话题

【编者的话】近期在按照业务划分项目时,我们组被分了好多的项目过来,大量的是基于 Node.js 的,也是我们组持续在使用的语言。 #现有流程中的一些问题 在维护多个项目的时候,会暴露出一些问题: 如何有效的使用 ...查看全部
【编者的话】近期在按照业务划分项目时,我们组被分了好多的项目过来,大量的是基于 Node.js 的,也是我们组持续在使用的语言。
#现有流程中的一些问题
在维护多个项目的时候,会暴露出一些问题:

  1. 如何有效的使用 测试用例
  2. 如何有效的使用 ESLint
  3. 部署上线还能再快一些吗

* 使用了 TypeScript 以后带来的额外成本

##测试用例
首先是测试用例,最初我们设计在了 git hooks 里边,在执行 git commit 之前会进行检查,在本地运行测试用例。 如果你想和更多 GitLab 技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态。

这会带来一个时间上的问题,如果是日常开发,这么操作还是没什么问题的,但如果是线上 bug 修复,执行测试用例的时间依据项目大小可能会持续几分钟。

而为了修复 bug,可能会采用 commit 的时候添加 -n 选项来跳过 hooks,在修复 bug 时这么做无可厚非,但是即使大家在日常开发中都采用commit -n 的方式来跳过繁琐的测试过程,这个也是没有办法管控的,毕竟是在本地做的这个校验,是否遵循这个规则,全靠大家自觉。

所以一段时间后发现,通过这种方式执行测试用例来规避一些风险的作用可能并不是很有效。
##ESLint
然后就是 ESLint,我们团队基于airbnb的 ESLint 规则自定义了一套更符合团队习惯的规则,我们会在编辑器中引入插件用来帮助高亮一些错误,以及进行一些自动格式化的操作。

同时我们也在 git hooks 中添加了对应的处理,也是在 git commit 的时候进行检查,如果不符合规范则不允许提交。

不过这个与测试用例是相同的问题:

  1. 编辑器是否安装 ESLint 插件无从得知,即使安装插件、是否人肉忽略错误提示也无从得知。
  2. git hooks 可以被绕过

##部署上线的方式
之前团队的部署上线是使用shipit周边套件进行部署的。

部署环境强依赖本地,因为需要在本地建立仓库的临时目录,并经过多次ssh XXX "command"的方式完成部署 + 上线的操作。

shipit提供了一个有效的回滚方案,就是在部署后的路径添加多个历史部署版本的记录,回滚时将当前运行的项目目录指向之前的某个版本即可。(不过有一点儿坑的是,很难去选择我要回滚到那个节点,以及保存历史记录需要占用额外的磁盘空间)

不过正因为如此,shipit在部署多台服务器时会遇到一些令人不太舒服的地方。

如果是多台新增的服务器,那么可以通过在shipit配置文件中传入多个目标服务器地址来进行批量部署。

但是假设某天需要上线一些小流量(比如四台机器中的一台),因为前边提到的shipit回滚策略,这会导致单台机器与其他三台机器的历史版本时间戳不一致(因为这几台机器不是同一时间上线的)

了这个时间戳就另外提一嘴,这个时间戳的生成是基于执行上线操作的那台机器的本地时间,之前有遇到过同事在本地测试代码,将时间调整为了几天前的时间,后时间没有改回正确的时间时进行了一次部署操作,代码出现问题后却发现回滚失败了,原因是该同事部署的版本时间戳太小,shipit 找不到之前的版本(shipit 可以设置保留历史版本的数量,当时最早的一次时间戳也是大于本次出问题的时间戳的)



也就是说,哪怕有一次进行过小流量上线,那么以后就用不了批量上线的功能了 (没有去仔细研究shipit官方文档,不知道会不会有类似--force之类的忽略历史版本的操作)

基于上述的情况,我们的部署上线耗时变为了: (__机器数量__)X(__基于本地网速的仓库克隆、多次 ssh 操作的耗时总和__)。 P.S. 为了保证仓库的有效性,每次执行 shipit 部署,它都会删除之前的副本,重新克隆

尤其是服务端项目,有时紧急的 bug 修复可能是在非工作时间,这意味着可能当时你所处的网络环境并不是很稳定。

我曾经晚上接到过同事的微信,让我帮他上线项目,他家的 Wi-Fi 是某博士的,下载项目依赖的时候出了些问题。

还有过使用移动设备开热点的方式进行上线操作,有一次非前后分离的项目上线后,直接就收到了联通的短信:「您本月流量已超出XXX」(当时还在用合约套餐,一月就800M流量)。
##TypeScript
在去年下半年开始,我们团队就一直在推动 TypeScript 的应用,因为在大型项目中,拥有明确类型的 TypeScript 显然维护性会更高一些。

但是大家都知道的, TypeScript 最终需要编译转换为 JavaScript(也有 tsc 那种的不生成 JS 文件,直接运行,不过这个更多的是在本地开发时使用,线上代码的运行我们还是希望变量越少越好)。

所以之前的上线流程还需要额外的增加一步,编译 TS。

而且因为shipit是在本地克隆的仓库并完成部署的,所以这就意味着我们必须要把生成后的 JS 文件也放入到仓库中,最直观的,从仓库的概览上看着就很丑(50% TS、50% JS),同时这进一步增加了上线的成本。

总结来说,现有的部署上线流程过于依赖本地环境,因为每个人的环境不同,这相当于给部署流程增加了很多不可控因素。
#如何解决这些问题
上边我们所遇到的一些问题,其实可以分为两块:

  1. 有效的约束代码质量
  2. 快速的部署上线

所以我们就开始寻找解决方案,因为我们的源码是使用自建的 GitLab 仓库来进行管理的,首先就找到了 GitLab CI/CD。

在研究了一番文档以后发现,它能够很好的解决我们现在遇到的这些问题。

要使用 GitLab CI/CD 是非常简单的,只需要额外的使用一台服务器安装 gitlab-runner,并将要使用 CI/CD 的项目注册到该服务上就可以了。

GitLab 官方文档中有非常详细的安装注册流程:

install | runner
register | runner
group register | repo 注册 Group 项目时的一些操作

上边的注册选择的是注册 group ,也就是整个 GitLab 某个分组下所有的项目。

主要目的是因为我们这边项目数量太多,单个注册太过繁琐(还要登录到 runner 服务器去执行命令才能够注册)
##安装时需要注意的地方
官网的流程已经很详细了,不过还是有一些地方可以做一些小提示,避免踩坑。
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner

这是 Linux 版本的安装命令,安装需要 root (管理员) 权限,后边跟的两个参数:

* --user 是 CI/CD 执行 job (后续所有的流程都是基于 job 的)时所使用的用户名
* --working-directory 是 CI/CD 执行时的根目录路径 个人的踩坑经验是将目录设置为一个空间大的磁盘上,因为 CI/CD 会生成大量的文件,尤其是如果使用 CI/CD 进行编译 TS 文件并且将其生成后的 JS 文件缓存;这样的操作会导致 innode 不足产生一些问题

--user 的意思就是 CI/CD 执行使用该用户进行执行,所以如果要编写脚本之类的,建议在该用户登录的状态下编写,避免出现无权限执行 sudo su gitlab-runner



##注册时需要注意的地方
在按照官网的流程执行时,我们的 tag 是留空的,暂时没有找到什么用途。

以及 executor 这个比较重要了,因为我们是从手动部署上线还是往这边靠拢的,所以稳妥的方式是一步步来,也就是说我们选择的是 shell ,最常规的一种执行方式,对项目的影响也是比较小的(官网示例给的是 Docker)。
##.gitlab-ci.yml 配置文件
上边的环境已经全部装好了,接下来就是需要让 CI/CD 真正的跑起来
runner 以哪种方式运行,就靠这个配置文件来描述了,按照约定需要将文件放置到 repo 仓库的根路径下。

当该文件存在于仓库中,执行 git push 命令后就会自动按照配置文件中所描述的动作进行执行了。

* quick start
* configuration

上边的两个链接里边信息非常完整,包含各种可以配置的选项。

一般来讲,配置文件的结构是这样的:
stages:
- stage1
- stage2
- stage3

job 1:
stage: stage1
script: echo job1

job 2:
stage: stage2
script: echo job2

job 3:
stage: stage2
script:
- echo job3-1
- echo job3-2

job 4:
stage: stage3
script: echo job4

stages 用来声明有效的可被执行的 stage,按照声明的顺序执行。

下边的那些 job XXX 名字不重要,这个名字是在 GitLab CI/CD Pipeline 界面上展示时使用的,重要的是那个 stage 属性,他用来指定当前的这一块 job 隶属于哪个 stage。

script 则是具体执行的脚本内容,如果要执行多行命令,就像job 3那种写法就好了。

如果我们将上述的 stage、job 之类的换成我们项目中的一些操作install_dependencies、test、eslint之类的,然后将script字段中的值换成类似npx eslint之类的,当你把这个文件推送到远端服务器后,你的项目就已经开始自动运行这些脚本了。

并且可以在Pipelines界面看到每一步执行的状态。

P.S. 默认情况下,上一个 stage 没有执行完时不会执行下一个 stage 的,不过也可以通过额外的配置来修改:

* allow failure
* when

设置仅在特定的情况下触发 CI/CD

上边的配置文件存在一个问题,因为在配置文件中并没有指定哪些分支的提交会触发 CI/CD 流程,所以默认的所有分支上的提交都会触发,这必然不是我们想要的结果。

CI/CD 的执行会占用系统的资源,如果因为一些开发分支的执行影响到了主干分支的执行,这是一件得不偿失的事情。

所以我们需要限定哪些分支才会触发这些流程,也就是要用到了配置中的 only 属性。

使用only可以用来设置哪些情况才会触发 CI/CD,一般我们这边常用的就是用来指定分支,这个是要写在具体的 job 上的,也就是大致是这样的操作:

具体的配置文档
job 1:
stage: stage1
script: echo job1
only:
- master
- dev

单个的配置是可以这样写的,不过如果 job 的数量变多,这么写就意味着我们需要在配置文件中大量的重复这几行代码,也不是一个很好看的事情。

所以这里可能会用到一个yaml的语法:

这是一步可选的操作,只是想在配置文件中减少一些重复代码的出现



.access_branch_template: &access_branch
only:
- master
- dev

job 1:
<<: *access_branch
stage: stage1
script: echo job1

job 2:
<<: *access_branch
stage: stage2
script: echo job2

一个类似模版继承的操作,官方文档中也没有提到,这个只是一个减少冗余代码的方式,可有可无。
##缓存必要的文件
因为默认情况下,CI/CD在执行每一步(job)时都会清理一下当前的工作目录,保证工作目录是干净的、不包含一些之前任务留下的数据、文件。

不过这在我们的 Node.js 项目中就会带来一个问题。

因为我们的 ESLint、单元测试 都是基于 node_modules 下边的各种依赖来执行的。

而目前的情况就相当于我们每一步都需要执行npm install,这显然是一个不必要的浪费。

所以就提到了另一个配置文件中的选项:cache

用来指定某些文件、文件夹是需要被缓存的,而不能清除:
cache:
key: ${CI_BUILD_REF_NAME}
paths:
- node_modules/

大致是这样的一个操作,CI_BUILD_REF_NAME是一个 CI/CD 提供的环境变量,该变量的内容为执行 CI/CD 时所使用的分支名,通过这种方式让两个分支之间的缓存互不影响。
##部署项目
如果基于上边的一些配置,我们将 单元测试、ESLint 对应的脚本放进去,他就已经能够完成我们想要的结果了,如果某一步执行出错,那么任务就会停在那里不会继续向后执行。

不过目前来看,后边已经没有多余的任务供我们执行了,所以是时候将 部署 这一步操作接过来了。

部署的话,我们目前选择的是通过 rsync 来进行同步多台服务器上的数据,一个比较简单高效的部署方式。

P.S. 部署需要额外的做一件事情,就是建立从gitlab runner所在机器gitlab-runner用户到目标部署服务器对应用户下的机器信任关系。

有 N 多种方法可以实现,最简单的就是在runner机器上执行 ssh-copy-id 将公钥写入到目标机器。

或者可以像我一样,提前将 runner 机器的公钥拿出来,需要与机器建立信任关系时就将这个字符串写入到目标机器的配置文件中。

类似这样的操作:ssh 10.0.0.1 "echo \"XXX\" >> ~/.ssh/authorized_keys"
大致的配置如下:
variables:
DEPLOY_TO: /home/XXX/repo # 要部署的目标服务器项目路径
deploy:
stage: deploy
script:
- rsync -e "ssh -o StrictHostKeyChecking=no" -arc --exclude-from="./exclude.list" --delete . 10.0.0.1:$DEPLOY_TO
- ssh 10.0.0.1 "cd $DEPLOY_TO; npm i --only=production"
- ssh 10.0.0.1 "pm2 start $DEPLOY_TO/pm2/$CI_ENVIRONMENT_NAME.json;"

同时用到的还有variables,用来提出一些变量,在下边使用。

`ssh 10.0.0.1 "pm2 start $DEPLOY_TO/pm2/$CI_ENVIRONMENT_NAME.json;"`,这行脚本的用途就是重启服务了,我们使用pm2来管理进程,默认的约定项目路径下的pm2文件夹存放着个个环境启动时所需的参数。

当然了,目前我们在用的没有这么简单,下边会统一提到。

并且在部署的这一步,我们会有一些额外的处理。

这是比较重要的一点,因为我们可能会更想要对上线的时机有主动权,所以 deploy 的任务并不是自动执行的,我们会将其修改为手动操作还会触发,这用到了另一个配置参数:
deploy:
stage: deploy
script: XXX
when: manual # 设置该任务只能通过手动触发的方式运行

当然了,如果不需要,这个移除就好了,比如说我们在测试环境就没有配置这个选项,仅在线上环境使用了这样的操作。
##更方便的管理 CI/CD 流程
如果按照上述的配置文件进行编写,实际上已经有了一个可用的、包含完整流程的 CI/CD 操作了。

不过它的维护性并不是很高,尤其是如果 CI/CD 被应用在多个项目中,想做出某项改动则意味着所有的项目都需要重新修改配置文件并上传到仓库中才能生效。

所以我们选择了一个更灵活的方式,最终我们的 CI/CD 配置文件是大致这样子的(省略了部分不相干的配置):
variables:
SCRIPTS_STORAGE: /home/gitlab-runner/runner-scripts
DEPLOY_TO: /home/XXX/repo # 要部署的目标服务器项目路径

stages:
- install
- test
- build
- deploy_development
- deploy_production

install_dependencies:
stage: install
script: bash $SCRIPTS_STORAGE/install.sh

unit_test:
stage: test
script: bash $SCRIPTS_STORAGE/test.sh

eslint:
stage: test
script: bash $SCRIPTS_STORAGE/eslint.sh

# 编译 TS 文件
build:
stage: build
script: bash $SCRIPTS_STORAGE/build.sh

deploy_development:
stage: deploy_development
script: bash $SCRIPTS_STORAGE/deploy.sh 10.0.0.1
only: dev # 单独指定生效分支

deploy_production:
stage: deploy_production
script: bash $SCRIPTS_STORAGE/deploy.sh 10.0.0.2
only: master # 单独指定生效分支

我们将每一步 CI/CD 所需要执行的脚本都放到了 runner 那台服务器上,在配置文件中只是执行了那个脚本文件。

这样当我们有什么策略上的调整,比如说 ESLint 规则的变更、部署方式之类的。

这些都完全与项目之间进行解耦,后续的操作基本都不会让正在使用 CI/CD 的项目重新修改才能够支持(部分需要新增环境变量的导入之类的确实需要项目的支持)。
##接入钉钉通知
实际上,当 CI/CD 执行成功或者失败,我们可以在 Pipeline 页面中看到,也可以设置一些邮件通知,但这些都不是时效性很强的。

鉴于我们目前在使用钉钉进行工作沟通,所以就研究了一波钉钉机器人。

发现有支持 GitLab 机器人,不过功能并不适用,只能处理一些 issues 之类的, CI/CD 的一些通知是缺失的,所以只好自己基于钉钉的消息模版实现一下了。

因为上边我们已经将各个步骤的操作封装了起来,所以这个修改对同事们是无感知的,我们只需要修改对应的脚本文件,添加钉钉的相关操作即可完成,封装了一个简单的函数:
function sendDingText() {
local text="$1"

curl -X POST "$DINGTALK_HOOKS_URL" \
-H 'Content-Type: application/json' \
-d '{
"msgtype": "text",
"text": {
"content": "'"$text"'"
}
}'
}

# 具体发送时传入的参数
sendDingText "proj: $CI_PROJECT_NAME[$CI_JOB_NAME]\nenv: $CI_ENVIRONMENT_NAME\ndeploy success\n$CI_PIPELINE_URL\ncreated by: $GITLAB_USER_NAME\nmessage: $CI_COMMIT_MESSAGE"

# 某些 case 失败的情况下 是否需要更多的信息就看自己自定义咯
sendDingText "error: $CI_PROJECT_NAME[$CI_JOB_NAME]\nenv: $CI_ENVIRONMENT_NAME"

上述用到的环境变量,除了DINGTALK_HOOKS_URL是我们自定义的机器人通知地址以外,其他的变量都是有 GitLab runenr所提供的。

各种变量可以从这里找到:predefined variables
##回滚处理
聊完了正常的流程,那么也该提一下出问题时候的操作了。

人非圣贤孰能无过,很有可能某次上线一些没有考虑到的地方就会导致服务出现异常,这时候首要任务就是让用户还可以照常访问,所以我们会选择回滚到上一个有效的版本去。

在项目中的 Pipeline 页面 或者 Enviroment 页面(这个需要在配置文件中某些 job 中手动添加这个属性,一般会写在 deploy 的那一步去),可以在页面上选择想要回滚的节点,然后重新执行 CI/CD 任务,即可完成回滚。

不过这在 TypeScript 项目中会有一些问题,因为我们回滚一般来讲是重新执行上一个版本 CI/CD 中的 deploy 任务,在 TS 项目中,我们在 runner 中缓存了 TS 转换 JS 之后的 dist 文件夹,并且部署的时候也是直接将该文件夹推送到服务器的(TS项目的源码就没有再往服务器上推过了)。

而如果我们直接点击 retry 就会带来一个问题,因为我们的 dist 文件夹是缓存的,而 deploy 并不会管这种事儿,他只会把对应的要推送的文件发送到服务器上,并重启服务。

而实际上 dist 还是最后一次(也就是出错的那次)编译出来的 JS 文件,所以解决这个问题有两种方法:

  1. 在 deploy 之前执行一下 build
  2. 在 deploy 的时候进行判断

第一个方案肯定是不可行的,因为严重依赖于操作上线的人是否知道有这个流程。

所以我们主要是通过第二种方案来解决这个问题。

我们需要让脚本在执行的时候知道,dist 文件夹里边的内容是不是自己想要的。

所以就需要有一个 __标识__,而做这个标识最简单有效唾手可得的就是,git commit id。

每一个 commit 都会有一个唯一的标识符号,而且我们的 CI/CD 执行也是依靠于新代码的提交(也就意味着一定有 commit)。

所以我们在 build 环节将当前的commit id也缓存了下来:
git rev-parse --short HEAD > git_version

同时在 deploy 脚本中添加额外的判断逻辑:
currentVersion=`git rev-parse --short HEAD`
tagVersion=`touch git_version; cat git_version`

if [ "$currentVersion" = "$tagVersion" ]
then
echo "git version match"
else
echo "git version not match, rebuild dist"
bash ~/runner-scripts/build.sh # 额外的执行 build 脚本
fi

这样一来,就避免了回滚时还是部署了错误代码的风险。

关于为什么不将 build 这一步操作与 deploy 合并的原因是这样的:

因为我们会有很多台机器,同时 job 会写很多个,类似 deploy_1、deploy_2、deploy_all,如果我们将 build 的这一步放到 deploy 中。

那就意味着我们每次 deploy,即使是一次部署,但因为我们选择一台台机器单独操作,它也会重新生成多次,这也会带来额外的时间成本。
##hot fix 的处理
在 CI/CD 运行了一段时间后,我们发现偶尔解决线上 bug 还是会比较慢,因为我们提交代码后要等待完整的 CI/CD 流程走完。

所以在研究后我们决定,针对某些特定情况hot fix,我们需要跳过ESLint、单元测试这些流程,快速的修复代码并完成上线。

CI/CD 提供了针对某些 Tag 可以进行不同的操作,不过我并不想这么搞了,原因有两点:

  1. 这需要修改配置文件(所有项目)
  2. 这需要开发人员熟悉对应的规则(打 Tag)

所以我们采用了另一种取巧的方式来实现,因为我们的分支都是只接收Merge Request那种方式上线的,所以他们的commit title实际上是固定的:Merge branch 'XXX'。

同时 CI/CD 会有环境变量告诉我们当前执行 CI/CD 的 commit message。
我们通过匹配这个字符串来检查是否符合某种规则来决定是否跳过这些job:
function checkHotFix() {
local count=`echo $CI_COMMIT_TITLE | grep -E "^Merge branch '(hot)?fix/\w+" | wc -l`

if [ $count -eq 0 ]
then
return 0
else
return 1
fi
}

# 使用方法

checkHotFix

if [ $? -eq 0 ]
then
echo "start eslint"
npx eslint --ext .js,.ts .
else
# 跳过该步骤
echo "match hotfix, ignore eslint"
fi

这样能够保证如果我们的分支名为 hotfix/XXX 或者 fix/XXX 在进行代码合并时, CI/CD 会跳过多余的代码检查,直接进行部署上线。 没有跳过安装依赖的那一步,因为 TS 编译还是需要这些工具的。
#小结
目前团队已经有超过一半的项目接入了 CI/CD 流程,为了方便同事接入(主要是编辑 .gitlab-ci.yml 文件),我们还提供了一个脚手架用于快速生成配置文件(包括自动建立机器之间的信任关系)。

相较之前,部署的速度明显的有提升,并且不再对本地网络有各种依赖,只要是能够将代码 push 到远程仓库中,后续的事情就和自己没有什么关系了,并且可以方便的进行小流量上线(部署单台验证有效性)。

以及在回滚方面则是更灵活了一些,可在多个版本之间快速切换,并且通过界面的方式,操作起来也更加直观。

最终可以说,如果没有 CI/CD,实际上开发模式也是可以忍受的,不过当使用了 CI/CD 以后,再去使用之前的部署方式,则会明显的感觉到不舒适。(没有对比,就没有伤害

如何实现“持续集成”?闲鱼把研发效率翻了个翻

Andy_Lee 发表了文章 • 0 个评论 • 266 次浏览 • 2019-05-25 09:03 • 来自相关话题

从前端开发人员到DevOps:CI / CD 简介

tiny2017 发表了文章 • 0 个评论 • 279 次浏览 • 2019-05-23 17:45 • 来自相关话题

【编者的话】如果你是一名前端开发者,且认为作为一名开发人员只要管好前端这摊子就算大功告成了,那你就out了。原文作者从一个前端开发者的角度,阐述了DevOps的理念,并结合一些示例和实践,告诉我们了解并运用持续集成和持续部署的诸多好处。但我认为,原文作者从始至 ...查看全部
【编者的话】如果你是一名前端开发者,且认为作为一名开发人员只要管好前端这摊子就算大功告成了,那你就out了。原文作者从一个前端开发者的角度,阐述了DevOps的理念,并结合一些示例和实践,告诉我们了解并运用持续集成和持续部署的诸多好处。但我认为,原文作者从始至终传递给我们一种持续学习的精神,这一点同样值得我们学习。
# 介绍
对于有抱负的前端开发者而言,2019年是充满憧憬的一年。

有不计其数的教材、课件和教程,每天还有无数的博客和文章,像雨后春笋般层出不穷。任何想成为专业人士的人都可以获得他们需要的一切——通常还是免费的。

许多人抓住这个机会自学成才,而且他们当中很多人有机会参与完整的项目,可以迅速上手写功能模块,修改Bug并以适当的方式组建代码。

经过一段时间,有些比较幸运的前端开发者就会在互联网的某个角落看到他们自己实现的功能模块,作为一个web app、门户或者一个website——对于初级开发者而言,这真的是一个荣耀的时刻。但令人惊讶的是,他们中很少有人会问一个非常重要的问题:我们开发的应用程序,是怎么放到互联网上的?
1.png

大家普遍认为这是由开发人员完成的,只不过是更“高级别”的开发人员而已。可能一些人听说过DevOps,运营商,云管理员,系统管理员以及其他更具神秘气息的东西。

嗯,这是真的——在某种程度上,编码和测试成功后发生的一切通常都和脚本,Linux命令和容器之类的黑科技有关。还有一条不成文的规定是,只有组织中最有经验和最值得信赖的开发人员/管理员才有资格完成最后的交付工作。

有必要这样吗?这确实是有一定道理的——毕竟,这是一项复杂且极其重要的任务。但是否就意味着这些技能只属于某些精英?当然不是。

作为前端开发者,我们可以选择忽略这一切并相信其他人会完成剩下的所有事情——但我们不应该这样。IT世界的竞争瞬息万变,无论是前端还是后端,对于技术栈的点滴积累将会使你成为一名更具竞争力的开发者。

如果你想在开发方面进步的更快或者在同龄人中脱颖而出,你迟早会需要这些知识。下面就让我告诉你为什么你会需要这些知识。
# 为什么开发人员都应该尝试自动化处理
正如我们前面提到的那样,写代码只是软件开发这个浩大的工程的一部分。我们先来看看任何产品交付所需的基本步骤——不一定是软件:
2.png

严格来说,我们在这里讨论的不一定都是编码。我们关注的是主要的开发阶段完成之后会发生什么?为什么它会如此重要?因为它有可能会很复杂 - 解决方案越严谨,这部分就越复杂。

假设有一个具有一些特定功能的Web应用。我们假定该应用的版本会按照一个一个的功能定期发布,前提是发布到生产环境之前,每一个功能都会进行测试。
3.png

问题来了,我们一般不会只招一名程序员来完成这项工作, 即会有一个团队来负责这些功能。这些假设意味着——除了每个开发人员本地的编码环境和最终稳定的生产环境之外——最好还有一个“staging”环境来验证这些功能。在这个环境中,测试人员/客户可以在实际投入生产环境之前评估它们的质量。

现在我们越来越接近这样的架构:
4.png

正如你所看到的,事情变得越来越复杂(相信我,我们在这里谈论的真的是一个非常简单的例子),但我们在这里不会涉及产品生命周期管理,我们只关注技术。

假设前端开发人员需要几分钟来构建一个应用程序。如果关心代码质量,他们需要运行linting,单元测试,集成测试或者用其他的方式确认之后才能提交。这个过程很耗时。

最后,将打包好的程序放到服务器额外还需要几分钟时间。如果我们给一个程序员分配了以上所有这些任务,请记住我们还没有考虑其切换上下文所需的时间(例如,更改代码分支,重新聚焦到他们的工作上等等)。

现在,谁想要手动部署每个功能?如果每天都测试了三个新功能怎么办?如果有15个呢?依据不同的部署规模,很有可能需要一个以上的全职人员来处理上述任务。

这就是为什么我们需要在这里运用计算机诞生之初的思想:我们应该用一台机器来为我们做这件事。
# 持续集成和持续部署的好处
在我们讨论用于构建,测试和部署代码的特定软件解决方案之前,我们先来熟悉一下描述该过程的两个术语。你可能已经听说过它们了:
5.png

注意,通常CD部分代表持续交付,这个概念略有不同,我们将不会在这篇文章中讨论。这种容易引起混淆的缩写通常是许多学术讨论的基础。Atlassian有一篇很棒的文章解释了它们之间的差异。

为什么有两个单独的短语,它们到底是什么意思?不用担心——为了避免混淆,让我们先弄清楚一点,然后描述两者背后的普遍意义。

CI / CD的持续集成部分涵盖了应用程序完整性的重复测试。从技术角度来看,这意味着我们需要不断执行linting,运行unit / E2E测试,检查源代码质量等。通过持续的方式,意味着必须在push新代码之前完成 - 即它应该自动完成。

例如,CI流程中可以定义一系列单元测试,这些单元测试将在拉取代码请求时一起运行。这种情形下,每次更新代码时,例如对于开发分支,一些机器会检查它是否符合标准且没有错误。

CI / CD的持续部署通常涵盖了构建和将应用程序部署到可用环境的一系列过程——这也是自动完成的。例如,它可以从指定的分支(例如:`master`)获取我们的应用程序代码,使用适当的工具(例如webpack)构建,并将其部署到正确的环境(例如,托管服务)。

它并不只限于生产环境;例如,我们可以配置一个Pipeline(管道)来构建应用程序的“staging”版本,并将其部署到适当的主机用于测试。

这两个术语是软件生命周期管理理论中完全不同源的独立概念,但在实践过程中,它们通常以互补的方式共存于一个大型Pipeline(管道)。为什么它们如此密不可分?因为通常CI和CD存在部分重叠。

例如,我们可能有一个项目,E2E测试和部署都需要用webpack构建前端代码。同时,在大多数“严苛”的生产级项目中,还有许多流程既有CI又有CD。

现在让我们想象在一个功能众多的项目中,CI / CD可以做些什么呢?
6.png

我知道越深入这个主题,流程图就越复杂 ——但是,这样一来在项目会议中用白板表示时,就显得很酷!

现在试想一下我们可以从上面的流程中得到些什么呢?我们从因与果的角度来分析,可以通过抽象特定的场景形成假想的工作流程。例如:

一名开发人员尝试push代码到公共代码库,然后需要执行一组单元测试。

通过这种方式,我们可以清晰得知道什么时候开始行动 - 我们可以通过使用脚本或其他机制实现自动化。在将来使用CI / CD的过程中,你可以为这些场景化的Pipeline命名。

注意上面的粗体字:当和然后,每个阶段都需要一个动作触发。为了运行特定的Pipeline,我们需要某种kickstart或触发器。这些可能是:

  • 计时类触发器(“每天下午6点构建staging版本的应用程序”)
  • 代码库触发器(“每次发布新的拉取请求时运行单元测试。”)
  • 手动触发器(“项目经理启动应用程序构建过程并部署到生产环境。”)
当然也可以通过其他的触发器触发特定的Pipeline,尤其是当我们需要通过许多单独构建的部分集成一个复杂应用程序的时候。好吧,理论说的差不多了,现在来说说为我们完成这些工作的软件。# CI / CD中用到的软件基本上,每个CI / CD软件说到底只是某种类型的任务执行工具,当触发某些操作时会运行Job(任务)。我们的主要工作是通过配置的方式提示要完成哪些Job以及何时完成等。基于这些基本描述,CI / CD软件有许多类型,规格和偏好 - 其中一些软件非常复杂以至于手册都有数百页。但也不用害怕:在本文结束之前,你将熟悉其中一个。对新手而言,我们可以把CI / CD软件分为两类:
  • 可安装软件:可以在你的电脑上或某些远程机器上安装的应用程序或服务(例如,Jenkins,TeamCity)
  • SaaS:由一个外部公司通过Web界面的方式提供的应用程序或服务(例如,CircleCI,Azure DevOps)
真的很难说哪一个更具优势,就像本文的主题一样,它取决于应用程序的要求,组织的预算和政策以及其他因素。值得一提的是,一些受欢迎的源代码托管商(例如,BitBucket)会维护自己的CI / CD Web服务,这些服务和源代码管理系统紧密联系,旨在简化配置过程。此外,一些云托管的CI / CD服务是免费且对公众开放的 - 只要该应用程序是开源的就可以。一个广受欢迎的例子就是CircleCI。我们将充分利用它的优势,简单几步就可以为我们的前端应用程序示例配置一个功能齐全的CI / CD的Pipeline。# 前提和计划CircleCI是一个云上的CI / CD服务,它能够与GitHub集成从而轻松获取源代码。该服务有一个有趣的规则:即pipeline在源代码内部定义。这意味着所有的操作和连锁反应都通过在源代码中配置一个特殊文件来实现,在CircleCI中是通过配置`.circleci`文件夹的`config.yml`文件实现的。本文为了实现教学目的,将执行以下操作:
  • 写一个简单的前端应用程序并将源代码公开放在GitHub上
  • 创建并push包含Pipeline的配置文件`config.yml`
  • 创建一个CircleCI帐户并关联GitHub帐户
  • 找一个地方部署应用程序(这里,我们使用Amazon S3的主机托管服务)
  • 最后,运行创建的Pipeline
整个过程不应超过30分钟。接下来,我们来看看准备工作的清单。你需要:# 第一步:环境设置首先,从上述代码库签一个分支并克隆到本地计算机。如果你是新人,可以看下这波操作都做了什么。上述操作执行成功后,跳转到目标目录并执行以下命令:
npm installnpm start
现在打开浏览器输入到http://localhost:8080,你应该看到这样的画面:
7.png
这是一个非常简单的前端应用程序,表明成功加载了`.js`和`.css`文件。你可以查看源代码,它用了一个非常简单的实现机制。当然,你也可以用该教程中使用你自己的应用,只需要适当的修改创建脚本的命令就可以。只要程序是基于npm之类的工具创建的标准应用,应该都没有问题。在使用自动化流程进行持续集成、持续部署之前,需要先构建应用程序并手动存到S3。这样,我们才能保证目标环境的配置没有问题。首先,我们在本地构建应用程序包。如果你使用我们提供的示例程序,就可以用`npm run build`命令来创建,然后在项目的根目录底下得到一个名为`dist`的文件夹:
8.png
好了,我们的应用程序已经构建并打包完毕。你可以在测试服务器上执行`npx serve -s dist`命令查看它的运行情况。这个例子会运行一个`serve`包,它是一个轻量的HTTP服务器,可用来分发`dist`目录下的内容。运行完命令以后可以通过http://localhost:5000查看,你会发现和它在开发环境服务器中的运行结果一致。OK,我们把应用部署到互联网上,这要从S3开始。作为AWS生态圈的一部分,Amazon S3的概念非常简单:它提供了一个存储,可以上传任何类型的文件(包括静态HTML,CSS和JavaScript等)并启用简单的HTTP服务器来分发这些内容。最棒的是(在某些情况下)它是免费的!首先,登录:
9.png
AWS登录步骤1:提供登录用的电子邮箱
10.png
AWS登录步骤2:输入密码接下来,点击服务按钮并在存储项中选择S3,跳转到S3控制面板。
11.png
现在,我们将创建一个新的存储桶来存储我们的Web应用程序。首先,输入名称,名称只支持字母数字字符和连字符。然后,为存储桶选择适当的域。注意要记录这两个值,稍后会用到。
12.png
13.png
有一点非常重要就是设置适当的权限,这样文件才是公开的类型。因此,单击下一步直到出现设置权限选项,然后点击取消前三个框以启用文件的公开托管方式:
14.png
这样,HTTP服务器就能将上传的文件作为网站公开。在设置完存储桶之后,你可以访问并查看文件列表:
15.png
点击上传按钮,系统将提示你选择要上传的文件,你可以从`dist`文件夹中选择三个包文件上传。这里的操作和之前的一样,一定要到设置权限处选择管理公共权限框下的对该对象赋予公共读取权限,这点非常重要。瞧!文件已经在了,最后一步是要在S3上启用托管服务。通过存储桶视图找到属性按钮,打开静态网站托管选项:
16.png
你需要添加`index.html`作为索引文档,它将作为应用程序的入口。现在,一切准备就绪。在对话框的开头会生成一个新站点的链接,点击就可以查看刚才部署的应用:
17.png
太棒了,我们有了一个网站——可惜这不是这次的目标,因为到此为止什么都没自动化。你肯定不希望用这种方式登录S3控制台并在每次更新时上传一堆文件,那是机器要做的事。那么,我们来建一个持续部署的流程吧!# 第二步:准备CircleCI配置如果仔细查看代码库中的示例程序,你会看见一个CD的定义文件,打开`.circleci/config.yml`文件。
version: 2.1orbs:  aws-s3: circleci/aws-s3@1.0.4jobs:  build:    docker:      - image: circleci/python:2.7-node    environment:      AWS_REGION: us-east-1    steps:      - checkout      - run: npm install      - run: npm run build      - aws-s3/sync:          from: dist          to: 's3://demo-ci-cd-article/'          arguments: |            --acl public-read \            --cache-control "max-age=86400" \          overwrite: trueworkflows:  version: 2.1  build:    jobs:      - build:          filters:            branches:              only: master
如上所述,`config.yml`是CircleCI可识别的文件,它包含了CD过程中定义好的pipeline信息。本文的例子中,26行代码包含了以下的内容:
  • 构建应用程序需要哪些工具
  • 构建应用程序需要哪些命令
  • 应用程序在哪里以及如何部署
如果你不熟悉YAML文件,你会注意到它大量使用制表格式。这就是这类文件的组织结构:每一部分都有子节点,而层次结构由一个有双空格组成的tab标志。现在,我们来逐层看一下文件结构:
version: 2.1orbs:  aws-s3: circleci/aws-s3@1.0.4
上面几行代码包含了解析器的版本信息,并定义了部署过程中会用到的附属包(CircleCI命名规则中的“orbs”)。在这里,我们需要导入一个名为`aws-s3`的orb ,它包含把文件发送到S3存储桶所需的工具。
jobs:  build:    docker:      - image: circleci/python:2.7-node    environment:      AWS_REGION: us-east-1    steps:      - checkout      - run: npm install      - run: npm run build      - aws-s3/sync:          from: dist          to: 's3://demo-ci-cd-article/'          arguments: |            --acl public-read \            --cache-control "max-age=86400" \          overwrite: true
上面几行代码包含了Job的定义信息,即Pipeline的核心内容。这里要注意的一点是,像上述第二行中所示我们把Job命名为`build`,稍后我们会在CircleCI控制台的报告中看到这个名字。下一行写着`docker`,这部分我们定义创建应用的容器(在哪台虚拟机运行)。如果不熟悉容器或者Docker,这一步你可以想象成选择一台虚拟机然后创建应用。这里,有一台预装了Python和Node.js的Linux虚拟机,我们需要用Python运行AWS S3的工具,用Node创建前端应用。`environment`和`AWS_REGION`是AWS运行用的环境变量,可以不用在意具体的参数。到此,S3就能运行了。下一部分,`steps`更具自描述性。实际上,它是完成Job需要执行的一系列步骤。这部分的示范定义如下:
  • `checkout`:从代码库中获取源代码
  • `run: npm install`:安装依赖包
  • `run: npm run build`:Pipeline的核心,用于构建代码
  • `aws-s3/sync`:另一个重要步骤,它部署(“同步”)S3存储桶中给定的`dist`路径下的内容。注意,这个示例把`demo-ci-cd-article`作为存储桶的名字,你需要修改存储桶名字,使它和示例中的名称一致。

# 解析CircleCI配置
基本上,你可以把它想象成运行在本地的一个包含一组操作的job,这样就是告诉VM一步一步如何完成。当然,你也可以认为它是具备一些特定功能的特殊的shell脚本。

job有一个重要原则那就是每一个步骤都得成功。有任何一个命令失败,就不再执行后续操作,当前的pipeline的状态也会标记为`FAILED`。Job执行失败的信息会在CI / CD控制台中显示,包括相关错误日志,有助于排错。

失败的原因有很多,比方说,对于一个执行自动测试的pipeline,它可能意味着一次单元测试的失败并且某个开发人员需要修复他的代码,或者也有可能是工具的配置有问题,从而导致构建和部署失败。无论是什么原因,CI / CD流程通常会通过电子邮件的方式通知管理员(或本人)执行失败的消息。

这就是为什么要以相对安全的方式定义jobs,以便在执行某一步出错时,确保之前的步骤不会产生任何永久的负面影响。

马上就要结束了,最后一部分是`workflows`:
workflows:
version: 2.1
perform_build:
jobs:
- build:
filters:
branches:
only: master

在CircleCI中“workflow”是一组互相协作的Job。由于之前我们只定义了一个Job(`build`),我们可以暂时不考虑这部分。但是,通过定义工作流的方式可以实现一个很重要的功能:branch filtering(筛选分支)。

你可以看到配置文件中的最后2行定义了`filters`。在这个例子中,它包含了`branches: only: master`,即定义了只有主分支上的代码更新才会执行构建代码Job。

这样就可以通过CI / CD流程筛选出需要“watched(监控)”的分支。例如,可以在不同的分支上调用不同的工作流(包含不同的Job),然后构建单独的版本,或仅在特定情况下测试等。
# 最后一步:CircleCI实践
如果你还没有完成,通过登录GitHub的方式,关联你的GitHub帐户与CircleCI
18.png

登录GitHub并授权给CircleCI后,你会在导航栏看见一个Add Project(添加项目)的选项。点击它可以查看你在GitHub中的代码库列表:
19.png

不管是拷贝的示例还是你自己准备的应用(记住有一个`.circleci/config.yml`文件),先假设你已经有一个代码库了。
然后,在列表中找到该项目后单击旁边的Set Up Project(设置项目)选项。你会看到一个描述CircleCI规则的画面:
20.png

看到底部Start building(开始构建)的按钮了吗?是的,就是它。点击它来启用我们的自动化流程,让机器为我们工作。
点击这个按钮后,你会看到……一个错误提示。
21.png

好吧,我们还需要配置一个地方:让CircleCI API授权给AWS的机制。到目前为止,我们还没有把AWS密码放入代码,GitHub或CircleCI里,所以AWS还不知道我们要把东西放入S3,所以报错。

通过改CircleCI面板中的项目设置来配置。单击右上角的齿轮图标,然后在左边找AWS权限选项卡,你会看到以下画面:
22.png

Access Key ID(访问秘钥ID)Secret Access Key(加密访问秘钥)是AWS的2个鉴权值,用于对CircleCI等第三方服务的鉴权。例如,将文件上传到S3存储桶。最初,这些密钥将具有与分配给它们的用户相同的权限。

你可以通过AWS控制台的IAM生成这些信息。进入IAM,打开访问秘钥Access Key ID(访问秘钥ID)Secret Access Key(加密访问秘钥)】窗口,点击创建新的访问密钥,生成可以复制到CircleCI的密钥对:
23.png

单击Save AWS keys(保存AWS秘钥)就可以了。你可以在CircleCI上尝试重新初始化代码库,也可以用更加快捷的方式:找到失败的报告,然后点击Rerun workflow(重新执行)工作流程按钮。
24.png

现在所有的问题都搞定了,构建应用应该不会再出状况了。
25.png

太棒了!你可以登录S3控制台并检查文件的修改时间,可以证明文件是新上传的。不过还没完事呢,我们来看看“持续”部分是如何工作的。我将返回代码编辑器,说一下应用(`index.html`)的一个小的变动:
26.png

现在,我们可以将代码推送到代码库:
 git add .
git commit -m “A small update!”
git push origin master

神奇的事情发生了。眨眼之间,在成功推送后,CircleCI已经在用更新后的代码构建应用了:
27.png

几秒钟后,会有一条执行`SUCCESS`的消息。现在,你可以刷新一下S3托管的web页面,就可以看到修改后的应用:
28.png

搞定!这一切都是自动执行的:推送完代码后互联网上的一些机器自动构建并部署到生产环境中。
# 进阶练习
当然,这只是一个简单的例子。现在我们来看一个更复杂的例子。例如,部署到多个环境并更改应用。

回到我们的示例代码,你会发现在`package.json`中有两个独立的构建脚本:一个用于`production`环境,一个用于`staging`环境。由于只是一个示例项目,所以不会发生大的变更。这里,它只是用一个不同的JavaScript控制台消息表示而已。

应用在`staging`环境运行之后打开浏览器,你可以在JavaScript控制台看到相应的日志信息:
29.png

现在,我们利用这个机制扩展构建应用的pipelines。请看以下的代码:
version: 2.1
orbs:
aws-s3: circleci/aws-s3@1.0.4
jobs:
build:
docker:
- image: circleci/python:2.7-node
environment:
AWS_REGION: us-east-1
steps:
- checkout
- run: npm install
- run: npm run build
- aws-s3/sync:
from: dist
to: 's3://demo-ci-cd-article/'
arguments: |
--acl public-read \
--cache-control "max-age=86400" \
overwrite: true
build-staging:
docker:
- image: circleci/python:2.7-node
environment:
AWS_REGION: us-east-1
steps:
- checkout
- run: npm install
- run: npm run build:staging
- aws-s3/sync:
from: dist
to: 's3://demo-ci-cd-article/'
arguments: |
--acl public-read \
--cache-control "max-age=86400" \
overwrite: true
workflows:
version: 2.1
build:
jobs:
- build:
filters:
branches:
only: master
build-staging:
jobs:
- build-staging:
filters:
branches:
only: develop

注意,我们添加了一个新的job和一个新的`build-staging`工作流程。有两点不同:新job调用前面提到的
`npm run build:staging`方法,同时用`develop`分支进行筛选。

这意味着所有到`develop`分支的推送都将用“staging”构建,而`master`分支上的所有变更都将保留其原始状态并触发“production”构建。在这里,双方都会在同一个S3存储桶中,但我们可以修改并让它们在相互隔离的目标环境中运行。

可以试一下:基于`master`分支创建一个新的`develop`分支并将代码推送到代码库。在CircleCI控制台,你会看到调用了不同的工作流程:
30.png

相应的变更推送到了S3存储桶,但这次在staging上构建来自`develop`分支的应用,实现了多版本的构建工作。很好,我们马上就能实现之前描述过的工作流程啦!
# 持续集成部分
我们已经完成了持续部署部分的内容,但什么是持续集成呢?正如之前说过的,这部分涉及到定期检查代码的质量,例如:执行测试。

如果仔细看示例的代码库就可以看到有一个单元测试样例,可以用`npm run test`命令执行该测试样例。它通过断言的方式比较了某些模式下虚函数的结果。
function getMessage() {
return 'True!';
}

// ...

module.exports = getMessage;


const getMessage = require('./jsChecker');
const assert = require('assert');

assert.equal(getMessage(), 'True!');

我们可以在管道中加入测试,然后设置成在每个拉取请求时执行就可以。实现方式是在`config.yml`里创建一个新job和一个新的工作流程:

config.yml



version: 2.1
orbs:
aws-s3: circleci/aws-s3@1.0.4
jobs:
build:
# ...
build-staging:
# ...
test:
docker:
- image: circleci/python:2.7-node
steps:
- checkout
- run: npm install
- run: npm run test
workflows:
version: 2.1
build:
# ...
build-staging:
# ...
test:
jobs:
- test

我们已经定义了一个新job和一个名为`test`的工作流程,唯一的目的是触发`npm run test`脚本。然后,将此文件推送到代码库,看一下CircleCI控制台会发生什么:
31.png

一个新的工作流程被自动触发,并完成了一次成功的测试。接下来把它和GitHub的代码库进行对接,这样一来每次拉取特定分支的请求都会触发该job。要实现这一点,只需要打开GitHub页面并到Settings(设置)页面,选择Branches(分支)
32.png

单击Add rule(添加规则),就可以添加一个新的策略。该策略将在合并拉取请求之前强制执行一系列检查,其中一项检查就是调用CircleCI工作流程,如下所示:
33.png

通过勾选Require status checks to pass before merging(合并之前要检查状态)并勾选的`ci/circleci: test`,就可以将规则设置为在拉取前执行该工作流。

该规则可以通过创建一个新的拉取请求来测试,然后打开Checks(检查)面板来查看测试情况:
34.png

当然,也可以测试该规则无效的情况。你可以提交一个会导致测试失败的变更,把它放到一个新分支上并执行一个拉取请求:
35.png

我们模拟了一个失败的测试,输出结果如下:
assert.equal(getMessage(), 'True!');
-->
[quote] node src/modules/jsChecker.test.js
assert.js:42
throw new errors.AssertionError({
^
AssertionError [ERR_ASSERTION]: 'True, but different!' == 'True!'
at Object.

现在这个拉取请求将无法合并,因为它引入了导致测试失败的代码:
36.png

赞!我们的示例项目成功覆盖了连续测试的各种情况,只要测试用例没问题,就不可能把错误的代码引入到生产分支。同样的机制还可用于执行代码linting,静态代码分析,E2E测试和其他自动化检查等。[/quote]

好的,就这样!虽然我们的示例项目非常简单,但它展现了真实且有效的CI / CD流程。无论是集成还是部署都由云上的工具执行,所以开发者可以将所有注意力集中到编码上。

无论涉及多少人,机器都将不知疲倦地工作,并检查一切是否到位。虽然设置这一切也需要花费一些时间,但从长远看,把机械性操作进行自动化处理是非常有意义的一件事。

当然,它不是永远的免税天堂:迟早会产生额外的费用。例如,CircleCI每月提供1,000分钟的免费构建。对于小型团队和简单的开源项目来说足够了,但对大型的企业级项目而言肯定会超过这个配额。
# 延伸阅读
我们学习了许多基础的知识,但这篇文章还有许多重要的内容还没来得及讲解。

有一点就是如何更好的使用环境变量。通常我们都不会直接在源代码中保存密码,API密钥和其他敏感信息。当引入CI / CD自动化流程后,首先需要向机器提供适当的变量,就像我们在示例中使用AWS密码一样。

除此之外,环境变量来可以用来控制构建的过程,例如:应该构建哪个或者应该在特定版本中启用哪些特征之类。你可以通过它们在CircleCI中的使用这篇文章中获得更多的信息。

另一个是:许多CI / CD流程引入了组件管理的概念。组件是对特定构建过程中产生的代码的通称。例如,一个包或具有特定版本的应用程序的容器镜像都可以看做组件。

在特定组织中,由于各种原因导致对组件版本的管理变得格外重要。例如:它们可能会被归类和归档以便用于回滚或其他用途。

另一个重要的部分是角色、权限和安全性。这篇文章涉及到定义Pipelines和工作流的基础操作,但在大型、真实的项目中,有必要将组织的流程和策略等考虑在内。例如,我们希望某个Pipeline只能由公司组织架构中的某个人调用或批准。

另一个例子是对特定管道的设置或VM的配置进行细粒度的控制。但同样,这取决于用什么软件以及特定项目或公司的要求,好的自动化流程没有一个单一的范式,就像好的IT项目没有单一的模式一样。
# 总结
好了,言归正传。

不知道这篇文章会让你有什么样的收获?重要的是,现在你已经对一些“重大”的项目中发生的事情有了一个大致的了解。无论使用何种方法和软件,一些基本的规则总是相似的:有任务、管道和代理执行此工作流程。希望通过这篇文章,会让你对许多概念有一个新的认识。最后,你可以试着创建实际工作中用到的CI / CD Pipeline,并用自动化的方式将应用部署到云上。

接下来你还可以做什么呢?

当然,继续扩充你的知识,努力做的更好。如果你正在为公司开发项目,可以尝试写写代码,创建你自己的测试/部署pipeline。你可以(甚至应该)在你的下一个开源项目中引入自动化测试、打包等。您还可以了解更多的CI / CD软件:例如Travis,Jenkins或Azure DevOps。

此外,你还可以查看我的个人资料中与前端开发相关的其他帖子。祝你好运!

原文链接:From front-end developer to a DevOps: An intro to CI/CD (翻译:Tiny Guo)

DockOne微信分享(二零六):容器环境下的持续集成最佳实践

大卫 发表了文章 • 0 个评论 • 1027 次浏览 • 2019-04-03 11:49 • 来自相关话题

【编者的话】本次分享结合 Drone + GitFlow + Kubernetes 介绍一些容器环境下持续集成、持续发布方面的实践经验。分享将展示从 Hello World 到单人单分支手动发布到团队多分支 GitFlow 工作流再到团队多分支 semanti ...查看全部
【编者的话】本次分享结合 Drone + GitFlow + Kubernetes 介绍一些容器环境下持续集成、持续发布方面的实践经验。分享将展示从 Hello World 到单人单分支手动发布到团队多分支 GitFlow 工作流再到团队多分支 semantic-release 语义化发布最后到通知 Kubernetes 全自动发布,如何从零开始一步一步搭建 CI 将团队开发、测试、发布的流程全部自动化的过程,最终能让开发人员只需要认真提交代码就可以完成日常的所有 DevOps 工作。

云原生(Cloud Native)是伴随的容器技术发展出现的的一个词,最早出自 Pivotal 公司(即开发了 Spring 的公司)的一本技术小册子 Migrating to Cloud-Native Application Architectures, 其中定义了云原生应用应当具备的一些特质,如无状态、可持续交付、微服务化等。随后云原生概念被广为引用,并围绕这一概念由数家大厂牵头,成立了 CNCF 基金会来推进云原生相关技术的发展,主要投资并孵化云原生生态内的若干项目,包括了如 Kubernetes / etcd / CoreDNS 等耳熟能详的重要项目。而这张大大的云原生版图仍然在不断的扩展和完善。

从个人理解来说,传统的应用由于年代久远,更多会考虑单机部署、进程间通信等典型的“单机问题”,虽然也能工作在容器下,但由于历史包袱等原因,架构上已经很难做出大的调整。而“云原生”的应用大都是从一开始就为容器而准备,很少考虑在单机环境下使用,有些甚至无法脱离容器环境工作;考虑的场景少,从而更轻量,迭代更快。比如 etcd 之于 ZooKeeper, Traefik 之于 Nginx 等,相信只要在容器环境下实现一次同样的功能,就能强烈的体会到云原生应用所特有的便捷之处。

在 CNCF 的版图下,持续集成与持续交付(Continuous Integration & Delivery)板块一直缺少一个钦定的主角,虽然也不乏 Travis CI、GitLab、Jenkins 这样的知名项目,但最能给人云原生应用感觉的,应该还是 Drone 这个项目,本文将围绕 Drone 结合 GitFlow 及 Kubernetes 介绍一些容器环境下持续集成、持续发布(CI/CD)方面的实践经验。
#主流 CI/CD 应用对比
之前我也介绍过基于 Travis CI 的一些持续集成实践。后来经过一些比较和调研,最终选择了 Drone 作为主力 CI 工具。截止本文,团队已经使用 Drone 有 2 年多的时间,从 v0.6 一路用到现在即将发布的 v1.0,虽然也踩了不少坑,但总的来说 Drone 还是可以满足大部分需求,并以不错的势头在完善和发展的。

下面这张表总结了主流的几个 CI/CD 应用的特点:
B1.png

Travis CI 和 CircleCI 是目前占有率最高的两个公有云 CI,易用性上相差无几,只是收费方式有差异。由于不支持私有部署,如果并行的任务量一大,按进程收费其实并不划算;而且由于服务器位置的原因,如果推送镜像到国内,速度很不理想。

GitLab CI 虽然好用,但和 GitLab 是深度绑定的,我们的代码托管在 GitHub,整体迁移代码库的成本太大,放弃。

Jenkins 作为老牌劲旅,也是目前市场占有率最高的 CI,几乎可以覆盖所有 CI 的使用场景,由于使用 Java 编写,配置文件使用 Groovy 语法,非常适合 Java 为主语言的团队。Jenkins 显然是可以满足我们需要的,只是团队并非 Java 为主,又已经习惯了使用 YAML 书写 CI 配置,抱着尝鲜的心态,将 Jenkins 作为了保底的选择。

综上,最终选择 Drone 的结论也就不难得出了,Drone 即开源,又可以私有化部署,同时作为云原生应用,官方提供了针对 Docker、Docker Swarm、Kubernetes 等多种容器场景下的部署方案,针对不同容器场景还有特别优化,比如在 Docker Swarm 下 Drone 是以 Agent 方式运行 CI 任务的,而在 Kubernetes 下则通过创建 Kubernetes Job 来实现,显然充分利用了容器的优势所在,这也是 Drone 优于其他 CI 应用之处。个人还觉得 Drone 的语法是所有 CI 中最容易理解和掌握的,由于 Drone 每一个步骤都是运行一个 Docker 容器,本地模拟或调试也非常容易。

一句话概况 Drone,可以将其看做是可以支持私有化部署的开源版 CircleCI,并且目前仍然没有看到有其他主打这个定位的 CI 工具,因此个人认为 Drone 是 CI/CD 方面云原生应用头把交椅的有力竞争者。
#容器环境下一次规范的发布应该包含哪些内容
技术选型完成后,我想首先演示一下最终的成果,希望能直观的体现出 CI 对自动化效率起到的提升,不过这就涉及到一个问题:在容器环境下,一次发布应该包含哪些内容,其中有哪些部分是可以被 CI 自动化完成的。这个问题虽然每家公司各不相同,不过按经验来说,容器环境下一次版本发布通常包含这样一个 Checklist:

* 代码的下载构建及编译
* 运行单元测试,生成单元测试报告及覆盖率报告等
* 在测试环境对当前版本进行测试
* 为待发布的代码打上版本号
* 编写 ChangeLog 说明当前版本所涉及的修改
* 构建 Docker 镜像
* 将 Docker 镜像推送到镜像仓库
* 在预发布环境测试当前版本
* 正式发布到生产环境

看上去很繁琐对吗,如果每次发布都需要人工去处理上述的所有内容,不仅容易出错,而且也无法应对 DevOps 时代一天至少数次的发布频率,那么下面就来使用 CI 来解决所有问题吧。
#CI 流程演示
为了对 CI 流程有最直观的认识,我创建了一个精简版的 GitHub 项目 AlloVince/drone-ci-demo 来演示完整的流程,同时项目对应的 CI 地址是 cloud.drone.io/AlloVince/drone-ci-demo ,项目自动构建的 Docker 镜像会推送到 docker registry 的 allovince/drone-ci-demo。为了方便说明,假设这个项目的核心文件只有 index.html 一个静态页面。
##单人开发模式
目前这个项目背后的 CI 都已经配置部署好,假设我是这个项目的唯一开发人员,如何开发一个新功能并发布新版本呢?

* Clone 项目到本地, 修改项目代码, 如将 Hello World 改为 Hello World V2。
* git add .,然后书写符合约定的 Commit 并提交代码, git commit -m "feature: hello world v2”。
* 推送代码到代码库git push,等待数分钟后,开发人员会看到单元测试结果,GitHub 仓库会产生一次新版本的 Release,Release 内容为当前版本的 ChangeLog, 同时线上已经完成了新功能的发布。

虽然在开发者看来,一次发布简单到只需 3 个指令,但背后经过了如下的若干次交互,这是一次发布实际产生交互的时序图,具体每个环节如何工作将在后文中详细说明。
01.png

##多人开发模式
一个项目一般不止一个开发人员,比如我是新加入这个项目的成员,在这个 Demo 中应该如何上线新功能呢?同样非常简单:

  1. Clone 项目到本地,创建一个分支来完成新功能的开发, git checkout -b feature/hello-world-v3。在这个分支修改一些代码,比如将Hello World V2修改为Hello World V3
  2. git add .,书写符合规范的 Commit 并提交代码, git commit -m "feature: hello world v3”
  3. 将代码推送到代码库的对应分支, git push origin feature/hello-world
  4. 如果功能已经开发完毕,可以向 Master 分支发起一个 Pull Request,并让项目的负责人 Code Review
  5. Review 通过后,项目负责人将分支合并入主干,GitHub 仓库会产生一次新版本的 Release,同时线上已经完成了新功能的发布。

这个流程相比单人开发来多了 2 个环节,很适用于小团队合作,不仅强制加入了 Code Review 把控代码质量,同时也避免新人的不规范行为对发布带来影响。实际项目中,可以在 GitHub 的设置界面对 Master 分支设置写入保护,这样就从根本上杜绝了误操作的可能。当然如果团队中都是熟手,就无需如此谨慎,每个人都可以负责 PR 的合并,从而进一步提升效率。
02.png

##GitFlow 开发模式
在更大的项目中,参与的角色更多,一般会有开发、测试、运维几种角色的划分;还会划分出开发环境、测试环境、预发布环境、生产环境等用于代码的验证和测试;同时还会有多个功能会在同一时间并行开发。可想而知 CI 的流程也会进一步复杂。

能比较好应对这种复杂性的,首选 GitFlow 工作流, 即通过并行两个长期分支的方式规范代码的提交。而如果使用了 GitHub,由于有非常好用的 Pull Request 功能,可以将 GitFlow 进行一定程度的简化,最终有这样的工作流:
03.png


* 以 dev 为主开发分支,Master 为发布分支
* 开发人员始终从 dev 创建自己的分支,如 feature-a
* feature-a 开发完毕后创建 PR 到 dev 分支,并进行 code review
* review 后 feature-a 的新功能被合并入 dev,如有多个并行功能亦然
* 待当前开发周期内所有功能都合并入 dev 后,从 dev 创建 PR 到 master
* dev 合并入 Master,并创建一个新的 Release

上述是从 Git 分支角度看代码仓库发生的变化,实际在开发人员视角里,工作流程是怎样的呢。假设我是项目的一名开发人员,今天开始一期新功能的开发:

  1. Clone 项目到本地,git checkout dev。从 dev 创建一个分支来完成新功能的开发, git checkout -b feature/feature-a。在这个分支修改一些代码,比如将Hello World V3修改为Hello World Feature A
  2. git add .,书写符合规范的 Commit 并提交代码, git commit -m "feature: hello world feature A"
  3. 将代码推送到代码库的对应分支, git push origin feature/feature-a:feature/feature-a
  4. 由于分支是以 feature/ 命名的,因此 CI 会运行单元测试,并自动构建一个当前分支的镜像,发布到测试环境,并自动配置一个当前分支的域名如 test-featue-a.avnpc.com
  5. 联系产品及测试同学在测试环境验证并完善新功能
  6. 功能通过验收后发起 PR 到 dev 分支,由 Leader 进行 code review
  7. Code Review 通过后,Leader 合并当前 PR,此时 CI 会运行单元测试,构建镜像,并发布到测试环境
  8. 此时 dev 分支有可能已经积累了若干个功能,可以访问测试环境对应 dev 分支的域名,如 test.avnpc.com,进行集成测试。
  9. 集成测试完成后,由运维同学从 Dev 发起一个 PR 到 Master 分支,此时会 CI 会运行单元测试,构建镜像,并发布到预发布环境
  10. 测试人员在预发布环境下再次验证功能,团队做上线前的其他准备工作
  11. 运维同学合并 PR,CI 将为本次发布的代码及镜像自动打上版本号并书写 ChangeLog,同时发布到生产环境。

由此就完成了上文中 Checklist 所需的所有工作。虽然描述起来看似冗长,但不难发现实际作为开发人员,并没有任何复杂的操作,流程化的部分全部由 CI 完成,开发人员只需要关注自己的核心任务:按照工作流规范,写好代码,写好 Commit,提交代码即可。

接下来将介绍这个以 CI 为核心的工作流,是如何一步步搭建的。
#Step by Step 构建 CI 工作流
##Step.0:基于 Kubernetes 部署 Drone v1.0.0
以 GitHub 为例,截止本文完成时间(2019 年 3 月 28 日), Drone 刚刚发布了第一个正式版本 v1.0.0。官方文档已经提供了分别基于 Docker、Kubernetes 的 Drone 部署说明,不过比较简略,因此这里给出一个相对完整的配置文件。

首先需要在 GitHub 创建一个 Auth App,用于 repo 的访问授权。应用创建好之后,会得到 Client ID 和 Client Secret 。同时 Authorization callback URL 应填写 Drone 服务对应域名下的 /login,如 https://ci.avnpc.com/login。

Drone 支持 SQLite、MySQL、Postgres、S3 等多种后端存储,主要用于记录 build logs 等文本信息,这些信息并不是特别重要,且我们的 CI 有可能做迁移,因此个人更推荐使用 SQLite。

而在 Kubernetes 环境下,SQLite 更适合用挂载 NAS 的方式供节点使用,因此首先将存储的部分独立为文件 drone-pvc.yml,可以根据实际情况配置 nfs.path 和 nfs.server:
kubectl apply -f drone-pvc.yaml

Drone 的配置主要涉及两个镜像:

* drone/kubernetes-secrets 加密数据服务,用于读取 Kubernetes 的 secrets
* drone/drone:1.0.0-rc.6 就是 Drone 的 server 端,由于在 Kubernetes 下 Drone 利用了 Job 机制,因此不需要部署 Agent。

这部分配置较长,可以直接参考示例 drone.yaml

主要涉及到的配置项包括:

* Drone/kubernetes-secrets 镜像中

* SECRET_KEY:数据加密传输所用的 key,可以使用 openssl rand -hex 16 生成一个

* Drone/Drone镜像中

* DRONE_KUBERNETES_ENABLED:开启 Kubernetes 模式
* DRONE_KUBERNETES_NAMESPACE:Drone 所使用的 Namespace, 这里使用 default
* DRONE_GITHUB_SERVER:GitHub 服务器地址,一般为 https://github.com
* DRONE_GITHUB_CLIENT_ID:上文创建 GitHub Auth App 得到的 Client ID
* DRONE_GITHUB_CLIENT_SECRET:上文创建 GitHub Auth App 得到的 Client Secret
* DRONE_SERVER_HOST:Drone 服务所使用的域名
* DRONE_SERVER_PROTO:http 或 https
* DRONE_DATABASE_DRIVER:Drone 使用的数据库类型,这里为 SQLite3
* DRONE_DATABASE_DATASOURCE:这里为 SQLite 数据库的存放路径
* DRONE_SECRET_SECRET:对应上文的 SECRET_KEY
* DRONE_SECRET_ENDPOINT:加密数据服务的地址,这里通过 Kubernetes Service 暴露,无需修改

最后部署即可:
kubectl apply -f drone.yaml

部署后首次登录 Drone 就会跳转到 GitHub Auth App 进行授权,授权完毕后可以看到所有能读写的 Repo,选择需要开启 CI 的 Repo,点击 ACTIVATE 即可。 如果开启成功,在 GitHub Repo 的 Settings > Webhooks 下可以看到 Drone 的回调地址。
##Step.1:Hello World for Drone
在正式开始搭建工作流之前,首先可以测试一下 Drone 是否可用。Drone 默认的配置文件是 .drone.yml, 在需要 CI 的 repo 根目录下创建.drone.yml, 内容如下,提交并git push到代码仓库即可触发 Drone 执行 CI。
kind: pipeline  
name: deploy

steps:
[list]
[*]name: hello-world[/*]
[/list] image: docker
commands:
- echo "hello world"

Drone v1 的语法主要参考的 Kubernetes 的语法,非常直观,无需阅读文档也可以知道,我们首先定义了一个管道(Pipeline),管道由若干步骤(step)组成,Drone 的每个步骤是都基于容器实现的,因此 Step 的语法就回到了我们熟悉的 Docker,一个 Step 会拉取 image 定义的镜像,然后运行该镜像,并顺序执行 commands 定义的指令。

在上例中,Drone 首先 clone git repo 代码到本地,然后根据 .drone.yml 所定义的,拉取 Docker 的官方镜像,然后运行该进行并挂载 git repo 的代码到 /drone/src 目录。

在 Drone 的界面中,也可以清楚的看到这一过程。
04.png

本阶段对应:

* 代码部分:https://github.com/AlloVince/drone-ci-demo/tree/hello-world
* Drone 构建记录:https://cloud.drone.io/AlloVince/drone-ci-demo/1
* Docker 镜像:无

##Step.2:单人工作流,自动化单元测试与 Docker 镜像构建
有了 Hello World 的基础,接下来我们尝试将这个工作流进行扩充。

为了方便说明,这里假设项目语言为 JavaScript,项目内新增了 test/index.js 文件用于模拟单元测试,一般在 CI 中,只要程序的返回值为 0,即代表运行成功。这个文件中我们仅仅输出一行 Log Unit test passed用于模拟单元测试通过。

我们希望将代码打包成 Docker 镜像,根目录下增加了 Dockerfile 文件,这里直接使用 Nginx 的官方镜像,构建过程只有 1 行 COPY index.html /usr/share/nginx/html/, 这样镜像运行后可以通过 http 请求看到 index.html 的内容。

至此我们可以将工作流改进为:

* 当 Master 分支接收到 push 后,运行单元测试
* 当 GitHub 发布一次 Release, 构建 Docker 镜像,并推送到镜像仓库

对应的 Drone 配置文件如下:
kind: pipeline  
name: deploy

steps:
- name: unit-test
image: node:10
commands:
- node test/index.js
when:
branch: master
event: push
- name: build-image
image: plugins/docker
settings:
repo: allovince/drone-ci-demo
username: allovince
password:
from_secret: DOCKER_PASSWORD
auto_tag: true
when:
event: tag

虽然比 Hello World 复杂了一些,但是可读性仍然很好,配置文件中出现了几个新概念:

Step 运行条件, 即 when 部分,上例中展示了当代码分支为 Master,且收到一个 push;以及当代码被标记 tag 这两种情况。Drone 还支持 repo、运行结果等很多其他条件,可以参考 Drone Conditions 文档。

Plugin 插件,上例中用于构建和推送镜像的是 plugins/docker 这个 Plugin, 一个 Plugin 本质上仍然是一个 Docker 镜像,只是按照 Drone 的规范接受特定的输入,并完成特定的操作。所以完全可以将 Plugin 看做一个无法更改 command 的 Docker 镜像。

Docker 这个 Plugin 由 Drone 官方提供,用于 Docker 镜像的构建和推送,具体的用法可以查看 Docker 插件的文档。例子中演示的是将镜像推送到私有仓库,如果不做特殊配置,镜像将被推送到 Docker 的官方仓库。

此外 Docker 插件还有一个很方便的功能,如果设置 auto_tag: true,将根据代码的版本号自动规划 Docker 镜像的标签,如代码版本为1.0.0,将为 Docker 镜像打三个标签 1, 1.0, 1.0.0。如果代码版本号不能被解析,则镜像标签为 latest。

目前 Drone 的插件已经有很多,可以覆盖主流的云服务商和常见的工作流,并且自己制作插件的成本也不高。

Secret 加密数据,镜像仓库的用户名和密码都属于敏感信息,因此可以使用 from_secret 获取加密数据。一条加密数据就是一个 key / value 对,如上例中的 DOCKER_PASSWORD 就是我们自己定义的加密数据 key。即便加密数据在 log 中被打印,UI 也只能看到 ***。加密数据的 value 需要提前保存好,保存的方式有 3 种:

* 通过 Drone UI 界面中, repo -> Settings -> Secrets 添加,所添加的加密数据将保存在 Drone 的数据库中,仅能在当前 repo 中使用。
* 通过 Drone cli 加密后保存在 .drone.yml 文件中, 使用范围仅限 yaml 文件内
* 通过 Kubernetes 保存为 Kubernetes Secret,称为 External Secrets,所有的 repo 都可以共享。如果是团队使用的话,这种保存方式显然是最方便的,但也要注意安全问题,因此 External Secrets 还支持 repo 级别的权限管理, 可以只让有当前 repo 写入权限的人才能使用对应 secret。

这个阶段对应:

* 代码仓库:https://github.com/AlloVince/drone-ci-demo/tree/single-person
* push 时触发的 Drone CI:https://cloud.drone.io/AlloVince/drone-ci-demo/4
* Release 时触发的 Drone CI:https://cloud.drone.io/AlloVince/drone-ci-demo/5
* Release 后 CI 构建的 Docker 镜像:allovince/drone-ci-demo:latest

##Step.3:GitFlow 多分支团队工作流
上面的工作流已经基本可以应付单人的开发了,而在团队开发时,这个工作流还需要一些扩展。不需要引入 Drone 的新功能,只需要在上文基础上根据分支做一点调整即可。

首先保证单元测试位于 steps 的第一位,并且限定团队工作的分支,在 push 和 pull_request 时,都能触发单元测试。

 - name: unit-test  
image: node:10
commands:
- node test/index.js
when:
branch:
include:
- feature/*
- master
- dev
event:
include:
- push
- pull_request

然后根据 GitFlow 的流程对于不同的分支构建 Docker 镜像并打上特定标签,以 feature 分支为例,下面的配置约定了当分支名满足 feature/*,并收到 push 时,会构建 Docker 镜像并打标签,标签名称为当前分支名去掉 feature/。如分支 feature/readme, 对应 Docker 镜像为 allovince/drone-ci-demo:readme,考虑到 feature 分支一般都出于开发阶段,因此新的镜像会覆盖旧的。配置如下:
- name: build-branch-image  
image: plugins/docker
settings:
repo: allovince/drone-ci-demo
username: allovince
password:
from_secret: DOCKER_PASSWORD
tag:
- ${DRONE_BRANCH[size=16]feature/} [/size]
when:
branch: feature/*
event: push

镜像的 Tag 处不再使用自动方式,其中 DRONE_BRANCH 是 Drone 的内置环境变量(Environment),对应当前的分支名。feature/ 是执行了一个字符串的替换操作(Substitution)。更多的环境变量和字符串操作都可以在文档中找到。

以此类推,可以查看这个阶段的完整 .drone.yml ,此时我们的工作流示例如下:

* 团队成员从 dev 分支 checkout 自己的分支 feature/readme
* 向feature/readme提交代码并 push, CI 运行单元测试,构建镜像allovince/drone-ci-demo:readme
* 功能开发完成后,团队成员向 dev 分支 发起 pull request , CI 运行单元测试
* 团队其他成员 merge pull request, CI 运行单元测试,构建镜像allovince/drone-ci-demo:test
* 运维人员从 dev 向 master 发起 pull request,CI 运行单元测试,并构建镜像allovince/drone-ci-demo:latest
* 运维人员 merge pull request, 并 Release 新版本 pre-0.0.2, CI 构建镜像allovince/drone-ci-demo:pre-0.0.2

可能细心的同学会发现 dev -> Master 的 pull request 时,构建镜像失败了,这是由于 Drone 出于安全考虑限制了在 pull request 时默认无法读取加密数据,因此无法得到 Docker Registry 密码。如果是私有部署的话,可以在 Repo Settings 中勾选 Allow Pull Requests,此处就可以构建成功。
##Step.4:语义化发布
上面基本完成了一个支持团队协作的半自动 CI 工作流,如果不是特别苛刻的话,完全可以用上面的工作流开始干活了。

不过基于这个工作流工作一段时间,会发现仍然存在痛点,那就是每次发布都要想一个版本号,写 ChangeLog,并且人工去 Release。

标记版本号涉及到上线后的回滚,追溯等一系列问题,应该是一项严肃的工作,其实如何标记早已有比较好的方案,即语义化版本。在这个方案中,版本号一共有 3 位,形如 1.0.0,分别代表:

  1. 主版本号:当你做了不兼容的 API 修改,
  2. 次版本号:当你做了向下兼容的功能性新增,
  3. 修订号:当你做了向下兼容的问题修正。

虽然有了这个指导意见,但并没有很方便的解决实际问题,每次发布要搞清楚代码的修改到底是不是向下兼容的,有哪些新的功能等,仍然要花费很多时间。

而语义化发布(Semantic Release)就能很好的解决这些问题。

语义化发布的原理很简单,就是让每一次 Commit 所附带的 Message 格式遵守一定规范,保证每次提交格式一致且都是可以被解析的,那么进行 Release 时,只要统计一下距离上次 Release 所有的提交,就分析出本次提交做了何种程度的改动,并可以自动生成版本号、自动生成 ChangeLog 等。

语义化发布中,Commit 所遵守的规范称为约定式提交(Conventional Commits)。比如 Node.js、 Angular、Electron 等知名项目都在使用这套规范。

语义化发布首先将 Commit 进行分类,常用的分类(Type)有:

* feat:新功能
* fix:BUG 修复
* docs:文档变更
* style:文字格式修改
* refactor:代码重构
* perf:性能改进
* test:测试代码
* chore:工具自动生成

每个 Commit 可以对应一个作用域(Scope),在一个项目中作用域一般可以指不同的模块。

当 Commit 内容较多时,可以追加正文和脚注,如果正文起始为BREAKING CHANGE,代表这是一个破坏性变更。

以下都是符合规范的 Commit:
feat:增加重置密码功能

fix(邮件模块):修复邮件发送延迟BUG

feat(API):API重构

BREAKING CHANGE:API v3上线,API v1停止支持

有了这些规范的 Commit,版本号如何变化就很容易确定了,目前语义化发布默认的规则如下:
Commit	版本号变更
BREAKING CHANGE 主版本号
feat 次版本号
fix / perf 修订号

因此在 CI 部署 semantic-release 之后,作为开发人员只需要按照规范书写 Commit 即可,其他的都由 CI 完成。

具体如何将语义化发布加入 CI 流程中呢, semantic-release 是 js 实现的,如果是 js 的项目,可以直接在 package.json 中增加配置项,而对于任意语言的项目,推荐像 Demo 中一样,在根目录下增加 配置文件release.config.js。这个配置目的是为了禁用默认开启的 npm 发布机制,可以直接套用。

semantic-release 要执行 GitHub Release,因此我们需要在 CI 中配置自己的 Personal access tokens 让 CI 有 GitHub Repo 的读写权限, 可以通过 GitHub 点击自己头像 -> Settings -> Developer settings -> Personal access tokens -> Generate new token 生成一个 Token。 然后在 Drone 的 repo 设置界面新增一个 Secret, key 为 GITHUB_TOKEN, value 填入刚生成的 Token。

最后在 .drone.yml 中增加这样一段就可以了。
- name: semantic-release  
image: gtramontina/semantic-release:15.13.3
environment:
GITHUB_TOKEN:
from_secret: GITHUB_TOKEN
entrypoint:
- semantic-release
when:
branch: master
event: push

来再次模拟一下流程,feature 分支部分与上文相同:

* 从 dev 向 master 发起 pull request,CI 运行单元测试,并构建镜像allovince/drone-ci-demo:latest
* merge pull request,CI 会执行单元测试并运行 semantic-release , 运行成功的话能看到 GitHub 新增 Release v1.0.0
* GitHub Release 再次触发CI 构建生产环境用 Docker 镜像allovince/drone-ci-demo:1.0.0

最终我们能得到这样一个赏心悦目的 Release。
05.png

##Step.5:Kubernetes 自动发布
Docker 镜像推送到仓库后,我们还剩最后一步就可以完成全自动发布的闭环,即通知 Kubernetes 将镜像发布到生产环境。这一步实现比较灵活,因为很多云服务商在容器服务都会提供 Trigger 机制,一般是提供一个 URL,只要请求这个 URL 就可以触发容器服务的发布。Demo 中我们使用更为通用的方法,就是将 kubectl 打包为容器,以客户端调用 Kubernetes 集群 Master 节点 API(kube-apiserver)的形式完成发布。

假设我们在生产环境下 drone-ci-demo 项目的 Kubernetes 发布文件如下:
---  
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: ci-demo-deployment
namespace: default
spec:
replicas: 1
template:
spec:
containers:
- image: allovince/drone-ci-demo
name: ci-demo
restartPolicy: Always

对应 .drone.yml 中增加 step 如下。这里使用的插件是 honestbee/drone-kubernetes, 插件中 kubectl 连接 API 使用的是证书 + Token 的方式鉴权,因此需要先获得证书及 Token, 已经授权的 Token 保存于 Kubernetes Secret,可以通过 kubectl get secret [ your default secret name ] -o yaml | egrep 'ca.crt:|token:' 获得并配置到 Drone 中,注意插件要求 Token 是明文的,需要 base64 解码一下:echo [ your token ] | base64 -d && echo ''。
- name: k8s-deploy  
image: quay.io/honestbee/drone-kubernetes
settings:
kubernetes_server:
from_secret: KUBERNETES_SERVER
kubernetes_cert:
from_secret: KUBERNETES_CERT
kubernetes_token:
from_secret: KUBERNETES_TOKEN
namespace: default
deployment: ci-demo-deployment
repo: allovince/drone-ci-demo
container: ci-demo
tag:
- ${DRONE_TAG}
when:
event: tag

在示例中,可以看到在语义化发布之后 CI 会将新版本的 Docker 镜像自动发布到 Kubernetes,这里为了演示仅打印了指令并未实际运行。相当于运行了如下的指令:
kubectl -n default set image deployment/ci-demo-deployment ci-demo=allovince/drone-ci-demo:v1.0.2

由于自动发布的环节势必要接触到生产服务器,需要格外注意安全问题,首推的方式当然是将 CI 和 Kubernetes 集群放于同一内网中,同时可以使用 Kubernetes 的 RBAC 权限控制,为自动发布单独创建一个用户,并删除不必要的权限。
#后话
总结一下,本文展示了从 Hello World 到单人单分支手动发布再到团队多分支 GitFlow 工作流再到团队多分支 semantic-release 语义化发布最后到 通知 Kubernetes 全自动发布,如何从零开始一步一步搭建 CI 将团队开发、测试、发布的流程全部自动化的过程,最终能让开发人员只需要认真提交代码就可以完成日常的所有 DevOps 工作。

最终 Step 的完成品可以适配之前的所有 Step,如果不太在意实现细节的话,可以在此基础上稍作修改,直接使用。

然而写好每一个 Commit 这个看似简单的要求,其实对于大多数团队来说并不容易做到,在实施过程中,经常会遇到团队成员不理解为什么要重视 Commit 规范,每个 Commit 都要深思熟虑是否过于吹毛求疵等等疑问。

以 Commit 作为 CI 的核心,个人认为主要会带来以下几方面的影响:

  1. 一个好的 Commit,代表着开发人员对当前改动之于整个系统的影响,有非常清楚的认识,代码的修改到底算 feat 还是 fix ,什么时候用 BREAKING CHANGE 等都是要仔细斟酌的,每个 Commit 都会在 ChangeLog 里“留底”,从而约束团队不随意提交未经思考的代码,提高代码质量。
  2. 一个好的 Commit 也代表开发人员有能力对所实现功能进行精细的划分,一个分支做的事情不宜过多,一个提交也应该专注于只解决一个问题,每次提交(至少是每次 push)都应该保持系统可构建、可运行、可测试,如果能坚持做到这些,对于合并代码时的冲突解决,以及集成测试都有很大帮助。
  3. 由于每次发布能清楚的看到所有关联的 Commit 以及 Commit 的重要程度,那么线上事故的回滚也会非常轻松,回滚到哪个版本,回滚后哪些功能会受到影响,只要看 CI 自动生成的 Release 记录就一目了然。如果没有这些,回滚误伤到预期外的功能从而引发连锁反应的惨痛教训,可能很多运维都有过类似经历吧。

因此 CI 自动化其实是锦上添花而非雪中送炭,如果团队原本就无视规范,Commit 全是空白或者没有任何意义的单词,分支管理混乱,发布困难,奢望引入一套自动化 CI 来能解决所有这些问题,无疑是不现实的。而只有原本就重视代码质量,有一定规范意识,再通过自动化 CI 来监督约束,团队在 CI 的帮助下代码质量提高,从而有机会进一步改进 CI 的效率,才能形成良心循环。

愿天下不再有难发布的版本。
#Q&A
Q:Kubernetes 上主流的 CI/CD 方案是啥?
A:其实这无关 Kubernetes,从市场占有率来看,前三名分别是

  1. Jenkins
  2. JetBrains TeamCity
  3. CircleCI

来源:https://www.datanyze.com/market-share/ci

Q:GitLab 自带的 CI 与 Jenkins 和 GitLab 结合的 CI,该如何选择?想知道更深层次的理解。
A:还是要结合自己团队的实际情况做选择。从成熟度来说,肯定是 Jenkins 用户最多,成熟度最高,缺点是侧重 Java,配置相对繁琐。GitLab 自带的 CI 相对简单,可以用 yaml,和 GitLab 结合的最好,但功能肯定没有 Jenkins 全面。如果是小团队新项目,GitLab CI 又已经可以满足需求的话,并不需要上 Jenkins,如果是较大的团队,又是偏 Java 的,个人更偏向 Jenkins。

Q:Jenkins 如果不想运行在 Kubernetes 里面,该怎么和 Kubernetes 集成?
A:从 CI 的流程来说,CI 应用是不是跑在 Kubernetes 的并不重要,CI 只要能访问代码库,有权限在生产环境发布,是不是跑在容器里从效果来说其实没有区别,只是用 Kubernetes 部署 Jenkins 的话,运维的一致性比较好,运维团队不用额外花时间维护一套物理机的部署方案。

Q:Kubernetes 的回滚方案是回滚代码,重做镜像,还是先切流量,后做修改?
A:代码一定是打包到镜像里的,镜像的版本就是代码的版本,所以一定是切镜像。至于回滚操作本身,Kubernetes 已经内置了很多滚动发布(Rolling update)的策略,无论是发新版本还是回滚版本,都可以做到用户无感知。

Q:镜像大到几 G 的话如何更新部署,有什么好的实践呢,以及如何回滚?
A:几个要点:

  1. 镜像仓库部署在内网,镜像推拉走内网,几个 G 的镜像传输起来也只是秒级别的
  2. 镜像构建时将不经常变动的部分划分到 1 层,因为 Docker 镜像是分层的,这样每次拉镜像可以利用到 Docker 的缓存传输的只有镜像发生变化的部分
  3. 选择 alpine 这样尽量小的镜像

回滚如果发生在相邻的版本,一般物理机上是有缓存的,速度都很快。

Q:Drone 开放 API 服务吗?这样方便其他系统集成。
A:可以调整一下思路,直接把需要的功能做成镜像在 Drone 里调用就好了。

Q:如果有 Drone 的 Server怎么做高可用?
A:Drone serve r用 Kubernetes 部署的话本身只起到了一个任务调度的作用,很难会遇到性能瓶颈。真的有性能问题可以尝试水平扩展 Drone server,共享同一数据库。

作者博客:https://avnpc.com/pages/drone-gitflow-kubernetes-for-cloud-native-ci

以上内容根据2019年4月2日晚微信群分享内容整理。分享人徐谦,高级架构师,10 年以上研发经验,2 次创业经历。现从事互联网金融相关研发,关注 Node.js、容器技术、机器学习等。DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiesd,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。

知乎部署系统演进

JetLee 发表了文章 • 0 个评论 • 1268 次浏览 • 2019-03-28 15:44 • 来自相关话题

应用部署是软件开发中重要的一环,保持快速迭代、持续部署,减少变更和试错成本,对于互联网公司尤为重要。本文将从部署系统的角度,介绍知乎应用平台从无到有的演进过程,希望可以对大家有所参考和帮助。 知乎部署系统由知乎工程效率团队打造,服务于 ...查看全部
应用部署是软件开发中重要的一环,保持快速迭代、持续部署,减少变更和试错成本,对于互联网公司尤为重要。本文将从部署系统的角度,介绍知乎应用平台从无到有的演进过程,希望可以对大家有所参考和帮助。

知乎部署系统由知乎工程效率团队打造,服务于公司几乎所有业务,每日部署次数在 2000 次左右,在启用蓝绿部署的情况下,大部分业务的生产环境上线时间可以在 10 秒以下(不包含金丝雀灰度验证过程)。

目前知乎部署系统主要实现了以下功能:

* 支持容器、物理机部署,支持在线、离线服务、定时任务以及静态文件的部署
* 支持办公网络预上线
* 支持金丝雀灰度验证,期间支持故障检测以及自动回滚
* 支持蓝绿部署,在蓝绿部署情况下,上线和回滚时间均在秒级
* 支持部署 Merge Request 阶段的代码,用于调试

下文将按时间顺序,对部署系统的功能演进进行介绍。
#技术背景
在介绍部署系统之前,首先需要对知乎的相关基础设施和网络情况进行简单的介绍。
##知乎网络情况
知乎的网络如图所示:
1.png

知乎网络环境简图

主要划分为三个部分:

* 生产环境网络:即知乎对外的在线服务器网络,基于安全性考虑,与其他网络环境完全隔离。
* 测试环境网络:应用在部署到生产环境之前,首先会部署在测试环境,测试环境网络上与生产环境完全隔离。
* 办公室网络:即知乎员工内部网络,可以直接访问测试环境,也可以通过跳板机访问生产环境服务器。

##流量管理
知乎采用 Nginx + HAProxy 的方式管理应用的流量走向:
2.png

知乎在线业务流量架构

应用开发者在 Nginx 平台上配置好 Location 和 HAProxy 的对应关系,再由 HAProxy 将流量分发到 Real Server 上去,同时 HAProxy 承担了负载均衡、限速、熔断等功能。
##持续集成
知乎采用 Jenkins + Docker 进行持续集成,详见《知乎容器化构建系统设计和实践》,持续集成完成后,会生成 Artifact,供部署系统以及其他系统使用。
#物理机部署
像大多数公司一样,知乎最开始是以物理机部署为主,业务自行编写脚本进行部署,部署时间长、风险大、难以回滚。在这种情况下,大约在 2015 年,初版的部署系统 nami (取名自《海贼王》娜美)诞生了。

最初的部署系统采用 Fabric 作为基础,将 CI 产生的 Artifact 上传到物理机上解压,并使用 Supervisor 进行进程管理,将服务启动起来:
3.png

物理机部署

初版的部署系统虽然简单,但是为了之后的改进奠定了基础,很多基础的概念,一直到现在还在使用。
##应用(App)与服务(Unit)
与 CI 相同,每个应用对应一个 GitLab Repo,这个很好理解。

但是在实际使用过程中,我们发现,同一套代码,往往对应着多个运行时的服务,比如以部署系统 nami 本身为例,虽然是同一套代码,但是在启动的时候,又要分为:

* API 服务
* 定时任务
* Celery 离线队列

这些运行单元的启动命令、参数等各不相同,我们称之为服务(Unit)。用户需要在部署系统的前端界面上,为每个 Unit 进行启动参数、环境变量等设置,整个应用才能正常启动起来。
##候选版本(Candidate)
所有的部署都是以 CI 产生 Artifact 作为基础,由于 Artifact 的不可变性,每次部署该 Artifact 的结果都是可预期的。也就是说,每个 Artifact 都是代码的一次快照,我们称之为部署的候选版本( Candidate)。

由于每次 CI 都是由 GitLab 的 Merge Request 产生,候选版本,其实就是一次 MR 的结果,也就是一次代码变更。通常情况下,一个候选版本对应一个 Merge Request:
4.png

每个候选版本对应一个 Merge Request

如图所示是某个应用的候选版本列表,每个候选版本,用户都可以将其部署到多个部署阶段(Stage)。
##部署阶段(Stage)
上文提到,知乎服务器网络分为测试环境和生产环境,网络之间完全隔离。应用总是先部署测试环境,成功后再部署生产环境。

在部署系统上,我们的做法是,对每个候选版本的部署,拆分成多个阶段(Stage):
5.png

构建/部署阶段

图中该应用有 6 个阶段:

* (B)构建阶段:即 CI 生成 Artifact 的过程。
* (T)测试环境:网络、数据都与生产环境相隔离。
* (O)办公室阶段:一个独立的容器,只有办公室网络可以访问,其他与线上环境相同,数据与生产环境共享。
* (C)金丝雀1:生产环境 1% 的容器,外网可访问。
* (C)金丝雀2:生产环境 20% 的容器,外网可访问。
* (P)生产环境:生产环境 100% 容器,外网可访问。

部署阶段从前到后依次进行,每个 Stage 的部署逻辑大致相同。

对于每个部署阶段,用户可以单独设置,是否进行自动部署。如果所有部署阶段都选择自动部署,那么应用就处于一个持续部署(Continuous Deployment)的过程。
##基于 Consul 和 HAProxy 的服务注册与发现
每次部署物理机时,都会先将机器从 Consul 上摘除,当部署完成后,重新注册到 Consul 上。

上文提到,我们通过 HAProxy 连接到 Real Server,原理就是基于 Consul Template 对 HAProxy 的配置进行更新,使其总是指向所有 RS 列表。

另外,在迁移到微服务架构之后,我们编写了一个称为 diplomat 的基础库,从 Consul 上拉取 RS 列表,用于 RPC 以及其他场景的服务发现。
#容器部署
##旧版容器系统 Bay
2015 年末,随着容器大潮的袭来,知乎也进入容器时代,我们基于 Mesos 做了初版的容器编排系统(称为 Bay),部署系统也很快支持了容器的部署。

Bay 的部署很简单,每个 Unit 对应一个容器组,用户可以手动设置容器组的数量和其他参数。每次部署的时候,滚动地上线新版本容器,下线旧版本容器,部署完成后所有旧版本容器就都已回收。对于一些拥有数百容器的大容器组,每次部署时间最长最长可以达到 18 分钟。
##各项功能完善
在迁移到容器部署的过程中,我们对部署系统也进行了其他方面的完善。

首先是健康检查,所有 HTTP、RPC 服务,都需要实现一个 /check_health 接口,在部署完成后会对其进行检查,当其 HTTP Code 为 200 时,部署才算成功,否则就会报错。

其次是在线/离线服务的拆分,对于 HTTP、RPC 等在线业务,采用滚动部署;对于其他业务,则是先启动全量新版本容器,再下线旧版本容器。
#预上线与灰度发布
基于容器,我们可以更灵活地增删 Real Server,这使得我们可以更简单地将流量拆分到不同候选版本的容器组中去,利用这一点,我们实现了办公室网络预上线和金丝雀灰度发布。
##办公室网络预上线
为了验证知乎主站的变更,我们决定在办公室网络,提前访问已经合并到主干分支、但还没有上线的代码。我们在 Nginx 层面做了流量拆分,当访问源是办公室网络的时候,流量流向办公室专属的 HAProxy:
6.png

办公室流量拆分

对于部署系统来说,所需要做的就是在「生产环境」这个 Stage 之前,加入一个「办公室」Stage,对于这个 Stage,只部署一个容器,并将这个容器注册到办公室专属的 HAProxy,从外网无法访问此容器。
##金丝雀灰度发布
在 2016 年以前,知乎部署都是全量上线,新的变更直接全量上线到外网,一旦出现问题,很可能导致整个网站宕机。

为了解决这个问题,我们在「办公室」和「生产环境」Stage 之间,加入了「金丝雀1」和「金丝雀2」两个 Stage,用于灰度验证。

原理是,部署一定量额外的新版本容器,通过 HAProxy,随机分发流量到这些新版本容器上,这样如果新版本代码存在问题,可以在指标系统上明显看出问题:
7.png

Nginx 指标大盘

其中,「金丝雀1」阶段只启动相当于「生产环境」阶段 1% 的容器,「金丝雀2」阶段则启动 20% 数量的容器。

为了避免每次部署到金丝雀后,都依赖人工去观察指标系统,我们在部署系统上,又开发了「金丝雀自动回滚」功能。主要原理是:

* 将金丝雀阶段的指标与生产环境的指标分离
* 金丝雀部署完成后,对指标进行检测,与生产环境进行对比,如果发现异常,则销毁金丝雀容器,并通知用户
* 如果在 6 分钟内没有发现指标异常,则认为代码没有明显问题,才允许用户部署「生产环境」Stage

8.png

金丝雀出现异常,回滚时会自动通知开发者

金丝雀阶段自动监测的指标包括该应用的错误数、响应时间、数据库慢查询数量、Sentry 报错数量、移动端 App 崩溃数量等等。
#新版容器部署
针对旧版容器系统 Bay 部署速度慢、稳定性差等问题,我们将容器编排从 Mesos 切换到 Kubernetes,在此基础上开发出新一代的容器系统 NewBay。

相应地,部署系统也针对 NewBay 进行了一番改造,使得其在功能、速度上均有明显提升。
##蓝绿部署
在旧版 Bay 中,每个 Unit 对应唯一的容器组,新版本容器会覆盖旧版本容器,这会导致:

* 一旦部署失败,服务将处于中间状态,新旧版本会同时在线
* 回滚旧版本代码速度较慢,而且有可能会失败

我们设计了一套新的部署逻辑,实现了蓝绿部署,即新旧版本容器组同时存在,使用 HAProxy 做流量切换:
9.png

蓝绿部署可以有效减少回滚时间

这使得:

* 流量的切换原子化,即使部署失败也不会存在新旧版本同时在线的情况
* 由于旧版本容器组会保留一段时间,这期间回滚代码仅需要将流量切回旧版本,回滚时间可以达到秒级

##预部署
使用 NewBay 之后,大型项目的部署时间由原来的 18 分钟降至 3 分钟左右,但这其中仍有优化的空间。

为了加快部署速度,我们会在金丝雀阶段,提前将「生产环境」Stage 所需要的全量容器异步地启动起来,这样在部署「生产环境」Stage 时,仅需要将流量切换为全量即可:
10.png

预部署可以有效减少上线时间

通过这方面的优化,在全量上线到生产环境时,上线时间同样可以达到秒级。
#分支部署
以上部署均是针对代码合并到主干分支后进行的部署操作,或者可以称之为「上线流程」。

但是实际上很多情况下,我们的代码在 Merge Request 阶段就需要进行部署,以方便开发者进行自测,或者交由 QA 团队测试。

我们在 CI/CD 层面对这种情况进行了支持,主要原理是在 MR 提交或者变更的时候就触发 CI/CD,将其部署到单独的容器上,方便开发者进行访问。
11.png

多个 Merge Request 同时部署和调试

分支部署实现细节较多,篇幅所限,在此不进行展开。
#部署系统平台化
为了方便用户使用 CI/CD,管理应用资源,处理排查故障等,我们将整套知乎的开发流程进行了平台化,最终实现了 ZAE(Zhihu App Engine):
12.png

ZAE 是一套完整的开发者平台

用户可以方便地查看部署进度和日志,并进行一些常规操作:
13.png

在 ZAE 上查看部署进度
#尾声
知乎部署系统从 2015 年开始开发,到目前为止,功能上已经比较成熟。其实,部署系统所承担的责任不仅仅是上线这么简单,而是关系到应用从开发、上线到维护的方方面面。良好的部署系统,可以加快业务迭代速度、规避故障发生,进而影响到一家公司的产品发布节奏。

原文链接:https://zhuanlan.zhihu.com/p/60627311

21 个好用的持续集成工具

YiGagyeong 发表了文章 • 0 个评论 • 1734 次浏览 • 2019-03-10 18:20 • 来自相关话题

【编者的话】好用的集成工具都在这儿了,总有一款适合你。 市场上持续集成工具众多,找到一个合适的工具并非易事,下面介绍了 21 个比较受欢迎的 CI 工具,并附上了下载链接。 # 1. Buddy 对 Web 开 ...查看全部
【编者的话】好用的集成工具都在这儿了,总有一款适合你。

市场上持续集成工具众多,找到一个合适的工具并非易事,下面介绍了 21 个比较受欢迎的 CI 工具,并附上了下载链接。
# 1. Buddy
对 Web 开发者来说,Buddy 是一个智能的 CI/CD 工具,降低了 DevOps 的入门门槛。Buddy 使用 `Delivery Pipeline` 进去软件构建、测试及发布,创建 Pipeline 时,100 多个就绪的操作可随时投入使用,就像砌砖房一样。

特点:

  • 清晰的配置,友好的交互,15分钟快速配置
  • 基于变更集(changeset)的快速部署
  • 构建运行在使用缓存依赖的独立容器中
  • 支持所有流行的语言、框架和任务管理器
  • Docker / Kubernetes 专用操作手册
  • 与 AWS,Google,DigitalOcean,Azure,Shopify,WordPress 等集成
  • 支持并行和 YAML 配置
下载链接:https://buddy.works# 2. JenkinsJenkins 是一个开源的持续集成工具,使用 Java 编程语言编写的。它有助于实时检测和报告较大代码库中的单一更改。该软件可帮助开发人员快速查找和解决代码库中的问题并自动测试其构建。特点:
  • 支持海量节点扩展并在节点中同等分发工作负载
  • 在各版本Linux、Mac OS 或 Windows 等全平台轻松更新
  • 提供了 WAR 格式的简易安装包,执行导入 JEE 容器中即可运行安装
  • 可以通过 Web 界面轻松设置和配置 Jenkins
  • 可轻松跨机器分发
下载链接:https://jenkins.io/download/# 3. TeamCityTeamCity 是一款拥有很多强大功能的持续集成服务器。特点:
  • 可扩展性和自定义
  • 为项目提供更好的代码质量
  • 即使没有运行构建,也能保持 CI 服务器健康稳定
  • 可在 DSL 中配置构建
  • 项目级云配置文件
  • 全面的 VCS 集成
  • 即时构建进度报告
  • 远程运行和预先测试的提交
下载链接:https://www.jetbrains.com/teamcity/download/#section=windows# 4. Travis CITravis 是一款流行的 CI 工具,可免费用于开源项目。在托管时,不必依赖任何平台。此 CI 工具为许多构建配置和语言提供支持,如 Node,PHP,Python,Java,Perl 等。特点:
  • Travis 使用虚拟机构建应用程序
  • 可通过 Slack,HipChat,电子邮件等通知
  • 允许运行并行测试
  • 支持 Linux、Mac 以及 iOS
  • 易于配置,无需安装。
  • 强大的 API 和命令行工具
下载链接:https://github.com/travis-ci/travis-ci# 5. GoCDGoCD 是一个开源的持续集成服务器。它可轻松模拟和可视化复杂的工作流程。此 CI 工具允许持续交付,并为构建 CD Pipeline 提供直观的界面。特点:
  • 支持并行和顺序执行,可以轻松配置依赖
  • 随时部署任何版本
  • 使用 Value Stream Map 实时可视化端到端工作流程
  • 安全地部署到生产环境
  • 支持用户身份验证和授权
  • 保持配置有序
  • 有大量的插件增强功能
  • 活跃的社区帮助和支持
下载链接:https://www.gocd.org/download/# 6. BambooBamboo 是一个持续集成的构建服务器,可以自动构建、测试和发布,并可与 JIRA 和 Bitbucket 无缝协作。Bamboo 支持多语言和平台,如 CodeDeply、Ducker、Git,SVN、Mercurial、AWS 及 Amazon S3 bucket。特点:
  • 可并行运行批量测试
  • 配置简单
  • 分环境权限功能允许开发人员和 QA 部署到他们的环境
  • 可以根据 repository 中检测到的更改触发构建,并从 Bitbucket 推送通知
  • 可托管或内部部署
  • 促进实时协作并与 HipChat 集成
  • 内置 Git 分支和工作流程,并自动合并分支
下载链接:https://www.atlassian.com/software/bamboo# 7. Gitlab CIGitLab CI 是 GitLab 的一部分。它是一个提供 API 的 Web 应用程序,可将其状态存储在数据库中。GitLab CI 可以管理项目并提供友好的用户界面,并充分利用 GitLab 所有功能。特点:
  • GitLab Container Registry 是安全的 Docker 镜像注册表
  • GitLab 提供了一种方便的方法来更改 issue 或 merge request 的元数据,而无需在注释字段中添加斜杠命令
  • 为大多数功能提供 API,允许开发人员进行更深入的集成
  • 通过发现开发过程中的改进领域,帮助开发人员将他们的想法投入生产
  • 可以通过机密问题保护您的信息安全
  • GitLab 中的内部项目允许促进内部存储库的内部 sourcing
下载链接:https://about.gitlab.com/installation/# 8. CircleCICircle CI 是一个灵活的 CI 工具,可在任何环境中运行,如跨平台移动应用程序、Python API 服务器或 Docker 集群,该工具可减少错误并提高应用程序的质量。特点:
  • 允许选择构建环境
  • 支持多语言及平台,如Linux,包括C ++,Javascript,NET,PHP,Python 和 Ruby
  • 支持 Docker,可以配置自定义环境
  • 触发较新的构建时,自动取消排队或正在运行的构建
  • 跨多容器分割和平衡测试,以减少总体构建时间
  • 禁止非管理员修改关键项目配置
  • 通过发送无错误的应用程序提高 Android 和 iOS 商店评级
  • 最佳缓存和并行性能,实现高性能
  • 与 VCS 工具集成
下载链接:https://circleci.com/# 9. CodeshipCodeship 是一个功能强大的 CI 工具,可自动化开发和部署工作流程。Codeship 通过简化到 repository 的 push 来触发自动化工作流程。特点:
  • 可完全控制 CI 和 CD 系统的设计。
  • 集中的团队管理和仪表板
  • 轻松访问调试版本和 SSH,有助于从 CI 环境进行调试
  • 可完全定制和优化 CI 和 CD 工作流程
  • 允许加密外部缓存的 Docker 镜像
  • 允许为您的组织和团队成员设置团队和权限
  • 有两个版本1)Basic 和 2)Pro
下载链接:https://codeship.com/# 10. BuildbotBuildbot 是一个软件开发 CI,可以自动完成编译/测试周期。它被广泛用于许多软件项目,用以验证代码更改。它提供跨平台 Job 的分布式并行执行。特点:
  • 为不同体系结构的多个测试主机提供支持。
  • 报告主机的内核崩溃
  • 维护单源 repository
  • 自动化构建
  • 每个提交都在集成机器上的主线上构建
  • 自动部署
  • 开源
下载链接:https://buildbot.net/# 11. NevercodeNevercode 是一个基于云端的 CI 传送服务器,可以构建、测试和分发应用程序而无需人工交互。此 CI 工具自动为每个提交构建项目,并在模拟器或真实硬件上运行所有单元测试 或 UI 测试。特点:
  • 基于云服务,因此无需维护服务器
  • 易于学习和使用
  • 良好的文档,易于阅读和理解
  • 通过持续集成和交付自动化整个开发过程
  • 与众多工具集成
下载链接:https://nevercode.io/# 12. IntegrityIntegrity 是一个持续集成服务器,仅适用于 GitHub。在此 CI 工具中,只要用户提交代码,它就构建并运行代码。它还会生成报告并向用户提供通知。 特点:
  • 目前仅适用于 Git,但它可以轻松地映射其他 SCM
  • 支持多通知机制,如 AMQP,电子邮件,HTTP,Amazon SES,Flowdock,Shell 和 TCP
  • HTTP 通告功能将以 HTTP POST 请求发送到特定URL
下载链接:http://integrity.github.io/# 13. StriderStrider 是一个开源工具,用 Node.JS / JavaScript 编写。它使用 MongoDB 作为后端存储。因此,MongoDB 和 Node.js 对于安装此 CI 至关重要。该工具为不同的插件提供支持,这些插件可修改数据库 schema 并注册HTTP路由。特点:
  • Strider 可与 GitHub,BitBucket,Gitlab 等集成。
  • 允许添加钩子来执行构建操作
  • 持续构建和测试软件项目
  • 与 GitHub 无缝集成
  • 发布和订阅 socket 事件
  • 支持创建和修改 Striders 用户界面
  • 强大的插件,定制默认功能
  • 支持 Docker
下载链接:https://github.com/Strider-CD/strider# 14. AutoRABITAutoRABIT 是一个端到端的持续交付套件,可以加快开发过程。它简化了完整的发布流程,并可以帮助任何规模的组织实现持续集成。特点:
  • 专门设计用于在 Salesforce Platform 上部署
  • 支持基于 120 多种元数据类型的更改,实现精简和快速部署
  • 从版本控制系统获取更改并自动部署到 Sandbox 中
  • 直接从 Sandbox 自动向版本控制系统提交更改
下载链接:http://www.autorabit.com/tag/autorabit-download/# 15. FinalBuilderFinalBuilder 是 VSoft 的构建工具。使用 FinalBuilder,无需编辑 XML 或编写脚本。在使用 Windows 调度程序调度构建脚本时,可以定义和调试构建脚本,或者与 Jenkins,Continua CI 等集成。特点:
  • 以逻辑结构化的图形界面呈现构建过程
  • 使用 try 和 catch 操作处理本地错误
  • 与 Windows 调度服务紧密集成,支持定时构建
  • 支持十几个版本控制系统
  • 提供脚本支持
  • 构建过程中所有操作的输出都将定向到构建日志
下载链接:https://www.finalbuilder.com/downloads/finalbuilde# 16. WerckerWercker 是一个 CI 工具,可自动构建和部署容器。它可以创建可以通过命令行界面执行的自动化管道。特点:
  • 与 GitHub 和 Bitbucket 完全集成
  • 使用 Wercker CLI 进行更快的本地迭代
  • 同时执行构建以保持团队的机动
  • 运行并行测试以减少团队的等待时间
  • 集成了 100 多种外部工具
  • 通过产品和电子邮件接收系统通知
下载链接:http://www.wercker.com/# 17. BuildkiteBuildkite 代理是一个可靠的跨平台构建工具。此 CI 工具可以在础架构上轻松地运行自动构建。它主要用于运行构建 Job,报告 Job 的状态代码并输出日志。特点:
  • 可在各种操作系统和体系结构上运行
  • 可以从任何版本控制系统运行代码
  • 允许在计算机上运行任意数量的构建代理
  • 可与 Slack,HipChat,Flowdock,Campfire 等工具集成
  • 永远不会读取源代码或密钥
  • 提供稳定的基础设施
下载链接:https://buildkite.com/# 18. SemaphoreSemaphore 是一个持续集成工具,只需按一下按钮即可测试和部署代码。它支持多种语言、框架并可与 GitHub 集成,还可以执行自动测试和部署。特点:
  • 配置简单
  • 允许自动并行测试
  • 市场上最快的 CI 之一
  • 可以轻松覆盖不同大小的项目数量
  • 与 GitHub 和 Bitbucket 无缝集成
下载链接:https://semaphoreci.com# 19. CruiseControlCruiseControl 既是 CI 工具又是一个可扩展的框架。它用于构建自定义连续的构建。它有许多用于各种源代码控制的插件,包括针对电子邮件和即时消息的构建技术。特点:
  • 与许多不同的源代码控制系统集成,如 vss,csv,svn,git,hg,perforce,clearcase,filesystem 等。
  • 允许在单个服务器上构建多个项目
  • 与其他外部工具集成,如 NAnt,NDepend,NUnit,MSBuild,MBUnit 和 Visual Studio
  • 支持远程管理
下载链接:http://cruisecontrol.sourceforge.net/download.html# 20. BitriseBitrise 是一个持续集成和交付 PaaS,它可以为整个团队提供移动持续集成和交付。它允许与 Slack,HipChat,HockeyApp,Crashlytics 等许多流行服务集成。特点:
  • 允许在终端中创建和测试工作流程
  • 无需手动控制即可获得应用程序
  • 每个构建在其自己的虚拟机中单独运行,并且在构建结束时丢弃所有数据
  • 支持第三方 beta 测试和部署服务
  • 支持 GitHub Pull Request
下载链接:https://github.com/bitrise-io/bitrise#install-and-setup# 21. UrbanCodeIBM UrbanCode 是一个 CI 应用程序。它将强大的可见性,可追溯性和审计功能整合到一个软件包中。特点:
  • 通过自动化,可重复的部署流程提高软件交付频率
  • 减少部署失败
  • 简化多渠道应用程序的部署,无论是在本地还是在云中,都可以部署到所有环境
  • 企业级安全性和可扩展性
  • 混合云环境建模
  • 拖放自动化

下载链接:https://www.ibm.com/ms-en/marketplace/application-release-automation

原文链接:20 Best Continuous Integration(CI) Tools in 2019 (翻译:李加庆

即便一个小项目也有它的CI/CD流水线

colstuwjx 发表了文章 • 0 个评论 • 888 次浏览 • 2019-02-08 21:49 • 来自相关话题

【编者的话】本文作者通过一个简单的小项目详细介绍了如何使用Docker,GitLab,Portainer等组件搭建一套CICD流水线。 现如今,使用市面上的一些工具配置一套简单的CI/CD流水线并不是一件难事。给一个副项目弄一套这样的 ...查看全部
【编者的话】本文作者通过一个简单的小项目详细介绍了如何使用Docker,GitLab,Portainer等组件搭建一套CICD流水线。

现如今,使用市面上的一些工具配置一套简单的CI/CD流水线并不是一件难事。给一个副项目弄一套这样的流水线也是一个学习许多东西的好方法。Docker,Gitlab,Portainer这些优秀的组件可以用来搭建这个流水线。
# 示例项目
作为一名法国索菲亚科技园区(位于法国南部)的技术活动组织者,我经常被问到是否有办法知道所有即将举行的活动(会议,灌水,由当地协会组织的聚会等……)。由于此前并没有一个单独的地方列出所有的这些活动,我便开发了https://sophia.events ,这是一个非常简单的网站页面,它会尝试维护一份最新的活动列表。此项目的代码可以在 GitLab 上找到。

声明:这个项目超级简单,但是项目本身的复杂度并不是本文的重点。这里我们将详细介绍到的CI/CD流水线的各个组件可以用几乎相同的方式应用到更复杂的项目上。它们也非常适合微服务的场景。
# 快速过一下代码
为了简化起见,这里有一份events.json文件,每个新事件均会被添加到里面。该文件的部分内容见下面的代码段(抱歉里面掺杂了一些法语):
 {
“events”: [
{
“title”: “All Day DevOps 2018”,
“desc”: “We’re back with 100, 30-minute practitioner-led sessions and live Q&A on Slack. Our 5 tracks include CI/CD, Cloud-Native Infrastructure, DevSecOps, Cultural Transformations, and Site Reliability Engineering. 24 hours. 112 speakers. Free online.”,
“date”: “17 octobre 2018, online event”,
“ts”: “20181017T000000”,
“link”: “https://www.alldaydevops.com/",
“sponsors”: [{“name”: “all-day-devops”}]
},
{
“title”: “Création d’une Blockchain d’entreprise (lab) & introduction aux smart contracts”,
“desc”: “Venez avec votre laptop ! Nous vous proposons de nous rejoindre pour réaliser la création d’un premier prototype d’une Blockchain d’entreprise (Lab) et avoir une introduction aux smart contracts.”,
“ts”: “20181004T181500”,
“date”: “4 octobre à 18h15 au CEEI”,
“link”: “https://www.meetup.com/fr-FR/IBM-Cloud-Cote-d-Azur-Meetup/events/254472667/",
“sponsors”: [{“name”: “ibm”}]
},

]
}

此文件将会被一个mustache模板渲染并生成最终的网站素材。
## Docker多阶段构建
一旦生成了最终的网站素材,它们将会被拷贝到一个Nginx镜像里,该镜像将会被部署到目标机器上。

得益于多阶段构建(multi-stage build),本次构建分为两部分:

* 网站素材的生成
* 包含网站素材的最终镜像的创建

用来构建镜像的Dockerfile如下:
# 生成素材
FROM node:8.12.0-alpine AS build
COPY . /build
WORKDIR /build
RUN npm i
RUN node clean.js
RUN ./node_modules/mustache/bin/mustache events.json index.mustache > index.html

# 构建托管它们的最终镜像
FROM nginx:1.14.0
COPY --from=build /build/*.html /usr/share/nginx/html/
COPY events.json /usr/share/nginx/html/
COPY css /usr/share/nginx/html/css
COPY js /usr/share/nginx/html/js
COPY img /usr/share/nginx/html/img

## 本地测试
为了测试生成站点,只需克隆该仓库然后运行test.sh脚本即可。它将随后创建出一个镜像并运行一个容器:
$ git clone git@gitlab.com:lucj/sophia.events.git

$ cd sophia.events

$ ./test.sh
Sending build context to Docker daemon 2.588MB
Step 1/12 : FROM node:8.12.0-alpine AS build
---> df48b68da02a
Step 2/12 : COPY . /build
---> f4005274aadf
Step 3/12 : WORKDIR /build
---> Running in 5222c3b6cf12
Removing intermediate container 5222c3b6cf12
---> 81947306e4af
Step 4/12 : RUN npm i
---> Running in de4e6182036b
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN www@1.0.0 No repository field.
added 2 packages from 3 contributors and audited 2 packages in 1.675s
found 0 vulnerabilities
Removing intermediate container de4e6182036b
---> d0eb4627e01f
Step 5/12 : RUN node clean.js
---> Running in f4d3c4745901
Removing intermediate container f4d3c4745901
---> 602987ce7162
Step 6/12 : RUN ./node_modules/mustache/bin/mustache events.json index.mustache > index.html
---> Running in 05b5ebd73b89
Removing intermediate container 05b5ebd73b89
---> d982ff9cc61c
Step 7/12 : FROM nginx:1.14.0
---> 86898218889a
Step 8/12 : COPY --from=build /build/*.html /usr/share/nginx/html/
---> Using cache
---> e0c25127223f
Step 9/12 : COPY events.json /usr/share/nginx/html/
---> Using cache
---> 64e8a1c5e79d
Step 10/12 : COPY css /usr/share/nginx/html/css
---> Using cache
---> e524c31b64c2
Step 11/12 : COPY js /usr/share/nginx/html/js
---> Using cache
---> 1ef9dece9bb4
Step 12/12 : COPY img /usr/share/nginx/html/img
---> e50bf7836d2f
Successfully built e50bf7836d2f
Successfully tagged registry.gitlab.com/lucj/sophia.events:latest
=> web site available on http://localhost:32768

我们可以使用上述输出的末尾提供的URL访问网站页面。
1.png

# 目标环境
## 云厂商创建的一台虚拟机
或许你也注意到了,这个网站并不是那么关键(每天只有几十次访问),也因此它只需要跑在一台单个的虚拟机上即可。该虚拟机是由Exoscale,一个伟大的欧洲云厂商,它上面的Docker Machine创建出来的。

顺便一提,如果你想试试Exoscale的服务的话,知会我一声,我可以提供20欧元的优惠券。
## 以Swarm模式启动的Docker守护进程
在上面这台虚拟机上运行的Docker守护进程被配置成以Swarm模式运行,因此它支持使用Docker Swarm原生提供的stack,service,config以及secret等原语和它强大(且易于使用)的编排功能。
## 以docker stack形式运行的应用
下述文件内容里定义了一个包含网站素材的nginx web服务器作为一个服务(service)运行。
version: "3.7"
services:
www:
image: registry.gitlab.com/lucj/sophia.events
networks:
- proxy
deploy:
mode: replicated
replicas: 2
update_config:
parallelism: 1
delay: 10s
restart_policy:
condition: on-failure
networks:
proxy:
external: true

这里有几处需要解释下:

* 镜像存储在托管到gitlab.com的私有镜像仓库(这里没涉及到Docker Hub)
* 服务是以2个副本的形式运行在副本模式下,这也就意味着同一时间该服务会有两个正在运行中的任务/容器。Swarm的service会关联一个VIP(虚拟IP地址),这样一来目标是该服务的每个请求会在两个副本之间实现负载均衡。
* 每次完成服务更新时(部署一个新版本的网站),其中一个副本会被更新,然后在10秒后更新第二个副本。这可以确保在更新期间整个网站仍然可用。我们也可以使用回滚策略,但是在这里没有必要。
* 服务会被绑定到一个外部的代理网络,这样一来TLS termination(在Swarm里部署的,跑在另外一个服务里,但是超出本项目的范畴)可以发送请求给www服务。

要运行这个stack只需要执行如下命令:
$ docker stack deploy -c sophia.yml sophia_events

## 统御一切的Portainer
Portainer是一套很棒的Wbe UI工具,它可以很方便地管理Docker宿主机和Docker Swarm集群。下面是Portainer操作界面的一张截图,里面列出了Swarm集群里当前可用的stack。
2.png

当前设定下有3个stack:

* Portainer自己
* 包含了跑着我们网站的服务的sophia_events
* tls,TLS termination服务

如果列出跑在sophia_events stack里的www服务的明细的话,我们将可以看到该服务的webhook已经处于激活状态。Portainer 1.19.2(迄今为止最新的版本)已经加入了这一功能的支持,它允许定义一个HTTP Post端点,可以在被调用后触发一次服务的更新。正如我们稍后将会看到的,GitLab runner会负责调用这个webhook。
3.png

备注:从屏幕截图中可以看到,笔者是通过localhost:8888这个地址访问Portainer的用户界面。由于笔者不想将Portainer实例对外暴露,因此是通过SSH隧道访问,该隧道可以通过如下命令开启:
ssh -i ~/.docker/machine/machines/labs/id_rsa -NL 8888:localhost:9000 $USER@$HOST

这样一来,目标是本地机器上的8888端口的所有请求均会通过SSH转发到虚拟机上的9000端口上。9000端口是Portainer在虚拟机上运行时监听的端口,但是并未对外开放,因为它被Exoscale配置的一个安全组禁用了。

备注:在上述命令里,用来连接虚拟机的ssh key是在虚拟机创建时由Docker Machine生成的一个key。
## GitLab runner
Gitlab的runner是一个负责执行定义在.gitlab-ci.yml文件里的一组action的进程。就我们这个项目来说,我们定义了一个我们自己的runner,它在虚拟机上以一个容器的形式运行。

第一步就是带上一堆参数来注册该runner。
CONFIG_FOLDER=/tmp/gitlab-runner-config
docker run — rm -t -i \
-v $CONFIG_FOLDER:/etc/gitlab-runner \
gitlab/gitlab-runner register \
--non-interactive \
--executor "docker" \
—-docker-image docker:stable \
--url "https://gitlab.com/" \
—-registration-token "$PROJECT_TOKEN" \
—-description "Exoscale Docker Runner" \
--tag-list "docker" \
--run-untagged \
—-locked="false" \
--docker-privileged

在上述参数中,PROJECT_TOKEN可以在GitLab.com的项目页面上找到,并可以用来注册外部的runner。
4.png

用来注册一个新的runner的注册token。

一旦runner注册上了,我们需要启动它:
CONFIG_FOLDER=/tmp/gitlab-runner-config
docker run -d \
--name gitlab-runner \
—-restart always \
-v $CONFIG_FOLDER:/etc/gitlab-runner \
-v /var/run/docker.sock:/var/run/docker.sock \
gitlab/gitlab-runner:latest

等到它注册上了而且启动起来了,该runner便会出现在GitLab.com上的项目页面里。
5.png

为此项目创建的runner。

每当有新的commit推送到仓库,此runner随后便会接收到一些要做的任务。它会按顺序执行.gitlab-ci.yml文件里定义好的测试、构建和部署几个阶段。
variables:
CONTAINER_IMAGE: registry.gitlab.com/$CI_PROJECT_PATH
DOCKER_HOST: tcp://docker:2375
stages:
- test
- build
- deploy
test:
stage: test
image: node:8.12.0-alpine
script:
- npm i
- npm test
build:
stage: build
image: docker:stable
services:
- docker:dind
script:
- docker image build -t $CONTAINER_IMAGE:$CI_BUILD_REF -t $CONTAINER_IMAGE:latest .
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com
- docker image push $CONTAINER_IMAGE:latest
- docker image push $CONTAINER_IMAGE:$CI_BUILD_REF
only:
- master
deploy:
stage: deploy
image: alpine
script:
- apk add --update curl
- curl -XPOST $WWW_WEBHOOK
only:
- master


* 测试阶段(test stage)将会运行一些预备检查,确保events.json文件格式正确,并且这里没有遗漏镜像
* 构建阶段(build stage)会做镜像的构建并将它推送到GitLab上的镜像仓库
* 部署阶段(deploy stage)将会通过发送给Portainer的一个webhook触发一次服务的更新。WWW_WEBHOOK变量的定义可以在Gitlab.com上项目页面的CI/CD设置里找到。

6.png

备注:

* runner在Swarm上是以一个容器的形式运行。我们可以使用一个共享的runner,这是一些公用的runner,它们会在托管到GitLab的不同项目所需的任务之间分配时间。但是,由于runner需要访问Portainer的端点(用来发送webhook),也因为笔者不希望Portainer能够从外界访问到,将runner跑在集群里会更安全一些。

* 再者,由于runner跑在一个容器里,为了能够通过Portainer暴露在宿主机上的9000端口连到Portainer,它会将webhook请求发送到Docker0桥接网络上的IP地址。也因此,webhook将遵循如下格式:http://172.17.0.1:9000/api[…]a7-4af2-a95b-b748d92f1b3b

# 部署流程
新版本的站点更新遵循如下流程:
7.png


  1. 一个开发者推送了一些变更到GitLab。这些变更基本上囊括了events.json文件里一个或多个新的事件加上一些额外赞助商的logo。
  2. Gitlab runner执行在.gitlab-ci.yml里定义好的一组action。
  3. Gitlab runner调用在Portainer中定义的webhook。
  4. 在接收到webhook后,Portainer将会部署新版本的www服务。它通过调用Docker Swarm的API实现这一点。Portainer可以通过在启动时绑定挂载的/var/run/docker.sock套接字来访问该API。

如果你想知道更多此unix套接字用法的相关信息,也许你会对之前这篇文章《Docker Tips : about /var/run/docker.sock》感兴趣。

  1. 随后,用户便能看到新版本的站点。

## 示例
让我们一起来修改代码里的一些内容随后提交/推送这些变更。
$ git commit -m 'Fix image'

$ git push origin master

如下截图展示了GitLab.com上的项目页面里的commit触发的流水线作业。
8.png

在Portainer一侧,它将会收到一个webhook请求,随后会执行一次服务的更新操作。这里可能看不太清,但是一个副本已经完成了更新,通过第二个副本可以访问站点。随后,几秒钟之后,第二个副本也更新完毕。
9.png

## 小结
即便对于这样一个小项目,为它建立一套CI/CD流水线也是一个很好的练习,尤其是可以更加熟悉GitLab(这一直在笔者要学习的列表里面),它是一个非常出色而且专业的产品。这也是一次体验大家期待已久的Portainer的最新版本(1.19.2)推出的webhook功能的机会。此外,对于像这样的副项目,Docker Swarm的使用是无脑上手的,很酷而且易于使用......

原文链接:Even the smallest side project deserves its CI/CD pipeline(译者:吴佳兴)

容器环境应用一键拉起实践

大卫 发表了文章 • 0 个评论 • 938 次浏览 • 2019-01-18 20:43 • 来自相关话题

#背景 ##PaaS平台 基于唯品会Noah云平台,我们已经在内部研发环境建立了一整套PaaS基础设施,实现对开发和测试过程的支撑。通过PaaS平台上的CI/CD流水线,实现了从代码到镜像到环境部署的整体流程;通过自研的环境管理功能(P ...查看全部
#背景
##PaaS平台
基于唯品会Noah云平台,我们已经在内部研发环境建立了一整套PaaS基础设施,实现对开发和测试过程的支撑。通过PaaS平台上的CI/CD流水线,实现了从代码到镜像到环境部署的整体流程;通过自研的环境管理功能(Pandora),实现了对多种不同内部开发测试部署环境的管理,支撑业务快速创建部署环境和利用容器镜像拉起应用,提高业务开发效率。
01.jpg

如上图所示,我们的PaaS容器部署环境目前分为功能测试环境,联调测试环境和回归测试环境三大类,其中功能测试环境包含了多个有业务开发灵活实时创建的隔离环境,用于功能验证使用,而联调和回归环境部署了所有可以上云的应用,用于各类应用的集成大联调以及上线前的回归测试验证。各个环境间通过Kubernetes的namespace进行隔离,同时在微服务应用级别也通过我们微服务基础设施提供的分区功能实现逻辑上的配置和路由隔离,从而基于同一套Kubernetes集群提供面向业务的多个逻辑上独立的环境。
##功能环境与基础设施
针对功能环境的管理,PaaS平台的Pandora组件通过图形化的配置界面,对用户隔离了Kubernetes平台的各项细节,在Noah云基础上极大的简化了Kubernetes上的应用部署和管理。主要提供了以下功能:

* 逻辑隔离环境的管理;
* 应用镜像的部署,参数化配置和应用实例管理;
* 基础资源服务拉起:在Kubernetes集群中为单独环境独立拉起Memcached,Redis,MySQL等服务;
* 基础组件设施的集成和管理:为了支撑微服务应用生态,我们有配置中心,消息中间件服务,数据库中间件服务等。这些基础服务通过逻辑分区的方式可以灵活的提供独立环境支持;
* 服务路由管理:在微服务架构下,为实现一个功能链路,单独一个应用是无法独立完成的,往往是需要依赖与其它的应用服务。被依赖的服务可以在环境内部直接访问,或者通过环境依赖的配置将请求动态路由到其它预先指定的环境内的应用(这里除了功能环境,当然也包含联调和回归环境,最大层度的重用已有服务)。

2.jpg

##实际问题
尽管通过PaaS平台的Pandora图形界面极大的简化了在环境创建和应用部署管理工作,应用本身的部署依赖不是单单依靠容器的编排可以解决的问题。

通常我们部署一个容器化的应用前,需要为这个应用在配置中心创建/修改相关的配置,需要为隔离环境开通消息服务,需要准备新的数据库、Schema和初始数据或Redis容器实例为独立的应用服务,等等一系列的操作。

对于一个应用的业务测试部署人员而言,这些信息相对容易获取,但是其他人需要在一个环境中部署自己或其它开发组织开发的应用用于测试和联调时,就必须依赖阅读现有文档或者需求其他人的帮助以便正确配置依赖的基础服务,这同时也是一项比较耗时和容易出错的工作。这些准备工作往往会占用比部署应用本身更多的时间和精力。

我们希望达到的目标是能够非常快速的和可重复的一键拉起环境和应用,自动化的解决各项依赖问题,从而极大的减少人工工作,使得业务开发专注于业务的创新,从各项环境部署和管理工作中解脱出来,提高工作效率。

对于一个自动化的部署流程,我们期望是可以在分钟级别完成整套独立环境的创建,各项基础服务的开通,数据资源容器的拉起和数据初始化以及应用的启动。并且整个流程的执行是可以动态创建和监控的,从而可以同时作为一个工具服务提供给外部自动化测试以及软件工程管理系统使用。
#应用场景
对于一键拉起功能,主要有三大应用场景和阶段:

* 应用的开发和集成测试环境:通过版本化的环境定义,自动创建环境和应用以及相关数据服务和Mock服务,从而实现组件自动化测试中的全自动化部署。
* 软件项目的生命周期自动化的一部分:将实际测试环境管理创建工作从开发和测试团队工作中分离出来。开发测试只需要定义软件相关依赖和配置,各种环境完全根据配置创建出来,并且在项目结束后自动回收。作为整体项目周期,实现从项目创建开始,关联代码repo和分支,代码质量,部署配置,自动化测试,软件部署环境以及项目结束周期。
* 基于Noah云的整体CI/CD流程关键支撑:通过将版本化的环境定义作为单一数据源,实现研发测试流程中环境的创建测试验证过程中配置数据的管理标准化,进而打通测试完成发布上线步骤的配置变更自动识别生成对比,实现和推动全CI/CD流程自动化。

目前我们主要关注的是第一部分,即应用的开发和集成测试环境的快速自动化环境,用于简化开发测试阶段的环境管理。
##一键拉起与开发/集成测试环境
目前在业务团队中,开发与测试还是遵循比较传统的分工方式:开发完成代码修改,在测试环境或者本地完成简单功能验证后提交测试部署和完成测试验收。其中的自动化过程主要有单元测试,集成测试和系统测试。通常开发只负责单元测试和部分可在本地运行的集成测试自动化,其余涉及数据准备和环境部署的测试代码通常由测试团队完成和维护。

当我们需要一个敏捷团队以最快的速度来交付稳定的产品时,这种模式无法很好的帮助我们的业务开发团队在保证高质量的同时提高交付速度:

* 测试开发分离,需要更多的沟通和协调;
* 可用环境有限,提测需要排队,多个不同功能分支无法并发执行测试;
* 反馈周期长,开发阶段不能发现的问题需要等到提测合并到发布分支后才能得到反馈。

相对传统单元测试而言(更多偏向于单个类/文件本身逻辑的测试),目前更多的测试用例实际上是类似集成测试的,对于环境包括数据库、缓存以及其他服务有一定的依赖。这种模式下相应带来的好处在于我们可以更多的按照实际业务流程以BDD(Behavior-driven development)的方式来进行测试设计开发,专注于重要的业务需求。这也要求开发人员能够更多的参与到自动化集成测试用例的设计与执行过程中去,从而更快的获取测试执行结果,了解和解决出现的问题。

目前的自动化测试运行分两类:

* 类似单元测试,但是依赖数据库/缓存和简单mock服务运行:共享外部数据库资源,没有隔离,容易发生冲突,需要协调;
* AutoV集成测试,需要本地启动数据库/缓存以及依赖的服务或mock来运行:本地资源占用大,运行和调试比较费时,速度和体验不佳。

对于开发阶段,单个应用的自动化集成测试,主要需要关注的问题包括:

* 测试环境应该尽量只包含待测应用和相关基础服务依赖,如数据库、缓存等,其余需要依赖的服务尽量以mock形式配置,减少不确定性和环境复杂性;
* 测试环境可以随时可重复的创建和销毁,保证环境和测试的可重复性以及有效利用资源,更多的运行测试;
* 应用的行为可以通过参数化的方式来配置或者实时控制(例如通过配置中心下发配置改变应用行为)。

同样,对于同一产品线下面多个应用的集成测试也是需要遵循类似的原则,保证环境的可重复创建性以及通过Mock来隔离外部的依赖。

通过一键拉起方式管理环境,可以极大的简化环境的部署配置工作,让一般比较少接触应用部署的开发人员也可以方便快速的获取单独的开发/测试环境,进而推动和方便开发人员编写和运行更多的自动化测试用例,尽早发现问题,提高整体的交付效率。
#解决方案与工作流程
要解决实际使用中的困难,统一定义和维护一套适配唯品会应用基础设施实际情况的标准化应用模型是首先需要解决的问题。我们的应用模型具体会体现为应用描述的yaml文件,可以单独和自动化测试工程保存在git repo中,也可以与应用包和镜像的版本关联,保存到应用目录服务中,从而和应用一起实现版本化的同步管理。

针对我们的实际情况,应用会需要部署到不同的环境中,在不同环境需要对应用的配置按需调整,所以我们需要将应用部署描述和环境部署描述做一个分离,从而实现应用和环境描述的解耦,通过环境对应用的引用来自动化的动态组合各个应用。

对于单个应用而言,部署时除了标准的容器CPU/内存需求之外,还会有如下应用相关的配置元素:

* 应用启动需要的环境变量配置;
* 应用在配置中心的各种变量配置;
* 应用需要创建的消息队列;
* 应用需要消费的消息队列;
* 应用对于数据资源服务的要求:MC,Redis,MySQL以及通过数据中间件接入的配置;
* 应用对数据库Schema/初始数据的配置。

3.jpg

相应在部署阶段,会有对环境的描述要求,包括:

* 环境基础服务的依赖;
* 环境资源服务:是否有公用资源服务,还是需要单独拉起资源使用,资源如何分配给每个应用;
* 环境需要部署的应用,版本,系统资源分配;
* 环境依赖信息:额外的域名解析配置,对其他环境服务的依赖,Mock服务依赖。

对于一个最简单的环境部署配置而言,只需要声明需要包含的应用和版本,系统应该自动根据应用部署描述生成环境配置:

* 环境基础服务依赖:预先配置好可用的基础服务集合,部署只需要声明需要哪一组服务;
* 系统资源分配:按应用类型默认值或者使用应用最低要求;
* 资源服务的拉起:按应用描述要求收集汇总,拉起共享服务供应用使用;
* 环境依赖:需单独声明,和环境相关。

4.jpg

整体而言,我们需要实现:

* 通过流水线生成收集版本化的应用描述模板,生成应用目录;
* 基于应用目录,结合软件项目,定义每个项目的环境部署需求,生成环境部署描述;
* 基于应用描述和环境描述自动构建独立部署环境和应用配置:

* 自动关联环境依赖的各项基础服务;
* 自动创建所需数据服务资源,并且与特定应用自动绑定;
* 自动部署应用集成版本到环境,自动处理环境更新;
* 自动处理应用对环境的各项依赖。

##自动化测试的开发和运行
基于以上解决方案,对于开发人员,我们可以比较方便的通过环境描述的声明来快速的获取一个用于开发的部署环境。

在最简单的情况下,通过声明需要的应用版本即可快速创建一个环境供使用。需要依赖的服务,可以通过对Vmock的配置获取Mock服务或者直接在当前环境中拉起实例使用。由于预先提供的应用描述已经包含数据库、缓存等信息,对应的服务也会自动在这个隔离的环境中创建出来,并且初始化完成。

对于本地测试的开发,可以直接连接这个环境运行各项测试用例而不会干扰其他人的工作或者被其他正在执行的测试干扰。

对于持续集成,也可以在代码提交编译通过后自动创建单独隔离环境运行。从而快速的获取反馈,了解修改带来的影响以及是否成功完成。

同时,如果应用已经集成了Flyway的数据迁移工具,流水线可以自动打包相关迁移脚本,从而在环境创建/应用更新过程中执行迁移,保证数据库的自动同步,让数据库版本和应用版本一起管理,减少数据不一致带来额外排查负担。

更进一步,对于开发过程中的联调,我们也不再需要依赖某一个特定环境。对方应用提供一个简单的应用配置放入环境描述以后,我们就可以在隔离环境快速的获取一个用于联调的应用实例,更好的对接口功能进行验证。
#架构与实现
5.jpg

如前所述,我们在Pandora环境管理应用中实现了一键拉起的功能并且结合应用目录服务提供给脚本,项目管理工具和自动化测试使用。应用部署功能主要通过Noah提供的API实现容器镜像部署,同时也通过监控Kubernetes事件来实现应用和服务启动过程的跟踪,驱动整个环境拉起工作流。
##核心功能设计与实现
一键拉起的整体基于Pandora已经实现的环境管理功能,核心部分在于通过应用模型和环境模型的解析,根据声明和依赖动态生成环境、应用拉起工作流,进而通过对工作流任务的并发和顺序执行,完成资源准备,服务开通和应用部署等各项任务。

相对于目前公用云和各项开源软件提供的服务,一大优势在于用户无需显式的关注一个应用或者一组应用的初始化过程,仅仅需要按照标准模型来描述应用和部署环境要求,系统会自动确定各项任务的执行顺序和并发执行能力,以最快的路径来执行应用部署流程。

对于应用的版本更新,原理是一样的。用户只需要提供一个环境中应用的最终版本要求,系统会自动对比差异,确定需要重新部署或者重新启动的应用,自动完成环境更新。
6.jpg

##事件监控与分布式处理转发
工作流的执行是一个异步过程,因此我们需要一个事件管理机制来实现事件的统一处理和转发。这里的事件包含多个事件来源:

* Kubernetes Events:数据基础服务容器和应用容器的启动监控
* 服务调用完成事件:各基础服务初始化,数据初始化等事件
* 流程管理事件:比如取消流程执行等处理

我们使用Apache Kakfa作为统一的事件总线,多个Pandora流程调度器实例会订阅该事件Topic,各自实现对当前实例内管理的流程的处理。从而可以实现动态的扩容,无需担心大规模使用后流程的分布式管理问题。
7.jpg

##数据库的初始化与版本管理
数据的管理通常是比较复杂的。不同应用会采取不同的方式来管理Schema和初始数据。我们也提供了多种方式来兼容不同的数据管理机制,包括:

* SQL脚本;
* 从集中的数据Schema治理系统获取数据Schema;
* 基于Flyway的数据自动初始化和数据迁移管理工具。

从初始数据的注入来看,由于有些应用没有很好的维护数据脚本,通常需要从一个基础的测试数据库导出初始化的SQL脚本。这种情况下数据集通常会比较大。我们在初始化执行过程中进行了优化,采用批量提交执行的策略,能够快速的批量导入数据,降低数据准备时间。
#总结与使用实践
在完成核心功能开发以后,我们和一个业务小组联合对实际经常需要同时部署的一组应用做了试用和验证,总体而言比较好的满足了复杂应用部署的要求。

这一组应用的基本情况如下:

* 多个应用域(7~20个),需要在同一个环境部署实现端到端的验证;
* 资源服务配置复杂,目前维护了众多的虚拟机:

* MySQL,多达128个分库。初始数据多达18000行SQL;
* Redis:单实例/Sharding/Cluster配置并存;
* 数据库中间件:不同应用多项服务注册使用,并且有不同应用共用同一服务的情况.

* 消息队列服务众多,往常需要逐条Channel/Queue进行配置;
* 环境变量配置工作繁重:包括多个MySQL分库,Redis实例的配置信息;
* 经常需要切换各个应用配置适应不同测试场景。

我们从现有部署环境导出了应用描述以后,经过简单定义修改数据服务相关描述,快速实现了在6~7分钟内完成所有数据库实例的初始化和应用的配置和启动,并且该过程是可以重复执行的,也就是说可以非常方便的随时根据测试场景要求快速准备出测试环境,无需手工维护各项配置和应用。对于应用版本更新,由于各基础服务已经存在,我们只需要创建新增的基础服务以及重新部署应用,可以在1~2分钟内完成。

原文链接:https://mp.weixin.qq.com/s/uug7xeawWLQKXGxeXm5wkw

基于GitOps的企业级CI/CD

yahoon 发表了文章 • 0 个评论 • 1390 次浏览 • 2019-01-11 20:53 • 来自相关话题

【编者的话】实现企业级的持续交付(CD)是一个巨大的挑战。每一个公司都在对他们的软件交付方式做创新,我们需要允许各个团队学习构建并优化他们自己的交付流水线。在云原生的世界里正在发生,许多的最佳实践正在萌芽。 尽管如此,给予团队灵活性做创新试验时,也需要兼顾公司 ...查看全部
【编者的话】实现企业级的持续交付(CD)是一个巨大的挑战。每一个公司都在对他们的软件交付方式做创新,我们需要允许各个团队学习构建并优化他们自己的交付流水线。在云原生的世界里正在发生,许多的最佳实践正在萌芽。 尽管如此,给予团队灵活性做创新试验时,也需要兼顾公司的安全和合规方面的要求。在这篇文章里面,我将讲述我们如何为一个大型企业客户成功使用GitOps架构模型来获得灵活性和安全性之间的平衡。
# 背景
这篇文章关注的对象是拥有数十个或者数百个开发团队的公司。我假定他们都以Kubernetes作为应用的运行平台。虽然文章里面的原则也可以用在其他地方,但是Kubernetes确实让我们的持续交付平台的实现变得简化,这种简化也使得这篇文章能更加聚焦在我们需要讨论的问题上。最后声明,虽然这是一篇技术文档,但是不会非常的深入。我将会使用方框和箭头来描述解决方案,以后我可能会另写一篇文章叙述技术细节。
# 灵活性激发创新
如果一个大公司里面的每个团队都被锁定在同一个开发流程里面,创新是非常困难的。 因为创新常需要从上至下而且常常充满风险;任何新的方法都是为了取代旧的。在一个大的复杂组织里面改变工作流程是一项困难的工作,所以: 从小处开始非常必要;之后有足够大的空间去成长。

这里给出两个例子来描述一个团队可能采用的不同流程。第一个严重依赖人力工作和批准;第一个比较成熟一些,依赖于自动化测试且省去了管理批准。这2个例子说明了在一个组织里面给几十或者几百个团队强制一种解决方案是多么的荒唐。每个团队有自身的独特情况,会基于此来对他们的软件交付方式做创新。
001.png

P1: 通过人工的QA批准的持续交付流程

002.png

P2: 通过全自动的测试实现的持续交付流程
# 合规与安全
让我们近距离观察合规与安全需求! 在合规方面,企业自身有严格的需求之外,也必须符合政府相关的规范。

在软件交付领域中一般有如下的合规&安全需求:

  1. 访问控制——控制谁能够在什么环境部署什么内容
  2. 审计追溯——记录对环境的所有变更,包括谁改动了什么,为什么而改
  3. 批准流程——对特定环境的变更需要得到授权批准以后才能进行

由于持续交付流程改变了关键的软件环境,它在满足这些需求方面也有很大的责任。如果开发团队不准接触生产环境,那么CD平台也不行,除非它有能够做这个部署操作的授权。这仅仅是一个例子而已。我们接下来深入阐述创建一个满足灵活,安全,合规的平台有多困难。
# 从Jenkins部署
我们先看一下使用流行的Jenkins Pipeline插件实现的原生的持续交付流水线。 这是实现CD流水线的一种简单方式,对大多数环境已经足够了,但是不适用于我们的场景。
003.png

这个流水线会构建Docker image并部署到Kubernetes上,非常好。但是问题在哪?

首先,Jenkins必须有访问所有Kubernetes集群的帐号信息(包括生产的)。 这意味着对Jenkins的admin权限必须限制到有权限访问生产的人, 这就限制了开发团队选择自己工具的能力。

其次,任何可以访问Jenkinsfile的人都可以修改流水线,他们可以直接打印生产环境的访问账户,并对生产环境做修改。这表示我们甚至不能让开发人员去修改Jenkinsfile。这会是一个极大的限制,因为每个团队的pipeline都是不同的,甚至同一个团队的不同服务也是不同的(灵活性的需求)。

总之,我们在Jenkins里面存放访问信息会严重限制开发团队的灵活性。这明显不是我们希望的,也说明了这种直接的方式为什么是不够的。
# GitOps
我们已经看到我们的目标是实现一个安全,合规,同时又灵活的CI/CD系统。我们该如何做呢?

我们可以将部署交付流程从部署流程中剥离开始。 这样集群的访问信息就从执行交付的Jenkins流水线里面移除了,部署流程可以放到另一个Jenkins或者其他工具中——这里我为了简单起见仍然用Jenkins。

隔离以后,我们需要一种方式让交付流程通知部署流程: 你需要部署某个应用的某个版本或者对环境做变更(比如增加一个新的环境变量)。一种简单或者相当强大的解决方案是让Git当作这两个流程的中间人。 我们可以创建一个Git repo,里面包括我们所有的环境相关信息,然后让部署流程监听这个repo的变化。
004.png

P3: 基于GitOps的持续交付架构

注意Git repo是如何将我们的部署流程从交付流程中剥离的。 从交付流程来看,部署就代表着往中间的git repo做一个commit。 这样交付流程不会接触到Kubernetes集群。同时任何时候有代码merge进这个git repo中交付流程就会启动。

GitOps最近由Weaveworks的Alexis Richardson提出,已经获得了很多瞩目。基础设施即代码的思想已经存在好几年了,它随公有云而生,因为所有的资源都可以定义为代码。GitOps是将这种思想逻辑延伸到Kubernetes里面运行着的应用上。Kubernetes的部署机制允许我们以文本文件描述我们的应用,并放到Git中。

让我们看看GitOps是如何帮助我们解决三个合规性问题的:

  1. 访问控制:只有对环境的配置的git repo有写权限的人才能对这个环境做部署。
  2. 批准流程:对于敏感的环境,我们可以允许开发团队的Jenkins创建PR,但是只有授权的人才能merge。 这就用很小的实现开销创建了一个非常好的批准流程。
  3. 审计:因为Git是一个版本控制系统,它天然的记录的更新的所有信息。 每个Git commit的都包括了谁做的更改,已经对更改的描述。

最后引入GitOps让我们能够在满足合规需求的情况下创建了一个安全的软件交付流程。如果运维的repo上的用户管理配置合理——只有授权以后的人才能做merge PR的操作。开发团队在没有运维团队授权的情况下是无法更改生产环境的。
# 我们只是重新做了一遍“基础设施即代码”?
基础设施即代码的思想还没到达GitOps这个层次。类似于CloudFormation和Terraform的工具很流行,可以让我们轻松的管理基础设施代码。这些工具用来描述基础设施,而Kubernetes聚集于以容器的方式运行应用(微服务)。在容器里面不断更新的代码是我们需要做持续交付的地方。 我们喜欢利用已经有的CI工具来处理这些快速的发布,而且CD本身就是CI的持续。
# 结论
使用Git做为中介来将部署流程从交付流水线里面解耦是一种有效的方式来实现在安全和合规限制严苛的环境下实现持续交付的手段。它省去了我们必须找到一个完美的工具,既能够做好持续交付和部署的灵活性;同时又有高级的用户管理,授权以及审计的能力。 虽然解耦后的方案增加了一定的复杂度,但是我们能够实现为不同的交付流水线和执行部署选择不同工具的目标。

在一个企业的环境中,这样的自由性有很大的收益——可以允许一次改动一小部分,也让未来迁移到更好的工具变得容易许多。

原文链接: Enterprise grade CI/CD with GitOps(翻译:姚洪)

如何使用Docker和GitLab构建CI/CD Pipeline?

xiaoyh 发表了文章 • 0 个评论 • 3637 次浏览 • 2018-10-14 20:50 • 来自相关话题

【编者的话】本文主要讲述了如何在GitLab上使用Docker镜像构建一个CI/CD的Pipeline。 现如今持续集成(CI)和持续交付(CD)大家已经不陌生了,它们是为了辅助你的产品/工程项目能够更快、更容易地运行最新版本。在这篇 ...查看全部
【编者的话】本文主要讲述了如何在GitLab上使用Docker镜像构建一个CI/CD的Pipeline。

现如今持续集成(CI)和持续交付(CD)大家已经不陌生了,它们是为了辅助你的产品/工程项目能够更快、更容易地运行最新版本。在这篇文章中,我将讲述如何使用Docker镜像和GitLab的CI/CD工具构建一个Pipeline,在一个VPS/KVM Linux服务器上进行部署。
#前提要求

  • 对Linux、Docker以及CI/CD有基本的了解。
  • GitLab帐号(免费计划即可)。
  • 一台具备SSH访问权限的Linux服务器(非root用户即可)。我使用的是带有LAMP技术栈的Ubuntu 16.04 LTS系统。
  • 装有SSH和LFTP的轻量级Docker镜像。

在开始之前,你需要确保:

* 你已经登录GitLab
* 你是某个project/repository的拥有者
* 你能够在本地机器通过Git访问这个repo进行pull和push操作

我用的是GitKraken,一个Git GUI工具,能够较为方面的进行Git操作。
#关于GitLab的CI/CD
GitLab提供了一种通过Docker和Shared Runners处理CI/CD Pipeline的简单方法。每次运行Pipeline时,GitLab都会创建一个独立的虚拟机并构建一个Docker镜像。Pipeline可以使用YAML配置文件进行配置,一个Pipeline可以有多个job,但如果job太多,Pipeline的运行时间就较长。我们肯定不希望这样,因为使用免费计划,每月最多可以有2000分钟的构建时间

“GitLab.com上的Shared Runners自动缩放模式运行,由DigitalOcean提供支持。自动缩放意味着减少启动构建的等待时间,并为每个项目建立隔离虚拟机,从而最大限度地提高安全性。”
——来自GitLab文档中的描述



#为GitLab的runner创建SSH密钥
注意:即使你的服务器上已有具备SSH访问方式,还是建议你为CI/CD创建一套新的密钥,同时为部署流程创建一个新的非root用户。

我们将在Docker容器中通过SSH连接我们的服务器,这就意味着我们不能输入用户密码(即非交互式登录),因此我们需要在本地计算机中创建无密码的SSH密钥对。通常我会创建一个2048字节的RSA密钥,因为这足够安全。
$ ssh-keygen rsa -b 2048

输入以上命令,跟随创建步骤,如果对创建步骤有疑问,使用`man ssh-key`。记住不要为密钥对设置密码。创建完成后,我们需要把私钥导入我们的服务器:
$ ssh-copy-id -i /path/to/key user@host

现在你可以尝试通过以下命令连接:
$ ssh -i /path/to/key user@host

连接过程应该不会让你输入密码。这个私钥我们后面会使用到。
#选择Dockerfile
我使用Docker Hub来存放我的定制化Dockerfile,这个Dockerfile将基于Alpine构建一个安装有OpenSSH和LFTP的轻量级镜像(大约8Mb)。在GitLab的CI/CD中我们需要使用这个镜像来运行Pipeline的job和脚本,镜像越轻量意味着下载镜像的时间就越少。你可以用你自己的镜像或者用我的Dockerfile
#Pipleline的配置
在正式构建前,你需要在你repo的根目录创建一个".gitlab-ci.yml"文件。接下来我将解释我使用的配置文件,如果有兴趣,你可以先到GitLab官网阅读配置文件格式以及所有可以使用的配置项。

我的配置文件如下:
image: jimmyadaro/gitlab-ci-cd:latest
Deploy:
stage: deploy
only:
— ‘master’
when: manual
allow_failure: false
before_script:
#Create .ssh directory
— mkdir -p ~/.ssh
#Save the SSH private key
— echo “$SSH_PRIVATE_KEY” > ~/.ssh/id_rsa
— chmod 700 ~/.ssh
— chmod 600 ~/.ssh/id_rsa
— eval $(ssh-agent -s)
— ssh-add ~/.ssh/id_rsa
script:
#Backup everything in /var/www/html/
— ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa $USERNAME@$HOST “zip -q -r /var/backups/www/01-Deploy-$(date +%F_%H-%M-%S).zip /var/www/html/”
#Deploy new files to /var/www/html
— lftp -d -u $USERNAME, -e ‘set sftp:auto-confirm true; set sftp:connect-program “ssh -a -x -i ~/.ssh/id_rsa”; mirror -Rnev ./ /var/www/html — ignore-time — exclude-glob .git* — exclude .git/; exit’ sftp://$HOST
— rm -f ~/.ssh/id_rsa
— ‘echo Deploy done: $(date “+%F %H:%M:%S”)’

让我们逐行看看配置文件的每一步都在做什么。
image: jimmyadaro/gitlab-ci-cd:latest

这行将告诉runner从Docker Hub上拉取并运行最新版本的容器。你可以在这里设置你想要使用的镜像,但别忘了给镜像安装OpenSSH和LFTP
Deploy:

这行设置了pipeline的job名字,创建一个job必须设置这行内容。
stage: deploy

这行设置了job的stage名字,如果你需要运行多个stage,例如“backup”、“build”、“deploy”等,stage名字将帮助你识别当前Pipeline处于什么状态。由于我不需要其他stage,所以我只用了一个job,并且这个job只有一个stage。对于job和stage的名字可以任意设置,例如你的job可以叫“ASDF”,stage可以叫“GHJK”,不过如果你有多个stage,你肯定需要鉴别不同的stage,因此我建议还是规范化这些名字。
only:
— ‘master’

这行表示Pipeline只有当你repo的`master`分支收到一个更新(例如git merge)时才会被触发。因此,我建议开发使用其他分支(例如`development`、`wip`等),然后使用`master`分支作为“产品分支”。
when: manual

这行表示你需要进入你的project的CI/CD配置中手动触发整个部署流程。当然,这一步是可以跳过的,只是我更喜欢手动触发Pipeline。如果去掉这行,你所选分支(本例中为master)的任何改动都会触发一次Pipeline。
allow_failure: false

这行表示如果你的Pipeline中有其他stage,当一个job中发生错误时,不允许继续执行剩余任务。这是一个可选配置。
before_script:
#Create .ssh directory
— mkdir -p ~/.ssh
#Save the SSH private key
— echo “$SSH_PRIVATE_KEY” > ~/.ssh/id_rsa
— chmod 700 ~/.ssh
— chmod 600 ~/.ssh/id_rsa
— eval $(ssh-agent -s)
— ssh-add ~/.ssh/id_rsa

在`before_script`单元设置的所有命令都会在执行主单元(main script)之前执行。如你所见,每行shell命令需要用短横线(“-“)指定。上面的命令将把我们刚刚生成的SSH私钥保存到容器默认的SSH路径下,这样我们就可以免密连接我们的服务器。

刚刚生成的私钥将作为Protected变量保存在我的project的CI/CD配置中,在GitLab的web UI上,点击Settings > CI/CD > Variables将看到这个变量。同样,我将服务器地址和部署使用的用户名(非root用户)也使用Protected变量保存。
1.png

script:
#Backup everything in /var/www/html/
— ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa $USERNAME@$HOST “zip -q -r /var/backups/www/01-Deploy-$(date +%F_%H-%M-%S).zip /var/www/html/”
#Deploy new files to /var/www/html
— lftp -d -u $USERNAME, -e ‘set sftp:auto-confirm true; set sftp:connect-program “ssh -a -x -i ~/.ssh/id_rsa”; mirror -Rnev ./ /var/www/html — ignore-time — exclude-glob .git* — exclude .git/; exit’ sftp://$HOST
— rm -f ~/.ssh/id_rsa
— ‘echo Deploy done: $(date “+%F %H:%M:%S”)’

script下的内容就是GitLab的runner执行的主单元。首先,我会连接到我的服务器将所有内容备份到一个ZIP文件中,这个ZIP文件将使用当前时间(格式为yyyy-mm-dd_hh-mm-ss)进行命名:
— ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa $USERNAME@$HOST “zip -q -r /var/backups/www/01-Deploy-$(date +%F_%H-%M-%S).zip /var/www/html/”

注意:你需要在你的服务器上安装ZIP CLI。

在将`/var/www/html`备份后,使用LFTP连接到我的服务器并且上传最新的repo文件。这里我用的是SFTP,FTP配置有点不一样:
— lftp -d -u $USERNAME, -e ‘set sftp:auto-confirm true; set sftp:connect-program “ssh -a -x -i ~/.ssh/id_rsa”; mirror -Rnev ./ /var/www/html — ignore-time — exclude-glob .git* — exclude .git/; exit’ sftp://$HOST

使用`mirror -Rnev ./ /var/www/html`让LFTP上传`./`(我repo的根目录)下的所有文件到我服务器的`/var/www/html`路径下。上面部分参数的意思如下:

* `-u`设置了我们`sftp://$HOST`的SSH用户名。
* `-e`用于设置执行命令(使用单引号进行配置)。
* `-R`用于设置reverse mirror。
* `-n`表示只上传新的文件。
* `-e`用于删除在我们源中不存在的文件。
* `-v`用于配置verbose日志。
* `ignore-time`将在决定是否下载时忽略时间。
`exclude-glob .git`将会排除任何目录中匹配`.git*`的所有文件(例如`.gitignore`以及`.gitkeep`)。你可以在这里设置其他文件匹配方式。
* `exclude .git/`这个配置将会保证不上传我们repo中的git文件。
* `exit`将会停止LFTP和SSH执行。

注意:所有在我们服务上但是不在我们repository中的文件将被删除,记住上面所述的'源'指的就是我们GitLab的repository。

最终,脚本会在shared runner的容器中删除我们的私钥(这是一个安全措施),并且输出带有当前时间的结束语句。
— rm -f ~/.ssh/id_rsa
— ‘echo Deploy done: $(date “+%F %H:%M:%S”)’

以上部分就是我配置文件的所有内容。在GitLab中一个成功的Pipeline执行流程如下图所示:
2.png

运行Docker镜像

3.png

Pipeline的最终状态
#结论
我尝试了一些其他的方式,例如使用rsync替代LFTP、使用多阶段以及缓存依赖(我能够重用SSH密钥)的Jobs、使用Docker的ENTRYPOINT和CMD等等,但我发现上面描述的方式对我来说是最快和最容易的。

原文链接:Build a CI/CD Pipeline with Docker and GitLab(翻译:肖远昊)

怎样在Kubernetes中实现CI/CD的发布流程,大家都贡献出来一下吧!

回复

Twilight 回复了问题 • 2 人关注 • 1 个回复 • 894 次浏览 • 2019-03-25 11:50 • 来自相关话题

我们应该如何基于容器来进行软件的持续交付(二)?

回复

wise2c 发起了问题 • 1 人关注 • 0 个回复 • 2788 次浏览 • 2016-12-26 10:07 • 来自相关话题

有没有人试过持续集成中Jenkins节点用Docker集群实现?

回复

sxdocker 回复了问题 • 2 人关注 • 1 个回复 • 2918 次浏览 • 2016-11-30 23:18 • 来自相关话题

做持续交付(CD)是什么概念

回复

ns208 回复了问题 • 4 人关注 • 2 个回复 • 3233 次浏览 • 2016-07-12 15:53 • 来自相关话题

有用过Drone做CI和CD的吗?

回复

CMGS 回复了问题 • 3 人关注 • 1 个回复 • 7476 次浏览 • 2015-05-22 09:43 • 来自相关话题

GitLab CI/CD 在 Node.js 项目中的实践

尼古拉斯 发表了文章 • 0 个评论 • 179 次浏览 • 2019-06-03 09:58 • 来自相关话题

【编者的话】近期在按照业务划分项目时,我们组被分了好多的项目过来,大量的是基于 Node.js 的,也是我们组持续在使用的语言。 #现有流程中的一些问题 在维护多个项目的时候,会暴露出一些问题: 如何有效的使用 ...查看全部
【编者的话】近期在按照业务划分项目时,我们组被分了好多的项目过来,大量的是基于 Node.js 的,也是我们组持续在使用的语言。
#现有流程中的一些问题
在维护多个项目的时候,会暴露出一些问题:

  1. 如何有效的使用 测试用例
  2. 如何有效的使用 ESLint
  3. 部署上线还能再快一些吗

* 使用了 TypeScript 以后带来的额外成本

##测试用例
首先是测试用例,最初我们设计在了 git hooks 里边,在执行 git commit 之前会进行检查,在本地运行测试用例。 如果你想和更多 GitLab 技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态。

这会带来一个时间上的问题,如果是日常开发,这么操作还是没什么问题的,但如果是线上 bug 修复,执行测试用例的时间依据项目大小可能会持续几分钟。

而为了修复 bug,可能会采用 commit 的时候添加 -n 选项来跳过 hooks,在修复 bug 时这么做无可厚非,但是即使大家在日常开发中都采用commit -n 的方式来跳过繁琐的测试过程,这个也是没有办法管控的,毕竟是在本地做的这个校验,是否遵循这个规则,全靠大家自觉。

所以一段时间后发现,通过这种方式执行测试用例来规避一些风险的作用可能并不是很有效。
##ESLint
然后就是 ESLint,我们团队基于airbnb的 ESLint 规则自定义了一套更符合团队习惯的规则,我们会在编辑器中引入插件用来帮助高亮一些错误,以及进行一些自动格式化的操作。

同时我们也在 git hooks 中添加了对应的处理,也是在 git commit 的时候进行检查,如果不符合规范则不允许提交。

不过这个与测试用例是相同的问题:

  1. 编辑器是否安装 ESLint 插件无从得知,即使安装插件、是否人肉忽略错误提示也无从得知。
  2. git hooks 可以被绕过

##部署上线的方式
之前团队的部署上线是使用shipit周边套件进行部署的。

部署环境强依赖本地,因为需要在本地建立仓库的临时目录,并经过多次ssh XXX "command"的方式完成部署 + 上线的操作。

shipit提供了一个有效的回滚方案,就是在部署后的路径添加多个历史部署版本的记录,回滚时将当前运行的项目目录指向之前的某个版本即可。(不过有一点儿坑的是,很难去选择我要回滚到那个节点,以及保存历史记录需要占用额外的磁盘空间)

不过正因为如此,shipit在部署多台服务器时会遇到一些令人不太舒服的地方。

如果是多台新增的服务器,那么可以通过在shipit配置文件中传入多个目标服务器地址来进行批量部署。

但是假设某天需要上线一些小流量(比如四台机器中的一台),因为前边提到的shipit回滚策略,这会导致单台机器与其他三台机器的历史版本时间戳不一致(因为这几台机器不是同一时间上线的)

了这个时间戳就另外提一嘴,这个时间戳的生成是基于执行上线操作的那台机器的本地时间,之前有遇到过同事在本地测试代码,将时间调整为了几天前的时间,后时间没有改回正确的时间时进行了一次部署操作,代码出现问题后却发现回滚失败了,原因是该同事部署的版本时间戳太小,shipit 找不到之前的版本(shipit 可以设置保留历史版本的数量,当时最早的一次时间戳也是大于本次出问题的时间戳的)



也就是说,哪怕有一次进行过小流量上线,那么以后就用不了批量上线的功能了 (没有去仔细研究shipit官方文档,不知道会不会有类似--force之类的忽略历史版本的操作)

基于上述的情况,我们的部署上线耗时变为了: (__机器数量__)X(__基于本地网速的仓库克隆、多次 ssh 操作的耗时总和__)。 P.S. 为了保证仓库的有效性,每次执行 shipit 部署,它都会删除之前的副本,重新克隆

尤其是服务端项目,有时紧急的 bug 修复可能是在非工作时间,这意味着可能当时你所处的网络环境并不是很稳定。

我曾经晚上接到过同事的微信,让我帮他上线项目,他家的 Wi-Fi 是某博士的,下载项目依赖的时候出了些问题。

还有过使用移动设备开热点的方式进行上线操作,有一次非前后分离的项目上线后,直接就收到了联通的短信:「您本月流量已超出XXX」(当时还在用合约套餐,一月就800M流量)。
##TypeScript
在去年下半年开始,我们团队就一直在推动 TypeScript 的应用,因为在大型项目中,拥有明确类型的 TypeScript 显然维护性会更高一些。

但是大家都知道的, TypeScript 最终需要编译转换为 JavaScript(也有 tsc 那种的不生成 JS 文件,直接运行,不过这个更多的是在本地开发时使用,线上代码的运行我们还是希望变量越少越好)。

所以之前的上线流程还需要额外的增加一步,编译 TS。

而且因为shipit是在本地克隆的仓库并完成部署的,所以这就意味着我们必须要把生成后的 JS 文件也放入到仓库中,最直观的,从仓库的概览上看着就很丑(50% TS、50% JS),同时这进一步增加了上线的成本。

总结来说,现有的部署上线流程过于依赖本地环境,因为每个人的环境不同,这相当于给部署流程增加了很多不可控因素。
#如何解决这些问题
上边我们所遇到的一些问题,其实可以分为两块:

  1. 有效的约束代码质量
  2. 快速的部署上线

所以我们就开始寻找解决方案,因为我们的源码是使用自建的 GitLab 仓库来进行管理的,首先就找到了 GitLab CI/CD。

在研究了一番文档以后发现,它能够很好的解决我们现在遇到的这些问题。

要使用 GitLab CI/CD 是非常简单的,只需要额外的使用一台服务器安装 gitlab-runner,并将要使用 CI/CD 的项目注册到该服务上就可以了。

GitLab 官方文档中有非常详细的安装注册流程:

install | runner
register | runner
group register | repo 注册 Group 项目时的一些操作

上边的注册选择的是注册 group ,也就是整个 GitLab 某个分组下所有的项目。

主要目的是因为我们这边项目数量太多,单个注册太过繁琐(还要登录到 runner 服务器去执行命令才能够注册)
##安装时需要注意的地方
官网的流程已经很详细了,不过还是有一些地方可以做一些小提示,避免踩坑。
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner

这是 Linux 版本的安装命令,安装需要 root (管理员) 权限,后边跟的两个参数:

* --user 是 CI/CD 执行 job (后续所有的流程都是基于 job 的)时所使用的用户名
* --working-directory 是 CI/CD 执行时的根目录路径 个人的踩坑经验是将目录设置为一个空间大的磁盘上,因为 CI/CD 会生成大量的文件,尤其是如果使用 CI/CD 进行编译 TS 文件并且将其生成后的 JS 文件缓存;这样的操作会导致 innode 不足产生一些问题

--user 的意思就是 CI/CD 执行使用该用户进行执行,所以如果要编写脚本之类的,建议在该用户登录的状态下编写,避免出现无权限执行 sudo su gitlab-runner



##注册时需要注意的地方
在按照官网的流程执行时,我们的 tag 是留空的,暂时没有找到什么用途。

以及 executor 这个比较重要了,因为我们是从手动部署上线还是往这边靠拢的,所以稳妥的方式是一步步来,也就是说我们选择的是 shell ,最常规的一种执行方式,对项目的影响也是比较小的(官网示例给的是 Docker)。
##.gitlab-ci.yml 配置文件
上边的环境已经全部装好了,接下来就是需要让 CI/CD 真正的跑起来
runner 以哪种方式运行,就靠这个配置文件来描述了,按照约定需要将文件放置到 repo 仓库的根路径下。

当该文件存在于仓库中,执行 git push 命令后就会自动按照配置文件中所描述的动作进行执行了。

* quick start
* configuration

上边的两个链接里边信息非常完整,包含各种可以配置的选项。

一般来讲,配置文件的结构是这样的:
stages:
- stage1
- stage2
- stage3

job 1:
stage: stage1
script: echo job1

job 2:
stage: stage2
script: echo job2

job 3:
stage: stage2
script:
- echo job3-1
- echo job3-2

job 4:
stage: stage3
script: echo job4

stages 用来声明有效的可被执行的 stage,按照声明的顺序执行。

下边的那些 job XXX 名字不重要,这个名字是在 GitLab CI/CD Pipeline 界面上展示时使用的,重要的是那个 stage 属性,他用来指定当前的这一块 job 隶属于哪个 stage。

script 则是具体执行的脚本内容,如果要执行多行命令,就像job 3那种写法就好了。

如果我们将上述的 stage、job 之类的换成我们项目中的一些操作install_dependencies、test、eslint之类的,然后将script字段中的值换成类似npx eslint之类的,当你把这个文件推送到远端服务器后,你的项目就已经开始自动运行这些脚本了。

并且可以在Pipelines界面看到每一步执行的状态。

P.S. 默认情况下,上一个 stage 没有执行完时不会执行下一个 stage 的,不过也可以通过额外的配置来修改:

* allow failure
* when

设置仅在特定的情况下触发 CI/CD

上边的配置文件存在一个问题,因为在配置文件中并没有指定哪些分支的提交会触发 CI/CD 流程,所以默认的所有分支上的提交都会触发,这必然不是我们想要的结果。

CI/CD 的执行会占用系统的资源,如果因为一些开发分支的执行影响到了主干分支的执行,这是一件得不偿失的事情。

所以我们需要限定哪些分支才会触发这些流程,也就是要用到了配置中的 only 属性。

使用only可以用来设置哪些情况才会触发 CI/CD,一般我们这边常用的就是用来指定分支,这个是要写在具体的 job 上的,也就是大致是这样的操作:

具体的配置文档
job 1:
stage: stage1
script: echo job1
only:
- master
- dev

单个的配置是可以这样写的,不过如果 job 的数量变多,这么写就意味着我们需要在配置文件中大量的重复这几行代码,也不是一个很好看的事情。

所以这里可能会用到一个yaml的语法:

这是一步可选的操作,只是想在配置文件中减少一些重复代码的出现



.access_branch_template: &access_branch
only:
- master
- dev

job 1:
<<: *access_branch
stage: stage1
script: echo job1

job 2:
<<: *access_branch
stage: stage2
script: echo job2

一个类似模版继承的操作,官方文档中也没有提到,这个只是一个减少冗余代码的方式,可有可无。
##缓存必要的文件
因为默认情况下,CI/CD在执行每一步(job)时都会清理一下当前的工作目录,保证工作目录是干净的、不包含一些之前任务留下的数据、文件。

不过这在我们的 Node.js 项目中就会带来一个问题。

因为我们的 ESLint、单元测试 都是基于 node_modules 下边的各种依赖来执行的。

而目前的情况就相当于我们每一步都需要执行npm install,这显然是一个不必要的浪费。

所以就提到了另一个配置文件中的选项:cache

用来指定某些文件、文件夹是需要被缓存的,而不能清除:
cache:
key: ${CI_BUILD_REF_NAME}
paths:
- node_modules/

大致是这样的一个操作,CI_BUILD_REF_NAME是一个 CI/CD 提供的环境变量,该变量的内容为执行 CI/CD 时所使用的分支名,通过这种方式让两个分支之间的缓存互不影响。
##部署项目
如果基于上边的一些配置,我们将 单元测试、ESLint 对应的脚本放进去,他就已经能够完成我们想要的结果了,如果某一步执行出错,那么任务就会停在那里不会继续向后执行。

不过目前来看,后边已经没有多余的任务供我们执行了,所以是时候将 部署 这一步操作接过来了。

部署的话,我们目前选择的是通过 rsync 来进行同步多台服务器上的数据,一个比较简单高效的部署方式。

P.S. 部署需要额外的做一件事情,就是建立从gitlab runner所在机器gitlab-runner用户到目标部署服务器对应用户下的机器信任关系。

有 N 多种方法可以实现,最简单的就是在runner机器上执行 ssh-copy-id 将公钥写入到目标机器。

或者可以像我一样,提前将 runner 机器的公钥拿出来,需要与机器建立信任关系时就将这个字符串写入到目标机器的配置文件中。

类似这样的操作:ssh 10.0.0.1 "echo \"XXX\" >> ~/.ssh/authorized_keys"
大致的配置如下:
variables:
DEPLOY_TO: /home/XXX/repo # 要部署的目标服务器项目路径
deploy:
stage: deploy
script:
- rsync -e "ssh -o StrictHostKeyChecking=no" -arc --exclude-from="./exclude.list" --delete . 10.0.0.1:$DEPLOY_TO
- ssh 10.0.0.1 "cd $DEPLOY_TO; npm i --only=production"
- ssh 10.0.0.1 "pm2 start $DEPLOY_TO/pm2/$CI_ENVIRONMENT_NAME.json;"

同时用到的还有variables,用来提出一些变量,在下边使用。

`ssh 10.0.0.1 "pm2 start $DEPLOY_TO/pm2/$CI_ENVIRONMENT_NAME.json;"`,这行脚本的用途就是重启服务了,我们使用pm2来管理进程,默认的约定项目路径下的pm2文件夹存放着个个环境启动时所需的参数。

当然了,目前我们在用的没有这么简单,下边会统一提到。

并且在部署的这一步,我们会有一些额外的处理。

这是比较重要的一点,因为我们可能会更想要对上线的时机有主动权,所以 deploy 的任务并不是自动执行的,我们会将其修改为手动操作还会触发,这用到了另一个配置参数:
deploy:
stage: deploy
script: XXX
when: manual # 设置该任务只能通过手动触发的方式运行

当然了,如果不需要,这个移除就好了,比如说我们在测试环境就没有配置这个选项,仅在线上环境使用了这样的操作。
##更方便的管理 CI/CD 流程
如果按照上述的配置文件进行编写,实际上已经有了一个可用的、包含完整流程的 CI/CD 操作了。

不过它的维护性并不是很高,尤其是如果 CI/CD 被应用在多个项目中,想做出某项改动则意味着所有的项目都需要重新修改配置文件并上传到仓库中才能生效。

所以我们选择了一个更灵活的方式,最终我们的 CI/CD 配置文件是大致这样子的(省略了部分不相干的配置):
variables:
SCRIPTS_STORAGE: /home/gitlab-runner/runner-scripts
DEPLOY_TO: /home/XXX/repo # 要部署的目标服务器项目路径

stages:
- install
- test
- build
- deploy_development
- deploy_production

install_dependencies:
stage: install
script: bash $SCRIPTS_STORAGE/install.sh

unit_test:
stage: test
script: bash $SCRIPTS_STORAGE/test.sh

eslint:
stage: test
script: bash $SCRIPTS_STORAGE/eslint.sh

# 编译 TS 文件
build:
stage: build
script: bash $SCRIPTS_STORAGE/build.sh

deploy_development:
stage: deploy_development
script: bash $SCRIPTS_STORAGE/deploy.sh 10.0.0.1
only: dev # 单独指定生效分支

deploy_production:
stage: deploy_production
script: bash $SCRIPTS_STORAGE/deploy.sh 10.0.0.2
only: master # 单独指定生效分支

我们将每一步 CI/CD 所需要执行的脚本都放到了 runner 那台服务器上,在配置文件中只是执行了那个脚本文件。

这样当我们有什么策略上的调整,比如说 ESLint 规则的变更、部署方式之类的。

这些都完全与项目之间进行解耦,后续的操作基本都不会让正在使用 CI/CD 的项目重新修改才能够支持(部分需要新增环境变量的导入之类的确实需要项目的支持)。
##接入钉钉通知
实际上,当 CI/CD 执行成功或者失败,我们可以在 Pipeline 页面中看到,也可以设置一些邮件通知,但这些都不是时效性很强的。

鉴于我们目前在使用钉钉进行工作沟通,所以就研究了一波钉钉机器人。

发现有支持 GitLab 机器人,不过功能并不适用,只能处理一些 issues 之类的, CI/CD 的一些通知是缺失的,所以只好自己基于钉钉的消息模版实现一下了。

因为上边我们已经将各个步骤的操作封装了起来,所以这个修改对同事们是无感知的,我们只需要修改对应的脚本文件,添加钉钉的相关操作即可完成,封装了一个简单的函数:
function sendDingText() {
local text="$1"

curl -X POST "$DINGTALK_HOOKS_URL" \
-H 'Content-Type: application/json' \
-d '{
"msgtype": "text",
"text": {
"content": "'"$text"'"
}
}'
}

# 具体发送时传入的参数
sendDingText "proj: $CI_PROJECT_NAME[$CI_JOB_NAME]\nenv: $CI_ENVIRONMENT_NAME\ndeploy success\n$CI_PIPELINE_URL\ncreated by: $GITLAB_USER_NAME\nmessage: $CI_COMMIT_MESSAGE"

# 某些 case 失败的情况下 是否需要更多的信息就看自己自定义咯
sendDingText "error: $CI_PROJECT_NAME[$CI_JOB_NAME]\nenv: $CI_ENVIRONMENT_NAME"

上述用到的环境变量,除了DINGTALK_HOOKS_URL是我们自定义的机器人通知地址以外,其他的变量都是有 GitLab runenr所提供的。

各种变量可以从这里找到:predefined variables
##回滚处理
聊完了正常的流程,那么也该提一下出问题时候的操作了。

人非圣贤孰能无过,很有可能某次上线一些没有考虑到的地方就会导致服务出现异常,这时候首要任务就是让用户还可以照常访问,所以我们会选择回滚到上一个有效的版本去。

在项目中的 Pipeline 页面 或者 Enviroment 页面(这个需要在配置文件中某些 job 中手动添加这个属性,一般会写在 deploy 的那一步去),可以在页面上选择想要回滚的节点,然后重新执行 CI/CD 任务,即可完成回滚。

不过这在 TypeScript 项目中会有一些问题,因为我们回滚一般来讲是重新执行上一个版本 CI/CD 中的 deploy 任务,在 TS 项目中,我们在 runner 中缓存了 TS 转换 JS 之后的 dist 文件夹,并且部署的时候也是直接将该文件夹推送到服务器的(TS项目的源码就没有再往服务器上推过了)。

而如果我们直接点击 retry 就会带来一个问题,因为我们的 dist 文件夹是缓存的,而 deploy 并不会管这种事儿,他只会把对应的要推送的文件发送到服务器上,并重启服务。

而实际上 dist 还是最后一次(也就是出错的那次)编译出来的 JS 文件,所以解决这个问题有两种方法:

  1. 在 deploy 之前执行一下 build
  2. 在 deploy 的时候进行判断

第一个方案肯定是不可行的,因为严重依赖于操作上线的人是否知道有这个流程。

所以我们主要是通过第二种方案来解决这个问题。

我们需要让脚本在执行的时候知道,dist 文件夹里边的内容是不是自己想要的。

所以就需要有一个 __标识__,而做这个标识最简单有效唾手可得的就是,git commit id。

每一个 commit 都会有一个唯一的标识符号,而且我们的 CI/CD 执行也是依靠于新代码的提交(也就意味着一定有 commit)。

所以我们在 build 环节将当前的commit id也缓存了下来:
git rev-parse --short HEAD > git_version

同时在 deploy 脚本中添加额外的判断逻辑:
currentVersion=`git rev-parse --short HEAD`
tagVersion=`touch git_version; cat git_version`

if [ "$currentVersion" = "$tagVersion" ]
then
echo "git version match"
else
echo "git version not match, rebuild dist"
bash ~/runner-scripts/build.sh # 额外的执行 build 脚本
fi

这样一来,就避免了回滚时还是部署了错误代码的风险。

关于为什么不将 build 这一步操作与 deploy 合并的原因是这样的:

因为我们会有很多台机器,同时 job 会写很多个,类似 deploy_1、deploy_2、deploy_all,如果我们将 build 的这一步放到 deploy 中。

那就意味着我们每次 deploy,即使是一次部署,但因为我们选择一台台机器单独操作,它也会重新生成多次,这也会带来额外的时间成本。
##hot fix 的处理
在 CI/CD 运行了一段时间后,我们发现偶尔解决线上 bug 还是会比较慢,因为我们提交代码后要等待完整的 CI/CD 流程走完。

所以在研究后我们决定,针对某些特定情况hot fix,我们需要跳过ESLint、单元测试这些流程,快速的修复代码并完成上线。

CI/CD 提供了针对某些 Tag 可以进行不同的操作,不过我并不想这么搞了,原因有两点:

  1. 这需要修改配置文件(所有项目)
  2. 这需要开发人员熟悉对应的规则(打 Tag)

所以我们采用了另一种取巧的方式来实现,因为我们的分支都是只接收Merge Request那种方式上线的,所以他们的commit title实际上是固定的:Merge branch 'XXX'。

同时 CI/CD 会有环境变量告诉我们当前执行 CI/CD 的 commit message。
我们通过匹配这个字符串来检查是否符合某种规则来决定是否跳过这些job:
function checkHotFix() {
local count=`echo $CI_COMMIT_TITLE | grep -E "^Merge branch '(hot)?fix/\w+" | wc -l`

if [ $count -eq 0 ]
then
return 0
else
return 1
fi
}

# 使用方法

checkHotFix

if [ $? -eq 0 ]
then
echo "start eslint"
npx eslint --ext .js,.ts .
else
# 跳过该步骤
echo "match hotfix, ignore eslint"
fi

这样能够保证如果我们的分支名为 hotfix/XXX 或者 fix/XXX 在进行代码合并时, CI/CD 会跳过多余的代码检查,直接进行部署上线。 没有跳过安装依赖的那一步,因为 TS 编译还是需要这些工具的。
#小结
目前团队已经有超过一半的项目接入了 CI/CD 流程,为了方便同事接入(主要是编辑 .gitlab-ci.yml 文件),我们还提供了一个脚手架用于快速生成配置文件(包括自动建立机器之间的信任关系)。

相较之前,部署的速度明显的有提升,并且不再对本地网络有各种依赖,只要是能够将代码 push 到远程仓库中,后续的事情就和自己没有什么关系了,并且可以方便的进行小流量上线(部署单台验证有效性)。

以及在回滚方面则是更灵活了一些,可在多个版本之间快速切换,并且通过界面的方式,操作起来也更加直观。

最终可以说,如果没有 CI/CD,实际上开发模式也是可以忍受的,不过当使用了 CI/CD 以后,再去使用之前的部署方式,则会明显的感觉到不舒适。(没有对比,就没有伤害

如何实现“持续集成”?闲鱼把研发效率翻了个翻

Andy_Lee 发表了文章 • 0 个评论 • 266 次浏览 • 2019-05-25 09:03 • 来自相关话题

从前端开发人员到DevOps:CI / CD 简介

tiny2017 发表了文章 • 0 个评论 • 279 次浏览 • 2019-05-23 17:45 • 来自相关话题

【编者的话】如果你是一名前端开发者,且认为作为一名开发人员只要管好前端这摊子就算大功告成了,那你就out了。原文作者从一个前端开发者的角度,阐述了DevOps的理念,并结合一些示例和实践,告诉我们了解并运用持续集成和持续部署的诸多好处。但我认为,原文作者从始至 ...查看全部
【编者的话】如果你是一名前端开发者,且认为作为一名开发人员只要管好前端这摊子就算大功告成了,那你就out了。原文作者从一个前端开发者的角度,阐述了DevOps的理念,并结合一些示例和实践,告诉我们了解并运用持续集成和持续部署的诸多好处。但我认为,原文作者从始至终传递给我们一种持续学习的精神,这一点同样值得我们学习。
# 介绍
对于有抱负的前端开发者而言,2019年是充满憧憬的一年。

有不计其数的教材、课件和教程,每天还有无数的博客和文章,像雨后春笋般层出不穷。任何想成为专业人士的人都可以获得他们需要的一切——通常还是免费的。

许多人抓住这个机会自学成才,而且他们当中很多人有机会参与完整的项目,可以迅速上手写功能模块,修改Bug并以适当的方式组建代码。

经过一段时间,有些比较幸运的前端开发者就会在互联网的某个角落看到他们自己实现的功能模块,作为一个web app、门户或者一个website——对于初级开发者而言,这真的是一个荣耀的时刻。但令人惊讶的是,他们中很少有人会问一个非常重要的问题:我们开发的应用程序,是怎么放到互联网上的?
1.png

大家普遍认为这是由开发人员完成的,只不过是更“高级别”的开发人员而已。可能一些人听说过DevOps,运营商,云管理员,系统管理员以及其他更具神秘气息的东西。

嗯,这是真的——在某种程度上,编码和测试成功后发生的一切通常都和脚本,Linux命令和容器之类的黑科技有关。还有一条不成文的规定是,只有组织中最有经验和最值得信赖的开发人员/管理员才有资格完成最后的交付工作。

有必要这样吗?这确实是有一定道理的——毕竟,这是一项复杂且极其重要的任务。但是否就意味着这些技能只属于某些精英?当然不是。

作为前端开发者,我们可以选择忽略这一切并相信其他人会完成剩下的所有事情——但我们不应该这样。IT世界的竞争瞬息万变,无论是前端还是后端,对于技术栈的点滴积累将会使你成为一名更具竞争力的开发者。

如果你想在开发方面进步的更快或者在同龄人中脱颖而出,你迟早会需要这些知识。下面就让我告诉你为什么你会需要这些知识。
# 为什么开发人员都应该尝试自动化处理
正如我们前面提到的那样,写代码只是软件开发这个浩大的工程的一部分。我们先来看看任何产品交付所需的基本步骤——不一定是软件:
2.png

严格来说,我们在这里讨论的不一定都是编码。我们关注的是主要的开发阶段完成之后会发生什么?为什么它会如此重要?因为它有可能会很复杂 - 解决方案越严谨,这部分就越复杂。

假设有一个具有一些特定功能的Web应用。我们假定该应用的版本会按照一个一个的功能定期发布,前提是发布到生产环境之前,每一个功能都会进行测试。
3.png

问题来了,我们一般不会只招一名程序员来完成这项工作, 即会有一个团队来负责这些功能。这些假设意味着——除了每个开发人员本地的编码环境和最终稳定的生产环境之外——最好还有一个“staging”环境来验证这些功能。在这个环境中,测试人员/客户可以在实际投入生产环境之前评估它们的质量。

现在我们越来越接近这样的架构:
4.png

正如你所看到的,事情变得越来越复杂(相信我,我们在这里谈论的真的是一个非常简单的例子),但我们在这里不会涉及产品生命周期管理,我们只关注技术。

假设前端开发人员需要几分钟来构建一个应用程序。如果关心代码质量,他们需要运行linting,单元测试,集成测试或者用其他的方式确认之后才能提交。这个过程很耗时。

最后,将打包好的程序放到服务器额外还需要几分钟时间。如果我们给一个程序员分配了以上所有这些任务,请记住我们还没有考虑其切换上下文所需的时间(例如,更改代码分支,重新聚焦到他们的工作上等等)。

现在,谁想要手动部署每个功能?如果每天都测试了三个新功能怎么办?如果有15个呢?依据不同的部署规模,很有可能需要一个以上的全职人员来处理上述任务。

这就是为什么我们需要在这里运用计算机诞生之初的思想:我们应该用一台机器来为我们做这件事。
# 持续集成和持续部署的好处
在我们讨论用于构建,测试和部署代码的特定软件解决方案之前,我们先来熟悉一下描述该过程的两个术语。你可能已经听说过它们了:
5.png

注意,通常CD部分代表持续交付,这个概念略有不同,我们将不会在这篇文章中讨论。这种容易引起混淆的缩写通常是许多学术讨论的基础。Atlassian有一篇很棒的文章解释了它们之间的差异。

为什么有两个单独的短语,它们到底是什么意思?不用担心——为了避免混淆,让我们先弄清楚一点,然后描述两者背后的普遍意义。

CI / CD的持续集成部分涵盖了应用程序完整性的重复测试。从技术角度来看,这意味着我们需要不断执行linting,运行unit / E2E测试,检查源代码质量等。通过持续的方式,意味着必须在push新代码之前完成 - 即它应该自动完成。

例如,CI流程中可以定义一系列单元测试,这些单元测试将在拉取代码请求时一起运行。这种情形下,每次更新代码时,例如对于开发分支,一些机器会检查它是否符合标准且没有错误。

CI / CD的持续部署通常涵盖了构建和将应用程序部署到可用环境的一系列过程——这也是自动完成的。例如,它可以从指定的分支(例如:`master`)获取我们的应用程序代码,使用适当的工具(例如webpack)构建,并将其部署到正确的环境(例如,托管服务)。

它并不只限于生产环境;例如,我们可以配置一个Pipeline(管道)来构建应用程序的“staging”版本,并将其部署到适当的主机用于测试。

这两个术语是软件生命周期管理理论中完全不同源的独立概念,但在实践过程中,它们通常以互补的方式共存于一个大型Pipeline(管道)。为什么它们如此密不可分?因为通常CI和CD存在部分重叠。

例如,我们可能有一个项目,E2E测试和部署都需要用webpack构建前端代码。同时,在大多数“严苛”的生产级项目中,还有许多流程既有CI又有CD。

现在让我们想象在一个功能众多的项目中,CI / CD可以做些什么呢?
6.png

我知道越深入这个主题,流程图就越复杂 ——但是,这样一来在项目会议中用白板表示时,就显得很酷!

现在试想一下我们可以从上面的流程中得到些什么呢?我们从因与果的角度来分析,可以通过抽象特定的场景形成假想的工作流程。例如:

一名开发人员尝试push代码到公共代码库,然后需要执行一组单元测试。

通过这种方式,我们可以清晰得知道什么时候开始行动 - 我们可以通过使用脚本或其他机制实现自动化。在将来使用CI / CD的过程中,你可以为这些场景化的Pipeline命名。

注意上面的粗体字:当和然后,每个阶段都需要一个动作触发。为了运行特定的Pipeline,我们需要某种kickstart或触发器。这些可能是:

  • 计时类触发器(“每天下午6点构建staging版本的应用程序”)
  • 代码库触发器(“每次发布新的拉取请求时运行单元测试。”)
  • 手动触发器(“项目经理启动应用程序构建过程并部署到生产环境。”)
当然也可以通过其他的触发器触发特定的Pipeline,尤其是当我们需要通过许多单独构建的部分集成一个复杂应用程序的时候。好吧,理论说的差不多了,现在来说说为我们完成这些工作的软件。# CI / CD中用到的软件基本上,每个CI / CD软件说到底只是某种类型的任务执行工具,当触发某些操作时会运行Job(任务)。我们的主要工作是通过配置的方式提示要完成哪些Job以及何时完成等。基于这些基本描述,CI / CD软件有许多类型,规格和偏好 - 其中一些软件非常复杂以至于手册都有数百页。但也不用害怕:在本文结束之前,你将熟悉其中一个。对新手而言,我们可以把CI / CD软件分为两类:
  • 可安装软件:可以在你的电脑上或某些远程机器上安装的应用程序或服务(例如,Jenkins,TeamCity)
  • SaaS:由一个外部公司通过Web界面的方式提供的应用程序或服务(例如,CircleCI,Azure DevOps)
真的很难说哪一个更具优势,就像本文的主题一样,它取决于应用程序的要求,组织的预算和政策以及其他因素。值得一提的是,一些受欢迎的源代码托管商(例如,BitBucket)会维护自己的CI / CD Web服务,这些服务和源代码管理系统紧密联系,旨在简化配置过程。此外,一些云托管的CI / CD服务是免费且对公众开放的 - 只要该应用程序是开源的就可以。一个广受欢迎的例子就是CircleCI。我们将充分利用它的优势,简单几步就可以为我们的前端应用程序示例配置一个功能齐全的CI / CD的Pipeline。# 前提和计划CircleCI是一个云上的CI / CD服务,它能够与GitHub集成从而轻松获取源代码。该服务有一个有趣的规则:即pipeline在源代码内部定义。这意味着所有的操作和连锁反应都通过在源代码中配置一个特殊文件来实现,在CircleCI中是通过配置`.circleci`文件夹的`config.yml`文件实现的。本文为了实现教学目的,将执行以下操作:
  • 写一个简单的前端应用程序并将源代码公开放在GitHub上
  • 创建并push包含Pipeline的配置文件`config.yml`
  • 创建一个CircleCI帐户并关联GitHub帐户
  • 找一个地方部署应用程序(这里,我们使用Amazon S3的主机托管服务)
  • 最后,运行创建的Pipeline
整个过程不应超过30分钟。接下来,我们来看看准备工作的清单。你需要:# 第一步:环境设置首先,从上述代码库签一个分支并克隆到本地计算机。如果你是新人,可以看下这波操作都做了什么。上述操作执行成功后,跳转到目标目录并执行以下命令:
npm installnpm start
现在打开浏览器输入到http://localhost:8080,你应该看到这样的画面:
7.png
这是一个非常简单的前端应用程序,表明成功加载了`.js`和`.css`文件。你可以查看源代码,它用了一个非常简单的实现机制。当然,你也可以用该教程中使用你自己的应用,只需要适当的修改创建脚本的命令就可以。只要程序是基于npm之类的工具创建的标准应用,应该都没有问题。在使用自动化流程进行持续集成、持续部署之前,需要先构建应用程序并手动存到S3。这样,我们才能保证目标环境的配置没有问题。首先,我们在本地构建应用程序包。如果你使用我们提供的示例程序,就可以用`npm run build`命令来创建,然后在项目的根目录底下得到一个名为`dist`的文件夹:
8.png
好了,我们的应用程序已经构建并打包完毕。你可以在测试服务器上执行`npx serve -s dist`命令查看它的运行情况。这个例子会运行一个`serve`包,它是一个轻量的HTTP服务器,可用来分发`dist`目录下的内容。运行完命令以后可以通过http://localhost:5000查看,你会发现和它在开发环境服务器中的运行结果一致。OK,我们把应用部署到互联网上,这要从S3开始。作为AWS生态圈的一部分,Amazon S3的概念非常简单:它提供了一个存储,可以上传任何类型的文件(包括静态HTML,CSS和JavaScript等)并启用简单的HTTP服务器来分发这些内容。最棒的是(在某些情况下)它是免费的!首先,登录:
9.png
AWS登录步骤1:提供登录用的电子邮箱
10.png
AWS登录步骤2:输入密码接下来,点击服务按钮并在存储项中选择S3,跳转到S3控制面板。
11.png
现在,我们将创建一个新的存储桶来存储我们的Web应用程序。首先,输入名称,名称只支持字母数字字符和连字符。然后,为存储桶选择适当的域。注意要记录这两个值,稍后会用到。
12.png
13.png
有一点非常重要就是设置适当的权限,这样文件才是公开的类型。因此,单击下一步直到出现设置权限选项,然后点击取消前三个框以启用文件的公开托管方式:
14.png
这样,HTTP服务器就能将上传的文件作为网站公开。在设置完存储桶之后,你可以访问并查看文件列表:
15.png
点击上传按钮,系统将提示你选择要上传的文件,你可以从`dist`文件夹中选择三个包文件上传。这里的操作和之前的一样,一定要到设置权限处选择管理公共权限框下的对该对象赋予公共读取权限,这点非常重要。瞧!文件已经在了,最后一步是要在S3上启用托管服务。通过存储桶视图找到属性按钮,打开静态网站托管选项:
16.png
你需要添加`index.html`作为索引文档,它将作为应用程序的入口。现在,一切准备就绪。在对话框的开头会生成一个新站点的链接,点击就可以查看刚才部署的应用:
17.png
太棒了,我们有了一个网站——可惜这不是这次的目标,因为到此为止什么都没自动化。你肯定不希望用这种方式登录S3控制台并在每次更新时上传一堆文件,那是机器要做的事。那么,我们来建一个持续部署的流程吧!# 第二步:准备CircleCI配置如果仔细查看代码库中的示例程序,你会看见一个CD的定义文件,打开`.circleci/config.yml`文件。
version: 2.1orbs:  aws-s3: circleci/aws-s3@1.0.4jobs:  build:    docker:      - image: circleci/python:2.7-node    environment:      AWS_REGION: us-east-1    steps:      - checkout      - run: npm install      - run: npm run build      - aws-s3/sync:          from: dist          to: 's3://demo-ci-cd-article/'          arguments: |            --acl public-read \            --cache-control "max-age=86400" \          overwrite: trueworkflows:  version: 2.1  build:    jobs:      - build:          filters:            branches:              only: master
如上所述,`config.yml`是CircleCI可识别的文件,它包含了CD过程中定义好的pipeline信息。本文的例子中,26行代码包含了以下的内容:
  • 构建应用程序需要哪些工具
  • 构建应用程序需要哪些命令
  • 应用程序在哪里以及如何部署
如果你不熟悉YAML文件,你会注意到它大量使用制表格式。这就是这类文件的组织结构:每一部分都有子节点,而层次结构由一个有双空格组成的tab标志。现在,我们来逐层看一下文件结构:
version: 2.1orbs:  aws-s3: circleci/aws-s3@1.0.4
上面几行代码包含了解析器的版本信息,并定义了部署过程中会用到的附属包(CircleCI命名规则中的“orbs”)。在这里,我们需要导入一个名为`aws-s3`的orb ,它包含把文件发送到S3存储桶所需的工具。
jobs:  build:    docker:      - image: circleci/python:2.7-node    environment:      AWS_REGION: us-east-1    steps:      - checkout      - run: npm install      - run: npm run build      - aws-s3/sync:          from: dist          to: 's3://demo-ci-cd-article/'          arguments: |            --acl public-read \            --cache-control "max-age=86400" \          overwrite: true
上面几行代码包含了Job的定义信息,即Pipeline的核心内容。这里要注意的一点是,像上述第二行中所示我们把Job命名为`build`,稍后我们会在CircleCI控制台的报告中看到这个名字。下一行写着`docker`,这部分我们定义创建应用的容器(在哪台虚拟机运行)。如果不熟悉容器或者Docker,这一步你可以想象成选择一台虚拟机然后创建应用。这里,有一台预装了Python和Node.js的Linux虚拟机,我们需要用Python运行AWS S3的工具,用Node创建前端应用。`environment`和`AWS_REGION`是AWS运行用的环境变量,可以不用在意具体的参数。到此,S3就能运行了。下一部分,`steps`更具自描述性。实际上,它是完成Job需要执行的一系列步骤。这部分的示范定义如下:
  • `checkout`:从代码库中获取源代码
  • `run: npm install`:安装依赖包
  • `run: npm run build`:Pipeline的核心,用于构建代码
  • `aws-s3/sync`:另一个重要步骤,它部署(“同步”)S3存储桶中给定的`dist`路径下的内容。注意,这个示例把`demo-ci-cd-article`作为存储桶的名字,你需要修改存储桶名字,使它和示例中的名称一致。

# 解析CircleCI配置
基本上,你可以把它想象成运行在本地的一个包含一组操作的job,这样就是告诉VM一步一步如何完成。当然,你也可以认为它是具备一些特定功能的特殊的shell脚本。

job有一个重要原则那就是每一个步骤都得成功。有任何一个命令失败,就不再执行后续操作,当前的pipeline的状态也会标记为`FAILED`。Job执行失败的信息会在CI / CD控制台中显示,包括相关错误日志,有助于排错。

失败的原因有很多,比方说,对于一个执行自动测试的pipeline,它可能意味着一次单元测试的失败并且某个开发人员需要修复他的代码,或者也有可能是工具的配置有问题,从而导致构建和部署失败。无论是什么原因,CI / CD流程通常会通过电子邮件的方式通知管理员(或本人)执行失败的消息。

这就是为什么要以相对安全的方式定义jobs,以便在执行某一步出错时,确保之前的步骤不会产生任何永久的负面影响。

马上就要结束了,最后一部分是`workflows`:
workflows:
version: 2.1
perform_build:
jobs:
- build:
filters:
branches:
only: master

在CircleCI中“workflow”是一组互相协作的Job。由于之前我们只定义了一个Job(`build`),我们可以暂时不考虑这部分。但是,通过定义工作流的方式可以实现一个很重要的功能:branch filtering(筛选分支)。

你可以看到配置文件中的最后2行定义了`filters`。在这个例子中,它包含了`branches: only: master`,即定义了只有主分支上的代码更新才会执行构建代码Job。

这样就可以通过CI / CD流程筛选出需要“watched(监控)”的分支。例如,可以在不同的分支上调用不同的工作流(包含不同的Job),然后构建单独的版本,或仅在特定情况下测试等。
# 最后一步:CircleCI实践
如果你还没有完成,通过登录GitHub的方式,关联你的GitHub帐户与CircleCI
18.png

登录GitHub并授权给CircleCI后,你会在导航栏看见一个Add Project(添加项目)的选项。点击它可以查看你在GitHub中的代码库列表:
19.png

不管是拷贝的示例还是你自己准备的应用(记住有一个`.circleci/config.yml`文件),先假设你已经有一个代码库了。
然后,在列表中找到该项目后单击旁边的Set Up Project(设置项目)选项。你会看到一个描述CircleCI规则的画面:
20.png

看到底部Start building(开始构建)的按钮了吗?是的,就是它。点击它来启用我们的自动化流程,让机器为我们工作。
点击这个按钮后,你会看到……一个错误提示。
21.png

好吧,我们还需要配置一个地方:让CircleCI API授权给AWS的机制。到目前为止,我们还没有把AWS密码放入代码,GitHub或CircleCI里,所以AWS还不知道我们要把东西放入S3,所以报错。

通过改CircleCI面板中的项目设置来配置。单击右上角的齿轮图标,然后在左边找AWS权限选项卡,你会看到以下画面:
22.png

Access Key ID(访问秘钥ID)Secret Access Key(加密访问秘钥)是AWS的2个鉴权值,用于对CircleCI等第三方服务的鉴权。例如,将文件上传到S3存储桶。最初,这些密钥将具有与分配给它们的用户相同的权限。

你可以通过AWS控制台的IAM生成这些信息。进入IAM,打开访问秘钥Access Key ID(访问秘钥ID)Secret Access Key(加密访问秘钥)】窗口,点击创建新的访问密钥,生成可以复制到CircleCI的密钥对:
23.png

单击Save AWS keys(保存AWS秘钥)就可以了。你可以在CircleCI上尝试重新初始化代码库,也可以用更加快捷的方式:找到失败的报告,然后点击Rerun workflow(重新执行)工作流程按钮。
24.png

现在所有的问题都搞定了,构建应用应该不会再出状况了。
25.png

太棒了!你可以登录S3控制台并检查文件的修改时间,可以证明文件是新上传的。不过还没完事呢,我们来看看“持续”部分是如何工作的。我将返回代码编辑器,说一下应用(`index.html`)的一个小的变动:
26.png

现在,我们可以将代码推送到代码库:
 git add .
git commit -m “A small update!”
git push origin master

神奇的事情发生了。眨眼之间,在成功推送后,CircleCI已经在用更新后的代码构建应用了:
27.png

几秒钟后,会有一条执行`SUCCESS`的消息。现在,你可以刷新一下S3托管的web页面,就可以看到修改后的应用:
28.png

搞定!这一切都是自动执行的:推送完代码后互联网上的一些机器自动构建并部署到生产环境中。
# 进阶练习
当然,这只是一个简单的例子。现在我们来看一个更复杂的例子。例如,部署到多个环境并更改应用。

回到我们的示例代码,你会发现在`package.json`中有两个独立的构建脚本:一个用于`production`环境,一个用于`staging`环境。由于只是一个示例项目,所以不会发生大的变更。这里,它只是用一个不同的JavaScript控制台消息表示而已。

应用在`staging`环境运行之后打开浏览器,你可以在JavaScript控制台看到相应的日志信息:
29.png

现在,我们利用这个机制扩展构建应用的pipelines。请看以下的代码:
version: 2.1
orbs:
aws-s3: circleci/aws-s3@1.0.4
jobs:
build:
docker:
- image: circleci/python:2.7-node
environment:
AWS_REGION: us-east-1
steps:
- checkout
- run: npm install
- run: npm run build
- aws-s3/sync:
from: dist
to: 's3://demo-ci-cd-article/'
arguments: |
--acl public-read \
--cache-control "max-age=86400" \
overwrite: true
build-staging:
docker:
- image: circleci/python:2.7-node
environment:
AWS_REGION: us-east-1
steps:
- checkout
- run: npm install
- run: npm run build:staging
- aws-s3/sync:
from: dist
to: 's3://demo-ci-cd-article/'
arguments: |
--acl public-read \
--cache-control "max-age=86400" \
overwrite: true
workflows:
version: 2.1
build:
jobs:
- build:
filters:
branches:
only: master
build-staging:
jobs:
- build-staging:
filters:
branches:
only: develop

注意,我们添加了一个新的job和一个新的`build-staging`工作流程。有两点不同:新job调用前面提到的
`npm run build:staging`方法,同时用`develop`分支进行筛选。

这意味着所有到`develop`分支的推送都将用“staging”构建,而`master`分支上的所有变更都将保留其原始状态并触发“production”构建。在这里,双方都会在同一个S3存储桶中,但我们可以修改并让它们在相互隔离的目标环境中运行。

可以试一下:基于`master`分支创建一个新的`develop`分支并将代码推送到代码库。在CircleCI控制台,你会看到调用了不同的工作流程:
30.png

相应的变更推送到了S3存储桶,但这次在staging上构建来自`develop`分支的应用,实现了多版本的构建工作。很好,我们马上就能实现之前描述过的工作流程啦!
# 持续集成部分
我们已经完成了持续部署部分的内容,但什么是持续集成呢?正如之前说过的,这部分涉及到定期检查代码的质量,例如:执行测试。

如果仔细看示例的代码库就可以看到有一个单元测试样例,可以用`npm run test`命令执行该测试样例。它通过断言的方式比较了某些模式下虚函数的结果。
function getMessage() {
return 'True!';
}

// ...

module.exports = getMessage;


const getMessage = require('./jsChecker');
const assert = require('assert');

assert.equal(getMessage(), 'True!');

我们可以在管道中加入测试,然后设置成在每个拉取请求时执行就可以。实现方式是在`config.yml`里创建一个新job和一个新的工作流程:

config.yml



version: 2.1
orbs:
aws-s3: circleci/aws-s3@1.0.4
jobs:
build:
# ...
build-staging:
# ...
test:
docker:
- image: circleci/python:2.7-node
steps:
- checkout
- run: npm install
- run: npm run test
workflows:
version: 2.1
build:
# ...
build-staging:
# ...
test:
jobs:
- test

我们已经定义了一个新job和一个名为`test`的工作流程,唯一的目的是触发`npm run test`脚本。然后,将此文件推送到代码库,看一下CircleCI控制台会发生什么:
31.png

一个新的工作流程被自动触发,并完成了一次成功的测试。接下来把它和GitHub的代码库进行对接,这样一来每次拉取特定分支的请求都会触发该job。要实现这一点,只需要打开GitHub页面并到Settings(设置)页面,选择Branches(分支)
32.png

单击Add rule(添加规则),就可以添加一个新的策略。该策略将在合并拉取请求之前强制执行一系列检查,其中一项检查就是调用CircleCI工作流程,如下所示:
33.png

通过勾选Require status checks to pass before merging(合并之前要检查状态)并勾选的`ci/circleci: test`,就可以将规则设置为在拉取前执行该工作流。

该规则可以通过创建一个新的拉取请求来测试,然后打开Checks(检查)面板来查看测试情况:
34.png

当然,也可以测试该规则无效的情况。你可以提交一个会导致测试失败的变更,把它放到一个新分支上并执行一个拉取请求:
35.png

我们模拟了一个失败的测试,输出结果如下:
assert.equal(getMessage(), 'True!');
-->
[quote] node src/modules/jsChecker.test.js
assert.js:42
throw new errors.AssertionError({
^
AssertionError [ERR_ASSERTION]: 'True, but different!' == 'True!'
at Object.

现在这个拉取请求将无法合并,因为它引入了导致测试失败的代码:
36.png

赞!我们的示例项目成功覆盖了连续测试的各种情况,只要测试用例没问题,就不可能把错误的代码引入到生产分支。同样的机制还可用于执行代码linting,静态代码分析,E2E测试和其他自动化检查等。[/quote]

好的,就这样!虽然我们的示例项目非常简单,但它展现了真实且有效的CI / CD流程。无论是集成还是部署都由云上的工具执行,所以开发者可以将所有注意力集中到编码上。

无论涉及多少人,机器都将不知疲倦地工作,并检查一切是否到位。虽然设置这一切也需要花费一些时间,但从长远看,把机械性操作进行自动化处理是非常有意义的一件事。

当然,它不是永远的免税天堂:迟早会产生额外的费用。例如,CircleCI每月提供1,000分钟的免费构建。对于小型团队和简单的开源项目来说足够了,但对大型的企业级项目而言肯定会超过这个配额。
# 延伸阅读
我们学习了许多基础的知识,但这篇文章还有许多重要的内容还没来得及讲解。

有一点就是如何更好的使用环境变量。通常我们都不会直接在源代码中保存密码,API密钥和其他敏感信息。当引入CI / CD自动化流程后,首先需要向机器提供适当的变量,就像我们在示例中使用AWS密码一样。

除此之外,环境变量来可以用来控制构建的过程,例如:应该构建哪个或者应该在特定版本中启用哪些特征之类。你可以通过它们在CircleCI中的使用这篇文章中获得更多的信息。

另一个是:许多CI / CD流程引入了组件管理的概念。组件是对特定构建过程中产生的代码的通称。例如,一个包或具有特定版本的应用程序的容器镜像都可以看做组件。

在特定组织中,由于各种原因导致对组件版本的管理变得格外重要。例如:它们可能会被归类和归档以便用于回滚或其他用途。

另一个重要的部分是角色、权限和安全性。这篇文章涉及到定义Pipelines和工作流的基础操作,但在大型、真实的项目中,有必要将组织的流程和策略等考虑在内。例如,我们希望某个Pipeline只能由公司组织架构中的某个人调用或批准。

另一个例子是对特定管道的设置或VM的配置进行细粒度的控制。但同样,这取决于用什么软件以及特定项目或公司的要求,好的自动化流程没有一个单一的范式,就像好的IT项目没有单一的模式一样。
# 总结
好了,言归正传。

不知道这篇文章会让你有什么样的收获?重要的是,现在你已经对一些“重大”的项目中发生的事情有了一个大致的了解。无论使用何种方法和软件,一些基本的规则总是相似的:有任务、管道和代理执行此工作流程。希望通过这篇文章,会让你对许多概念有一个新的认识。最后,你可以试着创建实际工作中用到的CI / CD Pipeline,并用自动化的方式将应用部署到云上。

接下来你还可以做什么呢?

当然,继续扩充你的知识,努力做的更好。如果你正在为公司开发项目,可以尝试写写代码,创建你自己的测试/部署pipeline。你可以(甚至应该)在你的下一个开源项目中引入自动化测试、打包等。您还可以了解更多的CI / CD软件:例如Travis,Jenkins或Azure DevOps。

此外,你还可以查看我的个人资料中与前端开发相关的其他帖子。祝你好运!

原文链接:From front-end developer to a DevOps: An intro to CI/CD (翻译:Tiny Guo)

GitLab Auto DevOps功能与Kubernetes集成教程

Rancher 发表了文章 • 1 个评论 • 606 次浏览 • 2019-04-23 18:37 • 来自相关话题

介 绍 === 在这篇文章中,我们将介绍如何将GitLab的Auto DevOps功能与Rancher管理的Kubernetes集群连接起来,利用Rancher v2.2.0中引入的授权集群端点 ...查看全部
介 绍
===



在这篇文章中,我们将介绍如何将GitLab的Auto DevOps功能与Rancher管理的Kubernetes集群连接起来,利用Rancher v2.2.0中引入的授权集群端点的功能。通过本文,你将能全面了解GitLab如何与Kubernetes集成,以及Rancher如何使用授权集群端点简化这一集成工作的流程。本文非常适合Kubernetes管理员、DevOps工程师,或任何想将其开发工作流与Kubernetes进行集成的人。




背 景
===



什么是GitLab Auto DevOps?
----------------------



Auto DevOps是在GitLab 10.0中引入的功能,它让用户可以设置自动检测、构建、测试和部署项目的DevOps管道。将GitLab Auto DevOps与Kubernetes集群配合使用,这意味着用户可以无需配置CI / CD资源和其他工具,即可以部署应用程序。



什么是Rancher的授权集群端点?
------------------



从v2.2.0开始,Rancher引入了一项名为Authorized Cluster Endpoint的新功能,用户可以直接访问Kubernetes而无需通过Rancher进行代理。在v2.2.0之前,如果要直接与下游Kubernetes集群通信,用户必须从各个节点手动检索kubeconfig文件以及API服务器地址。这不仅增加了操作的复杂度,而且还没有提供一种机制来控制通过Rancher管理集群时可用的细化权限。



从Rancher v2.2.0开始,部署Rancher管理的集群时,默认情况下会启用授权群集端点(ACE)功能。ACE将部分Rancher身份验证和授权机制推送到下游Kubernetes集群,允许Rancher用户直接连接到这些集群,同时仍遵守安全策略。



如果您已为某些项目中的某个用户明确授予了权限,则当该用户使用授权集群端点进行连接时,这些权限能自动生效应用。现在,无论用户是通过Rancher还是直接连接到Kubernetes集群,安全性都能得到保障。



授权集群端点功能的相关文档对此有更详细的说明:

https://rancher.com/docs/rancher/v2.x/en/cluster-provisioning/rke-clusters/options/#authorized-cluster-endpoint



注意
>
> 目前,授权集群端点功能暂时仅适用于使用Rancher Kubernetes Engine(RKE)启动的下游Kubernetes进群。




前期准备
====



要将GitLab Auto DevOps与Rancher管理的Kubernetes集群进行对接,您需要实现准备好:

- 一个GitLab.com帐户,或一个自托管GitLab实例上的帐户(需已启用Auto
DevOps):GitLab.com帐户需要已经配置好了Auto
DevOps。如果您使用的是自托管GitLab实例,则可以参考这一GitLab文档了解如何启用Auto
DevOps:https://docs.gitlab.com/ee/topics/autodevops/

  • 运行版本v2.2.0或更高版本的Rancher实例:您可以以单节点模式启动Rancher(https://rancher.com/quick-start/),也可以创建HA安装(https://rancher.com/docs/rancher/v2.x/en/installation/ha/)。
  • Rancher管理的Kubernetes集群:您还需要一个通过RKE配置的、Rancher上管理的集群。此外,集群中需要有一个管理员用户,如果您使用的是GitLab.com,则需要通过公共网络访问控制平面节点。

设置Rancher和Kubernetes
====================


首先,我们需要先将Rancher和Kubernetes设置好。该过程的第一部分主要涉及收集信息。



注意
>
> 为简单起见,这些步骤使用的是Rancher中默认的admin帐户。最佳实践要求您使用独立用户执行此类过程,并限制该用户对正在集成GitLab的集群的权限。





登录Rancher并导航到要集成的下游集群。在本演示中,我们将在EC2实例上创建一个名为testing的集群,该集群在Amazon中运行:




在集群的仪表板上,单击顶部的Kubeconfig File按钮。这将打开kubeconfig集群的文件,其中包括授权集群端点的信息。



kubeconfig文件中的第一个条目是通过Rancher服务器的集群端点。向下滚动以标识此集群的授权群集端点,该集群列为单独的集群条目:






在我的示例中,此集群的名称是testing-testing-2,并且端点server是AWS提供的公共IP。



复制server和certificate-authority-data字段的值,不包括引号,并保存它们。



在kubeconfig文件中进一步向下滚动并找到您的用户名和token:




复制token字段(不包括引号)并保存。



接下来解码证书授权机构数据的base64版本,将其转换回原始版本并保存。根据您的工具,一些可行的选项包括:




设置GitLab项目
==========



通过我们从Rancher收集的信息,我们现在可以配置GitLab了。我们将首先在GitLab中创建一个新项目,该项目将使用Auto DevOps功能与我们的Kubernetes集群集成。



首先,登录GitLab,然后选择New Project。



在“新建项目”页面上,选择“从模板创建”选项卡。这将为您提供要使用的模板项目列表。选择NodeJS Express,然后单击“Use template”:




为项目命名,并将“可见性级别”设置为“ 公共”。完成后单击“ 创建项目”。



注意
>
> 在我撰写本文时,可见性级别可以设为“私密”,不过这是GitLab的Auto DevOps实验性功能。





在项目页面左侧的菜单窗格中,选择“设置”>“CI / CD”。展开“ 环境变量”部分,并设置以下变量:




我们这次会禁用下图这些功能,因为我们的简单示例暂时不需要它们,并且它们会延长部署所需的时间。在实际项目中,您可以根据您的实际需求启用其中一些选项:




单击“ 保存变量”以完成GitLab项目配置。




连接GitLab和Rancher
================



现在,我们已准备好将我们的GitLab项目与Rancher管理的Kubernetes集群集成。



在GitLab中,选择新克隆的项目。在左侧菜单中,选择“ 操作”>“Kubernetes”。单击绿色“添加Kubernetes集群”按钮。在下一页上,选择“添加现有集群”选项卡。

按以下信息填写相应字段:







单击“ 添加Kubernetes集群”。GitLab将添加集群,并在其中创建新的命名空间。您可以查看Rancher接口,确认新创建的命名空间已经创建成功。



注意
>
> GitLab连接到集群时所做的第一件事就是为项目创建一个命名空间。如果您在一段时间后没有看到创建名称空间,则说明可能出现了一些问题。





将集群添加到GitLab后,将显示要安装到集群中的应用程序列表。第一个是Helm Tiller。继续,单击“ 安装”将其添加到集群。



接下来,安装Ingress,它将允许GitLab将流量路由到您的应用程序:




根据您配置进群的方式,您的入口端点可能会自动填充,也可能不会。在本教程中,我将使用xip.io主机名来指向单个节点的流量。至于您的用例,您可能需要设置通配符域并将其指向此ingress(或指向您的节点IP等)。



部署好ingress后,滚动到页面顶部并找到“基本域”字段。输入其中一个节点的公共IP地址,然后输入.xip.io。这将创建一个解析为该IP地址的通配符域,这对于我们的示例就足够了:




接下来,在导航栏中,选择“设置”>“CI / CD”。展开“ 自动DevOps”部分,然后选中“默认为自动DevOps管道”框。这不仅意味着Auto DevOps已被设为默认值,还能够触发构建。将“部署策略”设置为“ 继续部署到生产”:




检查Auto DevOps框后,管道运行将开始。导航到GitLab中的CI / CD>管道。您应该看到类似于下图的内容,这表明GitLab正在部署您的应用程序:




验证Rancher中的部署
=============



下面让我们回到Rancher,查看一下我们的部署的情况,看看资源是如何转换为Rancher界面中的Kubernetes对象的。



在Rancher中,导航到您的进群,然后单击顶部导航菜单中的Projects / Namespaces。



GitLab代表您创建了两个命名空间:一个是gitlab-managed-apps,另一个是唯一的应用程序命名空间。gitlab-managed-apps命名空间包含资源,如用于部署应用程序的nginx 和Helm tiller实例。那个应用程序的唯一命名空间,包含着应用程序的部署。



为了将这些进一步可视化,我们可以将这些命名空间移动到我们的Default项目中。您也可以使用任何其他项目。单击“移动”按钮,然后选择所需的项目:




移动命名空间后,导航到他们所属的项目,然后导航到Workloads页面。该页面将在其特定于应用程序的命名空间中显示您的新部署:




请注意部署名称下的443 / https链接。单击该链接,您就可以跳转至您的部署的通配符域的ingress。如果一切顺利,你将可以看到这个象征着成功的页面:





结 语



恭喜!您刚刚成功地使用授权集群端点将GitLab的Auto DevOps与Rancher管理的Kubernetes集群连接,以实现更安全、直接的连接了!



当探索Rancher的其他区域时,你可能会注意到GitLab以你的名义为你创建的其他对象。例如,“负载均衡”选项卡显示已部署的L7 ingress以及创建的主机名。您还可以在“服务发现”选项卡下查看部署的应用程序的内部服务。



GitLab的Auto DevOps功能不仅易于使用,而且可定制且功能强大。在本文的演示中,我们禁用了一些高级功能,如自动测试、依赖项扫描和许可管理。这些功能在后期也可以重新启用,并通过配置GitLab,为您的开发环境提供更多意想不到的便利与价值。除了Auto DevOps之外,GitLab还为CI / CD提供了.gitlab-ci.yml文件,用户可以借此进行更多的扩展定制。在GitLab的文档中您可以了解到更多信息:

https://docs.gitlab.com




在Kubernetes和Rancher上构建CI / CD流水线



Kubernetes的一大价值,就是为企业优化开发操作流程,而CI工作流与Kubernetes的集成,是大多团队极关注的重要部分。



本周三(4月24日)晚20:30,Rancher将举办免费的在线培训《企业如何构建CI/CD流水线》,本次直播中,我们将分享:

- 如何对接GitLab


- 构建镜像


- 发布镜像到内置的镜像仓库


- 发布镜像到远端仓库

- 通过流水线部署应用

- 通过应用商店发布自己的应用

- 如何设置流水线通知

您可以点击链接:http://live.vhall.com/729465809 预约此次课程,周三晚使用同一链接即可观看直播!


DockOne微信分享(二零六):容器环境下的持续集成最佳实践

大卫 发表了文章 • 0 个评论 • 1027 次浏览 • 2019-04-03 11:49 • 来自相关话题

【编者的话】本次分享结合 Drone + GitFlow + Kubernetes 介绍一些容器环境下持续集成、持续发布方面的实践经验。分享将展示从 Hello World 到单人单分支手动发布到团队多分支 GitFlow 工作流再到团队多分支 semanti ...查看全部
【编者的话】本次分享结合 Drone + GitFlow + Kubernetes 介绍一些容器环境下持续集成、持续发布方面的实践经验。分享将展示从 Hello World 到单人单分支手动发布到团队多分支 GitFlow 工作流再到团队多分支 semantic-release 语义化发布最后到通知 Kubernetes 全自动发布,如何从零开始一步一步搭建 CI 将团队开发、测试、发布的流程全部自动化的过程,最终能让开发人员只需要认真提交代码就可以完成日常的所有 DevOps 工作。

云原生(Cloud Native)是伴随的容器技术发展出现的的一个词,最早出自 Pivotal 公司(即开发了 Spring 的公司)的一本技术小册子 Migrating to Cloud-Native Application Architectures, 其中定义了云原生应用应当具备的一些特质,如无状态、可持续交付、微服务化等。随后云原生概念被广为引用,并围绕这一概念由数家大厂牵头,成立了 CNCF 基金会来推进云原生相关技术的发展,主要投资并孵化云原生生态内的若干项目,包括了如 Kubernetes / etcd / CoreDNS 等耳熟能详的重要项目。而这张大大的云原生版图仍然在不断的扩展和完善。

从个人理解来说,传统的应用由于年代久远,更多会考虑单机部署、进程间通信等典型的“单机问题”,虽然也能工作在容器下,但由于历史包袱等原因,架构上已经很难做出大的调整。而“云原生”的应用大都是从一开始就为容器而准备,很少考虑在单机环境下使用,有些甚至无法脱离容器环境工作;考虑的场景少,从而更轻量,迭代更快。比如 etcd 之于 ZooKeeper, Traefik 之于 Nginx 等,相信只要在容器环境下实现一次同样的功能,就能强烈的体会到云原生应用所特有的便捷之处。

在 CNCF 的版图下,持续集成与持续交付(Continuous Integration & Delivery)板块一直缺少一个钦定的主角,虽然也不乏 Travis CI、GitLab、Jenkins 这样的知名项目,但最能给人云原生应用感觉的,应该还是 Drone 这个项目,本文将围绕 Drone 结合 GitFlow 及 Kubernetes 介绍一些容器环境下持续集成、持续发布(CI/CD)方面的实践经验。
#主流 CI/CD 应用对比
之前我也介绍过基于 Travis CI 的一些持续集成实践。后来经过一些比较和调研,最终选择了 Drone 作为主力 CI 工具。截止本文,团队已经使用 Drone 有 2 年多的时间,从 v0.6 一路用到现在即将发布的 v1.0,虽然也踩了不少坑,但总的来说 Drone 还是可以满足大部分需求,并以不错的势头在完善和发展的。

下面这张表总结了主流的几个 CI/CD 应用的特点:
B1.png

Travis CI 和 CircleCI 是目前占有率最高的两个公有云 CI,易用性上相差无几,只是收费方式有差异。由于不支持私有部署,如果并行的任务量一大,按进程收费其实并不划算;而且由于服务器位置的原因,如果推送镜像到国内,速度很不理想。

GitLab CI 虽然好用,但和 GitLab 是深度绑定的,我们的代码托管在 GitHub,整体迁移代码库的成本太大,放弃。

Jenkins 作为老牌劲旅,也是目前市场占有率最高的 CI,几乎可以覆盖所有 CI 的使用场景,由于使用 Java 编写,配置文件使用 Groovy 语法,非常适合 Java 为主语言的团队。Jenkins 显然是可以满足我们需要的,只是团队并非 Java 为主,又已经习惯了使用 YAML 书写 CI 配置,抱着尝鲜的心态,将 Jenkins 作为了保底的选择。

综上,最终选择 Drone 的结论也就不难得出了,Drone 即开源,又可以私有化部署,同时作为云原生应用,官方提供了针对 Docker、Docker Swarm、Kubernetes 等多种容器场景下的部署方案,针对不同容器场景还有特别优化,比如在 Docker Swarm 下 Drone 是以 Agent 方式运行 CI 任务的,而在 Kubernetes 下则通过创建 Kubernetes Job 来实现,显然充分利用了容器的优势所在,这也是 Drone 优于其他 CI 应用之处。个人还觉得 Drone 的语法是所有 CI 中最容易理解和掌握的,由于 Drone 每一个步骤都是运行一个 Docker 容器,本地模拟或调试也非常容易。

一句话概况 Drone,可以将其看做是可以支持私有化部署的开源版 CircleCI,并且目前仍然没有看到有其他主打这个定位的 CI 工具,因此个人认为 Drone 是 CI/CD 方面云原生应用头把交椅的有力竞争者。
#容器环境下一次规范的发布应该包含哪些内容
技术选型完成后,我想首先演示一下最终的成果,希望能直观的体现出 CI 对自动化效率起到的提升,不过这就涉及到一个问题:在容器环境下,一次发布应该包含哪些内容,其中有哪些部分是可以被 CI 自动化完成的。这个问题虽然每家公司各不相同,不过按经验来说,容器环境下一次版本发布通常包含这样一个 Checklist:

* 代码的下载构建及编译
* 运行单元测试,生成单元测试报告及覆盖率报告等
* 在测试环境对当前版本进行测试
* 为待发布的代码打上版本号
* 编写 ChangeLog 说明当前版本所涉及的修改
* 构建 Docker 镜像
* 将 Docker 镜像推送到镜像仓库
* 在预发布环境测试当前版本
* 正式发布到生产环境

看上去很繁琐对吗,如果每次发布都需要人工去处理上述的所有内容,不仅容易出错,而且也无法应对 DevOps 时代一天至少数次的发布频率,那么下面就来使用 CI 来解决所有问题吧。
#CI 流程演示
为了对 CI 流程有最直观的认识,我创建了一个精简版的 GitHub 项目 AlloVince/drone-ci-demo 来演示完整的流程,同时项目对应的 CI 地址是 cloud.drone.io/AlloVince/drone-ci-demo ,项目自动构建的 Docker 镜像会推送到 docker registry 的 allovince/drone-ci-demo。为了方便说明,假设这个项目的核心文件只有 index.html 一个静态页面。
##单人开发模式
目前这个项目背后的 CI 都已经配置部署好,假设我是这个项目的唯一开发人员,如何开发一个新功能并发布新版本呢?

* Clone 项目到本地, 修改项目代码, 如将 Hello World 改为 Hello World V2。
* git add .,然后书写符合约定的 Commit 并提交代码, git commit -m "feature: hello world v2”。
* 推送代码到代码库git push,等待数分钟后,开发人员会看到单元测试结果,GitHub 仓库会产生一次新版本的 Release,Release 内容为当前版本的 ChangeLog, 同时线上已经完成了新功能的发布。

虽然在开发者看来,一次发布简单到只需 3 个指令,但背后经过了如下的若干次交互,这是一次发布实际产生交互的时序图,具体每个环节如何工作将在后文中详细说明。
01.png

##多人开发模式
一个项目一般不止一个开发人员,比如我是新加入这个项目的成员,在这个 Demo 中应该如何上线新功能呢?同样非常简单:

  1. Clone 项目到本地,创建一个分支来完成新功能的开发, git checkout -b feature/hello-world-v3。在这个分支修改一些代码,比如将Hello World V2修改为Hello World V3
  2. git add .,书写符合规范的 Commit 并提交代码, git commit -m "feature: hello world v3”
  3. 将代码推送到代码库的对应分支, git push origin feature/hello-world
  4. 如果功能已经开发完毕,可以向 Master 分支发起一个 Pull Request,并让项目的负责人 Code Review
  5. Review 通过后,项目负责人将分支合并入主干,GitHub 仓库会产生一次新版本的 Release,同时线上已经完成了新功能的发布。

这个流程相比单人开发来多了 2 个环节,很适用于小团队合作,不仅强制加入了 Code Review 把控代码质量,同时也避免新人的不规范行为对发布带来影响。实际项目中,可以在 GitHub 的设置界面对 Master 分支设置写入保护,这样就从根本上杜绝了误操作的可能。当然如果团队中都是熟手,就无需如此谨慎,每个人都可以负责 PR 的合并,从而进一步提升效率。
02.png

##GitFlow 开发模式
在更大的项目中,参与的角色更多,一般会有开发、测试、运维几种角色的划分;还会划分出开发环境、测试环境、预发布环境、生产环境等用于代码的验证和测试;同时还会有多个功能会在同一时间并行开发。可想而知 CI 的流程也会进一步复杂。

能比较好应对这种复杂性的,首选 GitFlow 工作流, 即通过并行两个长期分支的方式规范代码的提交。而如果使用了 GitHub,由于有非常好用的 Pull Request 功能,可以将 GitFlow 进行一定程度的简化,最终有这样的工作流:
03.png


* 以 dev 为主开发分支,Master 为发布分支
* 开发人员始终从 dev 创建自己的分支,如 feature-a
* feature-a 开发完毕后创建 PR 到 dev 分支,并进行 code review
* review 后 feature-a 的新功能被合并入 dev,如有多个并行功能亦然
* 待当前开发周期内所有功能都合并入 dev 后,从 dev 创建 PR 到 master
* dev 合并入 Master,并创建一个新的 Release

上述是从 Git 分支角度看代码仓库发生的变化,实际在开发人员视角里,工作流程是怎样的呢。假设我是项目的一名开发人员,今天开始一期新功能的开发:

  1. Clone 项目到本地,git checkout dev。从 dev 创建一个分支来完成新功能的开发, git checkout -b feature/feature-a。在这个分支修改一些代码,比如将Hello World V3修改为Hello World Feature A
  2. git add .,书写符合规范的 Commit 并提交代码, git commit -m "feature: hello world feature A"
  3. 将代码推送到代码库的对应分支, git push origin feature/feature-a:feature/feature-a
  4. 由于分支是以 feature/ 命名的,因此 CI 会运行单元测试,并自动构建一个当前分支的镜像,发布到测试环境,并自动配置一个当前分支的域名如 test-featue-a.avnpc.com
  5. 联系产品及测试同学在测试环境验证并完善新功能
  6. 功能通过验收后发起 PR 到 dev 分支,由 Leader 进行 code review
  7. Code Review 通过后,Leader 合并当前 PR,此时 CI 会运行单元测试,构建镜像,并发布到测试环境
  8. 此时 dev 分支有可能已经积累了若干个功能,可以访问测试环境对应 dev 分支的域名,如 test.avnpc.com,进行集成测试。
  9. 集成测试完成后,由运维同学从 Dev 发起一个 PR 到 Master 分支,此时会 CI 会运行单元测试,构建镜像,并发布到预发布环境
  10. 测试人员在预发布环境下再次验证功能,团队做上线前的其他准备工作
  11. 运维同学合并 PR,CI 将为本次发布的代码及镜像自动打上版本号并书写 ChangeLog,同时发布到生产环境。

由此就完成了上文中 Checklist 所需的所有工作。虽然描述起来看似冗长,但不难发现实际作为开发人员,并没有任何复杂的操作,流程化的部分全部由 CI 完成,开发人员只需要关注自己的核心任务:按照工作流规范,写好代码,写好 Commit,提交代码即可。

接下来将介绍这个以 CI 为核心的工作流,是如何一步步搭建的。
#Step by Step 构建 CI 工作流
##Step.0:基于 Kubernetes 部署 Drone v1.0.0
以 GitHub 为例,截止本文完成时间(2019 年 3 月 28 日), Drone 刚刚发布了第一个正式版本 v1.0.0。官方文档已经提供了分别基于 Docker、Kubernetes 的 Drone 部署说明,不过比较简略,因此这里给出一个相对完整的配置文件。

首先需要在 GitHub 创建一个 Auth App,用于 repo 的访问授权。应用创建好之后,会得到 Client ID 和 Client Secret 。同时 Authorization callback URL 应填写 Drone 服务对应域名下的 /login,如 https://ci.avnpc.com/login。

Drone 支持 SQLite、MySQL、Postgres、S3 等多种后端存储,主要用于记录 build logs 等文本信息,这些信息并不是特别重要,且我们的 CI 有可能做迁移,因此个人更推荐使用 SQLite。

而在 Kubernetes 环境下,SQLite 更适合用挂载 NAS 的方式供节点使用,因此首先将存储的部分独立为文件 drone-pvc.yml,可以根据实际情况配置 nfs.path 和 nfs.server:
kubectl apply -f drone-pvc.yaml

Drone 的配置主要涉及两个镜像:

* drone/kubernetes-secrets 加密数据服务,用于读取 Kubernetes 的 secrets
* drone/drone:1.0.0-rc.6 就是 Drone 的 server 端,由于在 Kubernetes 下 Drone 利用了 Job 机制,因此不需要部署 Agent。

这部分配置较长,可以直接参考示例 drone.yaml

主要涉及到的配置项包括:

* Drone/kubernetes-secrets 镜像中

* SECRET_KEY:数据加密传输所用的 key,可以使用 openssl rand -hex 16 生成一个

* Drone/Drone镜像中

* DRONE_KUBERNETES_ENABLED:开启 Kubernetes 模式
* DRONE_KUBERNETES_NAMESPACE:Drone 所使用的 Namespace, 这里使用 default
* DRONE_GITHUB_SERVER:GitHub 服务器地址,一般为 https://github.com
* DRONE_GITHUB_CLIENT_ID:上文创建 GitHub Auth App 得到的 Client ID
* DRONE_GITHUB_CLIENT_SECRET:上文创建 GitHub Auth App 得到的 Client Secret
* DRONE_SERVER_HOST:Drone 服务所使用的域名
* DRONE_SERVER_PROTO:http 或 https
* DRONE_DATABASE_DRIVER:Drone 使用的数据库类型,这里为 SQLite3
* DRONE_DATABASE_DATASOURCE:这里为 SQLite 数据库的存放路径
* DRONE_SECRET_SECRET:对应上文的 SECRET_KEY
* DRONE_SECRET_ENDPOINT:加密数据服务的地址,这里通过 Kubernetes Service 暴露,无需修改

最后部署即可:
kubectl apply -f drone.yaml

部署后首次登录 Drone 就会跳转到 GitHub Auth App 进行授权,授权完毕后可以看到所有能读写的 Repo,选择需要开启 CI 的 Repo,点击 ACTIVATE 即可。 如果开启成功,在 GitHub Repo 的 Settings > Webhooks 下可以看到 Drone 的回调地址。
##Step.1:Hello World for Drone
在正式开始搭建工作流之前,首先可以测试一下 Drone 是否可用。Drone 默认的配置文件是 .drone.yml, 在需要 CI 的 repo 根目录下创建.drone.yml, 内容如下,提交并git push到代码仓库即可触发 Drone 执行 CI。
kind: pipeline  
name: deploy

steps:
[list]
[*]name: hello-world[/*]
[/list] image: docker
commands:
- echo "hello world"

Drone v1 的语法主要参考的 Kubernetes 的语法,非常直观,无需阅读文档也可以知道,我们首先定义了一个管道(Pipeline),管道由若干步骤(step)组成,Drone 的每个步骤是都基于容器实现的,因此 Step 的语法就回到了我们熟悉的 Docker,一个 Step 会拉取 image 定义的镜像,然后运行该镜像,并顺序执行 commands 定义的指令。

在上例中,Drone 首先 clone git repo 代码到本地,然后根据 .drone.yml 所定义的,拉取 Docker 的官方镜像,然后运行该进行并挂载 git repo 的代码到 /drone/src 目录。

在 Drone 的界面中,也可以清楚的看到这一过程。
04.png

本阶段对应:

* 代码部分:https://github.com/AlloVince/drone-ci-demo/tree/hello-world
* Drone 构建记录:https://cloud.drone.io/AlloVince/drone-ci-demo/1
* Docker 镜像:无

##Step.2:单人工作流,自动化单元测试与 Docker 镜像构建
有了 Hello World 的基础,接下来我们尝试将这个工作流进行扩充。

为了方便说明,这里假设项目语言为 JavaScript,项目内新增了 test/index.js 文件用于模拟单元测试,一般在 CI 中,只要程序的返回值为 0,即代表运行成功。这个文件中我们仅仅输出一行 Log Unit test passed用于模拟单元测试通过。

我们希望将代码打包成 Docker 镜像,根目录下增加了 Dockerfile 文件,这里直接使用 Nginx 的官方镜像,构建过程只有 1 行 COPY index.html /usr/share/nginx/html/, 这样镜像运行后可以通过 http 请求看到 index.html 的内容。

至此我们可以将工作流改进为:

* 当 Master 分支接收到 push 后,运行单元测试
* 当 GitHub 发布一次 Release, 构建 Docker 镜像,并推送到镜像仓库

对应的 Drone 配置文件如下:
kind: pipeline  
name: deploy

steps:
- name: unit-test
image: node:10
commands:
- node test/index.js
when:
branch: master
event: push
- name: build-image
image: plugins/docker
settings:
repo: allovince/drone-ci-demo
username: allovince
password:
from_secret: DOCKER_PASSWORD
auto_tag: true
when:
event: tag

虽然比 Hello World 复杂了一些,但是可读性仍然很好,配置文件中出现了几个新概念:

Step 运行条件, 即 when 部分,上例中展示了当代码分支为 Master,且收到一个 push;以及当代码被标记 tag 这两种情况。Drone 还支持 repo、运行结果等很多其他条件,可以参考 Drone Conditions 文档。

Plugin 插件,上例中用于构建和推送镜像的是 plugins/docker 这个 Plugin, 一个 Plugin 本质上仍然是一个 Docker 镜像,只是按照 Drone 的规范接受特定的输入,并完成特定的操作。所以完全可以将 Plugin 看做一个无法更改 command 的 Docker 镜像。

Docker 这个 Plugin 由 Drone 官方提供,用于 Docker 镜像的构建和推送,具体的用法可以查看 Docker 插件的文档。例子中演示的是将镜像推送到私有仓库,如果不做特殊配置,镜像将被推送到 Docker 的官方仓库。

此外 Docker 插件还有一个很方便的功能,如果设置 auto_tag: true,将根据代码的版本号自动规划 Docker 镜像的标签,如代码版本为1.0.0,将为 Docker 镜像打三个标签 1, 1.0, 1.0.0。如果代码版本号不能被解析,则镜像标签为 latest。

目前 Drone 的插件已经有很多,可以覆盖主流的云服务商和常见的工作流,并且自己制作插件的成本也不高。

Secret 加密数据,镜像仓库的用户名和密码都属于敏感信息,因此可以使用 from_secret 获取加密数据。一条加密数据就是一个 key / value 对,如上例中的 DOCKER_PASSWORD 就是我们自己定义的加密数据 key。即便加密数据在 log 中被打印,UI 也只能看到 ***。加密数据的 value 需要提前保存好,保存的方式有 3 种:

* 通过 Drone UI 界面中, repo -> Settings -> Secrets 添加,所添加的加密数据将保存在 Drone 的数据库中,仅能在当前 repo 中使用。
* 通过 Drone cli 加密后保存在 .drone.yml 文件中, 使用范围仅限 yaml 文件内
* 通过 Kubernetes 保存为 Kubernetes Secret,称为 External Secrets,所有的 repo 都可以共享。如果是团队使用的话,这种保存方式显然是最方便的,但也要注意安全问题,因此 External Secrets 还支持 repo 级别的权限管理, 可以只让有当前 repo 写入权限的人才能使用对应 secret。

这个阶段对应:

* 代码仓库:https://github.com/AlloVince/drone-ci-demo/tree/single-person
* push 时触发的 Drone CI:https://cloud.drone.io/AlloVince/drone-ci-demo/4
* Release 时触发的 Drone CI:https://cloud.drone.io/AlloVince/drone-ci-demo/5
* Release 后 CI 构建的 Docker 镜像:allovince/drone-ci-demo:latest

##Step.3:GitFlow 多分支团队工作流
上面的工作流已经基本可以应付单人的开发了,而在团队开发时,这个工作流还需要一些扩展。不需要引入 Drone 的新功能,只需要在上文基础上根据分支做一点调整即可。

首先保证单元测试位于 steps 的第一位,并且限定团队工作的分支,在 push 和 pull_request 时,都能触发单元测试。

 - name: unit-test  
image: node:10
commands:
- node test/index.js
when:
branch:
include:
- feature/*
- master
- dev
event:
include:
- push
- pull_request

然后根据 GitFlow 的流程对于不同的分支构建 Docker 镜像并打上特定标签,以 feature 分支为例,下面的配置约定了当分支名满足 feature/*,并收到 push 时,会构建 Docker 镜像并打标签,标签名称为当前分支名去掉 feature/。如分支 feature/readme, 对应 Docker 镜像为 allovince/drone-ci-demo:readme,考虑到 feature 分支一般都出于开发阶段,因此新的镜像会覆盖旧的。配置如下:
- name: build-branch-image  
image: plugins/docker
settings:
repo: allovince/drone-ci-demo
username: allovince
password:
from_secret: DOCKER_PASSWORD
tag:
- ${DRONE_BRANCH[size=16]feature/} [/size]
when:
branch: feature/*
event: push

镜像的 Tag 处不再使用自动方式,其中 DRONE_BRANCH 是 Drone 的内置环境变量(Environment),对应当前的分支名。feature/ 是执行了一个字符串的替换操作(Substitution)。更多的环境变量和字符串操作都可以在文档中找到。

以此类推,可以查看这个阶段的完整 .drone.yml ,此时我们的工作流示例如下:

* 团队成员从 dev 分支 checkout 自己的分支 feature/readme
* 向feature/readme提交代码并 push, CI 运行单元测试,构建镜像allovince/drone-ci-demo:readme
* 功能开发完成后,团队成员向 dev 分支 发起 pull request , CI 运行单元测试
* 团队其他成员 merge pull request, CI 运行单元测试,构建镜像allovince/drone-ci-demo:test
* 运维人员从 dev 向 master 发起 pull request,CI 运行单元测试,并构建镜像allovince/drone-ci-demo:latest
* 运维人员 merge pull request, 并 Release 新版本 pre-0.0.2, CI 构建镜像allovince/drone-ci-demo:pre-0.0.2

可能细心的同学会发现 dev -> Master 的 pull request 时,构建镜像失败了,这是由于 Drone 出于安全考虑限制了在 pull request 时默认无法读取加密数据,因此无法得到 Docker Registry 密码。如果是私有部署的话,可以在 Repo Settings 中勾选 Allow Pull Requests,此处就可以构建成功。
##Step.4:语义化发布
上面基本完成了一个支持团队协作的半自动 CI 工作流,如果不是特别苛刻的话,完全可以用上面的工作流开始干活了。

不过基于这个工作流工作一段时间,会发现仍然存在痛点,那就是每次发布都要想一个版本号,写 ChangeLog,并且人工去 Release。

标记版本号涉及到上线后的回滚,追溯等一系列问题,应该是一项严肃的工作,其实如何标记早已有比较好的方案,即语义化版本。在这个方案中,版本号一共有 3 位,形如 1.0.0,分别代表:

  1. 主版本号:当你做了不兼容的 API 修改,
  2. 次版本号:当你做了向下兼容的功能性新增,
  3. 修订号:当你做了向下兼容的问题修正。

虽然有了这个指导意见,但并没有很方便的解决实际问题,每次发布要搞清楚代码的修改到底是不是向下兼容的,有哪些新的功能等,仍然要花费很多时间。

而语义化发布(Semantic Release)就能很好的解决这些问题。

语义化发布的原理很简单,就是让每一次 Commit 所附带的 Message 格式遵守一定规范,保证每次提交格式一致且都是可以被解析的,那么进行 Release 时,只要统计一下距离上次 Release 所有的提交,就分析出本次提交做了何种程度的改动,并可以自动生成版本号、自动生成 ChangeLog 等。

语义化发布中,Commit 所遵守的规范称为约定式提交(Conventional Commits)。比如 Node.js、 Angular、Electron 等知名项目都在使用这套规范。

语义化发布首先将 Commit 进行分类,常用的分类(Type)有:

* feat:新功能
* fix:BUG 修复
* docs:文档变更
* style:文字格式修改
* refactor:代码重构
* perf:性能改进
* test:测试代码
* chore:工具自动生成

每个 Commit 可以对应一个作用域(Scope),在一个项目中作用域一般可以指不同的模块。

当 Commit 内容较多时,可以追加正文和脚注,如果正文起始为BREAKING CHANGE,代表这是一个破坏性变更。

以下都是符合规范的 Commit:
feat:增加重置密码功能

fix(邮件模块):修复邮件发送延迟BUG

feat(API):API重构

BREAKING CHANGE:API v3上线,API v1停止支持

有了这些规范的 Commit,版本号如何变化就很容易确定了,目前语义化发布默认的规则如下:
Commit	版本号变更
BREAKING CHANGE 主版本号
feat 次版本号
fix / perf 修订号

因此在 CI 部署 semantic-release 之后,作为开发人员只需要按照规范书写 Commit 即可,其他的都由 CI 完成。

具体如何将语义化发布加入 CI 流程中呢, semantic-release 是 js 实现的,如果是 js 的项目,可以直接在 package.json 中增加配置项,而对于任意语言的项目,推荐像 Demo 中一样,在根目录下增加 配置文件release.config.js。这个配置目的是为了禁用默认开启的 npm 发布机制,可以直接套用。

semantic-release 要执行 GitHub Release,因此我们需要在 CI 中配置自己的 Personal access tokens 让 CI 有 GitHub Repo 的读写权限, 可以通过 GitHub 点击自己头像 -> Settings -> Developer settings -> Personal access tokens -> Generate new token 生成一个 Token。 然后在 Drone 的 repo 设置界面新增一个 Secret, key 为 GITHUB_TOKEN, value 填入刚生成的 Token。

最后在 .drone.yml 中增加这样一段就可以了。
- name: semantic-release  
image: gtramontina/semantic-release:15.13.3
environment:
GITHUB_TOKEN:
from_secret: GITHUB_TOKEN
entrypoint:
- semantic-release
when:
branch: master
event: push

来再次模拟一下流程,feature 分支部分与上文相同:

* 从 dev 向 master 发起 pull request,CI 运行单元测试,并构建镜像allovince/drone-ci-demo:latest
* merge pull request,CI 会执行单元测试并运行 semantic-release , 运行成功的话能看到 GitHub 新增 Release v1.0.0
* GitHub Release 再次触发CI 构建生产环境用 Docker 镜像allovince/drone-ci-demo:1.0.0

最终我们能得到这样一个赏心悦目的 Release。
05.png

##Step.5:Kubernetes 自动发布
Docker 镜像推送到仓库后,我们还剩最后一步就可以完成全自动发布的闭环,即通知 Kubernetes 将镜像发布到生产环境。这一步实现比较灵活,因为很多云服务商在容器服务都会提供 Trigger 机制,一般是提供一个 URL,只要请求这个 URL 就可以触发容器服务的发布。Demo 中我们使用更为通用的方法,就是将 kubectl 打包为容器,以客户端调用 Kubernetes 集群 Master 节点 API(kube-apiserver)的形式完成发布。

假设我们在生产环境下 drone-ci-demo 项目的 Kubernetes 发布文件如下:
---  
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: ci-demo-deployment
namespace: default
spec:
replicas: 1
template:
spec:
containers:
- image: allovince/drone-ci-demo
name: ci-demo
restartPolicy: Always

对应 .drone.yml 中增加 step 如下。这里使用的插件是 honestbee/drone-kubernetes, 插件中 kubectl 连接 API 使用的是证书 + Token 的方式鉴权,因此需要先获得证书及 Token, 已经授权的 Token 保存于 Kubernetes Secret,可以通过 kubectl get secret [ your default secret name ] -o yaml | egrep 'ca.crt:|token:' 获得并配置到 Drone 中,注意插件要求 Token 是明文的,需要 base64 解码一下:echo [ your token ] | base64 -d && echo ''。
- name: k8s-deploy  
image: quay.io/honestbee/drone-kubernetes
settings:
kubernetes_server:
from_secret: KUBERNETES_SERVER
kubernetes_cert:
from_secret: KUBERNETES_CERT
kubernetes_token:
from_secret: KUBERNETES_TOKEN
namespace: default
deployment: ci-demo-deployment
repo: allovince/drone-ci-demo
container: ci-demo
tag:
- ${DRONE_TAG}
when:
event: tag

在示例中,可以看到在语义化发布之后 CI 会将新版本的 Docker 镜像自动发布到 Kubernetes,这里为了演示仅打印了指令并未实际运行。相当于运行了如下的指令:
kubectl -n default set image deployment/ci-demo-deployment ci-demo=allovince/drone-ci-demo:v1.0.2

由于自动发布的环节势必要接触到生产服务器,需要格外注意安全问题,首推的方式当然是将 CI 和 Kubernetes 集群放于同一内网中,同时可以使用 Kubernetes 的 RBAC 权限控制,为自动发布单独创建一个用户,并删除不必要的权限。
#后话
总结一下,本文展示了从 Hello World 到单人单分支手动发布再到团队多分支 GitFlow 工作流再到团队多分支 semantic-release 语义化发布最后到 通知 Kubernetes 全自动发布,如何从零开始一步一步搭建 CI 将团队开发、测试、发布的流程全部自动化的过程,最终能让开发人员只需要认真提交代码就可以完成日常的所有 DevOps 工作。

最终 Step 的完成品可以适配之前的所有 Step,如果不太在意实现细节的话,可以在此基础上稍作修改,直接使用。

然而写好每一个 Commit 这个看似简单的要求,其实对于大多数团队来说并不容易做到,在实施过程中,经常会遇到团队成员不理解为什么要重视 Commit 规范,每个 Commit 都要深思熟虑是否过于吹毛求疵等等疑问。

以 Commit 作为 CI 的核心,个人认为主要会带来以下几方面的影响:

  1. 一个好的 Commit,代表着开发人员对当前改动之于整个系统的影响,有非常清楚的认识,代码的修改到底算 feat 还是 fix ,什么时候用 BREAKING CHANGE 等都是要仔细斟酌的,每个 Commit 都会在 ChangeLog 里“留底”,从而约束团队不随意提交未经思考的代码,提高代码质量。
  2. 一个好的 Commit 也代表开发人员有能力对所实现功能进行精细的划分,一个分支做的事情不宜过多,一个提交也应该专注于只解决一个问题,每次提交(至少是每次 push)都应该保持系统可构建、可运行、可测试,如果能坚持做到这些,对于合并代码时的冲突解决,以及集成测试都有很大帮助。
  3. 由于每次发布能清楚的看到所有关联的 Commit 以及 Commit 的重要程度,那么线上事故的回滚也会非常轻松,回滚到哪个版本,回滚后哪些功能会受到影响,只要看 CI 自动生成的 Release 记录就一目了然。如果没有这些,回滚误伤到预期外的功能从而引发连锁反应的惨痛教训,可能很多运维都有过类似经历吧。

因此 CI 自动化其实是锦上添花而非雪中送炭,如果团队原本就无视规范,Commit 全是空白或者没有任何意义的单词,分支管理混乱,发布困难,奢望引入一套自动化 CI 来能解决所有这些问题,无疑是不现实的。而只有原本就重视代码质量,有一定规范意识,再通过自动化 CI 来监督约束,团队在 CI 的帮助下代码质量提高,从而有机会进一步改进 CI 的效率,才能形成良心循环。

愿天下不再有难发布的版本。
#Q&A
Q:Kubernetes 上主流的 CI/CD 方案是啥?
A:其实这无关 Kubernetes,从市场占有率来看,前三名分别是

  1. Jenkins
  2. JetBrains TeamCity
  3. CircleCI

来源:https://www.datanyze.com/market-share/ci

Q:GitLab 自带的 CI 与 Jenkins 和 GitLab 结合的 CI,该如何选择?想知道更深层次的理解。
A:还是要结合自己团队的实际情况做选择。从成熟度来说,肯定是 Jenkins 用户最多,成熟度最高,缺点是侧重 Java,配置相对繁琐。GitLab 自带的 CI 相对简单,可以用 yaml,和 GitLab 结合的最好,但功能肯定没有 Jenkins 全面。如果是小团队新项目,GitLab CI 又已经可以满足需求的话,并不需要上 Jenkins,如果是较大的团队,又是偏 Java 的,个人更偏向 Jenkins。

Q:Jenkins 如果不想运行在 Kubernetes 里面,该怎么和 Kubernetes 集成?
A:从 CI 的流程来说,CI 应用是不是跑在 Kubernetes 的并不重要,CI 只要能访问代码库,有权限在生产环境发布,是不是跑在容器里从效果来说其实没有区别,只是用 Kubernetes 部署 Jenkins 的话,运维的一致性比较好,运维团队不用额外花时间维护一套物理机的部署方案。

Q:Kubernetes 的回滚方案是回滚代码,重做镜像,还是先切流量,后做修改?
A:代码一定是打包到镜像里的,镜像的版本就是代码的版本,所以一定是切镜像。至于回滚操作本身,Kubernetes 已经内置了很多滚动发布(Rolling update)的策略,无论是发新版本还是回滚版本,都可以做到用户无感知。

Q:镜像大到几 G 的话如何更新部署,有什么好的实践呢,以及如何回滚?
A:几个要点:

  1. 镜像仓库部署在内网,镜像推拉走内网,几个 G 的镜像传输起来也只是秒级别的
  2. 镜像构建时将不经常变动的部分划分到 1 层,因为 Docker 镜像是分层的,这样每次拉镜像可以利用到 Docker 的缓存传输的只有镜像发生变化的部分
  3. 选择 alpine 这样尽量小的镜像

回滚如果发生在相邻的版本,一般物理机上是有缓存的,速度都很快。

Q:Drone 开放 API 服务吗?这样方便其他系统集成。
A:可以调整一下思路,直接把需要的功能做成镜像在 Drone 里调用就好了。

Q:如果有 Drone 的 Server怎么做高可用?
A:Drone serve r用 Kubernetes 部署的话本身只起到了一个任务调度的作用,很难会遇到性能瓶颈。真的有性能问题可以尝试水平扩展 Drone server,共享同一数据库。

作者博客:https://avnpc.com/pages/drone-gitflow-kubernetes-for-cloud-native-ci

以上内容根据2019年4月2日晚微信群分享内容整理。分享人徐谦,高级架构师,10 年以上研发经验,2 次创业经历。现从事互联网金融相关研发,关注 Node.js、容器技术、机器学习等。DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiesd,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。

一起探索Kubernetes

新牛哥 发表了文章 • 0 个评论 • 693 次浏览 • 2019-03-31 21:23 • 来自相关话题

【编者的话】本文用图示详细分析了GitLab如何与Kubernetes集群集成,进行CI/CD流水线的配置,从而实现更高效的DevOps流程。 我将介绍使用DigitalOcean创建新的Kubernetes集群(或简称k8s)的经验 ...查看全部
【编者的话】本文用图示详细分析了GitLab如何与Kubernetes集群集成,进行CI/CD流水线的配置,从而实现更高效的DevOps流程。

我将介绍使用DigitalOcean创建新的Kubernetes集群(或简称k8s)的经验,配置我的GitLab项目以使用Kubernetes集群,以及为部署配置CI/CD进程。 如果你想了解现代堆栈的运行是多么简单,请继续阅读。
1.jpg

Fatos Bytyqi拍摄
# 创建一个Kubernetes集群
Kubernetes是一个容器编排平台,由于其简单性而受到广泛欢迎。 Kubernetes很棒,因为你可以使用配置文件定义部署配置,存储和网络,集群将确保你的应用程序始终在该配置中运行。

从源代码构建Kubernetes集群是一项艰巨的任务,但是我们可以通过大型云提供商的几次点击来实现这一目标。 我个人更喜欢DigitalOcean的简单性,我很幸运能够成为他们管理的Kubernetes产品的LTD版本的一部分。

让我们深入了解如何在DigitalOcean上创建集群。

单击“创建Kubernetes”选项后,无论是通过侧面导航还是顶部导航中的下拉菜单,都会显示此屏幕。
2.png

DigitalOceans的Kubernetes集群创建示例

在撰写本文时,Kubernetes版本1.13.1是最新版本,因此我在离我最近的地区选择了该版本。

下一步是配置节点池,标签并选择名称。 我选择用一个低成本节点来保持简单,以便学习。 这可以在将来更改,因此从小规格开始不会限制你的未来容量。

标签是可选的,名称可以是你想要的任何名称。 我发现添加“k8s”标签可以快速识别集群中的droplet。
3.png

配置节点,标签和名字

单击“创建群集”后,该过程大约需要4-5分钟才能完成。 在此期间,我们可以让你的机器设置连接到新的Kubernetes群集。

用于与Kubernetes群集交互的主要的命令行程序是`kubectl`。 对于MacOS用户,可以使用`brew`通过运行以下命令来安装它。
➜ brew install kubernetes-cli

brew完成安装后,你需要从DigitalOcean下载集群配置文件,以使`kubectl`命令知道你的集群所在的位置。 为此,请在DigitalOcean Kubernetes群集安装页面上一直向下滚动到以下部分,然后单击“下载配置文件”按钮
4.png

该文件将保存到`~/Downloads`目录中。 为了简化操作,请将文件复制或移动到`~/.kube/config`文件。 该文件将由`kubectl`命令自动读取。
➜ mkdir -p ~/.kube
➜ mv ~/Downloads/[k8s-cluster].yaml ~/.kube/config

创建集群后,通过运行`kubectl get nodes`测试连接。 这将显示群集中的单个节点。
➜ kubectl get nodes
NAME STATUS ROLES AGE VERSION
tender-einstein-8m4m Ready 21m v1.13.1

在我的例子中,节点(这是一个DigitalOcean Droplet)被命名为“tender-einstein-8m4m”,我们可以在上面看到。 如果看到类似的输出,则表明你的Kubernetes集群已成功创建,并且你可以通过`kubectl`命令行程序与其建立连接。
# 将GitLab连接到Kubernetes
GitLab具有本地集成Kubernetes的功能,我们可以配置任何组或项目来使用它。 你需要在GitLab项目上提升(项目创建者和/或管理员)权限才能设置Kubernetes集成。

首先,选择Operations菜单下的Kubernetes选项卡,然后单击Add Kubernetes Cluster。
5.png

GitLab - 添加 Kubernetes 集群

在下一个屏幕上,单击“添加现有群集”选项卡。 在这里,系统将提示你输入一些不同的项目以允许GitLab连接到你的Kubernetes群集。 GitLab有关于如何添加集群的优秀文档,我建议你阅读这些文档以全面了解集成。我将在此强调所需的步骤。
## 创建帐户
首先,我们需要为GitLab创建一个新的系统级帐户来连接。 此帐户称为ServiceAccount。 为此,我们可以使用`kubectl`命令行程序。 我们将使用YAML语法(在整个Kubernetes中使用)定义帐户,如下所示:
➜ kubectl create -f - <
  apiVersion: v1
kind: ServiceAccount
metadata:
name: gitlab
namespace: default
EOF

此YAML定义将在“default”命名空间中创建名为“gitlab”的ServiceAccount。

下一步是授予GitLab帐户集群管理员权限,以便它可以代表你自由创建和销毁服务。 我们将再次使用`kubectl`和YAML定义。
➜ kubectl create -f - <
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: gitlab-cluster-admin
subjects:
[list]
[*]kind: ServiceAccount[/*]
[/list] name: gitlab
namespace: default
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
EOF

## 连接群集
现在,让我们看一下GitLab连接到Kubernetes集群所需的所有信息。
6.png

GitLab - 添加Kubernetes集群时呈现的表格

第一个字段是Kubernetes集群名称。 这对你来说可以帮助你识别k8s群集。 它并没有真正使用那么多,所以不要花太多时间为它命名。

可以通过运行以下命令获取下一个字段API URL:
➜ kubectl cluster-info | grep 'Kubernetes master' | awk '/http/ {print $NF}'
https://xxxxxx.k8s.ondigitalocean.com

获取命令返回的URL并将其粘贴到API URL字段中。

可以通过从创建GitLab帐户时创建的“secret”中提取数据来获取CA证书和token。 Kubernetes具有`secret`资源的概念,旨在存储敏感信息。 除了设置过程,你还可以创建自己的secret来存储应用程序敏感信息,如数据库凭据,API密钥等。

列出项目运行中的所有secret:
➜ kubectl get secrets                                                                                                                            
NAME TYPE DATA
default-token-xfxg9 kubernetes.io/service-account-token 3
gitlab-token-vxhxq kubernetes.io/service-account-token 3

在这里,我们看到群集中有2个secret对象。 我们感兴趣的是名为`gitlab-token-vxhxq`的名字。 找到以`gitlab-token-*`开头的secret,并在下一个命令中使用它:
➜ kubectl get secret  -o jsonpath="{['data']['ca\.crt']}" | base64 --decode
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----

复制并粘贴从命令返回的所有内容,从`----- BEGIN CERTIFICATE -----`开始,以`----- END CERTIFICATE -----`结尾,进入CA Certificate字段。

可以通过执行以下操作以类似的方式获取token:
➜ kubectl get secret  -o jsonpath="{['data']['token']}" | base64 --decode
WlhsS2FHSkhZMmxQYVVwVFZYc...

再一次,将返回的值粘贴到GitLab中的Token字段中。

至于Project Namespace,我把它留空了。 确保选中RBAC启用的群集复选框。 准备好后,继续并单击Add Kubernetes Cluster按钮。
7.png

GitLab - 集群所有的必填字段均已填入
# 设置CI/CD
在GitLab中设置持续集成和持续部署流水线非常简单。它融入了GitLab产品,只需将`.gitlab-ci`文件添加到项目的根目录即可轻松配置。将代码推送到GitLab仓库时会触发CI/CD流水线。流水线必须在服务器上运行,该服务器称为“Runner”。Runner可以是虚拟专用服务器,公共服务器,也可以是安装GitLab Runner客户端的任何地方。在我们的用例中,我们将在Kubernetes群集上安装一个运行器,以便在Pod中执行Job。此客户端还使其可扩展,因此我们可以并行运行多个Job。

要在群集上安装GitLab Runner客户端,我们首先需要安装另一个名为Helm的工具。 Helm是Kubernetes的软件包管理器,简化了软件的安装。我觉得Helm与Brew for Mac类似,他们都有一个可以安装到系统上的软件回购。
##安装Helm
通过Gitlab安装Helm只需要单击“安装”按钮。假设一切都配置正确,安装只需几秒钟。
8.png

在Kubernetes集群上安装helm tiller

完成之后,让我们看看群集,看看GitLab安装了什么。 使用`kubectl get ns`命令,我们可以看到Gitlab已经创建了自己的命名空间,命名为gitlab-managed-apps。
➜ kubectl get ns 
NAME STATUS AGE
default Active 1d
gitlab-managed-apps Active 23s
kube-public Active 1d
kube-system Active 1d

如果我们运行`kubectl get pods`,当未指定命名空间时,默认使用`default`,我们将看不到任何内容。 要查看GitLab命名空间中的Pod,请运行`kubectl get pods -n gitlab-managed-apps`。
➜ kubectl get pods -n gitlab-managed-apps
NAME READY STATUS RESTARTS AGE
tiller-deploy-7dd47f89cc-27cmt 1/1 Running 0 5m

在这里,我们看到“Helm tiller”已成功创建并正在运行。
##安装GitLab Runner
如前所述,GitLab运行程序允许我们的CI/CD Job在Kubernetes集群中运行。 使用GitLab安装Runner很简单,只需单击“安装”按钮即可。
9.png

GitLab - 在Kubernetes集群上安装运行器

我的群集花了大约1分钟。 完成之后,让我们再看看Pod,你应该看到GitLab Runner的新Pod。
➜ kubectl get pods -n gitlab-managed-apps 
NAME READY STATUS RESTARTS
runner-gitlab-runner-5cffc648d7-xr9rq 1/1 Running 0
tiller-deploy-7dd47f89cc-27cmt 1/1 Running 0

你可以通过查看GitLab中的设置➜CI/CD➜Runner部分来验证Runner是否已连接到你的项目。
10.png

GitLab - Kubernetes运行器已在本项目激活
##运行流水线
很好,所以现在我们有一个功能齐全的GitLab项目,连接到Kubernetes,Runner准备执行我们的CI/CD流水线。 让我们设置一个示例Golang项目,看看如何触发这些流水线。 对于这个项目,我们将运行一个简单的HTTP服务器,返回经典的“Hello World”。

首先,写下Go代码:
# main.go
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc(
"/hello",
func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World!")
},
)
log.Fatal(http.ListenAndServe(":8080", nil))
}

然后是运行的Dockerfile:
# Dockerfile
FROM golang:1.11
WORKDIR /go/src/app
COPY . .
RUN go get -d -v ./...
RUN go install -v ./...
CMD ["app"]

接下来,我们将创建一个.gitlab-ci.yml文件来定义我们的CI/CD流水线。该文件将在每次代码推送时进行评估,如果分支或标签与任何Job匹配,它们将由我们之前配置的GitLab运行程序之一自动执行。

我们流水线的第一步是在我们推送到主分支时创建应用程序的Docker镜像。 我们可以使用以下配置执行此操作:
# Gitlab CI Definition (.gitlab-ci.yml)
stages:
- build
- deploy
services:
- docker:dind
variables:
DOCKER_HOST: tcp://localhost:2375
build_app:
image: docker:latest
stage: build
only:
- master
script:
- docker build -t ${CI_REGISTRY}/${CI_PROJECT_PATH}:${CI_COMMIT_REF_NAME} .
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker push ${CI_REGISTRY}/${CI_PROJECT_PATH}:${CI_COMMIT_REF_NAME}

让我们浏览文件中的每个块。`stages:` 块定义流水线中阶段的顺序。我们只有2个阶段,构建然后部署。

`services:` 块包括官方Docker-in-Docker(或dind)镜像,将在所有Job中链接。我们需要这个,因为我们将成为GitLab CI Docker容器内的应用程序Docker容器。

接下来我们有`build_app:` Job。这个名字由我们的项目组成,可以是你想要的任何名称。`image`表明我们正在使用Docker Hub中的最新Docker镜像。`stage`告诉GitLab这个Job处于什么阶段。要记住的一件好事是,同一阶段的Job将并行运行。`only:`标签表示我们只会在提交到`master`分支时运行此Job。最后,`scrips:`是Job的核心,它将运行`docker build`命令来创建我们的镜像,然后`docker login`到GitLab注册表,然后`docker push`将该镜像推送到我们的注册表。

此时我们可以提交并推送代码,你应该在GitLab注册表中看到一个全新的镜像。

在构建镜像并将其保存在注册表上之后,下一步是部署它。我们需要定义部署配置,告诉Kubernetes我们想要如何运行应用程序。以下yaml文件正是如此:
# Deployment Configuration (deployment-template.yaml)
apiVersion: apps/v1
kind: Deployment
metadata:
name: example-deployment
labels:
app: example
spec:
replicas: 1
selector:
matchLabels:
app: example
template:
metadata:
labels:
app: example
spec:
containers:
- name: example
image: registry.gitlab.com/thisiskj/example:latest
ports:
- containerPort: 8080

此文件定义了一个部署,其中包含一个将从项目运行镜像的单个副本(registry.gitlab.com/thisiskj/example:latest)。

为了触发部署,我已经配置了.gitlab-ci.yml文件,以便在标记代码repo时执行此操作。 这是Job定义:
deploy_app:
image: thisiskj/kubectl-envsubst
stage: deploy
environment: production
only:
- tags
script:
- envsubst \$CI_COMMIT_TAG < deployment-template.yaml > deployment.yaml
- kubectl apply -f deployment.yaml

此Job将运行envsubst命令,以使用触发构建的Git标签的名称替换deployment-template.yaml中的$ CI_COMMIT_TAG变量。 环境变量$CI_COMMIT_TAG由GitLab运行器设置,我们告诉envsubst基本上搜索并替换文件中的变量。
##查看应用程序
此时所有内容都已连线,我们的部署将在每个新标签上运行。

我们可以看到正在运行的Pod:
➜  kubectl -n example-10311640 get pods
NAME READY STATUS RESTARTS
example-deployment-756c8f6dc5-jk85w 1/1 Running 0

现在,Pod正在运行,但我们无法从外部访问Golang HTTP服务。 为了允许外部访问,我们可以创建LoadBalancer类型的服务。 将以下规范添加到部署yaml以在DigitalOcean上创建LoadBalancer。
---
kind: Service
apiVersion: v1
metadata:
name: example-loadbalancer-service
spec:
selector:
app: example
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: LoadBalancer

在下一次部署中,我们可以监视LoadBalancer的创建。 外部IP可能需要几分钟才能显示。
➜  kubectl -n example-10311640 get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
example-loadbalancer-service LoadBalancer 10.245.40.9 80:30897/TCP 4s
...
➜ kubectl -n example-10311640 get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
example-loadbalancer-service LoadBalancer 10.245.40.9 157.230.64.204 80:30897/TCP 2m9s

我们还可以在DigitialOcean控制台中监控负载均衡器的创建:
11.png

DigitalOcean — 网络控制台

最后,我们可以通过导航到浏览器中的IP地址来查看我们的应用程序:
12.png

服务开始运行
#总结一下
我希望你发现此演练有助于你设置自己的群集和现代DevOps工作流程。

你可以在https://gitlab.com/thisiskj/example上查看与该项目相关的所有代码。

如果你有任何其他方法可以将GitLab CI/CD与Kubernetes部署集成,请随时分享评论。

原文链接:Lets Explore Kubernetes(翻译:池剑锋)

知乎部署系统演进

JetLee 发表了文章 • 0 个评论 • 1268 次浏览 • 2019-03-28 15:44 • 来自相关话题

应用部署是软件开发中重要的一环,保持快速迭代、持续部署,减少变更和试错成本,对于互联网公司尤为重要。本文将从部署系统的角度,介绍知乎应用平台从无到有的演进过程,希望可以对大家有所参考和帮助。 知乎部署系统由知乎工程效率团队打造,服务于 ...查看全部
应用部署是软件开发中重要的一环,保持快速迭代、持续部署,减少变更和试错成本,对于互联网公司尤为重要。本文将从部署系统的角度,介绍知乎应用平台从无到有的演进过程,希望可以对大家有所参考和帮助。

知乎部署系统由知乎工程效率团队打造,服务于公司几乎所有业务,每日部署次数在 2000 次左右,在启用蓝绿部署的情况下,大部分业务的生产环境上线时间可以在 10 秒以下(不包含金丝雀灰度验证过程)。

目前知乎部署系统主要实现了以下功能:

* 支持容器、物理机部署,支持在线、离线服务、定时任务以及静态文件的部署
* 支持办公网络预上线
* 支持金丝雀灰度验证,期间支持故障检测以及自动回滚
* 支持蓝绿部署,在蓝绿部署情况下,上线和回滚时间均在秒级
* 支持部署 Merge Request 阶段的代码,用于调试

下文将按时间顺序,对部署系统的功能演进进行介绍。
#技术背景
在介绍部署系统之前,首先需要对知乎的相关基础设施和网络情况进行简单的介绍。
##知乎网络情况
知乎的网络如图所示:
1.png

知乎网络环境简图

主要划分为三个部分:

* 生产环境网络:即知乎对外的在线服务器网络,基于安全性考虑,与其他网络环境完全隔离。
* 测试环境网络:应用在部署到生产环境之前,首先会部署在测试环境,测试环境网络上与生产环境完全隔离。
* 办公室网络:即知乎员工内部网络,可以直接访问测试环境,也可以通过跳板机访问生产环境服务器。

##流量管理
知乎采用 Nginx + HAProxy 的方式管理应用的流量走向:
2.png

知乎在线业务流量架构

应用开发者在 Nginx 平台上配置好 Location 和 HAProxy 的对应关系,再由 HAProxy 将流量分发到 Real Server 上去,同时 HAProxy 承担了负载均衡、限速、熔断等功能。
##持续集成
知乎采用 Jenkins + Docker 进行持续集成,详见《知乎容器化构建系统设计和实践》,持续集成完成后,会生成 Artifact,供部署系统以及其他系统使用。
#物理机部署
像大多数公司一样,知乎最开始是以物理机部署为主,业务自行编写脚本进行部署,部署时间长、风险大、难以回滚。在这种情况下,大约在 2015 年,初版的部署系统 nami (取名自《海贼王》娜美)诞生了。

最初的部署系统采用 Fabric 作为基础,将 CI 产生的 Artifact 上传到物理机上解压,并使用 Supervisor 进行进程管理,将服务启动起来:
3.png

物理机部署

初版的部署系统虽然简单,但是为了之后的改进奠定了基础,很多基础的概念,一直到现在还在使用。
##应用(App)与服务(Unit)
与 CI 相同,每个应用对应一个 GitLab Repo,这个很好理解。

但是在实际使用过程中,我们发现,同一套代码,往往对应着多个运行时的服务,比如以部署系统 nami 本身为例,虽然是同一套代码,但是在启动的时候,又要分为:

* API 服务
* 定时任务
* Celery 离线队列

这些运行单元的启动命令、参数等各不相同,我们称之为服务(Unit)。用户需要在部署系统的前端界面上,为每个 Unit 进行启动参数、环境变量等设置,整个应用才能正常启动起来。
##候选版本(Candidate)
所有的部署都是以 CI 产生 Artifact 作为基础,由于 Artifact 的不可变性,每次部署该 Artifact 的结果都是可预期的。也就是说,每个 Artifact 都是代码的一次快照,我们称之为部署的候选版本( Candidate)。

由于每次 CI 都是由 GitLab 的 Merge Request 产生,候选版本,其实就是一次 MR 的结果,也就是一次代码变更。通常情况下,一个候选版本对应一个 Merge Request:
4.png

每个候选版本对应一个 Merge Request

如图所示是某个应用的候选版本列表,每个候选版本,用户都可以将其部署到多个部署阶段(Stage)。
##部署阶段(Stage)
上文提到,知乎服务器网络分为测试环境和生产环境,网络之间完全隔离。应用总是先部署测试环境,成功后再部署生产环境。

在部署系统上,我们的做法是,对每个候选版本的部署,拆分成多个阶段(Stage):
5.png

构建/部署阶段

图中该应用有 6 个阶段:

* (B)构建阶段:即 CI 生成 Artifact 的过程。
* (T)测试环境:网络、数据都与生产环境相隔离。
* (O)办公室阶段:一个独立的容器,只有办公室网络可以访问,其他与线上环境相同,数据与生产环境共享。
* (C)金丝雀1:生产环境 1% 的容器,外网可访问。
* (C)金丝雀2:生产环境 20% 的容器,外网可访问。
* (P)生产环境:生产环境 100% 容器,外网可访问。

部署阶段从前到后依次进行,每个 Stage 的部署逻辑大致相同。

对于每个部署阶段,用户可以单独设置,是否进行自动部署。如果所有部署阶段都选择自动部署,那么应用就处于一个持续部署(Continuous Deployment)的过程。
##基于 Consul 和 HAProxy 的服务注册与发现
每次部署物理机时,都会先将机器从 Consul 上摘除,当部署完成后,重新注册到 Consul 上。

上文提到,我们通过 HAProxy 连接到 Real Server,原理就是基于 Consul Template 对 HAProxy 的配置进行更新,使其总是指向所有 RS 列表。

另外,在迁移到微服务架构之后,我们编写了一个称为 diplomat 的基础库,从 Consul 上拉取 RS 列表,用于 RPC 以及其他场景的服务发现。
#容器部署
##旧版容器系统 Bay
2015 年末,随着容器大潮的袭来,知乎也进入容器时代,我们基于 Mesos 做了初版的容器编排系统(称为 Bay),部署系统也很快支持了容器的部署。

Bay 的部署很简单,每个 Unit 对应一个容器组,用户可以手动设置容器组的数量和其他参数。每次部署的时候,滚动地上线新版本容器,下线旧版本容器,部署完成后所有旧版本容器就都已回收。对于一些拥有数百容器的大容器组,每次部署时间最长最长可以达到 18 分钟。
##各项功能完善
在迁移到容器部署的过程中,我们对部署系统也进行了其他方面的完善。

首先是健康检查,所有 HTTP、RPC 服务,都需要实现一个 /check_health 接口,在部署完成后会对其进行检查,当其 HTTP Code 为 200 时,部署才算成功,否则就会报错。

其次是在线/离线服务的拆分,对于 HTTP、RPC 等在线业务,采用滚动部署;对于其他业务,则是先启动全量新版本容器,再下线旧版本容器。
#预上线与灰度发布
基于容器,我们可以更灵活地增删 Real Server,这使得我们可以更简单地将流量拆分到不同候选版本的容器组中去,利用这一点,我们实现了办公室网络预上线和金丝雀灰度发布。
##办公室网络预上线
为了验证知乎主站的变更,我们决定在办公室网络,提前访问已经合并到主干分支、但还没有上线的代码。我们在 Nginx 层面做了流量拆分,当访问源是办公室网络的时候,流量流向办公室专属的 HAProxy:
6.png

办公室流量拆分

对于部署系统来说,所需要做的就是在「生产环境」这个 Stage 之前,加入一个「办公室」Stage,对于这个 Stage,只部署一个容器,并将这个容器注册到办公室专属的 HAProxy,从外网无法访问此容器。
##金丝雀灰度发布
在 2016 年以前,知乎部署都是全量上线,新的变更直接全量上线到外网,一旦出现问题,很可能导致整个网站宕机。

为了解决这个问题,我们在「办公室」和「生产环境」Stage 之间,加入了「金丝雀1」和「金丝雀2」两个 Stage,用于灰度验证。

原理是,部署一定量额外的新版本容器,通过 HAProxy,随机分发流量到这些新版本容器上,这样如果新版本代码存在问题,可以在指标系统上明显看出问题:
7.png

Nginx 指标大盘

其中,「金丝雀1」阶段只启动相当于「生产环境」阶段 1% 的容器,「金丝雀2」阶段则启动 20% 数量的容器。

为了避免每次部署到金丝雀后,都依赖人工去观察指标系统,我们在部署系统上,又开发了「金丝雀自动回滚」功能。主要原理是:

* 将金丝雀阶段的指标与生产环境的指标分离
* 金丝雀部署完成后,对指标进行检测,与生产环境进行对比,如果发现异常,则销毁金丝雀容器,并通知用户
* 如果在 6 分钟内没有发现指标异常,则认为代码没有明显问题,才允许用户部署「生产环境」Stage

8.png

金丝雀出现异常,回滚时会自动通知开发者

金丝雀阶段自动监测的指标包括该应用的错误数、响应时间、数据库慢查询数量、Sentry 报错数量、移动端 App 崩溃数量等等。
#新版容器部署
针对旧版容器系统 Bay 部署速度慢、稳定性差等问题,我们将容器编排从 Mesos 切换到 Kubernetes,在此基础上开发出新一代的容器系统 NewBay。

相应地,部署系统也针对 NewBay 进行了一番改造,使得其在功能、速度上均有明显提升。
##蓝绿部署
在旧版 Bay 中,每个 Unit 对应唯一的容器组,新版本容器会覆盖旧版本容器,这会导致:

* 一旦部署失败,服务将处于中间状态,新旧版本会同时在线
* 回滚旧版本代码速度较慢,而且有可能会失败

我们设计了一套新的部署逻辑,实现了蓝绿部署,即新旧版本容器组同时存在,使用 HAProxy 做流量切换:
9.png

蓝绿部署可以有效减少回滚时间

这使得:

* 流量的切换原子化,即使部署失败也不会存在新旧版本同时在线的情况
* 由于旧版本容器组会保留一段时间,这期间回滚代码仅需要将流量切回旧版本,回滚时间可以达到秒级

##预部署
使用 NewBay 之后,大型项目的部署时间由原来的 18 分钟降至 3 分钟左右,但这其中仍有优化的空间。

为了加快部署速度,我们会在金丝雀阶段,提前将「生产环境」Stage 所需要的全量容器异步地启动起来,这样在部署「生产环境」Stage 时,仅需要将流量切换为全量即可:
10.png

预部署可以有效减少上线时间

通过这方面的优化,在全量上线到生产环境时,上线时间同样可以达到秒级。
#分支部署
以上部署均是针对代码合并到主干分支后进行的部署操作,或者可以称之为「上线流程」。

但是实际上很多情况下,我们的代码在 Merge Request 阶段就需要进行部署,以方便开发者进行自测,或者交由 QA 团队测试。

我们在 CI/CD 层面对这种情况进行了支持,主要原理是在 MR 提交或者变更的时候就触发 CI/CD,将其部署到单独的容器上,方便开发者进行访问。
11.png

多个 Merge Request 同时部署和调试

分支部署实现细节较多,篇幅所限,在此不进行展开。
#部署系统平台化
为了方便用户使用 CI/CD,管理应用资源,处理排查故障等,我们将整套知乎的开发流程进行了平台化,最终实现了 ZAE(Zhihu App Engine):
12.png

ZAE 是一套完整的开发者平台

用户可以方便地查看部署进度和日志,并进行一些常规操作:
13.png

在 ZAE 上查看部署进度
#尾声
知乎部署系统从 2015 年开始开发,到目前为止,功能上已经比较成熟。其实,部署系统所承担的责任不仅仅是上线这么简单,而是关系到应用从开发、上线到维护的方方面面。良好的部署系统,可以加快业务迭代速度、规避故障发生,进而影响到一家公司的产品发布节奏。

原文链接:https://zhuanlan.zhihu.com/p/60627311

谷歌开源Kubernetes原生CI/CD构建框架Tekton

尼古拉斯 发表了文章 • 0 个评论 • 535 次浏览 • 2019-03-25 16:54 • 来自相关话题

Tekton 是一个功能强大且灵活的 Kubernetes 原生框架,用于创建 CI/CD 系统。通过抽象出底层实现细节,允许开发者跨多云环境或本地系统进行构建、测试与部署。 特性包括: * 工件管理:存储、 ...查看全部
Tekton 是一个功能强大且灵活的 Kubernetes 原生框架,用于创建 CI/CD 系统。通过抽象出底层实现细节,允许开发者跨多云环境或本地系统进行构建、测试与部署。

特性包括:

* 工件管理:存储、管理和保护工件,同时 Tetkon 管道可以很好地与其它第三方工具相配合。
* 部署管道:部署管道旨在支持复杂的工作流程,包括跨多个环境的部署以及金丝雀部署和蓝/绿部署。
* 结果:作为内置结果存储 API 的一部分,通过日志可以深入了解测试与构建结果。

详情查看 https://cloud.google.com/tekton

21 个好用的持续集成工具

YiGagyeong 发表了文章 • 0 个评论 • 1734 次浏览 • 2019-03-10 18:20 • 来自相关话题

【编者的话】好用的集成工具都在这儿了,总有一款适合你。 市场上持续集成工具众多,找到一个合适的工具并非易事,下面介绍了 21 个比较受欢迎的 CI 工具,并附上了下载链接。 # 1. Buddy 对 Web 开 ...查看全部
【编者的话】好用的集成工具都在这儿了,总有一款适合你。

市场上持续集成工具众多,找到一个合适的工具并非易事,下面介绍了 21 个比较受欢迎的 CI 工具,并附上了下载链接。
# 1. Buddy
对 Web 开发者来说,Buddy 是一个智能的 CI/CD 工具,降低了 DevOps 的入门门槛。Buddy 使用 `Delivery Pipeline` 进去软件构建、测试及发布,创建 Pipeline 时,100 多个就绪的操作可随时投入使用,就像砌砖房一样。

特点:

  • 清晰的配置,友好的交互,15分钟快速配置
  • 基于变更集(changeset)的快速部署
  • 构建运行在使用缓存依赖的独立容器中
  • 支持所有流行的语言、框架和任务管理器
  • Docker / Kubernetes 专用操作手册
  • 与 AWS,Google,DigitalOcean,Azure,Shopify,WordPress 等集成
  • 支持并行和 YAML 配置
下载链接:https://buddy.works# 2. JenkinsJenkins 是一个开源的持续集成工具,使用 Java 编程语言编写的。它有助于实时检测和报告较大代码库中的单一更改。该软件可帮助开发人员快速查找和解决代码库中的问题并自动测试其构建。特点:
  • 支持海量节点扩展并在节点中同等分发工作负载
  • 在各版本Linux、Mac OS 或 Windows 等全平台轻松更新
  • 提供了 WAR 格式的简易安装包,执行导入 JEE 容器中即可运行安装
  • 可以通过 Web 界面轻松设置和配置 Jenkins
  • 可轻松跨机器分发
下载链接:https://jenkins.io/download/# 3. TeamCityTeamCity 是一款拥有很多强大功能的持续集成服务器。特点:
  • 可扩展性和自定义
  • 为项目提供更好的代码质量
  • 即使没有运行构建,也能保持 CI 服务器健康稳定
  • 可在 DSL 中配置构建
  • 项目级云配置文件
  • 全面的 VCS 集成
  • 即时构建进度报告
  • 远程运行和预先测试的提交
下载链接:https://www.jetbrains.com/teamcity/download/#section=windows# 4. Travis CITravis 是一款流行的 CI 工具,可免费用于开源项目。在托管时,不必依赖任何平台。此 CI 工具为许多构建配置和语言提供支持,如 Node,PHP,Python,Java,Perl 等。特点:
  • Travis 使用虚拟机构建应用程序
  • 可通过 Slack,HipChat,电子邮件等通知
  • 允许运行并行测试
  • 支持 Linux、Mac 以及 iOS
  • 易于配置,无需安装。
  • 强大的 API 和命令行工具
下载链接:https://github.com/travis-ci/travis-ci# 5. GoCDGoCD 是一个开源的持续集成服务器。它可轻松模拟和可视化复杂的工作流程。此 CI 工具允许持续交付,并为构建 CD Pipeline 提供直观的界面。特点:
  • 支持并行和顺序执行,可以轻松配置依赖
  • 随时部署任何版本
  • 使用 Value Stream Map 实时可视化端到端工作流程
  • 安全地部署到生产环境
  • 支持用户身份验证和授权
  • 保持配置有序
  • 有大量的插件增强功能
  • 活跃的社区帮助和支持
下载链接:https://www.gocd.org/download/# 6. BambooBamboo 是一个持续集成的构建服务器,可以自动构建、测试和发布,并可与 JIRA 和 Bitbucket 无缝协作。Bamboo 支持多语言和平台,如 CodeDeply、Ducker、Git,SVN、Mercurial、AWS 及 Amazon S3 bucket。特点:
  • 可并行运行批量测试
  • 配置简单
  • 分环境权限功能允许开发人员和 QA 部署到他们的环境
  • 可以根据 repository 中检测到的更改触发构建,并从 Bitbucket 推送通知
  • 可托管或内部部署
  • 促进实时协作并与 HipChat 集成
  • 内置 Git 分支和工作流程,并自动合并分支
下载链接:https://www.atlassian.com/software/bamboo# 7. Gitlab CIGitLab CI 是 GitLab 的一部分。它是一个提供 API 的 Web 应用程序,可将其状态存储在数据库中。GitLab CI 可以管理项目并提供友好的用户界面,并充分利用 GitLab 所有功能。特点:
  • GitLab Container Registry 是安全的 Docker 镜像注册表
  • GitLab 提供了一种方便的方法来更改 issue 或 merge request 的元数据,而无需在注释字段中添加斜杠命令
  • 为大多数功能提供 API,允许开发人员进行更深入的集成
  • 通过发现开发过程中的改进领域,帮助开发人员将他们的想法投入生产
  • 可以通过机密问题保护您的信息安全
  • GitLab 中的内部项目允许促进内部存储库的内部 sourcing
下载链接:https://about.gitlab.com/installation/# 8. CircleCICircle CI 是一个灵活的 CI 工具,可在任何环境中运行,如跨平台移动应用程序、Python API 服务器或 Docker 集群,该工具可减少错误并提高应用程序的质量。特点:
  • 允许选择构建环境
  • 支持多语言及平台,如Linux,包括C ++,Javascript,NET,PHP,Python 和 Ruby
  • 支持 Docker,可以配置自定义环境
  • 触发较新的构建时,自动取消排队或正在运行的构建
  • 跨多容器分割和平衡测试,以减少总体构建时间
  • 禁止非管理员修改关键项目配置
  • 通过发送无错误的应用程序提高 Android 和 iOS 商店评级
  • 最佳缓存和并行性能,实现高性能
  • 与 VCS 工具集成
下载链接:https://circleci.com/# 9. CodeshipCodeship 是一个功能强大的 CI 工具,可自动化开发和部署工作流程。Codeship 通过简化到 repository 的 push 来触发自动化工作流程。特点:
  • 可完全控制 CI 和 CD 系统的设计。
  • 集中的团队管理和仪表板
  • 轻松访问调试版本和 SSH,有助于从 CI 环境进行调试
  • 可完全定制和优化 CI 和 CD 工作流程
  • 允许加密外部缓存的 Docker 镜像
  • 允许为您的组织和团队成员设置团队和权限
  • 有两个版本1)Basic 和 2)Pro
下载链接:https://codeship.com/# 10. BuildbotBuildbot 是一个软件开发 CI,可以自动完成编译/测试周期。它被广泛用于许多软件项目,用以验证代码更改。它提供跨平台 Job 的分布式并行执行。特点:
  • 为不同体系结构的多个测试主机提供支持。
  • 报告主机的内核崩溃
  • 维护单源 repository
  • 自动化构建
  • 每个提交都在集成机器上的主线上构建
  • 自动部署
  • 开源
下载链接:https://buildbot.net/# 11. NevercodeNevercode 是一个基于云端的 CI 传送服务器,可以构建、测试和分发应用程序而无需人工交互。此 CI 工具自动为每个提交构建项目,并在模拟器或真实硬件上运行所有单元测试 或 UI 测试。特点:
  • 基于云服务,因此无需维护服务器
  • 易于学习和使用
  • 良好的文档,易于阅读和理解
  • 通过持续集成和交付自动化整个开发过程
  • 与众多工具集成
下载链接:https://nevercode.io/# 12. IntegrityIntegrity 是一个持续集成服务器,仅适用于 GitHub。在此 CI 工具中,只要用户提交代码,它就构建并运行代码。它还会生成报告并向用户提供通知。 特点:
  • 目前仅适用于 Git,但它可以轻松地映射其他 SCM
  • 支持多通知机制,如 AMQP,电子邮件,HTTP,Amazon SES,Flowdock,Shell 和 TCP
  • HTTP 通告功能将以 HTTP POST 请求发送到特定URL
下载链接:http://integrity.github.io/# 13. StriderStrider 是一个开源工具,用 Node.JS / JavaScript 编写。它使用 MongoDB 作为后端存储。因此,MongoDB 和 Node.js 对于安装此 CI 至关重要。该工具为不同的插件提供支持,这些插件可修改数据库 schema 并注册HTTP路由。特点:
  • Strider 可与 GitHub,BitBucket,Gitlab 等集成。
  • 允许添加钩子来执行构建操作
  • 持续构建和测试软件项目
  • 与 GitHub 无缝集成
  • 发布和订阅 socket 事件
  • 支持创建和修改 Striders 用户界面
  • 强大的插件,定制默认功能
  • 支持 Docker
下载链接:https://github.com/Strider-CD/strider# 14. AutoRABITAutoRABIT 是一个端到端的持续交付套件,可以加快开发过程。它简化了完整的发布流程,并可以帮助任何规模的组织实现持续集成。特点:
  • 专门设计用于在 Salesforce Platform 上部署
  • 支持基于 120 多种元数据类型的更改,实现精简和快速部署
  • 从版本控制系统获取更改并自动部署到 Sandbox 中
  • 直接从 Sandbox 自动向版本控制系统提交更改
下载链接:http://www.autorabit.com/tag/autorabit-download/# 15. FinalBuilderFinalBuilder 是 VSoft 的构建工具。使用 FinalBuilder,无需编辑 XML 或编写脚本。在使用 Windows 调度程序调度构建脚本时,可以定义和调试构建脚本,或者与 Jenkins,Continua CI 等集成。特点:
  • 以逻辑结构化的图形界面呈现构建过程
  • 使用 try 和 catch 操作处理本地错误
  • 与 Windows 调度服务紧密集成,支持定时构建
  • 支持十几个版本控制系统
  • 提供脚本支持
  • 构建过程中所有操作的输出都将定向到构建日志
下载链接:https://www.finalbuilder.com/downloads/finalbuilde# 16. WerckerWercker 是一个 CI 工具,可自动构建和部署容器。它可以创建可以通过命令行界面执行的自动化管道。特点:
  • 与 GitHub 和 Bitbucket 完全集成
  • 使用 Wercker CLI 进行更快的本地迭代
  • 同时执行构建以保持团队的机动
  • 运行并行测试以减少团队的等待时间
  • 集成了 100 多种外部工具
  • 通过产品和电子邮件接收系统通知
下载链接:http://www.wercker.com/# 17. BuildkiteBuildkite 代理是一个可靠的跨平台构建工具。此 CI 工具可以在础架构上轻松地运行自动构建。它主要用于运行构建 Job,报告 Job 的状态代码并输出日志。特点:
  • 可在各种操作系统和体系结构上运行
  • 可以从任何版本控制系统运行代码
  • 允许在计算机上运行任意数量的构建代理
  • 可与 Slack,HipChat,Flowdock,Campfire 等工具集成
  • 永远不会读取源代码或密钥
  • 提供稳定的基础设施
下载链接:https://buildkite.com/# 18. SemaphoreSemaphore 是一个持续集成工具,只需按一下按钮即可测试和部署代码。它支持多种语言、框架并可与 GitHub 集成,还可以执行自动测试和部署。特点:
  • 配置简单
  • 允许自动并行测试
  • 市场上最快的 CI 之一
  • 可以轻松覆盖不同大小的项目数量
  • 与 GitHub 和 Bitbucket 无缝集成
下载链接:https://semaphoreci.com# 19. CruiseControlCruiseControl 既是 CI 工具又是一个可扩展的框架。它用于构建自定义连续的构建。它有许多用于各种源代码控制的插件,包括针对电子邮件和即时消息的构建技术。特点:
  • 与许多不同的源代码控制系统集成,如 vss,csv,svn,git,hg,perforce,clearcase,filesystem 等。
  • 允许在单个服务器上构建多个项目
  • 与其他外部工具集成,如 NAnt,NDepend,NUnit,MSBuild,MBUnit 和 Visual Studio
  • 支持远程管理
下载链接:http://cruisecontrol.sourceforge.net/download.html# 20. BitriseBitrise 是一个持续集成和交付 PaaS,它可以为整个团队提供移动持续集成和交付。它允许与 Slack,HipChat,HockeyApp,Crashlytics 等许多流行服务集成。特点:
  • 允许在终端中创建和测试工作流程
  • 无需手动控制即可获得应用程序
  • 每个构建在其自己的虚拟机中单独运行,并且在构建结束时丢弃所有数据
  • 支持第三方 beta 测试和部署服务
  • 支持 GitHub Pull Request
下载链接:https://github.com/bitrise-io/bitrise#install-and-setup# 21. UrbanCodeIBM UrbanCode 是一个 CI 应用程序。它将强大的可见性,可追溯性和审计功能整合到一个软件包中。特点:
  • 通过自动化,可重复的部署流程提高软件交付频率
  • 减少部署失败
  • 简化多渠道应用程序的部署,无论是在本地还是在云中,都可以部署到所有环境
  • 企业级安全性和可扩展性
  • 混合云环境建模
  • 拖放自动化

下载链接:https://www.ibm.com/ms-en/marketplace/application-release-automation

原文链接:20 Best Continuous Integration(CI) Tools in 2019 (翻译:李加庆

即便一个小项目也有它的CI/CD流水线

colstuwjx 发表了文章 • 0 个评论 • 888 次浏览 • 2019-02-08 21:49 • 来自相关话题

【编者的话】本文作者通过一个简单的小项目详细介绍了如何使用Docker,GitLab,Portainer等组件搭建一套CICD流水线。 现如今,使用市面上的一些工具配置一套简单的CI/CD流水线并不是一件难事。给一个副项目弄一套这样的 ...查看全部
【编者的话】本文作者通过一个简单的小项目详细介绍了如何使用Docker,GitLab,Portainer等组件搭建一套CICD流水线。

现如今,使用市面上的一些工具配置一套简单的CI/CD流水线并不是一件难事。给一个副项目弄一套这样的流水线也是一个学习许多东西的好方法。Docker,Gitlab,Portainer这些优秀的组件可以用来搭建这个流水线。
# 示例项目
作为一名法国索菲亚科技园区(位于法国南部)的技术活动组织者,我经常被问到是否有办法知道所有即将举行的活动(会议,灌水,由当地协会组织的聚会等……)。由于此前并没有一个单独的地方列出所有的这些活动,我便开发了https://sophia.events ,这是一个非常简单的网站页面,它会尝试维护一份最新的活动列表。此项目的代码可以在 GitLab 上找到。

声明:这个项目超级简单,但是项目本身的复杂度并不是本文的重点。这里我们将详细介绍到的CI/CD流水线的各个组件可以用几乎相同的方式应用到更复杂的项目上。它们也非常适合微服务的场景。
# 快速过一下代码
为了简化起见,这里有一份events.json文件,每个新事件均会被添加到里面。该文件的部分内容见下面的代码段(抱歉里面掺杂了一些法语):
 {
“events”: [
{
“title”: “All Day DevOps 2018”,
“desc”: “We’re back with 100, 30-minute practitioner-led sessions and live Q&A on Slack. Our 5 tracks include CI/CD, Cloud-Native Infrastructure, DevSecOps, Cultural Transformations, and Site Reliability Engineering. 24 hours. 112 speakers. Free online.”,
“date”: “17 octobre 2018, online event”,
“ts”: “20181017T000000”,
“link”: “https://www.alldaydevops.com/",
“sponsors”: [{“name”: “all-day-devops”}]
},
{
“title”: “Création d’une Blockchain d’entreprise (lab) & introduction aux smart contracts”,
“desc”: “Venez avec votre laptop ! Nous vous proposons de nous rejoindre pour réaliser la création d’un premier prototype d’une Blockchain d’entreprise (Lab) et avoir une introduction aux smart contracts.”,
“ts”: “20181004T181500”,
“date”: “4 octobre à 18h15 au CEEI”,
“link”: “https://www.meetup.com/fr-FR/IBM-Cloud-Cote-d-Azur-Meetup/events/254472667/",
“sponsors”: [{“name”: “ibm”}]
},

]
}

此文件将会被一个mustache模板渲染并生成最终的网站素材。
## Docker多阶段构建
一旦生成了最终的网站素材,它们将会被拷贝到一个Nginx镜像里,该镜像将会被部署到目标机器上。

得益于多阶段构建(multi-stage build),本次构建分为两部分:

* 网站素材的生成
* 包含网站素材的最终镜像的创建

用来构建镜像的Dockerfile如下:
# 生成素材
FROM node:8.12.0-alpine AS build
COPY . /build
WORKDIR /build
RUN npm i
RUN node clean.js
RUN ./node_modules/mustache/bin/mustache events.json index.mustache > index.html

# 构建托管它们的最终镜像
FROM nginx:1.14.0
COPY --from=build /build/*.html /usr/share/nginx/html/
COPY events.json /usr/share/nginx/html/
COPY css /usr/share/nginx/html/css
COPY js /usr/share/nginx/html/js
COPY img /usr/share/nginx/html/img

## 本地测试
为了测试生成站点,只需克隆该仓库然后运行test.sh脚本即可。它将随后创建出一个镜像并运行一个容器:
$ git clone git@gitlab.com:lucj/sophia.events.git

$ cd sophia.events

$ ./test.sh
Sending build context to Docker daemon 2.588MB
Step 1/12 : FROM node:8.12.0-alpine AS build
---> df48b68da02a
Step 2/12 : COPY . /build
---> f4005274aadf
Step 3/12 : WORKDIR /build
---> Running in 5222c3b6cf12
Removing intermediate container 5222c3b6cf12
---> 81947306e4af
Step 4/12 : RUN npm i
---> Running in de4e6182036b
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN www@1.0.0 No repository field.
added 2 packages from 3 contributors and audited 2 packages in 1.675s
found 0 vulnerabilities
Removing intermediate container de4e6182036b
---> d0eb4627e01f
Step 5/12 : RUN node clean.js
---> Running in f4d3c4745901
Removing intermediate container f4d3c4745901
---> 602987ce7162
Step 6/12 : RUN ./node_modules/mustache/bin/mustache events.json index.mustache > index.html
---> Running in 05b5ebd73b89
Removing intermediate container 05b5ebd73b89
---> d982ff9cc61c
Step 7/12 : FROM nginx:1.14.0
---> 86898218889a
Step 8/12 : COPY --from=build /build/*.html /usr/share/nginx/html/
---> Using cache
---> e0c25127223f
Step 9/12 : COPY events.json /usr/share/nginx/html/
---> Using cache
---> 64e8a1c5e79d
Step 10/12 : COPY css /usr/share/nginx/html/css
---> Using cache
---> e524c31b64c2
Step 11/12 : COPY js /usr/share/nginx/html/js
---> Using cache
---> 1ef9dece9bb4
Step 12/12 : COPY img /usr/share/nginx/html/img
---> e50bf7836d2f
Successfully built e50bf7836d2f
Successfully tagged registry.gitlab.com/lucj/sophia.events:latest
=> web site available on http://localhost:32768

我们可以使用上述输出的末尾提供的URL访问网站页面。
1.png

# 目标环境
## 云厂商创建的一台虚拟机
或许你也注意到了,这个网站并不是那么关键(每天只有几十次访问),也因此它只需要跑在一台单个的虚拟机上即可。该虚拟机是由Exoscale,一个伟大的欧洲云厂商,它上面的Docker Machine创建出来的。

顺便一提,如果你想试试Exoscale的服务的话,知会我一声,我可以提供20欧元的优惠券。
## 以Swarm模式启动的Docker守护进程
在上面这台虚拟机上运行的Docker守护进程被配置成以Swarm模式运行,因此它支持使用Docker Swarm原生提供的stack,service,config以及secret等原语和它强大(且易于使用)的编排功能。
## 以docker stack形式运行的应用
下述文件内容里定义了一个包含网站素材的nginx web服务器作为一个服务(service)运行。
version: "3.7"
services:
www:
image: registry.gitlab.com/lucj/sophia.events
networks:
- proxy
deploy:
mode: replicated
replicas: 2
update_config:
parallelism: 1
delay: 10s
restart_policy:
condition: on-failure
networks:
proxy:
external: true

这里有几处需要解释下:

* 镜像存储在托管到gitlab.com的私有镜像仓库(这里没涉及到Docker Hub)
* 服务是以2个副本的形式运行在副本模式下,这也就意味着同一时间该服务会有两个正在运行中的任务/容器。Swarm的service会关联一个VIP(虚拟IP地址),这样一来目标是该服务的每个请求会在两个副本之间实现负载均衡。
* 每次完成服务更新时(部署一个新版本的网站),其中一个副本会被更新,然后在10秒后更新第二个副本。这可以确保在更新期间整个网站仍然可用。我们也可以使用回滚策略,但是在这里没有必要。
* 服务会被绑定到一个外部的代理网络,这样一来TLS termination(在Swarm里部署的,跑在另外一个服务里,但是超出本项目的范畴)可以发送请求给www服务。

要运行这个stack只需要执行如下命令:
$ docker stack deploy -c sophia.yml sophia_events

## 统御一切的Portainer
Portainer是一套很棒的Wbe UI工具,它可以很方便地管理Docker宿主机和Docker Swarm集群。下面是Portainer操作界面的一张截图,里面列出了Swarm集群里当前可用的stack。
2.png

当前设定下有3个stack:

* Portainer自己
* 包含了跑着我们网站的服务的sophia_events
* tls,TLS termination服务

如果列出跑在sophia_events stack里的www服务的明细的话,我们将可以看到该服务的webhook已经处于激活状态。Portainer 1.19.2(迄今为止最新的版本)已经加入了这一功能的支持,它允许定义一个HTTP Post端点,可以在被调用后触发一次服务的更新。正如我们稍后将会看到的,GitLab runner会负责调用这个webhook。
3.png

备注:从屏幕截图中可以看到,笔者是通过localhost:8888这个地址访问Portainer的用户界面。由于笔者不想将Portainer实例对外暴露,因此是通过SSH隧道访问,该隧道可以通过如下命令开启:
ssh -i ~/.docker/machine/machines/labs/id_rsa -NL 8888:localhost:9000 $USER@$HOST

这样一来,目标是本地机器上的8888端口的所有请求均会通过SSH转发到虚拟机上的9000端口上。9000端口是Portainer在虚拟机上运行时监听的端口,但是并未对外开放,因为它被Exoscale配置的一个安全组禁用了。

备注:在上述命令里,用来连接虚拟机的ssh key是在虚拟机创建时由Docker Machine生成的一个key。
## GitLab runner
Gitlab的runner是一个负责执行定义在.gitlab-ci.yml文件里的一组action的进程。就我们这个项目来说,我们定义了一个我们自己的runner,它在虚拟机上以一个容器的形式运行。

第一步就是带上一堆参数来注册该runner。
CONFIG_FOLDER=/tmp/gitlab-runner-config
docker run — rm -t -i \
-v $CONFIG_FOLDER:/etc/gitlab-runner \
gitlab/gitlab-runner register \
--non-interactive \
--executor "docker" \
—-docker-image docker:stable \
--url "https://gitlab.com/" \
—-registration-token "$PROJECT_TOKEN" \
—-description "Exoscale Docker Runner" \
--tag-list "docker" \
--run-untagged \
—-locked="false" \
--docker-privileged

在上述参数中,PROJECT_TOKEN可以在GitLab.com的项目页面上找到,并可以用来注册外部的runner。
4.png

用来注册一个新的runner的注册token。

一旦runner注册上了,我们需要启动它:
CONFIG_FOLDER=/tmp/gitlab-runner-config
docker run -d \
--name gitlab-runner \
—-restart always \
-v $CONFIG_FOLDER:/etc/gitlab-runner \
-v /var/run/docker.sock:/var/run/docker.sock \
gitlab/gitlab-runner:latest

等到它注册上了而且启动起来了,该runner便会出现在GitLab.com上的项目页面里。
5.png

为此项目创建的runner。

每当有新的commit推送到仓库,此runner随后便会接收到一些要做的任务。它会按顺序执行.gitlab-ci.yml文件里定义好的测试、构建和部署几个阶段。
variables:
CONTAINER_IMAGE: registry.gitlab.com/$CI_PROJECT_PATH
DOCKER_HOST: tcp://docker:2375
stages:
- test
- build
- deploy
test:
stage: test
image: node:8.12.0-alpine
script:
- npm i
- npm test
build:
stage: build
image: docker:stable
services:
- docker:dind
script:
- docker image build -t $CONTAINER_IMAGE:$CI_BUILD_REF -t $CONTAINER_IMAGE:latest .
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com
- docker image push $CONTAINER_IMAGE:latest
- docker image push $CONTAINER_IMAGE:$CI_BUILD_REF
only:
- master
deploy:
stage: deploy
image: alpine
script:
- apk add --update curl
- curl -XPOST $WWW_WEBHOOK
only:
- master


* 测试阶段(test stage)将会运行一些预备检查,确保events.json文件格式正确,并且这里没有遗漏镜像
* 构建阶段(build stage)会做镜像的构建并将它推送到GitLab上的镜像仓库
* 部署阶段(deploy stage)将会通过发送给Portainer的一个webhook触发一次服务的更新。WWW_WEBHOOK变量的定义可以在Gitlab.com上项目页面的CI/CD设置里找到。

6.png

备注:

* runner在Swarm上是以一个容器的形式运行。我们可以使用一个共享的runner,这是一些公用的runner,它们会在托管到GitLab的不同项目所需的任务之间分配时间。但是,由于runner需要访问Portainer的端点(用来发送webhook),也因为笔者不希望Portainer能够从外界访问到,将runner跑在集群里会更安全一些。

* 再者,由于runner跑在一个容器里,为了能够通过Portainer暴露在宿主机上的9000端口连到Portainer,它会将webhook请求发送到Docker0桥接网络上的IP地址。也因此,webhook将遵循如下格式:http://172.17.0.1:9000/api[…]a7-4af2-a95b-b748d92f1b3b

# 部署流程
新版本的站点更新遵循如下流程:
7.png


  1. 一个开发者推送了一些变更到GitLab。这些变更基本上囊括了events.json文件里一个或多个新的事件加上一些额外赞助商的logo。
  2. Gitlab runner执行在.gitlab-ci.yml里定义好的一组action。
  3. Gitlab runner调用在Portainer中定义的webhook。
  4. 在接收到webhook后,Portainer将会部署新版本的www服务。它通过调用Docker Swarm的API实现这一点。Portainer可以通过在启动时绑定挂载的/var/run/docker.sock套接字来访问该API。

如果你想知道更多此unix套接字用法的相关信息,也许你会对之前这篇文章《Docker Tips : about /var/run/docker.sock》感兴趣。

  1. 随后,用户便能看到新版本的站点。

## 示例
让我们一起来修改代码里的一些内容随后提交/推送这些变更。
$ git commit -m 'Fix image'

$ git push origin master

如下截图展示了GitLab.com上的项目页面里的commit触发的流水线作业。
8.png

在Portainer一侧,它将会收到一个webhook请求,随后会执行一次服务的更新操作。这里可能看不太清,但是一个副本已经完成了更新,通过第二个副本可以访问站点。随后,几秒钟之后,第二个副本也更新完毕。
9.png

## 小结
即便对于这样一个小项目,为它建立一套CI/CD流水线也是一个很好的练习,尤其是可以更加熟悉GitLab(这一直在笔者要学习的列表里面),它是一个非常出色而且专业的产品。这也是一次体验大家期待已久的Portainer的最新版本(1.19.2)推出的webhook功能的机会。此外,对于像这样的副项目,Docker Swarm的使用是无脑上手的,很酷而且易于使用......

原文链接:Even the smallest side project deserves its CI/CD pipeline(译者:吴佳兴)