首先启动一个gitlab runner服务,并注册一个docker executor,这是比较简单的,此处暂不赘述
本文主要讲述使用docker executor,如何处理缓存,工件,构建docker镜像的问题,如何尽可能在保证安全的前提下加速编译过程。
以java项目为例,配置ci/cd文件
stages:
- package
- build
variables:
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
mvn:package:
stage: package
tags:
- gr1
image: maven:3.6.3-jdk-8
#缓存mvn库
cache:
key: mvn_repo
paths:
- .m2/repository
artifacts:
paths:
- target/multi-renter.jar
script:
- mvn package
缓存:
cache:
key:mvn_repo
paths:
- .m2/repository
这是指定一个缓存,在当前的gr1这个runner中的任何job中有效,缓存会保存在主机的另一个docker container(dockers ps -a 可以看到 gitlab runner helper),并最终挂在到主机的一个位置。为什么不直接挂载到主机上呢,那样效率不是更高吗?当然,直接挂载到主机上,效率更高,但是却让当前的job和主机的某个目录耦合,试想,如果其他人的job也往主机挂载,并且恰好挂载到和你一样的目录了呢?另外,不同的主机类型,比如windows和linux,目录格式不一样,因此你的配置要根据主机os类型而变化。而且你还要关注gitlab-runner服务可读写的目录权限问题。wow,真是太复杂了。但是挂载到另一个容器,gitlab-runner帮你解决了所有的问题。只是牺牲了一点点效率。无非就是增加了压缩和解压缓存的时间。缓存在所属的gitlab-runner(上述例子中所属为:gr1)后续的所有pipeline可见。
工件:
artifacts:
paths:
- target/multi-renter.jar
工件和缓存类似,只不过其往往在一个pipeline中生存工件的后续job中可见。在web ui中也可见,但是在另一个pipeline中不可见。比如,我在mvn:package:这个job中编译了一个jar文件,在编译的下一个stage中想把这个jar复制到docker镜像,就应该用工件去传递这个jar,而不是使用缓存。
构建docker image
构建docker镜像,官方给了几种方式
对于docker executor,每次都是启动一个新的container构建,新的container是一个非常clear的环境,因此上次构建使用到的base image和构建的缓存都无法使用,所以阅读下述方式时,重点关注如何加速构建速度,解决方式是否复杂以及带来的问题。
kaniko
kaniko可以在不使用docker特权模式下构建docker镜像,并且可以利用container registry加速构建。主要过程如下
- kaniko在--cache-dir目录下查找base image缓存(即docker file中的from命令)这里需要注意,如果这里没有命中,kaniko会去download image,但是不会写入缓存,因此,即使你通过卷持久化了这个目录,下次执行依然不会命中。因此我们需要提前将镜像缓存到--cache-dir,kanico关于cache base image的讲解。我使用了一下warmer,发现按照官网提供的命令,不加-f,如果缓存没命中,shell返回0,但实际报错了,加上-v debug可以看到,如果缓存命中了,什么都不发生。加上-f,则每次都会强制pull镜像覆盖cache,消耗一定时间。这可能是一个bug。我们要做的就是,先使用warmer下载base image,然后把缓存的目录挂载到kanico容器中使用(配置/srv/gitlab-runner/config/config.toml文件)。我只是想编译而已,为何要我解决如此麻烦的缓存问题?另外注意一下,kanico的executor和warmer都有相同名称的参数,所以这里需要自己体会下其含义,不要搞混了。
- kaniko对run命令进行缓存,因此执行run命令前,会到指定的远程registry查看有没有已经缓存的层,命中的话就使用缓存
- kaniko将构建好的image提交到registry
这是个好东西,我简单试用了一下,官方给的示例多是k8s中用,如果不在k8s中用,如何缓存base image是一个问题,我暂时还没有在官方找到一个比较好的方式。
kaniko拉取base image方式和docker pull不一样,测试使用warmer缓存java:8和使用docker pull java:8一个耗时不到一分钟,一个约3分钟。
docker:build:
stage: build
tags:
- gr1
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
script:
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG --insecure
docker in docker
这种方式需要以特权方式执行docker,带来不安全的因素。同时,构建也是非常慢的。因此也需要使用缓存去加速构建。原理是:docker先从registry拉取上次构建的镜像。然后构建的时候指定--cache-from=<last-image>,也就是说以上次的镜像作为缓存构建。构建完毕后,上次镜像,作为下次的构建缓存使用。但是需要注意的是,仍需要花费一部分时间在download image上。如果registry在内网,其实下载和上传都是蛮快的,可以接受。
docker:build:
stage: build
tags:
- gr1
image: docker:19.03.1
services:
- name: docker:19.03.1-dind
command: ['--insecure-registry=your.registry.com:port']
variables:
# Use TLS https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#tls-enabled
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
#DOCKER_TLS_CERTDIR: "/certs"
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker pull $CI_REGISTRY_IMAGE:latest || true
- docker build --cache-from $CI_REGISTRY_IMAGE:latest --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA --tag $CI_REGISTRY_IMAGE:latest --build-arg JAR_FILE=target/*.jar .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
DOCKER_HOST的配置参见docker dockerhub,主要注意一下证书如果配置了,端口号会是2376,否则是2375。
如果使用了不安全的registry,那么需要指定command: ['--insecure-registry=your.registry.com:port']
docker socket binding
由于此方式最终实际上是使用的host的docker daemon,因此,镜像和构建的缓存都是直接使用host中的缓存。当然,有利就有弊,此种方式的缺点有:
- 由于共享了主机的docker socket,所以docker命令都是向主机发出的命令,比如docker rm -f $(docker ps -a -q)会把主机上的所有docker容器删除了,包括gitlab-runner容器。
- 并发工作可能无法正常执行;创建具有特定名称的容器,则它们可能会相互冲突。
- 将源仓库中的文件和目录共享到容器中可能无法正常工作,因为卷安装是在主机而不是构建容器的上下文中完成的。
你必须时刻注意,你的docker命令是在主机执行的,而不是容器内部。 - 需要修改gitlab-runner的配置,挂载主机的docker socket,但是不同的host system的sock文件路径不同,这目前只能在host上配置。
- 在k8s中,docker binding脱离了k8s的控制,是非常危险的行为。有的k8s集群通过名称空间做环境隔离,想想一下,开发环境的docker命令删除了线上环境的容器。
如何绑定docker.sock?
修改gitlab-runner服务的配置文件 /srv/gitlab-runner/config/config.toml
volumes = ["/cache","/var/run/docker.sock:/var/run/docker.sock"]
[[runners]]
name = "gr1"
url = "http://gitlab.lbl.com"
token = "mJJWevdY42sJQ84syyN9"
executor = "docker"
[runners.custom_build_dir]
[runners.docker]
tls_verify = false
image = "ruby:2.6"
privileged = true
disable_entrypoint_overwrite = false
oom_kill_disable = false
disable_cache = false
volumes = ["/cache","/var/run/docker.sock:/var/run/docker.sock"]
shm_size = 0
[runners.cache]
[runners.cache.s3]
[runners.cache.gcs]
ci/cd文件配置
docker:build:
stage: build
tags:
- gr1
image:
name: docker:19.03.1
# before_script:
#登录到gitlab集成的注册表
# - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA --tag $CI_REGISTRY_IMAGE:latest --build-arg JAR_FILE=target/*.jar .
#推送到gitlab集成的注册表
# - docker push $CI_REGISTRY_IMAGE:latest
接下来,让我们看看,如果使用k8s执行器。