Docker容器技术与应用(项目5 Docker集群)

项目5 容器相关技术

任务5.1 Docker底层技术

Docker 底层使用的核心技术包括 Linux 上的命名空间(Namespaces)控制组(Control groups)联合文件系统(Union file systems)和容器格式(Container format)。

传统虚拟机(VM)通过在宿主主机中运行hypervisor 来模拟一整套完整的硬件环境,提供给虚拟机的操作系统。容器是借助Linux的命名空间权限隔离、控制组的资源分配控制、虚拟网络等技术实现进程之间的隔离,容器和宿主主机共享Linux内核。差别如下图:


图5.1虚拟机技术与容器技术的对比

在Linux操作系统中,包括内核、文件系统、网络、PID、UID、IPC、内存、硬盘、CPU 等等,所有的资源都是应用进程直接共享的。要实现虚拟化,除了要实现对内存、CPU、网络IO、硬盘IO、存储空间等限制外,还要实现文件系统、网络、PID、UID、IPC等等的相互隔离。

5.1.1命名空间nameSpace

命名空间是 Linux 内核一个强大的特性。每个容器都有自己单独的命名空间,运行在其中的应用都像独立的操作系统中运行一样。命名空间保证了容器之间彼此互不影响。Linux内核提供了六种namespace隔离的系统调用,如下表所示。

Namespace

系统调用参数

隔离内容

UTS

CLONE_NEWUTS

主机名与域名

IPC

CLONE_NEWIPC

信号量、消息队列和共享内存

PID

CLONE_NEWPID

进程编号

Network

CLONE_NEWNET

网络设备、网络栈、端口等等

Mount

CLONE_NEWNS

挂载点(文件系统)

User

CLONE_NEWUSER

用户和用户组

表5.1namespace六项隔离

Linux内核实现namespace的主要目的就是为了实现轻量级虚拟化(容器)服务,构建一个相对隔离的shell环境。在同一个namespace下的进程可以感知彼此的变化,而对外界的进程一无所知。这样让容器内的进程运行在一个独立的系统环境中,以此达到隔离的目的。

5.1.2控制组cgroup

cgroups是Linux内核提供的一种机制,cgroup可以根据特定的行为,把一系列系统任务及其子任务整合或分隔到不同的资源等级分组中,从而为系统资源管理提供一个统一的框架。主要的功能包括:

资源限制(Resource Limitation):cgroups可以对进程组使用的资源总额进行限制。如设定应用运行时使用内存的上限,一旦超过这个配额就发出OOM(Out of Memory)。

优先级分配(Prioritization):通过分配的CPU时间片数量及硬盘IO带宽大小,实际上就相当于控制了进程运行的优先级。

资源统计(Accounting): cgroups可以统计系统的资源使用量,如CPU使用时长、内存用量等等,这个功能非常适用于计费。

进程控制(Control):cgroups可以对进程组执行挂起、恢复等操作。

通俗的来说,cgroups可以限制、记录、隔离进程组所使用的物理资源(包括:CPU、memory、IO等),为容器实现虚拟化提供了基本保证,是构建Docker等一系列虚拟化管理工具的基础。Cgroup功能实现特点:

(1)cgroups的API以一个伪文件系统的方式实现,即用户可以通过文件操作实现cgroups的组织管理。

(2)cgroups的组织管理操作单元可以细粒度到线程级别,用户态代码也可以针对系统分配的资源创建和销毁cgroups,从而实现资源再分配和管理。

(3)所有资源管理的功能都以subsystem(子系统)的方式实现,接口统一。

(4)子进程创建之初与其父进程处于同一个cgroups的控制组。

cgroups的实现本质上是给系统进程挂上钩子(hooks),当task运行的过程中涉及到某个资源时就会触发钩子上所附带的subsystem进行检测,最终根据资源类别的不同使用对应的技术进行资源限制和优先级分配。

5.1.3联合文件系统UnionFS

联合文件系统(UnionFS)是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改按层(Layer)叠加,同时可以将不同目录挂载到同一个虚拟文件系统下。联合文件系统是Docker 镜像的基础。镜像可以通过分层来进行继承,基于基础镜像,可以制作各种具体的应用镜像。Aufs是Docker最初采用的文件系统,由于Aufs未能加入到Linux内核,考虑到兼容性问题,加入了Devicemapper的支持。Docker 目前支持的联合文件系统种类包括 Aufs、btrfs、vfs和DeviceMapper。

典型的Linux文件系统由bootfs和rootfs两部分组成,bootfs(boot file system)主要包含bootloader和kernel,bootloader主要是引导加载kernel,当kernel被加载到内存中后bootfs就被umount了。rootfs (root filesystem)包含的就是典型 Linux 系统中的/dev,/proc,/bin,/etc等标准目录和文件。

图5.2Linux加载结构

Docker容器建立在Aufs基础上的,Aufs是一种Union FS, 简单来说就是支持将不同的目录挂载到同一个虚拟文件系统下,并实现一种layer的概念。Aufs将挂载到同一虚拟文件系统下的多个目录分别设置成read-only,read-write以及whiteout-able权限,对read-only目录只能读,而写操作只能实施在read-write目录中。写操作是在read-only上的一种增量操作,不影响read-only目录。当挂载目录的时候要严格按照各目录之间的这种增量关系,将被增量操作的目录优先于在它基础上增量操作的目录挂载,待所有目录挂载结束了,继续挂载一个read-write目录,如此便形成了一种层次结构。

Docker镜像的典型结构如下图。传统的Linux加载bootfs时会先将rootfs设为read-only,然后在系统自检之后将rootfs从read-only改为read-write,然后就可以在rootfs上进行写和读的操作了。

但Docker的镜像却不是这样,它在bootfs自检完毕之后并不会把rootfs的read-only改为read-write。而是利用union mount(UnionFS的一种挂载机制)将一个或多个read-only的rootfs加载到之前的read-only的rootfs层之上。在加载了这么多层的rootfs之后,仍然让它看起来只像是一个文件系统,在Docker的体系里把union mount的这些read-only的rootfs叫做Docker的镜像。但是,此时的每一层rootfs都是read-only的,此时还不能对其进行操作。当创建一个容器,也就是将Docker镜像进行实例化,系统会在一层或是多层read-only的rootfs之上分配一层空的read-write的rootfs。

图5.3Docker 镜像结构

为了形象化Docker的镜像结构,docker pull一个ubuntu:14.04的镜像,使用docker images -tree查看结果如下:

[root@qingze qingze]#docker images -tree

Warning: '-tree' isdeprecated, it will be removed soon. See usage.

└─511136ea3c5a VirtualSize: 0 B

  └─3b363fd9d7da Virtual Size: 192.5 MB

   └─607c5d1cca71 Virtual Size: 192.7 MB

     └─f62feddc05dc Virtual Size: 192.7 MB

       └─8eaa4ff06b53 Virtual Size: 192.7 MBTags: ubuntu:14.04,

因为如果高Docker版本,docker image不再使用,可以使用“docker history 镜像ID”来查看。

可以看到Ubuntu的镜像中有多个长ID的layer,且以一种树状结构继承下来,如下图。其中,第n+1层继承了第n层,并在此基础上有了自己的内容,直观上的表现就是第n+1层占用磁盘空间增大。并且,不同的镜像可能会有相同的父镜像。例如,图中Tomcat和Nginx 继承于同一个Vim 镜像,这种组织方式起到共享的作用,节约了镜像在物理机上占用的空间。

图5.4镜像继承关系

使用docker save命令保存镜像tar,查询其结构。

[root@qingze qingze]# docker save -o ubuntu.tar ubuntu:14.04

[root@qingze qingze]#  tar -tf ubuntu.tar

3b363fd9d7dab4db9591058a3f43e806f6fa6f7e2744b63b2df4b84eadb0685a/

3b363fd9d7dab4db9591058a3f43e806f6fa6f7e2744b63b2df4b84eadb0685a/VERSION

3b363fd9d7dab4db9591058a3f43e806f6fa6f7e2744b63b2df4b84eadb0685a/json

3b363fd9d7dab4db9591058a3f43e806f6fa6f7e2744b63b2df4b84eadb0685a/layer.tar

511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158/

511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158/VERSION

511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158/json

511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158/layer.tar

607c5d1cca71dd3b6c04327c3903363079b72ab3e5e4289d74fb00a9ac7ec2aa/

607c5d1cca71dd3b6c04327c3903363079b72ab3e5e4289d74fb00a9ac7ec2aa/VERSION

