在使用gitlab的CI/CD流程进行自动化配置的时候,很容易碰到的一个需求就是,在容器内部打包一个容器镜像。这话听起来很绕,但是做过的人都知道。这个时候需要容器里有一个docker的后台进程,而在容器里配置一个docker进程它的成本是很高的,你需要修改启动的初始进程,那么就需要改镜像文件,这些改动过程中会遇到很多不可预料的问题。因此,业界也提出了一些基础方案,目前用的最多的两种是:
- 将物理机的
docker.socket
文件通过卷挂载的方式,挂载到容器内,这样在容器里只需要安装一个docker
客户端就可以使用主机的docker
环境 - 直接将主机的
docker
命令挂载进去,直接使用主机的docker
上面两种方法其实大同小异,但是在实际操作的时候还需要配置unprivileged=true
,即给容器不受限制的权限,此时容器内甚至可以直接操作宿主机上的环境(也称为容器泄露)。在内网里用一下还可以,但是在一些安全要求非常高的公网上或者公司内,这种方案基本上就是直接被否决的。
下面我们来看看在gitlab的CI/CD里,它是怎么满足这类需求的。
使用shell执行器
这种方式是在主机上安装runner时候可以使用,直接使用主机上安装好的docker命令,由gitlab-runner
用户来执行对应的命令,因此需要给它配置特殊的权限。具体的操作流程是:
- 在主机上安装对应平台的gitlab runner
- 注册runner,在注册参数里,使用shell类型的执行器,例如:
sudo gitlab-runner register -n \
--url https://yourgitlab.com/ \
--registration-token REGISTRATION-TOKEN \
--executor shell --description "Docker shell runner"
- 在安装gitlab runner的主机上,安装Docker引擎,例如免费版的docker-ce。
- 把gitlab-runner用户添加到docker组里去,命令是:
sudo usermod -aG docker gitlab-runner
- 验证gitlab-runner用户可以访问docker,验证命令是:
sudo -u gitlab-runner -H docker info
如果有权限,那么会输出docker的信息
- 在gitlab里,在.gitlab-ci.yml文件里,直接使用docker命令,示例如下:
before_script:
- docker info
build_image:
script:
- docker build -t my-docker-image .
- docker run my-docker-image /script/to/run/tests
Docker in Docker方案
Docker in Docker(dind)表示:
- 你使用Docker 执行器来注册runner(也是主机类型的runner)
- 执行器使用一个Docker官方提供的一个Docker容器镜像,来运行你的CI/CD任务。
在上面提到的Docker容器镜像里,安装了所有dockr工具,能够在特权模式下在镜像的环境里运行CI/CD任务。同时你需要指定这个镜像的具体版本,例如docker:19.03.12
,如果你使用docker:stable
这样的标签来使用镜像,那么镜像的版本是不确定的,特别是当新版本发布的时候。
dind的限制:
- 无法使用
docker-compose
命令,这个命令需要单独安装。 - 缓存,每个任务都运行在一个新环境里,并发任务可以执行的很好,因为每个任务都是独立的环境。但是任务执行速度会变慢,因为没有缓存层了(每次都创建的新容器)
启用dind的TLS
启用dind的TLS,可以让容器之间的通信更安全。启用方式是:
- 安装gitlab runner
- 注册gitlab runner,命令是:
sudo gitlab-runner register -n \
--url https://yourgitlab.com/ \
--registration-token REGISTRATION_TOKEN \
--executor docker \
--description "My Docker Runner" \
--docker-image "docker:19.03.12" \
--docker-privileged \
--docker-volumes "/certs/client"
上面的命令指定了执行器是docker类型,镜像版本,特权容器权限和挂载卷。挂载卷挂载的是物理机上docker的证书文件。上面的命令会创建一个config.toml 文件,内容如下:
[[runners]]
url = "https://gitlab.com/"
token = TOKEN
executor = "docker"
[runners.docker]
tls_verify = false
image = "docker:19.03.12"
privileged = true
disable_cache = false
volumes = ["/certs/client", "/cache"]
[runners.cache]
[runners.cache.s3]
[runners.cache.gcs]
- 然后你就可以在任务脚本里使用docker命令了,注意到docker: 19.03.12-dind服务。
image: docker:19.03.12
variables:
# 当你使用 dind 服务, 你必须让 Docker 和服务内部启动的进程沟通,进程是通过
# 网络连接的方式,而不是/var/run/docker.sock套接字。Docker 19.03会自动完成这个设置,
# 是通过设置entrypoint.sh文件里的DOCKER_HOST变量来实现的。
#
# 'docker' 主机名是服务容器的别名
# 注意Docker在哪里创建证书,Docker会在启动的时候自动创建这些证书,
# 并且在服务容器和任务容器之间共享/certs/client目录。
# 这个共享是通过config.toml文件里的卷挂载来实现的:
DOCKER_TLS_CERTDIR: "/certs"
services:
- docker:19.03.12-dind
before_script:
- docker info
build:
stage: build
script:
- docker build -t my-docker-image .
- docker run my-docker-image /script/to/run/tests
这种方式是直接使用服务容器里配置好的docker服务。
在k8s里启用TLS的dind
要在k8s里使用启用了TLS的dind,步骤如下:
- 使用Helm chart,更新values.yml文件,指定一个卷挂载
runners:
config: |
[[runners]]
[runners.kubernetes]
image = "ubuntu:20.04"
privileged = true
[[runners.kubernetes.volumes.empty_dir]]
name = "docker-certs"
mount_path = "/certs/client"
medium = "Memory"
上面不光指定了卷挂载,还指定了容器镜像名称和特权模式。
- 在任务脚本里使用
docker
,注意下面的docker: 19.03.13-dind
服务
image: docker:19.03.13
variables:
# 当你使用 dind 服务, 你必须让 Docker 和服务内部启动的进程沟通,进程是通过
# 网络连接的方式,而不是/var/run/docker.sock套接字。是通过设置下面的变量实现的
DOCKER_HOST: tcp://docker:2376
# docker主机名是服务容器的别名,如果你在使用k8s 1.6版本执行器类型的 GitLab Runner 12.7 或更早版本,
# 变量必须设置为 tcp://localhost:2376 因为这几个版本无法解析容器主机名,
# 注意Docker在哪里创建证书,Docker会在启动的时候自动创建这些证书,
# 并且在服务容器和任务容器之间共享/certs/client目录。
# 这个共享是通过config.toml文件里的卷挂载来实现的:
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_TLS_VERIFY: 1
DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
services:
- docker:19.03.13-dind
before_script:
- docker info
build:
stage: build
script:
- docker build -t my-docker-image .
- docker run my-docker-image /script/to/run/tests
上面就是三种使用方式,还有一些细节性的配置。后面再继续更新了。