Docker-in-Docker: Jenkins CI 内部如何运行 docker

Posted 琦彦

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Docker-in-Docker: Jenkins CI 内部如何运行 docker相关的知识,希望对你有一定的参考价值。

目录

Docker-in-Docker:简化了流程

Docker-in-Docker:很糟糕

Docker-in-Docker:很丑陋

Docker-in-Docker:它变得更糟

解决方案

Jenkins 支持 Docker

让容器使用宿主的docker

解决访问宿主 docker.sock 的权限问题

1. 修改宿主机 docker.sock 文件权限设置

2. 给予容器 docker 组权限

3. Jenkins启动时指定root用户或使用特权升级策略(privileged: true)


Docker-in-Docker的主要目的是帮助开发Docker本身。许多人使用它来运行CI(例如使用Jenkins),这看起来很好,但它们会遇到许多“有趣”的问题,可以通过将Docker套接字绑定到Jenkins容器来避免

让我们看看这意味着什么。如果你想要没有详细信息的简短解决方案,只需滚动到本文的底部即可。☺

Docker-in-Docker:简化了流程

两年多以前,我在Docker中贡献了-privileged标志 并编写了第一版dind。目标是帮助核心团队更快地开发Docker。在Docker-in-Docker之前,典型的开发周期是:

  • hackity hack (不知道这是什么意思😂)

  • 构建

  • 停止当前运行的Docker守护程序

  • 运行新的Docker守护进程

  • 测试

  • 重复

如果你想要一个漂亮的,可重现的构建(即在一个容器中),它会有点复杂:

  • hackity hack

  • 确保可运行的Docker版本正在运行

  • 使用旧Docker构建新的Docker

  • 停止Docker守护进程

  • 运行新的Docker守护进程

  • 测试

  • 停止新的Docker守护进程

  • 重复

随着Docker-in-Docker的出现,这被简化为:

  • hackity hack

  • 构建+一步完成

  • 重复

好多了,对吧?

Docker-in-Docker:很糟糕

然而,与流行的看法相反,Docker-in-Docker并非100%由闪光,小马和独角兽制成。我的意思是,有一些问题需要注意。

一个是关于像AppArmor和SELinux这样的LSM(Linux安全模块):当启动容器时,“内部Docker”可能会尝试应用会使“外部Docker”发生冲突或混淆的安全配置文件。这实际上是最难解决的问题。试图合并-privileged 标志的原始实现。我的更改在我的Debian机器和Ubuntu测试虚拟机上工作(并且所有测试都会通过),但它会在迈克尔克罗斯比的机器上崩溃并烧毁 (如果我记得很好的话,它就是Fedora)。我不记得问题的确切原因,但可能是因为迈克是一个聪明的人SELINUX=enforce (我使用的是AppArmor)并且我的更改没有将SELinux配置文件考虑在内。

Docker-in-Docker:很丑陋

第二个问题与存储驱动程序相关联。在Docker中运行Docker时,外部Docker运行在普通文件系统(EXT4,BTRFS,你有什么)之上,但内部Docker运行在写时复制系统(AUFS,BTRFS,Device Mapper等)之上。 。,取决于外部Docker设置使用的内容)。有许多组合不起作用。例如,你无法在AUFS之上运行AUFS。如果在BTRFS之上运行BTRFS,它应该首先工作,但是一旦嵌套子卷,删除父子卷将失败。Device Mapper不是命名空间,因此如果Docker的多个实例在同一台机器上使用它们,它们将能够看到(并影响)彼此的镜像和容器支持设备。没有bueno。

许多问题都有解决方法; 例如,如果你想在内部Docker中使用AUFS,只需 /var/lib/docker将其升级为一个卷,你就可以了。Docker为Device Mapper目标名称添加了一些基本的命名空间,因此如果Docker的多次调用在同一台机器上运行,它们就不会互相踩踏。

然而,设置并不完全是直截了当的,正如你可以 GitHub 上的存储库中的那些问题中看到的 那样 。 dind

Docker-in-Docker:它变得更糟

那么构建缓存呢?那个人也会变得非常棘手。人们常常问我:“我正在运行Docker-in-Docker; 我如何使用位于主机上的镜像,而不是在内部Docker中再次拉动所有镜像?“

一些喜欢冒险的人试图/var/lib/docker 从主机绑定到Docker-in-Docker容器。有时它们/var/lib/docker与多个容器共享。

img

Sterling Archer建议你不要共享/ var/lib/docker,thx

Docker守护程序明确设计为具有独占访问权限/var/lib/docker。没有别的东西可以触摸,戳或隐藏任何隐藏在那里的Docker文件。

这是为什么?这是dotCloud时代的经验教训之一。dotCloud容器引擎通过让多个进程/var/lib/dotcloud同时访问来工作。聪明的技巧,如原子文件替换(而不是就地编辑),通过咨询和强制锁定来编写代码,以及像SQLite和BDB这样的安全系统的其他实验只能让我们到目前为止; 当我们重构我们的容器引擎(最终成为Docker)时,一个重大的设计决策就是在一个守护进程下收集所有容器操作,并完成所有并发访问的废话。