607c5d1cca71dd3b6c04327c3903363079b72ab3e5e4289d74fb00a9ac7ec2aa/json

607c5d1cca71dd3b6c04327c3903363079b72ab3e5e4289d74fb00a9ac7ec2aa/layer.tar

8eaa4ff06b53ff7730c4d7a7e21b4426a4b46dee064ca2d5d90d757dc7ea040a/

8eaa4ff06b53ff7730c4d7a7e21b4426a4b46dee064ca2d5d90d757dc7ea040a/VERSION

8eaa4ff06b53ff7730c4d7a7e21b4426a4b46dee064ca2d5d90d757dc7ea040a/json

8eaa4ff06b53ff7730c4d7a7e21b4426a4b46dee064ca2d5d90d757dc7ea040a/layer.tar

f62feddc05dc67da9b725361f97d7ae72a32e355ce1585f9a60d090289120f73/

f62feddc05dc67da9b725361f97d7ae72a32e355ce1585f9a60d090289120f73/VERSION

f62feddc05dc67da9b725361f97d7ae72a32e355ce1585f9a60d090289120f73/json

f62feddc05dc67da9b725361f97d7ae72a32e355ce1585f9a60d090289120f73/layer.tar

repositories

每一个layer都有一个文件夹存在,且每个文件夹中包含VERSION,json,layer.tar三个文件,另外还有一个repositories文件。长ID为511136ea3c5a的layer没有继承任何layer,因此被称为base镜像,一般来说镜像都是以这个layer开始的,其内容也为空。

json文件包含了镜像以及其对应的容器的配置信息,其中有docker run时指定的,如CpuSharesCpuset,也有系统生成的如parent,正是这个属性记录了继承结构,从而实现了layer的层次关系。每个文件夹下都有一个layer.tar的文件,这个压缩文件存放的正是rootfs的内容,不过是增量存放的而已

5.1.4 容器管理

1)Libcontainer

在2013年Docker刚发布的时候,它是一款基于LXC的开源容器管理引擎。随着Docker的不断发展,它开始有了更为远大的目标,将底层实现都抽象化到Libcontainer的接口。这就意味着,底层容器的实现方式变成了一种可变的方案,无论是使用namespace、cgroups技术或是使用systemd等其他方案,只要实现了Libcontainer定义的一组接口,Docker都可以运行。这也为Docker实现全面的跨平台带来了可能。

Libcontainer功能实现上涵盖了包括namespaces使用、cgroups管理、Rootfs的配置启动、默认的Linux capability权限集、以及进程运行的环境变量配置。本身主要分为三大块工作内容,一是容器的创建及初始化,二是容器生命周期管理,三则是进程管理,调用方为Docker的execdriver。容器的监控主要通过cgroups的状态统计信息,未来会加入进程追踪等更丰富的功能。另一方面,Libcontainer在安全支持方面也为用户尽可能多的提供了支持和选择。

2)runC

Linux基金会于2015年6月成立OCI(Open Container Initiative)组织,旨在围绕容器格式和运行时制定一个开放的工业化标准。OCI的目标是为了避免容器的生态分裂为“小生态王国”,确保一个引擎上构建的容器可以运行在其他引擎之上。这是实现容器可移植性至关重要的部分。该组织一成立便得到了包括谷歌、微软、亚马逊、华为等一系列云计算厂商的支持。

OCI推进容器标准化具体完成:

(1)操作标准化:容器的标准化操作包括使用标准容器感觉创建、启动、停止容器,使用标准文件系统工具复制和创建容器快照,使用标准化网络工具进行下载和上传。

(2)内容无关:内容无关指不管针对的具体容器内容是什么,容器标准操作执行后都能产生同样的效果。如容器可以用同样的方式上传、启动,不管是php应用还是mysql数据库服务。

(3)基础设施无关:无论是个人的笔记本电脑还是AWS S3,亦或是Openstack,或者其他基础设施,都应该对支持容器的各项操作。

(4)为自动化量身定制:制定容器统一标准,是的操作内容无关化、平台无关化的根本目的之一,就是为了可以使容器操作全平台自动化。

(5)工业级交付:制定容器标准一大目标,就是使软件分发可以达到工业级交付成为现实。

runC是Docker贡献一个CLI工具,通过对Libcontainer的封装,按照开放容器格式标准(OCF, Open Container Format)。2016年6月份发布runC1.0版本。

3)containerd

2016年12月Docker 将Docker Engine 中的核心元件Containerd开源,作为一个独立的开源项目独立发展,目标是提供一个更加开放、稳定的容器运行基础设施。Containerd可以说是Docker 引擎的引擎(Runtime)。和包含在DockerEngine里containerd相比,独立的containerd将具有更多的功能,可以涵盖整个容器运行时管理的所有需求。

图5.5镜像、容器、runC、containerd自己的调用关系

Containerd面向运维人员而构建,目的构建工业级规模的Docker系统,在性能上进行了优化,Containerd利用runC来提供高级功能,比如热迁移(checkpoint &restore)seccomp以及用户命名空间支持,这将为Docker引入这些功能敞开大门。containerd的功能和架构如下:

图5.6Containerd架构图

Containerd分三层,最上层为API,包括GRPC API(Go RPC)和Metrics API。

中间层为内部子系统,包括三个:

(1)Distribution: 和DockerRegistry打交道,拉取镜像

(2)Bundle: 管理本地磁盘上面镜像的子系统。

(3)Runtime:创建容器、管理容器的子系统。

最下面为功能组件包括内容、元数据、快照、执行器和监控器。

任务5.2 存储

下面介绍Docker 内部以及容器之间管理数据。在容器中管理数据主要有两种方式。

5.2.1 数据卷

1)数据卷(Data volumes)

数据卷是一个可供一个或多个容器使用的特殊目录,它绕过UFS,可以提供很多有用的特性:

(1)数据卷可以在容器之间共享和重用。

(2)对数据卷的修改会立马生效。

(3)对数据卷的更新,不会影响镜像

(4)数据卷默认会一直存在,即使容器被删除。

数据卷的使用,类似于Linux下对目录或文件进行mount,镜像中的被指定为挂载点的目录中的文件会隐藏掉,能显示看到的是挂载的数据卷。

2)创建一个数据卷

在用 docker run 命令的时候,使用 -v 标记来创建一个数据卷并挂载到容器里。在一次 run 中多次使用可以挂载多个数据卷。下面创建一个名为 web 的容器,并加载一个数据卷到容器的/webapp 目录。

$ sudo docker run -d -P--name web -v /webapp training/webapp python app.py

可以在Dockerfile中使用 VOLUME 来添加一个或者多个新的卷到由该镜像创建的任意容器。

3)删除数据卷

数据卷是被设计用来持久化数据的,它的生命周期独立于容器,Docker不会在容器被删除后自动删除数据卷,并且也不存在垃圾回收这样的机制来处理没有任何容器引用的数据卷。如果需要在删除容器的同时移除数据卷。可以在删除容器的时候使用 docker rm -v 这个命令。

4)挂载一个主机目录作为数据卷

使用-v标记也可以指定挂载一个本地主机的目录到容器中去。

$ sudo docker run -d -P--name web -v /src/webapp:/opt/webapp training/webapp python app.py

上面的命令加载主机的/src/webapp目录到容器的/opt/webapp 目录。这个功能在进行测试的时候十分方便,比如用户可以放置一些程序到本地目录中,来查看容器是否正常工作。本地目录的路径必须是绝对路径,如果目录不存在Docker 会自动为你创建它。

Docker挂载数据卷的默认权限是读写,用户也可以通过:ro指定为只读。

$ sudo docker run -d -P--name web -v /src/webapp:/opt/webapp:ro

training/webapp pythonapp.py

5)查看数据卷的具体信息

在主机里使用以下命令可以查看指定容器的信息,在输出的内容中找到其中和数据卷相关的部分,可以看到所有的数据卷都是创建在主机的/var/lib/docker/volumes/下面的

$ docker inspect web

...

"Volumes": {

    "/webapp":"/var/lib/docker/volumes/fac362...80535"

},

"VolumesRW":{

    "/webapp": true

}

...

6)挂载一个本地主机文件作为数据卷

-v标记也可以从主机挂载单个文件到容器中

$ sudo docker run --rm-it -v ~/.bash_history:/.bash_history ubuntu /bin/bash

