• Post author:
  • Post category:运维
  • Post comments:0评论
  • Reading time:5 mins read

前言

现在越来越多的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分为三个阶段进行构建:

  1. build步骤,复制package.json和package-lock.json,并使用install进行依赖项的复原,之后再拷贝源码,进行编译。
  2. 使用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每次运行都无法使用上次构建的缓存,需要重新拉取基础镜像和重建项目依赖,这里我们需要将该步骤分为两个步骤:

  1. 构建Dockerfile中名为build的步骤,该步骤只会构建build为止,Nginx的部分则不会进行构建,构建完成后,打一个名为build的tag上传至镜像仓库。
  2. 完整构建整个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}

这样就修改完成了,在下次运行工作流的时候,步骤会变成这样:

  1. 拉取上次的build镜像 -> 根据上次的build镜像构建新镜像 -> 上传新build镜像 ->
  2. 拉取build镜像和最新的latest镜像 -> 根据这两个镜像构建新的镜像 -> 上传新的latest镜像 ->

这样就可以使上次构建的镜像成为本次构建的层缓存了。

那么我们接下来赶紧验证一下:

拉取上次的build镜像
npm install 使用了缓存 且step到build为止
拉取build和latest镜像
到npm install为止全部使用缓存

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

使用缓存后:

在我使用.netCore的构建流程时,原本时间4分钟的构建会被缩短到一分半以内。

结语

至此,整个drone搭建cicd的坑算是都趟平了,书写本系列文章的初衷仅仅是为了作为学习笔记,drone的相关资料国内实在是不算多,学习过程中只能去谷歌使用英文进行搜索,如果本文能帮助你解决问题的话实在倍感荣幸。

葫芦

葫芦,诞生于1992年8月11日,游戏宅,胶佬,爱好摸鱼,一个干过超市收银,工地里搬过砖,当过广告印刷狗,做过电焊铁艺的现役.Net程序员。

发表回复