从socket权限问题重新认识docker架构

jenkins构建出现的权限问题

docker化运行的jenkins实现对spring boot的docker部署,使用docker-maven-plugin插件。
默认情况下,此插件在docker内部的通过localhost:2375访问docker守护进程。

问题:

#jenkins构建过程中 mvn clean package docker:build 报错
14:48:53 [INFO] I/O exception (java.io.IOException) caught when processing request to {}->unix://localhost:80: No such file or directory
14:48:53 [INFO] Retrying request to {}->unix://localhost:80
14:48:53 [INFO] I/O exception (java.io.IOException) caught when processing request to {}->unix://localhost:80: No such file or directory
14:48:53 [INFO] Retrying request to {}->unix://localhost:80
14:48:53 [INFO] I/O exception (java.io.IOException) caught when processing request to {}->unix://localhost:80: No such file or directory
14:48:53 [INFO] Retrying request to {}->unix://localhost:80

原因:
docker-maven-plugin在jenkis 容器内中可以理解为一个docker client,但是容器内无法通过localhost:2375访问docker守护进程,因此会报此错误。

思考:
我们是否需要在jenkins容器内部安装docker,以便docker client能够访问docker守护进程。

对于这个疑问,我们先从docker架构入手找下答案。

Docker Engine

Docker Engine是一个client-server架构,主要由以下组件:

  1. docker daemon,守护进程dockerd;
  2. REST API,client与server进行通信及操作的接口;
  3. docker CLI,命令行界面的客户端;
    在这里插入图片描述基于client-server架构,Docker的工作机制如下:
    1.Docker客户端与Docker守护进程进行对话,该守护进程完成了构建,运行和分发Docker容器的繁重工作。
    2.Docker客户端和守护程序可以在同一系统上运行,或者您可以将Docker客户端连接到远程Docker守护程序。
    3.Docker客户端和守护程序在UNIX套接字或网络接口上使用REST API进行通信。

因此我们在服务器上安装的docker其实是包含client-server的,命令如下:

root@test:~# docker version
Client:
 Version:           18.09.1
 API version:       1.39
 Go version:        go1.10.6
 Git commit:        4c52b90
 Built:             Wed Jan  9 19:35:23 2019
 OS/Arch:           linux/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          18.09.1
  API version:      1.39 (minimum version 1.12)
  Go version:       go1.10.6
  Git commit:       4c52b90
  Built:            Wed Jan  9 19:02:44 2019
  OS/Arch:          linux/amd64
  Experimental:     false

既然client 和 server工作在同一系统上,那他们是如何通信的呢?

Daemon socket

Docker守护程序可以通过三种不同类型的Socket供Docker Engine API请求:unix,tcp和fd。

  1. unix:默认情况下,在/var/run/docker.sock上创建unix socket,需要root许可或Docker组成员身份;
  2. tcp:当我们需要远程访问守护进程dockered时,可以使用tcp socket;
  3. fd:在基于Systemd的系统上,您可以通过Systemd套接字激活与守护程序通信;
#unix socket
dockerd -H unix:///var/run/docker.sock
#tcp socket
dockerd -H tcp://0.0.0.0:2375
#fd socket
dockerd -H fd://

Docker客户端将使用DOCKER_HOST环境变量来为客户端设置-H标志:

docker -H tcp://0.0.0.0:2375 psexport DOCKER_HOST="tcp://0.0.0.0:2375"
docker ps

通过不同的socker,server不仅可以提供给本地client调用,还可以满足远程client的调用。只不过我们日常操作都是基于在/var/run/docker.socket 上创建的unix.socket 与本地的server进行交互。

解决方案

通过对docker engine 和 daemon socket的了解,jenkins容器内的docker client是否可以远程访问宿主机的docker守护进程, 这样可以在不增加镜像的大小的情况下解决问题。

联想到本地的client、server交互,我们通过映射宿主机的unix socket到容器内,使其像在本地一样和docker守护进程进行交互。

vim docker-compose.yml
#将本地socket映射到容器内
version: '3.7'
services:
  jenkins:
    image: jenkins/jenkins:lts
    container_name: jenkins
    restart: always
    ports:
      - "8080:8080"
      - "50000:50000"
    volumes:
      - "/media/yanggd/work/jenkins:/var/jenkins_home"
      - "/App/maven:/usr/local/maven"
      # 将本地socket映射到容器内
      - "/var/run/docker.sock:/var/run/docker.sock"

docker-compose重新启动后,jenkins再次构建报错:

14:58:02 [INFO] I/O exception (java.io.IOException) caught when processing request to {}->unix://localhost:80: Permission denied
14:58:02 [INFO] Retrying request to {}->unix://localhost:80
14:58:02 [INFO] I/O exception (java.io.IOException) caught when processing request to {}->unix://localhost:80: Permission denied
14:58:02 [INFO] Retrying request to {}->unix://localhost:80
14:58:02 [INFO] I/O exception (java.io.IOException) caught when processing request to {}->unix://localhost:80: Permission denied
14:58:02 [INFO] Retrying request to {}->unix://localhost:80

看来jenkins容器内的docker client能访问宿主的unix socket,但是因为权限问题访问被拒绝。
权限分析:

  1. 从jenkins的官方镜像得知,jenkins容器内部默认使用jenkins用户且uid、gid均为1000。而为了保证jenkins的备份,我们已经在宿主机新增jenkins用户,并且已经对jenkins_home授权
  2. uninx socket需要root许可或Docker组成员身份,我们在宿主机将jenkins用户添加到docker组中,以实现普通用户对/var/run/docker.sock 的访问。
root@test:~# usermod -G docker jenkins
root@test:~# id jenkins
uid=1001(jenkins) gid=1001(jenkins) groups=1001(jenkins),999(docker)
#重启docker
systemctl restart docker

经过修改后,jenkins构建仍然报错,这是为什么呢?难道这种方案不行吗?

#登录容器
dock exec -it jenkins /bin/bash
#查看权限
jenkins@3387b9e025bd:/$ cat /etc/group |grep 1000
jenkins:x:1000:
jenkins@3387b9e025bd:/$ cat /etc/passwd |grep 1000
jenkins:x:1000:1000::/var/jenkins_home:/bin/bash
#查看容器内的docker.sock
jenkins@3387b9e025bd:~/workspace/helloworld$ ls -l /var/run/docker.sock 
srw-rw---- 1 root 999 0 Mar 25 07:42 /var/run/docker.sock

通过登录容器查看权限发现,我们虽然在宿主机上将jenkins用户加入到docker组中,但是在容器内部docker.sock 的属组为999,而jenkins的uid及gid都为1000,因此由于gid的不同,在宿主机上授权并不代表容器内也授权成功。我们需要保证宿主机和jenkins容器内部的uid、gid保持一致。

由于宿主机在本地更改uid、gid会影响使用,在此我偷懒使用chmod临时解决。

chmod 666 /var/run/docker.sock

总结

此问题排查过程中,参考了大量的文档都不得解,静下心来通过现象看本质,不仅解决了问题还对docker加深了理解。

延伸

docker client 与 守护进程的交互可以通过sdk 或 docker api,如下:

#list containers
#http
curl -s --unix-socket /var/run/docker.sock http:/v1.39/info

使用sdk 或 docker api 与守护进程交互,可以我们在docker应用中提供更多的解决方案。

PS:如果你对博文感兴趣,请关注我的公众号“木讷大叔爱运维”,与你分享运维路上的点滴。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章