这样就可以记录在容器输入过的命令了。如果直接挂载一个文件,很多文件编辑工具,包括 vi 或者 sed --in-place,可能会造成文件 inode 的改变,所以最简单的办法就直接挂载文件的父目录。

5.2.2 数据卷容器

数据卷容器(Datavolume containers)涉及容器间共享的持久化、序列化的数据持久性的数据,最好创建数据卷容器。数据卷容器,其实就是一个正常的容器,专门用来提供数据卷供其它容器挂载的。

1)创建数据卷容器。

(1)首先,创建一个名为 dbdata 的数据卷容器:

$ sudo docker run -d -v/dbdata --name dbdata training/postgres echo Data-only container for postgres

2)然后,在其他容器中使用--volumes-from 来挂载 dbdata 容器中的数据卷。

$ sudo docker run -d--volumes-from dbdata --name db1 training/postgres

$ sudo docker run -d--volumes-from dbdata --name db2 training/postgres

3)可以使用超过一个的 --volumes-from 参数来指定从多个容器挂载不同的数据卷。也可以从其他已经挂载了数据卷的容器来级联挂载数据卷。

$ sudo docker run -d--name db3 --volumes-from db1 training/postgres

注意:使用--volumes-from 参数所挂载数据卷的容器自己并不需要保持在运行状态。

如果删除了挂载的容器(包括dbdata、db1 和 db2),数据卷并不会被自动删除。如果要删除一个数据卷,必须在删除最后一个还挂载着它的容器时使用 docker rm -v 命令来指定同时删除关联的容器。这可以让用户在容器之间升级和移动数据卷。

2)利用数据卷容器来备份、恢复、迁移数据卷。

可以利用数据卷对其中的数据进行进行备份、恢复和迁移。

1)备份:首先使用--volumes-from 标记来创建一个加载 dbdata 容器卷的容器,并从主机挂载当前目录到容器的 /backup 目录。命令如下:

$ sudo docker run--volumes-from dbdata -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /dbdata

容器启动后,使用了tar 命令来将dbdata 卷备份为容器中/backup/backup.tar 文件,也就是主机当前目录下的名为 backup.tar 的文件。

(2)恢复:如果要恢复数据到一个容器,首先创建一个带有空数据卷的容器dbdata2

$ sudo docker run -v/dbdata --name dbdata2 ubuntu /bin/bash

然后创建另一个容器,挂载 dbdata2 容器卷中的数据卷,并使用untar解压备份文件到挂载的容器卷中。

$ sudo docker run--volumes-from dbdata2 -v $(pwd):/backup busybox tar xvf

/backup/backup.tar

为了查看/验证恢复的数据,可以再启动一个容器挂载同样的容器卷来查看

$ sudo docker run--volumes-from dbdata2 busybox /bin/ls /dbdata

任务5.3 网络

5.3.1 Docker网络功能介绍

1)容器网络

Docker 的网络实现其实就是利用了 Linux 上的网络命名空间、网桥和虚拟网络设备(VETH)等实现。默认Docker安装完成后会创建一个网桥docker0。Docker中的网络接口默认都是虚拟的网络接口。Docker 容器网络在本地主机和容器内分别创建一个虚拟接口,并让它们彼此连通。

Docker 创建一个容器执行如下操作:

(1)创建一对虚拟接口,分别放到本地主机和新容器中;

(2)本地主机一端桥接到默认的docker0或指定网桥上,并具有一个唯一的名字,如veth65f9;

(3)容器一端放到新容器中,并修改名字作为eth0,这个接口只在容器的命名空间可见;

(4)从网桥可用地址段中获取一个空闲地址IP分配给容器的 eth0,并配置默认路由到桥接网卡veth65f9。

(5)完成这些之后,容器就可以使用eth0虚拟网卡来连接其他容器和其他网络。

另外,可以在dockerrun的时候通过--net参数来指定容器的网络配置,有4个可选值:

--net=bridge 这个是默认值,连接到默认的网桥。

--net=host 告诉 Docker 不要将容器网络放到隔离的命名空间中,使用本地主机的网络,它拥有完全的本地主机接口访问权限。容器进程可以跟主机其它root进程一样可以打开低范围的端口机。因此使用这个选项的时候要非常小心。如果进一步的使用 --privileged=true,容器会被允许直接配置主机的网络堆栈。

--net=container:NAME_or_ID让 Docker 将新建容器的进程放到一个已存在容器的网络栈中,新容器进程有自己的文件系统、进程列表和资源限制,但会和已存在的容器共享 IP 地址和端口等网络资源,两者进程可以直接通过 lo 环回接口通信。

--net=none 让 Docker 将新容器放到隔离的网络栈中,但是不进行网络配置。之后,用户可以自己进行配置。

当容器结束后,Docker 会清空容器,容器内的 eth0 会随网络命名空间一起被清除,A 接口也被自动从 docker0 卸载。


图5.7 Docker 容器网络示意图

2)网络配置细节

用户使用--net=none 后,可以自行配置网络,让容器达到跟平常一样具有访问网络的权限。通过这个过程,可以了解 Docker 配置网络的细节。

(1)首先,启动一个 /bin/bash 容器,指定--net=none 参数。

$ sudo docker run -i -t--rm --net=none base /bin/bash

root@63f36fc01b5f:/#

(2)在本地主机查找容器的进程 id,并为它创建网络命名空间。

$ sudo docker inspect-f '{{.State.Pid}}' 63f36fc01b5f

2778

$ pid=2778

$ sudo mkdir -p/var/run/netns

$ sudo ln -s/proc/$pid/ns/net /var/run/netns/$pid

(3)检查桥接网卡的 IP 和子网掩码信息。

$ ip addr show docker0

21: docker0: ...

inet 172.17.42.1/16scope global docker0

...

(4)创建一对 “veth pair” 接口 A 和 B,绑定 A 到网桥 docker0,并启用它

$ sudo ip link add Atype veth peer name B

$ sudo brctl addifdocker0 A

$ sudo ip link set A up

(5)将B放到容器的网络命名空间,命名为 eth0,启动它并配置一个可用 IP(桥接网段)和默认网关。

$ sudo ip link set Bnetns $pid

$ sudo ip netns exec$pid ip link set dev B name eth0

$ sudo ip netns exec$pid ip link set eth0 up

$ sudo ip netns exec$pid ip addr add 172.17.42.99/16 dev eth0

$ sudo ip netns exec$pid ip route add default via 172.17.42.1

以上,就是 Docker 配置网络的具体过程。

3外部访问容器,端口映射

容器中可以运行一些网络应用,要让外部也可以访问这些应用,可以通过 -P 或 -p 参数来指定端口映射。

(1)查看端口信息

当使用 -P 标记时,Docker 会随机映射一个 49000~49900 的端口到内部容器开放的网络端口。

使用 docker ps 可以看到,本地主机的 49155 被映射到了容器的 5000 端口。此时访问本机的 49155 端口即可访问容器内 web 应用提供的界面。

$ sudo docker run -d -Ptraining/webapp python app.py

$ sudo docker ps -l

CONTAINER ID  IMAGE                   COMMAND       CREATED        STATUS        PORTS                    NAMES

bc533791f3f5  training/webapp:latest  python app.py 5 seconds ago  Up 2 seconds 0.0.0.0:49155->5000/tcp nostalgic_morse

同样的,可以通过 docker logs 命令来查看应用的信息。

$ sudo docker logs -fnostalgic_morse

* Running onhttp://0.0.0.0:5000/

10.0.2.2 - -[23/May/2014 20:16:31] "GET / HTTP/1.1" 200 -

10.0.2.2 - -[23/May/2014 20:16:31] "GET /favicon.ico HTTP/1.1" 404 -

-p(小写的)则可以指定要映射的端口,并且,在一个指定端口上只可以绑定一个容器。支持的格式有

ip:hostPort:containerPort| ip::containerPort | hostPort:containerPort。

(2)映射所有接口地址

使用 hostPort:containerPort 格式本地的 5000 端口映射到容器的 5000 端口,可以执行

$ sudo docker run -d -p5000:5000 training/webapp python app.py

此时默认会绑定本地所有接口上的所有地址。

(3)映射到指定地址的指定端口

可以使用 ip:hostPort:containerPort 格式指定映射使用一个特定地址,比如 localhost 地址 127.0.0.1

$ sudo docker run -d -p127.0.0.1:5000:5000 training/webapp python app.py

(4)映射到指定地址的任意端口

