前言
现在越来越多的CICD平台都开始使用DockerInDocker环境进行自动构建,随之而来的问题就是因为构建是在容器内构建,而容器环境在任务结束时会就地销毁,导致下一次构建时无法使用上次构建的缓存,基础镜像和项目依赖全部都要重新构建,对于企业大型项目来说,动辄十来分钟的构建时间不提,国内下载项目依赖包的时候还有可能会导致超时失败,如此不稳定的工作流对于CICD肯定是无法接受的。
为了解决这个问题,我绕了一个很大的弯子,查阅了许多资料,比如:
- 使用drone的cache插件将每次构建的workspace整个缓存下来,然后下次构建时重新重建缓存空间。
- 直接挂载docker.sock,使用外部主机本身的docker进行构建,缓存自然也就在服务器而不是容器内了。
- 挂载var/lib/docker/,将容器内的docker缓存目录挂载到本地,这样每次创建新容器都可以使用缓存了。
但以上方案都有个问题:必须指定要缓存的目录,比较麻烦,而且还要解决在K8S这种多服务器环境下的数据挂载点的问题——每次跑构建的容器可能不在同一个服务器,hostVolume无法进行使用,只能使用云NAS或者S3之类的外部存储挂载才行,而且挂载本地目录对于cicd实际上是一个比较不安全的操作,官方也不推荐这样。
就在我为了缓存发愁的时候,有一篇文章映入我的眼帘:Caching Docker layers on serverless build hosts with multi-stage builds, –target, and –cache-from
这篇文章提到了docker的多阶段构建和cache-from参数,该文章声称Docker本身就有解决这个问题的方案,解铃还须系铃人。
多阶段构建
该文章提到的方案是使用dockerfile的多阶段构建,多阶段构建是指通过编写Dockerfile的方式,使用多个基础镜像或步骤之间进行引用的方式制作镜像,以一个最简单的vue3项目的dockerfile为例:
FROM node:14-alpine as build
WORKDIR /app
COPY ["trank/package.json","trank/"]
COPY ["trank/package-lock.json","trank/"]
WORKDIR /app/trank
RUN npm install
COPY . /app
RUN npm run build
FROM nginx
RUN mkdir /app
COPY --from=build /app/trank/dist /app
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
注意该dockerfile分为三个阶段进行构建:
- build步骤,复制package.json和package-lock.json,并使用install进行依赖项的复原,之后再拷贝源码,进行编译。
- 使用Nginx镜像,拷贝build步骤的产出和Nginx配置文件到镜像目录。
该多阶段构建使用【as】为第一个步骤起了个名称,方便在第二个步骤中引用。
需要注意的是,该多阶段构建把build为止的步骤独立了出来,这是为了之后方便复用缓存,在你项目依赖没有变化的情况下,该缓存在install为止的步骤前永不失效。
我之前以为必须把拷贝源码的部分也独立出一个步骤,错误的认为源码的更改会导致restore的缓存也跟着一起失效,实际上并不是这样的,docker会自行对比两个镜像的每一层,即使源码变了,也不会影响前一步的npm install。
修改工作流文件
本文的CICD方式是使用Drone,但–target和–cache-from两个参数是docker自带的,理论上将你在本地使用docker模拟这个步骤也是可行的,前言引用的文章便是使用docker命令来实现多阶段构建缓存的,有兴趣的可以自行翻阅,本文只介绍如何使用Drone在dind环境下让docker层缓存生效。
接下来我们看一下原本的drone的工作流文件步骤,这里只提取局部:
steps:
- name: docker
image: plugins/docker
settings:
username:
from_secret: username
password:
from_secret: password
repo: registry-vpc.cn-shanghai.aliyuncs.com/hulu0811/whatsfordinner
registry: registry-vpc.cn-shanghai.aliyuncs.com
tags:
- latest
- ${DRONE_COMMIT}
这是一个标准的docker build的step,该step每次运行都无法使用上次构建的缓存,需要重新拉取基础镜像和重建项目依赖,这里我们需要将该步骤分为两个步骤:
- 构建Dockerfile中名为build的步骤,该步骤只会构建build为止,Nginx的部分则不会进行构建,构建完成后,打一个名为build的tag上传至镜像仓库。
- 完整构建整个Dockerfile,但使用cache-from指明要使用哪些镜像进行缓存对照,这里制定两个镜像,分别是前一步骤中的build,和完整镜像latest。
接下来我们看一下改完的工作流文件:
steps:
- name: docker-build
image: plugins/docker
settings:
username:
from_secret: username
password:
from_secret: password
repo: registry-vpc.cn-shanghai.aliyuncs.com/hulu0811/whatsfordinner
registry: registry-vpc.cn-shanghai.aliyuncs.com
target: build
cache_from: registry-vpc.cn-shanghai.aliyuncs.com/hulu0811/whatsfordinner:build
tags:
- build
- name: docker-final
image: plugins/docker
settings:
username:
from_secret: username
password:
from_secret: password
repo: registry-vpc.cn-shanghai.aliyuncs.com/hulu0811/whatsfordinner
registry: registry-vpc.cn-shanghai.aliyuncs.com
cache_from:
- registry-vpc.cn-shanghai.aliyuncs.com/hulu0811/whatsfordinner:build
- registry-vpc.cn-shanghai.aliyuncs.com/hulu0811/whatsfordinner:latest
tags:
- latest
- ${DRONE_COMMIT}
这样就修改完成了,在下次运行工作流的时候,步骤会变成这样:
- 拉取上次的build镜像 -> 根据上次的build镜像构建新镜像 -> 上传新build镜像 ->
- 拉取build镜像和最新的latest镜像 -> 根据这两个镜像构建新的镜像 -> 上传新的latest镜像 ->
这样就可以使上次构建的镜像成为本次构建的层缓存了。
那么我们接下来赶紧验证一下:




接下来我们看下时间对比,使用缓存前:

使用缓存后:

在我使用.netCore的构建流程时,原本时间4分钟的构建会被缩短到一分半以内。
结语
至此,整个drone搭建cicd的坑算是都趟平了,书写本系列文章的初衷仅仅是为了作为学习笔记,drone的相关资料国内实在是不算多,学习过程中只能去谷歌使用英文进行搜索,如果本文能帮助你解决问题的话实在倍感荣幸。