The previous article introduced the semantic continuous integration Workflow implemented by Drone + Semantic Release in the container environment. In order to facilitate the demonstration, the process only gives the most important links in the Workflow, which may find many places worth optimizing in practice.

Therefore, on the basis of this workflow, some CI optimization and speed up methods in container environment are introduced. The method itself is not limited to the use of Drone, and the same idea can be applied to other CI tools.

Project overview before optimization

Taking a real project in a production environment as an example, the main structure of the project is as follows

├ ─ ─ Dockerfile ├ ─ ─ dist / ├ ─ ─ node_modules / ├ ─ ─ package. The json └ ─ ─ SRC /Copy the code

This is a relatively common based on the React the front end of the project, use NPM list | wc -l can see, there are 3952 depend on project will pass webpack packaging to the dist directory, packaged command is encapsulated into a NPM run build. Dist directory is packaged into Nginx Docker image through Dockerfile. Production environment can run the packaged image directly.

Dockerfile is written like this

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  
Copy the code

The multi-stage construction function of Docker is used, that is, the installation of NPM. Compilation is the first stage. After compilation, only the compiled result dist folder is copied out, and the remaining uncopied files are discarded.

Process Bottleneck analysis

The publishing process directly follows the Gitflow + Semantic Release workflow described earlier. As you can see, a release at this point is relatively slow. It takes 9:18 for push to master to build the staging image, and 6:24 for Production image with Semantic Release Tag.

In the process of Drone construction, it can be seen that the time of container construction accounts for more than 90%. On the one hand, NPM needs to download and install more than 3000 dependencies, and on the other hand, the compilation of Webpack also takes about 30s. If the network is unstable, the waiting time will undoubtedly be longer.

Another time-consuming culprit is also obvious. Due to the introduction of semantic Release, the two actions of push master and release will trigger two CI’s, and each CI is constructed with Docker image. However, if no exception occurs, The two Docker images correspond to the same code, which should be completely consistent, that is, the time spent in image construction during release is a waste of time.

There are of course other application-level optimizations, such as replacing NPM with YARN, using faster sources, and removing unnecessary dependencies, which are beyond the scope of this article and will be skipped.

Caching is introduced to reduce repeated downloads

With over 3000 dependencies downloaded per build, it would be tempting to cache these dependencies, but in this case, NPM downloads/compilations took place during the container build phase, which was difficult to introduce caching. So the first thing to do is move the download/compile process from the container to the CI, complete the download/compile from the CI, and copy the results to the container image.

On the basis of downloading/compiling transfer to CI, you can directly use the cache plug-in provided by Drone. At present, according to different file systems, Drone’s optional cache plug-in has

  • Volume Cache
  • AWS S3 Cache
  • SFTP Cache
  • Google Cloud Storage Cache

Here Volume Cache is used as an example,.drone.yml is as follows. The syntax here corresponds to drone-V1.0 or above, which may be different from the old official documents.

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
Copy the code

The Volume Cache plug-in is simple to use. First, you need to declare a Volume that corresponds to a folder on the host. In this case, / TMP/Cache is used. Restore: true copies files from the host to the container and is placed at the beginning of the pipeline. Rebuild: true copies files at the end of the pipeline.

Also note that using Volume requires setting the Repo as Trusted in the Drone.

At this point, the Dockerfile is only left with the file copy part

FROM nginx:1.15-alpine  
COPY ./dist /usr/share/nginx/html  
Copy the code

After increasing the cache, the build time dropped dramatically to 2:38, and the overall time dropped by more than 50%.

Omit duplicate builds via Docker Tag

On the basis of the above, it is not difficult to wonder if duplicate builds caused by push master and release can also be removed by caching. This is certainly possible in theory, but because caches are unstable, a more general approach is needed.

In the semantic release flow, the only difference between push Master and release is that release adds a Git tag. The git tag is essentially a reference to a specific commit and does not change the commit record. Therefore, the last COMMIT is the same in the CI triggered by push master and release. DRONE_COMMIT_SHA will not change.

With this in mind, we can use DRONE_COMMIT_SHA as an additional Tag for the Docker image in the build of the Push master. Just Tag images with the DRONE_COMMIT_SHA Tag with the final version number. There is no need to build the image from scratch in the release.

This process corresponds to.drone.yml as follows

  - 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
Copy the code

If the hash of the last commit is C0558777 and the release version is V1.0.9, the image will be created after the master is pushed

  • staging
  • sha_c0558777

Two tags. After release, the image will add a v1.0.9 Tag.

It should be noted that docker-in-docker was used to Tag the image, which required privileged Flag, i.e. privileged: true

In addition, commands such as docker tag depend on the startup of the Docker Daemon. Otherwise, an error will be reported

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

One way is to mount the host daemon /var/run/docker.sock, and the other way is to start the Docker daemon in a container. I used the latter, corresponding to nohup dockerd &, and in the release phase, Since the task is only to add an extra tag for the Docker image and notify the production environment to release it, the cache and other links mentioned above can be omitted through conditions, and the results are as follows.

After optimization, the time of release was shortened to less than 1 minute. Looking at the final result, the total time from code submission to release completion was less than 5 minutes, which was relatively friendly.