使用 ip::containerPort 绑定localhost 的任意端口到容器的 5000 端口,本地主机会自动分配一个端口。

$ sudo docker run -d -p127.0.0.1::5000 training/webapp python app.py

还可以使用 udp 标记来指定 udp 端口

$ sudo docker run -d -p127.0.0.1:5000:5000/udp training/webapp python app.py

(5)查看映射端口配置

使用 docker port来查看当前映射的端口配置,也可以查看到绑定的地址

$ docker portnostalgic_morse 5000

127.0.0.1:49155.

容器有自己的内部网络和 ip 地址,-p 标记可以多次使用来绑定多个端口,例如:

$ sudo docker run -d -p5000:5000  -p 3000:80 training/webapppython app.py

5.3.2 容器互联

容器的连接(linking)系统是除了端口映射外,另一种跟容器中应用交互的方式。该系统会在源和接收容器之间创建一个隧道,接收容器可以看到源容器指定的信息。

1)自定义容器命名

连接系统依据容器的名称来执行。因此,首先需要自定义一个好记的容器命名。使用 --name 标记可以为容器自定义命名。

$ sudo docker run -d -P--name web training/webapp python app.py

使用 docker ps 来验证设定的命名。

$ sudo docker ps -l

CONTAINER ID  IMAGE                  COMMAND        CREATED       STATUS       PORTS                    NAMES

aed84ee21bde  training/webapp:latest python app.py  12 hours ago Up 2 seconds 0.0.0.0:49154->5000/tcp web

也可以使用 docker inspect 来查看容器的名字。

$ sudo docker inspect-f "{{ .Name }}" aed84ee21bde

/web

注意:容器的名称是唯一的。如果已经命名了一个叫 web 的容器,当你要再次使用 web 这个名称的时候,需要先用docker rm 来删除之前创建的同名容器。

在执行 docker run 的时候如果添加 --rm 标记,则容器在终止后会立刻删除。注意,--rm 和 -d 参数不能同时使用。

2 )容器互联

使用 --link 参数可以让容器之间安全的进行交互。

下面先创建一个新的数据库容器。

$ sudo docker run -d--name db training/postgres

删除之前创建的 web 容器

$ docker rm -f web

然后创建一个新的 web 容器,并将它连接到 db 容器

$ sudo docker run -d -P--name web --link db:db training/webapp python app.py

此时,db 容器和 web 容器建立互联关系。

--link 参数的格式为 --link name:alias,其中 name 是要链接的容器的名称,alias是这个连接的别名。

使用 docker ps 来查看容器的连接

$ docker ps

CONTAINER ID  IMAGE                     COMMAND               CREATED             STATUS             PORTS                    NAMES

349169744e49  training/postgres:latest  su postgres -c '/usr  About a minute ago  Up About a minute  5432/tcp                 db, web/db

aed84ee21bde  training/webapp:latest    python app.py         16 hours ago        Up 2 minutes       0.0.0.0:49154->5000/tcp  web

可以看到自定义命名的容器db和 web,db 容器的 names 列有 db 也有 web/db。这表示 web 容器链接到 db 容器,web 容器将被允许访问 db 容器的信息。

Docker 在两个互联的容器之间创建了一个安全隧道,而且不用映射它们的端口到宿主主机上。在启动 db 容器的时候并没有使用 -p-P标记,从而避免了暴露数据库端口到外部网络上。

Docker 通过 2 种方式为容器公开连接信息:

(1).   环境变量

使用 env 命令来查看 web 容器的环境变量

$ sudo docker run --rm--name web2 --link db:db training/webapp env

. . .

DB_NAME=/web2/db

DB_PORT=tcp://172.17.0.5:5432

DB_PORT_5000_TCP=tcp://172.17.0.5:5432

DB_PORT_5000_TCP_PROTO=tcp

DB_PORT_5000_TCP_PORT=5432

DB_PORT_5000_TCP_ADDR=172.17.0.5

. . .

其中 DB_ 开头的环境变量是供 web 容器连接 db 容器使用,前缀采用大写的连接别名。

(2).   除了环境变量,Docker 还添加 host 信息到父容器的 /etc/hosts 的文件。下面是父容器 web 的 hosts 文件

这里有 2 个 hosts,第一个是 web 容器,web 容器用 id 作为他的主机名,第二个是 db 容器的 ip 和主机名。可以在 web 容器中安装 ping 命令来测试跟db容器的连通。

$ sudo docker run -t -i--rm --link db:db training/webapp /bin/bash

root@aed84ee21bde:/opt/webapp#cat /etc/hosts

172.17.0.7  aed84ee21bde

. . .

172.17.0.5  db

root@aed84ee21bde:/opt/webapp#apt-get install -yqq inetutils-ping

root@aed84ee21bde:/opt/webapp#ping db

PING db (172.17.0.5): 48 data bytes

56 bytes from172.17.0.5: icmp_seq=0 ttl=64 time=0.267 ms

56 bytes from172.17.0.5: icmp_seq=1 ttl=64 time=0.250 ms

56 bytes from172.17.0.5: icmp_seq=2 ttl=64 time=0.256 ms

用 ping 来测试db容器,它会解析成 172.17.0.5。注意:官方的 ubuntu镜像默认没有安装 ping,需要自行安装。

用户可以链接多个父容器到子容器,比如可以链接多个 web 到 db 容器上。

5.3.3 高级网络配置

本章将介绍 Docker 的一些高级网络配置和选项。

当 Docker 启动时,会自动在主机上创建一个docker0 虚拟网桥,实际上是 Linux 的一个 bridge,可以理解为一个软件交换机。它会在挂载到它的网口之间进行转发。

同时,Docker 随机分配一个本地未占用的私有网段(在 RFC1918 中定义)中的一个地址给 docker0 接口。比如典型的 172.17.42.1,掩码为 255.255.0.0。此后启动的容器内的网口也会自动分配一个同一网段(172.17.0.0/16)的地址。

当创建一个 Docker 容器的时候,同时会创建了一对 veth pair 接口(当数据包发送到一个接口时,另外一个接口也可以收到相同的数据包)。这对接口一端在容器内,即eth0;另一端在本地并被挂载到docker0 网桥,名称以 veth 开头(例如vethAQI2QT)。通过这种方式,主机可以跟容器通信,容器之间也可以相互通信。Docker 就创建了在主机和所有容器之间一个虚拟共享网络。

图5.8 Docker 网络

接下来的部分将介绍在一些场景中,Docker 所有的网络定制配置。以及通过Linux 命令来调整、补充、甚至替换 Docker 默认的网络配置。

1)快速配置指南

下面是一个跟 Docker 网络相关的命令列表。

其中有些命令选项只有在Docker 服务启动的时候才能配置,而且不能马上生效。

-b BRIDGE or--bridge=BRIDGE --指定容器挂载的网桥

--bip=CIDR --定制 docker0 的掩码

-H SOCKET... or--host=SOCKET... --Docker 服务端接收命令的通道

--icc=true|false --是否支持容器之间进行通信

--ip-forward=true|false --请看下文容器之间的通信

--iptables=true|false --是否允许 Docker 添加 iptables 规则

--mtu=BYTES --容器网络中的 MTU

下面2个命令选项既可以在启动服务时指定,也可以 Docker 容器启动(dockerrun)时候指定。在 Docker 服务启动的时候指定则会成为默认值,后面执行 docker run 时可以覆盖设置的默认值。

--dns=IP_ADDRESS... --使用指定的DNS服务器

--dns-search=DOMAIN... --指定DNS搜索域

最后这些选项只有在 docker run 执行时使用,因为它是针对容器的特性内容。

-h HOSTNAME or--hostname=HOSTNAME --配置容器主机名

--link=CONTAINER_NAME:ALIAS--添加到另一个容器的连接

--net=bridge|none|container:NAME_or_ID|host--配置容器的桥接模式

-p SPEC or--publish=SPEC --映射容器端口到宿主主机

-P or--publish-all=true|false --映射容器所有端口到宿主主机

2)配置 DNS

Docker 没有为每个容器专门定制镜像,那么怎么自定义配置容器的主机名和 DNS 配置呢?秘诀就是它利用虚拟文件来挂载到来容器的3个相关配置文件。

在容器中使用 mount 命令可以看到挂载信息:

$ mount

...

/dev/disk/by-uuid/1fec...ebdfon /etc/hostname type ext4 ...

/dev/disk/by-uuid/1fec...ebdfon /etc/hosts type ext4 ...

tmpfs on/etc/resolv.conf type tmpfs ...