(不要误解我的意思:完全有可能做一些好的,可靠的,快速的,涉及多个进程和最先进的并发管理;但我们认为它更简单,更容易编写和维护,与Docker的单一演员模型一起使用。)

这意味着如果你/var/lib/docker在多个Docker实例之间共享目录,那么你将度过一段美好时光。当然,它可能会起作用,特别是在早期测试期间。“看哪,我可以docker run ubuntu!”但是尝试做更多的事情(从两个不同的实例中拉出相同的镜像......)并观察世界燃烧。

这意味着,如果你的CI系统进行构建和重建,每次重新启动Docker-in-Docker容器时,你可能正在调整其缓存。这真的不酷。

解决方案

我们在这里退一步吧。你真的想要Docker-in-Docker吗?或者你只是希望能够从CI系统运行Docker(特别是:构建,运行,有时推送容器和镜像),而这个CI系统本身就在容器中?

我敢打赌,大多数人都想要后者。你想要的只是一个解决方案,以便像Jenkins这样的CI系统可以启动容器。

最简单的方法是将Docker套接字暴露给CI容器,方法是将其与-v标志绑定。

简单地说,当你启动CI容器(Jenkins或其他)时,不要与Docker-in-Docker一起攻击某些东西,而是启动它:

docker run -v /var/run/docker.sock:/var/run/docker.sock ...

现在这个容器可以访问Docker套接字,因此可以启动容器。除了不启动“子”容器,它将启动“兄弟”容器。

尝试使用docker官方镜像(包含Docker二进制文件):

docker run -v /var/run/docker.sock:/var/run/docker.sock \\
           -ti docker

这看起来像Docker-in-Docker,感觉就像Docker-in-Docker,但它不是Docker-in-Docker:当这个容器创建更多容器时,这些容器将在顶级Docker中创建。你将不会遇到嵌套副作用,并且将在多个调用之间共享构建缓存。

⚠️这篇文章的旧版本建议将docker二进制文件从主机绑定到容器。这不再可靠因为Docker Engine不再作为(几乎)静态库分发。

如果你想使用Jenkins CI系统中的Docker,你有多种选择

  • 使用基本映像的打包系统安装Docker CLI(即如果你的映像基于Debian,请使用.deb包),

  • 使用Docker API。

与其在容器里创建容器,不如在容器里挂载容器

Jenkins 支持 Docker

我们将 Jenkins 运行在容器中, 而 Jenkins CI 也需要运行 docker 的话, 就会遇到 docker in docker 的问题. 具体请参考: ~jpetazzo/Using Docker-in-Docker for your CI or testing environment? Think twice..

而我们知道, Docker 其实分为两部分: 服务端和客户端. 服务端通过 socket 套接字 或者监听端口接受客户端的命令. 具体可参考官方文档 Configure where the Docker daemon listens for connections.

也就是说如果我们在宿主机上运行了 docker 服务端的话, 我们在容器内可以只安装 docker 客户端, 然后通过 socket套接字 或者 ip+端口 的方式来直接使用宿主的docker 服务. 这样在容器内新建容器, 其实是在宿主机上新建容器.

因为 Docker 服务端和客户端默认使用 socket 套接字进行交互, 所以我们这里也使用 socket套接字 的方案, 即将宿主机的 /var/run/docker.sock 通过映射给 Jenkins 容器.

让容器使用宿主的docker

按照上面的思路, 我们在创建容器的 docker run 命令中增加如下 3 个参数:

-v /var/run/docker.sock:/var/run/docker.sock
-v /usr/bin/docker:/usr/bin/docker 
-v /etc/docker:/etc/docker

 

 

其中目录或文件

/var/run/docker.sock /usr/bin/docker /etc/docker

是安装docker后就有的,不需要新建

有关三者的说明

  • -v /var/run/docker.sock:/var/run/docker.sock, 通过映射主机的套接字文件到容器, 让容器内启动 docker 的时候并不是启动容器内的容器(子容器), 而是启动主机上的容器(兄弟容器).

  • -v /usr/bin/docker:/usr/bin/docker, 让容器中直接使用宿主机的 docker 客户端.

  • -v /etc/docker:/etc/docker, 让容器中的 docker 客户端使用宿主机的 docker 配置文件, 包括国内镜像 (mirrors) 和 非ssl安全访问白名单 等配置.

解决访问宿主 docker.sock 的权限问题

重启容器后, 我们通过 docker exec -it jenkins /bin/bash 命令进入容器, 执行 docker ps 验证 docker 命令是否可正常使用, 结果发现会遇到如下权限问题:

$ docker ps
Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock:

 Get http://%2Fvar%2Frun%2Fdocker.sock/v1.40/containers/json: dial unix /var/run/docker.sock: connect: permission denied

我们先看下 host 上 /var/run/docker.sock 的权限:

$ ll /var/run/docker.sock
srw-rw---- 1 root docker 0 Jul 16 10:17 /var/run/docker.sock=

可以看到 docker.sock 属于 root 用户 和 docker. 映射到容器内的权限为:

$ ls -al /var/run/docker.sock
srw-rw---- 1 root 128 0 Jul 16 10:17 /var/run/docker.sock

我们再在容器查看下 jenkins 用户的 user id 和 groupd id:

jenkins@5affdae1637b:/$ id
uid=1000(jenkins) gid=1000(jenkins) groups=1000(jenkins)

我们可以看到 jenkins 的用户id 为 1000, group id 也为 1000. 因此我们的解决方案也有 2 个:

  • 在宿主机将 docker.sock 文件的所有者改为 user id = 1000 的用户;

  • 在给容器内的用户增加 group id = 128 的权限.

  • Jenkins启动时指定root用户或使用特权升级策略(privileged: true)

下面分别讲解.

1. 修改宿主机 docker.sock 文件权限设置

将 host 上 docker socket 的拥有者修改为运行 uid=1000 的用户, 或者直接将权限修改为其他人可读写666:

修改宿主机上 socket 的 owner 为 id=1000 的用户
sudo chown 1000 /var/run/docker.sock
# 或修改 sock 的权限为 666
sudo chmod 666 /var/run/docker.sock

这个方案无需重启容器, 直接在容器内运行 docker ps 可以看到能输出正常结果.

这个方案是网上大多数文章给出的方案. 但是该方案有一个比较的缺陷, 那就是如果宿主机或者 docker 重启, 会重新创建 docker.sock 文件, 其所有者会被重置为 root 用户, 所以我们又需要再执行上面的命令修改权限.

2. 给予容器 docker 组权限

第二个方案是, 我们给容器内的 jenkins 用户增加 id=128 的组权限. 而正好 docker run 很友好地提供 groupd-add 参数支持该操作.

官方文档 Additional groups🔗

--group-add: Add additional groups to run as

By default, the docker container process runs with the supplementary groups looked up for the specified user. If one wants to add more to that list of groups, then one can use this flag:

$ docker run --rm --group-add audio --group-add nogroup --group-add 777 busybox id

uid=0(root) gid=0(root) groups=10(wheel),29(audio),99(nogroup),777

也就是说我们可一个通过 group-add 参数给容器中的用户通过 group name 或者 group id 添加多个额外的用户组权限, 但是注意: 这个用户组是指容器内的用户组, 其 id 可能跟宿主机上的 id 不一致. 而我们要让容器内的用户拥有 host 的某个 group 权限, 需要通过 id 来赋权.

因此这里我们先看 host 上 docker 组的 id.

$ cat /etc/group | grep docker
[sudo] password for fly:
docker:x:128:fly

可以看到 docker 用户组 id 为 128. 因此我们在创建容器的时候加上 --group-add=128 即可让容器内的 jenkins 用户拥有 /var/run/docker.sock 文件的读写权限:

# 先移除旧容器
sudo docker rm -f dao_jenkins_1
# 重新创建容器
sudo docker run -d -p 8081:8080 -p 50001:50000 \\
    -v /data2/jenkins/jenkins_home:/var/jenkins_home \\
    -v /etc/localtime:/etc/localtime:ro \\
    -v /var/run/docker.sock:/var/run/docker.sock \\
    -v /etc/docker:/etc/docker \\
    -v /usr/bin/docker:/usr/bin/docker \\
    --restart=always  \\
    --group-add=128 \\
    --name dao_jenkins_1 \\
    jenkins/jenkins:blueocean

3. Jenkins启动时指定root用户或使用特权升级策略(privileged: true)

version: "3"

services:

  jenkins:
    image: jenkinsci/blueocean
    restart: unless-stopped
    container_name: jenkins
    ports:
      - "8080:8080"
    networks:
      - sonarnet
    privileged: true
    user: root
    environment:
      - TZ=Asia/Shanghai
    volumes:
      - /home/data/jenkins/jenkins_home:/var/jenkins_home
      # 保持 docker 中的时区跟 host 保持一致, 否则日志等时间都使用 UTC+0 时区, 跟中国时间差 8 个小时
      - /etc/localtime:/etc/localtime:ro
      # 挂载宿主机本地的maven环境
      - /usr/local/jt/apache-maven-3.6.3:/usr/local/maven
      # 让容器使用宿主的docker
      - /var/run/docker.sock:/var/run/docker.sock
      - /usr/bin/docker:/usr/bin/docker
      - /etc/docker:/etc/docker

networks:
  sonarnet:
    driver: bridge

 

译文连接:

https://jpetazzo.github.io/2015/09/03/do-not-use-docker-in-docker-for-ci/

参考连接:

 

以上是关于Docker-in-Docker: Jenkins CI 内部如何运行 docker的主要内容,如果未能解决你的问题,请参考以下文章

4.3 runner里的docker-in-docker

Docker 容器网络与 Docker-in-Docker

无需 Docker-in-Docker 的私有 Gitlab Runner 代码质量

从 GitLab CI 运行器连接到 docker-in-docker

docker-in-docker (dind) 服务在 gitlab ci 中的作用

Docker-in-Docker 与 Gitlab 共享运行器,用于构建和推送 docker 镜像到注册表