前文介绍了容器环境下 Drone + semantic release 实现的语义化持续集成 Workflow,为了方便演示,流程仅给出了工做流中最重要的几个环节,实际用起来可能会发现很多值得优化的地方。html
所以本次在这个工做流的基础上,介绍一些容器环境下 CI 的优化及提速方法,方法自己不限定必定要使用 Drone,使用一样的思路彻底能够套用到其余的 CI 工具中。前端
以一个生产环境的实际项目为例,项目的主要结构以下node
├── Dockerfile
├── dist/
├── node_modules/
├── package.json
└── src/
复制代码
这是一个比较常见的基于 React 的前端项目,用 npm list | wc -l
能够看到有 3952 个依赖,项目会经过 webpack 打包到 dist
目录下,打包命令被封装成 npm run build
。最终 dist
目录经过 Dockerfile
被打包到 Nginx 的 Docker 镜像内, 生产环境直接运行打包后的镜像便可。webpack
Dockerfile 是这样编写的nginx
FROM node:10 as build
WORKDIR /app COPY . /app RUN npm install RUN npm run build
FROM nginx:1.15-alpine
COPY --from=build /app/dist /usr/share/nginx/html 复制代码
使用了 Docker 的多阶段构建功能,即 npm 的安装,编译做为第 1 个阶段,编译完成后仅将编译的结果 dist
文件夹复制出来,其他未复制的文件丢弃,这样打包后的镜像仅为 23.2MB,更利于部署。git
发布流程直接套用前文介绍的 Gitflow + semantic release 工做流。能够看到此时的一次发布是比较慢的,push 到 master 构建 staging 镜像用时 9:18,semantic release 打上 Tag 构建 production 镜像用时 6:24。github
这个过程当中到底慢在哪里呢, 在 Drone 的构建过程当中看到,容器构建的耗时占了 90%以上,一方面 npm 须要下载安装 3000 多个依赖,另外一方面 webpack 的编译也须要 30s 左右,若是网络再有不稳定,等待时间无疑会更长。web
另外一个耗时的元凶也很明显,因为引入了 semantic release, push master 和 release 两个动做会触发 2 次 CI,每次 CI 都进行了 Docker 镜像的构建,但其实若是没有异常发生,两个 Docker 镜像对应的实际上是同一份代码,应当是彻底一致的,即 release 时的镜像构建所花费的时间是浪费的。docker
其余固然还有应用层面的优化,好比能够用 yarn 替代 npm,使用更快的源,去除没必要要的依赖等等,但这些并不在本文的讨论范围内,就略过不提。npm
每次构建都要下载 3000 多个依赖,那么最容易想到的固然是将这些依赖缓存起来,可是在这个项目中,npm 下载/编译都发生在容器构建环节,这是比较难引入缓存的。所以首先要作的,是将下载/编译过程从容器转移到 CI,经过 CI 完成下载/编译,再将结果复制到容器镜像内。
在下载/编译转移到 CI 的基础上,能够直接使用 Drone 提供的缓存插件,目前根据不一样文件系统,Drone 可选的缓存插件有
这里以 Volume Cache 为例,.drone.yml
以下。这里的语法对应 Drone-v1.0 以上版本,可能与官方部分旧文档有出入。
steps:
- name: restore-cache
image: drillster/drone-volume-cache
settings:
restore: true
mount:
- ./.npm-cache
- ./node_modules
volumes:
- name: cache
path: /cache
- name: npm-install
image: node:10
commands:
- npm config set cache ./.npm-cache --global
- npm install
- name: build-dist
image: node:10
commands:
- npm run build
- name: rebuild-cache
image: drillster/drone-volume-cache
settings:
rebuild: true
mount:
- ./.npm-cache
- ./node_modules
volumes:
- name: cache
path: /cache
volumes:
- name: cache
host:
path: /tmp/cache
复制代码
Volume Cache 插件使用很简单,首先须要声明一个 Volume,对应主机的一个文件夹,这里使用的是/tmp/cache
。Volume Cache 插件的参数中,mount
列出须要缓存的文件夹,restore: true
会将文件从主机复制到容器,所以放在 pipeline 的开头,rebuild: true
则反之,放在 pipeline 最后。
另外注意使用 Volume 须要在 Drone 中将 Repo 设置为 Trusted。
而此时的 Dockerfile 就只剩下文件复制的部分了
FROM nginx:1.15-alpine
COPY ./dist /usr/share/nginx/html 复制代码
在增长了缓存后,构建的时长大幅降低到 2:38,总体耗时降低了 50%以上。
在上文的基础上,不难想到 push master 和 release 形成的重复构建,是否也能够一样经过缓存去除。这固然在理论上也是可行的,可是因为缓存并不稳定,所以须要更为通用的方法。
在 semantic release 的流程中,push master 和 release 的惟一区别就是 release 增长了一个 git tag。而 git tag 本质上只是对一个特定 commit 的引用,并不会改变 commit 记录,所以 push master 和 release 两次触发的 CI 中,最后一次 commit 是相同的,即 DRONE_COMMIT_SHA
不会改变。
基于这一点,咱们能够在 push master 的构建中,将DRONE_COMMIT_SHA
做为 Docker 镜像额外的 Tag,在 release 环节,只要给有 DRONE_COMMIT_SHA
Tag 的镜像再打上最终的版本号 Tag 便可,并不须要在 release 环节从头构建镜像。
这个过程对应 .drone.yml
以下
- name: push-docker-staging
image: plugins/docker
settings:
repo: allovince/xxx
username: allovince
password:
from_secret: DOCKER_PASSWORD
tag:
- staging
- sha_${DRONE_COMMIT_SHA}
when:
branch: master
event: push
- 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
- name: push-docker-production
image: plugins/docker
environment:
DOCKER_PASSWORD:
from_secret: DOCKER_PASSWORD
commands:
- docker -v
- nohup dockerd &
- docker login -u allovince -p $${DOCKER_PASSWORD}
- docker pull allovince/xxx:sha_$${DRONE_COMMIT_SHA}
- docker tag allovince/xxx:sha_$${DRONE_COMMIT_SHA} allovince/xxx:$${DRONE_TAG}
- docker push allovince/xxx:$${DRONE_TAG}
when:
event: tag
privileged: true
复制代码
假设最后一次 commit 的 hash 是 c0558777
, release 版本是 v1.0.9, push master 后, 镜像将打上
两个 Tag,在 release 后,镜像将再增长一个 v1.0.9
的 Tag。
须要注意的是为镜像打 Tag 使用到了 Docker-in-Docker,须要 privileged flag,即privileged: true
同时 docker tag 等命令依赖 docker daemon 的启动,不然会报错
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
一种方式是挂载主机的 daemon /var/run/docker.sock
,另外一种方式是在容器内启动 docker daemon,我这里使用的是后者,对应 nohup dockerd &
,而在 release 阶段,因为任务仅仅是为 docker 镜像额外增长一个 tag,以及通知生产环境发布,所以上文中的 cache 等环节均可以经过条件省略,结果以下。
如此优化后 release 环节的耗时缩短到 1 分钟之内,看看最终成果,从代码提交到发布完成,总耗时不到 5 分钟,是比较友好的。