...

这种机制可以让宿主主机DNS 信息发生更新后,所有 Docker 容器的 dns 配置通过 /etc/resolv.conf 文件立刻得到更新。

如果用户想要手动指定容器的配置,可以利用下面的选项。

-h HOSTNAME or--hostname=HOSTNAME 设定容器的主机名,它会被写到容器内的 /etc/hostname 和 /etc/hosts。但它在容器外部看不到,既不会在 docker ps 中显示,也不会在其他的容器的 /etc/hosts 看到。

--link=CONTAINER_NAME:ALIAS 选项会在创建容器的时候,添加一个其他容器的主机名到 /etc/hosts 文件中,让新容器的进程可以使用主机名ALIAS 就可以连接它。

--dns=IP_ADDRESS 添加DNS 服务器到容器的 /etc/resolv.conf 中,让容器用这个服务器来解析所有不在 /etc/hosts 中的主机名。

--dns-search=DOMAIN 设定容器的搜索域,当设定搜索域为 .example.com 时,在搜索一个名为 host 的主机时,DNS 不仅搜索host,还会搜索 host.example.com。 注意:如果没有上述最后 2 个选项,Docker 会默认用主机上的 /etc/resolv.conf 来配置容器。

3)容器访问控制

容器的访问控制,主要通过Linux 上的 iptables 防火墙来进行管理和实现。iptables 是 Linux 上默认的防火墙软件,在大部分发行版中都自带。

(1).   容器访问外部网络

容器要想访问外部网络,需要本地系统的转发支持。在Linux 系统中,检查转发是否打开。

$sysctlnet.ipv4.ip_forward

net.ipv4.ip_forward = 1

如果为 0,说明没有开启转发,则需要手动打开。

$sysctl -wnet.ipv4.ip_forward=1

如果在启动 Docker 服务的时候设定 --ip-forward=true,Docker 就会自动设定系统的 ip_forward 参数为 1。

(2).   容器之间访问

容器之间相互访问,需要两方面的支持。

容器的网络拓扑是否已经互联。默认情况下,所有容器都会被连接到 docker0 网桥上。

本地系统的防火墙软件 -- iptables 是否允许通过。

(3).   访问所有端口

当启动 Docker 服务时候,默认会添加一条转发策略到 iptables 的 FORWARD 链上。策略为通过(ACCEPT)还是禁止(DROP)取决于配置--icc=true(缺省值)还是 --icc=false。当然,如果手动指定 --iptables=false 则不会添加 iptables 规则。

可见,默认情况下,不同容器之间是允许网络互通的。如果为了安全考虑,可以在 /etc/default/docker 文件中配置 DOCKER_OPTS=--icc=false来禁止它。

(4).   访问指定端口

在通过 -icc=false 关闭网络访问后,还可以通过--link=CONTAINER_NAME:ALIAS 选项来访问容器的开放端口。

例如,在启动 Docker 服务时,可以同时使用 icc=false --iptables=true 参数来关闭允许相互的网络访问,并让 Docker 可以修改系统中的 iptables 规则。

此时,系统中的 iptables 规则可能是类似

$ sudo iptables -nL

...

Chain FORWARD (policy ACCEPT)

target     prot opt source               destination

DROP       all --  0.0.0.0/0            0.0.0.0/0

...

之后,启动容器(dockerrun)时使用 --link=CONTAINER_NAME:ALIAS 选项。Docker 会在 iptable 中为两个容器分别添加一条 ACCEPT 规则,允许相互访问开放的端口(取决于 Dockerfile 中的 EXPOSE 行)。

当添加了 --link=CONTAINER_NAME:ALIAS 选项后,添加了 iptables 规则。

$ sudo iptables -nL

...

Chain FORWARD (policy ACCEPT)

target     prot opt source               destination

ACCEPT     tcp --  172.17.0.2           172.17.0.3           tcp spt:80

ACCEPT     tcp --  172.17.0.3           172.17.0.2           tcp dpt:80

DROP       all --  0.0.0.0/0            0.0.0.0/0

注意:--link=CONTAINER_NAME:ALIAS 中的 CONTAINER_NAME 目前必须是 Docker 分配的名字,或使用 --name 参数指定的名字。主机名则不会被识别。

4)映射容器端口到宿主主机的实现

默认情况下,容器可以主动访问到外部网络的连接,但是外部网络无法访问到容器。

(1).   容器访问外部实现

容器所有到外部网络的连接,源地址都会被NAT成本地系统的IP地址。这是使用 iptables 的源地址伪装操作实现的。

查看主机的 NAT 规则。

$ sudo iptables -t nat-nL

...

Chain POSTROUTING (policy ACCEPT)

target     prot opt source               destination

MASQUERADE  all --  172.17.0.0/16       !172.17.0.0/16

...

其中,上述规则将所有源地址在 172.17.0.0/16 网段,目标地址为其他网段(外部网络)的流量动态伪装为从系统网卡发出。MASQUERADE 跟传统 SNAT 的好处是它能动态从网卡获取地址。

(2).   外部访问容器实现

容器允许外部访问,可以在 docker run 时候通过 -p 或 -P 参数来启用。

不管用那种办法,其实也是在本地的 iptable 的 nat 表中添加相应的规则。

使用 -P 时:

$ iptables -t nat -nL

...

Chain DOCKER (2 references)

target     prot opt source               destination

DNAT       tcp --  0.0.0.0/0            0.0.0.0/0            tcp dpt:49153 to:172.17.0.2:80

使用 -p 80:80 时:

$ iptables -t nat -nL

Chain DOCKER (2 references)

target     prot opt source               destination

DNAT       tcp --  0.0.0.0/0            0.0.0.0/0            tcp dpt:80 to:172.17.0.2:80

注意:

这里的规则映射了0.0.0.0,意味着将接受主机来自所有接口的流量。用户可以通过 -p IP:host_port:container_port 或 -p IP::port 来指定允许访问容器的主机上的IP、接口等,以制定更严格的规则。

配置文件 /etc/default/docker 中指定 DOCKER_OPTS="--ip=IP_ADDRESS",之后重启 Docker 服务即可生效。

5)配置 docker0 网桥

Docker 服务默认会创建一个 docker0 网桥(其上有一个 docker0 内部接口),它在内核层连通了其他的物理或虚拟网卡,这就将所有容器和本地主机都放到同一个物理网络。

Docker 默认指定了 docker0 接口的 IP 地址和子网掩码,让主机和容器之间可以通过网桥相互通信,它还给出了 MTU(接口允许接收的最大传输单元),通常是 1500 Bytes,或宿主主机网络路由上支持的默认值。这些值都可以在服务启动的时候进行配置。

--bip=CIDR -- IP 地址加掩码格式,例如 192.168.1.5/24

--mtu=BYTES -- 覆盖默认的 Docker mtu 配置

也可以在配置文件中配置DOCKER_OPTS,然后重启服务。 由于目前 Docker 网桥是 Linux 网桥,用户可以使用 brctl show 来查看网桥和端口连接信息。

$ sudo brctl show

bridge name     bridge id               STP enabled     interfaces

docker0         8000.3a1d7362b4ee       no              veth65f9

                                             vethdda6

*注:brctl 命令在 Debian、Ubuntu 中可以使用 sudo apt-get install bridge-utils 来安装。

每次创建一个新容器的时候,Docker从可用的地址段中选择一个空闲的 IP 地址分配给容器的 eth0 端口。使用本地主机上 docker0 接口的 IP 作为所有容器的默认网关。

$ sudo docker run -i -t--rm base /bin/bash

$ ip addr show eth0

24: eth0:<BROADCAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group defaultqlen 1000

    link/ether 32:6f:e0:35:57:91 brdff:ff:ff:ff:ff:ff

    inet 172.17.0.3/16 scope global eth0

       valid_lft forever preferred_lft forever

    inet6 fe80::306f:e0ff:fe35:5791/64 scopelink

       valid_lft forever preferred_lft forever

$ ip route

default via 172.17.42.1dev eth0

172.17.0.0/16 deveth0  proto kernel  scope link src 172.17.0.3

$ exit

6)自定义网桥

除了默认的 docker0 网桥,用户也可以指定网桥来连接各个容器。

在启动 Docker 服务的时候,使用 -b BRIDGE或--bridge=BRIDGE 来指定使用的网桥。

如果服务已经运行,那需要先停止服务,并删除旧的网桥。

$ sudo service dockerstop

$ sudo ip link set devdocker0 down

$ sudo brctl delbrdocker0

然后创建一个网桥 bridge0。

$ sudo brctl addbrbridge0

$ sudo ip addr add192.168.5.1/24 dev bridge0

$ sudo ip link set devbridge0 up

查看确认网桥创建并启动。

$ ip addr show bridge0

4: bridge0:<BROADCAST,MULTICAST> mtu 1500 qdisc noop state UP group default

    link/ether 66:38:d0:0d:76:18 brdff:ff:ff:ff:ff:ff

    inet 192.168.5.1/24 scope global bridge0

      valid_lft forever preferred_lft forever

配置 Docker 服务,默认桥接到创建的网桥上。

$ echo'DOCKER_OPTS="-b=bridge0"' >> /etc/default/docker

$ sudo service dockerstart

启动 Docker 服务。新建一个容器,可以看到它已经桥接到了 bridge0 上。

可以继续用 brctlshow 命令查看桥接的信息。另外,在容器中可以使用 ip addr 和 ip route 命令来查看 IP 地址配置和路由信息。

7)工具和示例

在介绍自定义网络拓扑之前,你可能会对一些外部工具和例子感兴趣:

pipework

Jérôme Petazzoni 编写了一个叫 pipework 的 shell 脚本,可以帮助用户在比较复杂的场景中完成容器的连接。

playground

Brandon Rhodes 创建了一个提供完整的 Docker 容器网络拓扑管理的 Python库,包括路由、NAT 防火墙;以及一些提供 HTTP, SMTP, POP, IMAP,Telnet, SSH, FTP 的服务器。

8)编辑网络配置文件

Docker 1.2.0 开始支持在运行中的容器里编辑 /etc/hosts, /etc/hostname 和 /etc/resolve.conf 文件。

但是这些修改是临时的,只在运行的容器中保留,容器终止或重启后并不会被保存下来。也不会被 docker commit 提交。

9)创建一个点到点连接

默认情况下,Docker 会将所有容器连接到由 docker0 提供的虚拟子网中。

用户有时候需要两个容器之间可以直连通信,而不用通过主机网桥进行桥接。

解决办法很简单:创建一对 peer 接口,分别放到两个容器中,配置成点到点链路类型即可。

首先启动 2 个容器:

$ sudo docker run -i -t--rm --net=none base /bin/bash

root@1f1f4c1f931a:/#

$ sudo docker run -i -t--rm --net=none base /bin/bash

root@12e343489d2f:/#

找到进程号,然后创建网络命名空间的跟踪文件。

$ sudo docker inspect-f '{{.State.Pid}}' 1f1f4c1f931a

2989

$ sudo docker inspect-f '{{.State.Pid}}' 12e343489d2f

3004

$ sudo mkdir -p/var/run/netns

$ sudo ln -s/proc/2989/ns/net /var/run/netns/2989

$ sudo ln -s/proc/3004/ns/net /var/run/netns/3004

创建一对 peer 接口,然后配置路由

$ sudo ip link add Atype veth peer name B

$ sudo ip link set Anetns 2989

$ sudo ip netns exec2989 ip addr add 10.1.1.1/32 dev A

$ sudo ip netns exec2989 ip link set A up

$ sudo ip netns exec2989 ip route add 10.1.1.2/32 dev A

$ sudo ip link set Bnetns 3004

$ sudo ip netns exec3004 ip addr add 10.1.1.2/32 dev B

$ sudo ip netns exec3004 ip link set B up

$ sudo ip netns exec3004 ip route add 10.1.1.1/32 dev B

现在这 2 个容器就可以相互 ping 通,并成功建立连接。点到点链路不需要子网和子网掩码。

此外,也可以不指定 --net=none 来创建点到点链路。这样容器还可以通过原先的网络来通信。

利用类似的办法,可以创建一个只跟主机通信的容器。但是一般情况下,更推荐使用 --icc=false 来关闭容器之间的通信。

任务5.4 Docker API

5.4.1 DockerAPI介绍

API 的主要类型:RESTful,基于 HTTP 协议,也能基于 RPC 协议。

所谓 RESTful, 是指通过如HTTP协议封装的各种请求,主要包括 GET、POST、UPDATE、DELETE、PUT等,保存创建,修改等操作。一般 RESTful 服务器提供这些请求的接口(地址,路径,参数),然后客户端可以通过过类似 Linux 命令 curl,Python 标准库 httplib、httplib2、urllib 等访问,需要注意的是,多数参数(数据)都是以 JSON/XML 打包。并且多数按照token 方式验证请求权限和安全性。 当然也支持其他类型的验证。

Docker生态系统中一共有三种api

Registry:提供了与来存储docker镜像的docker registry 集成的功能。

Docker hub api:提供了与docker hub 集成的功能。

Docker remote api:提供与docker守护进程进行集成的功能。

所有这3种api都是restful风格的。在本章中,将会着重对remote api进行介绍,因为它是通过程序与docker进行集成和交互的核心内容。

5.4.2 初识Remote API

让浏览一下dockerremote api,并看看它都提供了哪些功能。首先要牢记的是。Remote api 是由docker守护进程提供的。在默认情况下,docker守护进程会绑定到一个所在宿州机的套接字,即unix:///var/run/docker.sock. docker守护进程需要以root权限来运,以便它有足够的权限去管理所需要的资源。也正如在第2章所阐述的哪样,如果系统中存在一个名为docker的用户组,docker会将上面所说的套接字的所有者设为该用户组。因此任何属于docker用户组的用户都可以运行docker而无需root权限。

如果只查询在同一台宿主机上运行docker的Remote api,那么上面的机制看起来没什么问题,但是如果想远程访问Remote api, 就需要将docker守护进程绑定到一个网络接口上去。只需要给docker守护进程传递一个-H标志即可做到这一点。

如果用户可以在本地使用docker api,那么就可以使用nc命令来进行查询,如图所示:

在大多数操作系统上,可以通过编辑守护进程的启动配置文件将docker守护进程绑定到指定网络接口。需要编辑/usr/lib/system/system/docker.service文件。

让来看看如何在一个运行systemd的red hat 衍生版上将docker守护进程绑定到一个网络接口上。将编辑/usr/lib/system/system/docker.service文件,将代码清单所示的内容修改为以下内容。

ExecStart=/usr/bin/docker-current-d --selinu-enabled -H tcp://0.0.0.0:2375

这将把docker守护进程绑定到该宿主机的所有网络接口的2375端口上。之后需要使用systemctl命令来重新加载并启动该守护进程,如代码清单所示。

[root@docker ~]# sudosystemctl --system daemon-reload

现在可以通过docker客户端命令的-H标志来测试一下刚才的配置是否生效。让从一台远程主机来访问docker守护进程,如代码清单所示。

[root@docker ~]# dockerdaemon -H unix:///var/run/docker.sock -H 0.0.0.0:2375

WARN[0000] /!\ DON'TBIND ON ANY IP ADDRESS WITHOUT setting -tlsverify IF YOU DON'T KNOW WHAT YOU'REDOING /!\

WARN[0000] devmapper:Usage of loopback devices is strongly discouraged for production use. Pleaseuse `--storage-opt dm.thinpooldev` or use `man docker` to refer todm.thinpooldev section.

WARN[0000] devmapper:Base device already exists and has filesystem xfs on it. User specifiedfilesystem  will be ignored.

INFO[0000][graphdriver] using prior storage driver "devicemapper"

INFO[0000] Graphmigration to content-addressability took 0.00 seconds

INFO[0000] Firewalldrunning: false                    

INFO[0000] Defaultbridge (docker0)isassigned with an IP address 172.17.0.0/16. Daemon option --bip can be used toset a preferred IP address

INFO[0000] Loadingcontainers: start.                  

.

INFO[0000] Loadingcontainers: done.                   

INFO[0000] Daemon hascompleted initialization         

INFO[0000] Dockerdaemon                                 commit=3999ccb-unsupportedexecdriver=native-0.2 graphdriver=devicemapper version=1.10.3

INFO[0001] API listenon [::]:2375                     

INFO[0001] API listenon /var/run/docker.sock  

[root@docker ~]# sudodocker -H 127.0.0.1:2375 info

Containers: 1

 Running: 1

 Paused: 0

 Stopped: 0

Images: 22

Server Version: 1.10.3

Storage Driver:devicemapper

 Pool Name: docker-253:1-285241608-pool

 Pool Blocksize: 65.54 kB

 Base Device Size: 10.74 GB

 Backing Filesystem: xfs

 Data file: /dev/loop0

 Metadata file: /dev/loop1

 Data Space Used: 5.105 GB

 Data Space Total: 107.4 GB

 Data Space Available: 90.75 GB

 Metadata Space Used: 10.23 MB

 Metadata Space Total: 2.147 GB

 Metadata Space Available: 2.137 GB

 Udev Sync Supported: true

 Deferred Removal Enabled: false

 Deferred Deletion Enabled: false

 Deferred Deleted Device Count: 0

 Data loop file:/var/lib/docker/devicemapper/devicemapper/data

 WARNING: Usage of loopback devices is stronglydiscouraged for production use. Either use `--storage-opt dm.thinpooldev` oruse `--storage-opt dm.no_warn_on_loop_devices=true` to suppress this warning.

 Metadata loop file:/var/lib/docker/devicemapper/devicemapper/metadata

 Library Version: 1.02.135-RHEL7 (2016-09-28)

Execution Driver:native-0.2

Logging Driver:json-file

Plugins:

 Volume: local

 Network: host bridge null

Kernel Version:3.10.0-229.el7.x86_64

Operating System:CentOS Linux 7 (Core)

OSType: linux

Architecture: x86_64

Number of Docker Hooks:2

CPUs: 1

Total Memory: 1.954 GiB

Name: docker

ID:HRBL:3YXE:HAAO:MRAG:NKQO:MZWC:7EVD:Q2QK:UYK6:C7NF:GOYU:ELSR

WARNING:bridge-nf-call-iptables is disabled

WARNING:bridge-nf-call-ip6tables is disabled

Registries: docker.io (secure)

这里假定docker提供了更便捷的docker_host环境变量,这样即省掉了每次都需要设置-H标志的麻烦

[root@docker ~]# exportDOCKER_HOST="tcp://127.0.0.1:2375"

5.4.3 测试Docker RemoteAPI

现在已经通过docker程序建立并确认了与docker守护进程之间的网络连通性,接着来直接连接到api。用curl命令查询docker info 命令大致相同的信息。

[root@docker ~]# curlhttp://127.0.0.1:2375/info                  

{"ID":"HRBL:3YXE:HAAO:MRAG:NKQO:MZWC:7EVD:Q2QK:UYK6:C7NF:GOYU:ELSR","Containers":1,"ContainersRunning":1,"ContainersPaused":0,"ContainersStopped":0,"Images":22,"Driver":"devicemapper","DriverStatus":[["PoolName","docker-253:1-285241608-pool"],["Pool Blocksize","65.54kB"],["Base Device Size","10.74 GB"],["BackingFilesystem","xfs"],["Datafile","/dev/loop0"],["Metadatafile","/dev/loop1"],["Data Space Used","5.106GB"],["Data Space Total","107.4 GB"],["Data SpaceAvailable","90.75 GB"],["Metadata Space Used","10.23MB"],["Metadata Space Total","2.147GB"],["Metadata Space Available","2.137GB"],["Udev Sync Supported","true"],["DeferredRemoval Enabled","false"],["Deferred DeletionEnabled","false"],["Deferred Deleted DeviceCount","0"],["Data loop file","/var/lib/docker/devicemapper/devicemapper/data"],["Metadataloopfile","/var/lib/docker/devicemapper/devicemapper/metadata"],["LibraryVersion","1.02.135-RHEL7 (2016-09-28)"]],"SystemStatus":null,"Plugins":{"Volume":["local"],"Network":["host","bridge","null"],"Authorization":null},"MemoryLimit":true,"SwapLimit":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"IPv4Forwarding":true,"BridgeNfIptables":false,"BridgeNfIp6tables":false,"Debug":false,"NFd":21,"OomKillDisable":true,"NGoroutines":28,"SystemTime":"2016-12-30T03:24:27.654171066Z","ExecutionDriver":"native-0.2","LoggingDriver":"json-file","NEventsListener":0,"KernelVersion":"3.10.0-229.el7.x86_64","PkgVersion":"docker-common-1.10.3-59.el7.centos.x86_64","OperatingSystem":"CentOSLinux 7 (Core)","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","IndexServerName":"docker.io","RegistryConfig":{"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"InitSha1":"2195bfa7012bb8ffc50f899b658b487b565336e1","InitPath":"","NCPU":1,"MemTotal":2098581504,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"docker","Labels":null,"ExperimentalBuild":false,"ServerVersion":"1.10.3","ClusterStore":"","ClusterAdvertise":"","Registries":[{"Name":"docker.io","Secure":true}]}

1)通过API来管理docker镜像

让从一些基础的api开始:操作docker镜像的api。将从获取docker守护进程中所有进程中所有镜像的列表开始

[root@docker ~]# curlhttp://127.0.0.1:2375/images/json | python -m json.tool

  % Total   % Received % Xferd  AverageSpeed   Time    Time    Time  Current

                                 Dload  Upload  Total   Spent    Left Speed

100  6781   0  6781    0    0   245k      0 --:--:-- --:--:-- --:--:--  254k

[

    {

        "Created": 1482998541,

        "Id":"sha256:0c27604c726690da5f7368af31765903bbf196b8e5808c876a2abd25b681a17f",

        "Labels": null,

        "ParentId": "",

        "RepoDigests": null,

        "RepoTags": [

            "odoo:latest"

        ],

        "Size": 0,

        "VirtualSize": 0

    },

    {

        "Created": 1482966589,

        "Id":"sha256:411afb6272fbf5258c421ca5dd10bf212cddb40d78ac3962114b67bb78cc7c62",

        "Labels": {},

        "ParentId": "",

        "RepoDigests": [

           "docker.io/python@sha256:05d7f756d1980a83ca7419109334a6df47824e02eae93996c3d201f96baf7cf3"

        ],

        "RepoTags": [

            "docker.io/python:latest"

        ],

        "Size": 687122335,

        "VirtualSize": 687122335

},

这里使用了/images/json这个接入点,它将返回docker守护进程中的所有镜像的列表。

最后,也像命令行一样,也可以在docker hub上查找镜像。

[root@docker ~]# curlhttp://192.168.200.22:2375/images/search?term="jamtur01" | python-mjson.tool  

  % Total   % Received % Xferd  AverageSpeed   Time    Time    Time  Current

                                 Dload  Upload  Total   Spent    Left Speed

100  4387   0  4387    0    0   1023      0 --:--:--  0:00:04 --:--:--  1023

[

    {

        "description": "",

        "index_name":"docker.io",

        "is_automated": true,

        "is_official": false,

        "is_trusted": true,

        "name":"jamtur01/docker-presentation",

        "registry_name":"docker.io",

        "star_count": 5

    },

    {

        "description": "",

        "index_name":"docker.io",

        "is_automated": false,

        "is_official": false,

        "is_trusted": false,

        "name":"jamtur01/dockerjenkins",

        "registry_name":"docker.io",

        "star_count": 3

    },

这只是使用dockerapi能完成的工作的一个例子而已,实际上还能用api进行镜像构建、更新和删除。

2)通过api管理docker容器

Docker remote api 也提供了所有在命令行中能使用的对容器的所有操作可以使用/container接入点列出所有正在的容器。

[root@docker ~]# curl-s "http://127.0.0.1:2375/containers/json" | python -m json.tool     

[

    {

        "Command":"/entrypoint.sh /etc/docker/registry/config.yml",

        "Created": 1483003943,

        "HostConfig": {

            "NetworkMode":"default"

        },

        "Id":"09dda7fe410ba278db18ca1c0aade605083ac6151ce6e005527304f7cc3c5331",

        "Image":"docker.io/registry:latest",

        "ImageID":"sha256:c9bd19d022f6613fa0e3d73b2fe2b2cffe19ed629a426db9a652b597fccf07d4",

        "Labels": {},

        "Names": [

            "/registry"

        ],

        "NetworkSettings": {

            "Networks": {

                "bridge": {

                    "Aliases": null,

                    "EndpointID":"98594476f7c321b1301ea67aadbadbe412a6d44e49e3d6fb5ee152ba12eaab7f",

                    "Gateway":"172.17.0.1",

                   "GlobalIPv6Address": "",

                    "GlobalIPv6PrefixLen":0,

                    "IPAMConfig":null,

                    "IPAddress":"172.17.0.2",

                    "IPPrefixLen":16,

                    "IPv6Gateway":"",

                    "Links": null,

                    "MacAddress":"02:42:ac:11:00:02",

                    "NetworkID":""

                }

            }

        },

        "Ports": [

            {

                "IP":"0.0.0.0",

                "PrivatePort": 5000,

                "PublicPort": 5000,

                "Type":"tcp"

            }

        ],

        "Status": "Up 53minutes"

    }

]

也可以通过使用POST请求来调用/containers/create接入点来创建容器。

[root@docker ~]# curl-X POST -H "Content-Type: application/json"  http://127.0.0.1:2375/containers/create -d'{"image":"docker.io/python"}'

{"Id":"e7f1abb491ebfb1bb71b81ec9cb0eb5f72e7d4ce03bcaa5b491565c663ff1929","Warnings":null}

调用了/containers/create接入点,并POST了一个JSON散列数据,这个结构中包括要启动的镜像名。这个API返回了刚创建的容器的ID,以及可能的警告信息。这条命令将会创建一个容器。

任务5.5.安全

5.5.1 内核名字空间

Docker 容器和 LXC 容器很相似,所提供的安全特性也差不多。当用 docker run 启动一个容器时,在后台 Docker 为容器创建了一个独立的名字空间和控制组集合。

名字空间提供了最基础也是最直接的隔离,在容器中运行的进程不会被运行在主机上的进程和其它容器发现和作用。

每个容器都有自己独有的网络栈,意味着它们不能访问其他容器的 sockets 或接口。不过,如果主机系统上做了相应的设置,容器可以像跟主机交互一样的和其他容器交互。当指定公共端口或使用 links 来连接 2 个容器时,容器就可以相互通信了(可以根据配置来限制通信的策略)。

从网络架构的角度来看,所有的容器通过本地主机的网桥接口相互通信,就像物理机器通过物理交换机通信一样。

那么,内核中实现名字空间和私有网络的代码是否足够成熟?

内核名字空间从 2.6.15版本(2008 年 7 月发布)之后被引入,数年间,这些机制的可靠性在诸多大型生产系统中被实践验证。

实际上,名字空间的想法和设计提出的时间要更早,最初是为了在内核中引入一种机制来实现 OpenVZ 的特性。而 OpenVZ 项目早在 2005 年就发布了,其设计和实现都已经十分成熟。

5.5.2 控制组

控制组是 Linux 容器机制的另外一个关键组件,负责实现资源的审计和限制。

它提供了很多有用的特性;以及确保各个容器可以公平地分享主机的内存、CPU、磁盘 IO 等资源;当然,更重要的是,控制组确保了当容器内的资源使用产生压力时不会连累主机系统。

尽管控制组不负责隔离容器之间相互访问、处理数据和进程,它在防止拒绝服务(DDOS)攻击方面是必不可少的。尤其是在多用户的平台(比如公有或私有的 PaaS)上,控制组十分重要。例如,当某些应用程序表现异常的时候,可以保证一致地正常运行和性能。

控制组机制始于 2006 年,内核从 2.6.24 版本开始被引入。

5.5.3 服务端的防护

运行一个容器或应用程序的核心是通过 Docker 服务端。Docker 服务的运行目前需要 root 权限,因此其安全性十分关键。

首先,确保只有可信的用户才可以访问 Docker 服务。Docker 允许用户在主机和容器间共享文件夹,同时不需要限制容器的访问权限,这就容易让容器突破资源限制。例如,恶意用户启动容器的时候将主机的根目录/映射到容器的 /host目录中,那么容器理论上就可以对主机的文件系统进行任意修改了。这听起来很疯狂?但是事实上几乎所有虚拟化系统都允许类似的资源共享,而没法禁止用户共享主机根文件系统到虚拟机系统。

这将会造成很严重的安全后果。因此,当提供容器创建服务时(例如通过一个 web 服务器),要更加注意进行参数的安全检查,防止恶意的用户用特定参数来创建一些破坏性的容器

为了加强对服务端的保护,Docker的 REST API(客户端用来跟服务端通信)在 0.5.2 之后使用本地的 Unix 套接字机制替代了原先绑定在 127.0.0.1 上的 TCP 套接字,因为后者容易遭受跨站脚本攻击。现在用户使用 Unix 权限检查来加强套接字的访问安全。

用户仍可以利用 HTTP 提供 REST API 访问。建议使用安全机制,确保只有可信的网络或 VPN,或证书保护机制(例如受保护的 stunnel 和 ssl 认证)下的访问可以进行。此外,还可以使用 HTTPS 和证书来加强保护。

最近改进的 Linux 名字空间机制将可以实现使用非 root 用户来运行全功能的容器。这将从根本上解决了容器和主机之间共享文件系统而引起的安全问题。

终极目标是改进 2 个重要的安全特性:

将容器的 root 用户映射到本地主机上的非 root 用户,减轻容器和主机之间因权限提升而引起的安全问题;

允许 Docker 服务端在非 root 权限下运行,利用安全可靠的子进程来代理执行需要特权权限的操作。这些子进程将只允许在限定范围内进行操作,例如仅仅负责虚拟网络设定或文件系统管理、配置操作等。

最后,建议采用专用的服务器来运行 Docker 和相关的管理服务(例如管理服务比如 ssh 监控和进程监控、管理工具 nrpe、collectd 等)。其它的业务服务都放到容器中去运行。

5.5.4 内核能力机制

能力机制(Capability)是 Linux 内核一个强大的特性,可以提供细粒度的权限访问控制。 Linux 内核自 2.2 版本起就支持能力机制,它将权限划分为更加细粒度的操作能力,既可以作用在进程上,也可以作用在文件上。

例如,一个 Web 服务进程只需要绑定一个低于 1024 的端口的权限,并不需要 root 权限。那么它只需要被授权net_bind_service 能力即可。此外,还有很多其他的类似能力来避免进程获取 root 权限。

默认情况下,Docker 启动的容器被严格限制只允许使用内核的一部分能力。

使用能力机制对加强Docker 容器的安全有很多好处。通常,在服务器上会运行一堆需要特权权限的进程,包括有 ssh、cron、syslogd、硬件管理工具模块(例如负载模块)网络配置工具等等。容器跟这些进程是不同的,因为几乎所有的特权进程都由容器以外的支持系统来进行管理。

ssh 访问被主机上ssh服务来管理;

cron 通常应该作为用户进程执行,权限交给使用它服务的应用来处理;

日志系统可由 Docker 或第三方服务管理;

硬件管理无关紧要,容器中也就无需执行 udevd 以及类似服务;

网络管理也都在主机上设置,除非特殊需求,容器不需要对网络进行配置。

从上面的例子可以看出,大部分情况下,容器并不需要“真正的” root 权限,容器只需要少数的能力即可。为了加强安全,容器可以禁用一些没必要的权限。

完全禁止任何 mount 操作;

禁止直接访问本地主机的套接字;

禁止访问一些文件系统的操作,比如创建新的设备、修改文件属性等;

禁止模块加载。

这样,就算攻击者在容器中取得了 root 权限,也不能获得本地主机的较高权限,能进行的破坏也有限。

默认情况下,Docker采用 白名单 机制,禁用 必需功能 之外的其它权限。当然,用户也可以根据自身需求来为 Docker 容器启用额外的权限。

5.5.5 其它安全特性

除了能力机制之外,还可以利用一些现有的安全机制来增强使用 Docker 的安全性,例如TOMOYO, AppArmor, SELinux, GRSEC 等。

Docker 当前默认只启用了能力机制。用户可以采用多种方案来加强 Docker 主机的安全,例如:

在内核中启用 GRSEC 和 PAX,这将增加很多编译和运行时的安全检查;通过地址随机化避免恶意探测等。并且,启用该特性不需要 Docker 进行任何配置。

使用一些有增强安全特性的容器模板,比如带 AppArmor 的模板和 Redhat 带 SELinux 策略的模板。这些模板提供了额外的安全特性。

用户可以自定义访问控制机制来定制安全策略。

跟其它添加到 Docker 容器的第三方工具一样(比如网络拓扑和文件系统共享),有很多类似的机制,在不改变 Docker 内核情况下就可以加固现有的容器。

5.5.6 总结

总体来看,Docker 容器还是十分安全的,特别是在容器内不使用 root 权限来运行进程的话。

另外,用户可以使用现有工具,比如 Apparmor, SELinux, GRSEC 来增强安全性;甚至自己在内核中实现更复杂的安全机制。




猜你喜欢

转载自blog.csdn.net/kamroselee/article/details/80506693