使用插件扩展Docker

本文讲的是使用插件扩展Docker,【编者的话】本文介绍了通过插件扩展Docker的功能,介绍创建一个新插件的步骤以及详细API,鼓励Docker爱好者加入到插件的编写队伍中去。

Docker吸引我的,同时也是促使其成功的一个重要方面,是其开箱即用的特性。

"开箱即用"是指什么呢?简单来说,安装好Docker就可以马上使用。不需要任何额外的操作,诸如网络、进程、文件系统隔离等繁杂事情也不在你担心的范围内。

不过,经过一段时间的使用,你可能开始会考虑更多——诸如自定义网络,自定义保留IP地址,分布式文件系统等等。这些需求会在你将Docker应用到生产或者做进一步准备时候浮现而出。

幸运的是,Docker不仅仅是开箱即用,其中的功能点也是可以进行调整的。如何调整呢?通过Docker的插件!

“即使@Docker开箱即用,最终你还是想要更多。”—— @fntln

什么是Docker插件?

官方文档的描述是:

Docker插件是增强Docker引擎功能的进程外扩展。

这就表示,插件不会运行在Docker daemon中。你可以随时随地(如果需要可以在另一台主机上)启动你的插件。你只需要通过Plugin Discovery(我们后面会深入讨论)通知Docker daemon这儿有一个新的插件可用即可。

进程外体系的另一个优点就是你甚至可以不用重新建立一个Docker daemon来增加一个插件。

“你不需要重新编译@Docker的守护进程来增加一个插件。” ——fntlnz

你可以创建带有如下功能的各种插件:

授权(authz)

这个功能允许你的插件接管Docker守护进程和其远程调用接口的认证和授权。权限管理的插件在你需要进行权限认证管理,或者更精细地控制用户对于守护进程的权限时非常有用。

卷驱动(VolumnDriver)

基本上来说,卷驱动功能使得插件可以掌管每一个卷(Volumn)的生命周期。 这样的一个插件将自己注册成一个卷驱动,并且在主机指明这个卷驱动的名字,希望通过其分配卷的时候启用。 卷驱动插件将会为主机上的卷提供一个对应的挂载点(Mountpoint)。

卷驱动插件在管理分布式文件系统和有状态的卷的时候非常有用。

网络驱动(NetworkDriver)

网络驱动作为Libnetwork的一个远程驱动拓展了Docker引擎。 这意味着插件本身可以通过接入不同的终端(veth pairs等)或者沙盒(网络命名空间、FreeBSD Jails等)扮演网络中的各种角色。

Ipam驱动(IpamDrvier)

IPAM全称是IP地址管理(IP Address Management)。 IPAM是Libnetwork的一个负责管理网络和终端IP地址分配的接口。 Ipam驱动在你需要引入自定义容器IP地址分配规则的时候非常有用。

在创建插件之前我们需要做什么?

Docker 1.7之前的版本不支持插件机制,唯一可以控制守护进程的方式是通过封装其一系列的远程调用接口。 有许多的供应商提供这样的服务,基本而言,他们封装Docker原有的远程调用接口,暴露出和Docker守护进程类似自定义的接口来完成特定的自定义功能。

这么做带来的问题在于,接口的互相组合会变成一场灾难。举个最简单的例子,如果你需要同时运行两个插件,如何知道哪一个先被加载才合适呢?

就如我之前所说,新的插件运行在守护进程之外, 这意味着守护进程本身需要寻找一种合适的方式去和他们进行交互。 每个插件都内建了一个HTTP服务器,这个服务器会被守护进程所探测到,并且提供一系列的远程调用接口,通过HTTP POST方法来交换JSON化的信息。每个插件需要暴露的远程调用接口取决于其想实现的功能(授权、卷驱动、网络驱动和IPAM)。

插件发现机制

那么,“一个会被Docker守护进程所探测到的HTTP服务”是什么意思?

Docker包含一系列的方法去找到一个插件的HTTP服务。 它首先检查所有定义在/run/docker/plugins下的Unix的socket接口。比如你的插件名字是myplugin,那么对应的socket文件应该定义在如下位置: /run/docker/plugins/myplug.sock

除此之外,Docker也会检查目录/etc/docker/plugins或者/usr/lib/docker/plugins目录下包含的特定后缀的文件。目前有两种特定类型的文件可用:

  • *.json
  • *.spec

JSON规范(specification)文件(*.json)

这种文件只是一个普通的*.json文件,包含一些特定的信息:

  • Name:当前可发现的插件名称
  • Addr:插件的HTTP服务器实际可访问的地址信息
  • 传输安全配置(TLSConfig):这是一个可选项;当你需要指定通过SSL协议连接到HTTP服务器时才需要被设置

如下是一个插件的JSON规范文件的例子:

{
"Name": "myplugin",
"Addr": "https://fntlnz.wtf/myplugin",
"TLSConfig": {
"InsecureSkipVerify": false,
"CAFile": "/usr/shared/docker/certs/example-ca.pem",
"CertFile": "/usr/shared/docker/certs/example-cert.pem",
"KeyFile": "/usr/shared/docker/certs/example-key.pem",
}
} 

纯文本文件(*.spec)

你可以使用文件后缀为*.spec的纯文本来提供一个插件的信息。 这个文件需要指定插件的HTTP服务器的TCP或者UNIX接口地址,例如:

tcp://127.0.0.50:8080

unix:///path/to/myplugin.sock

激活机制

所有这些协议最基本的共同点便是均需实现一套插件的激活机制。 这个机制使得Docker可以知道某个插件支持哪些具体的协议来提供对应的功能。守护进程在必要的时候,会远程调用插件的/plugin.Activate远程调用,这个远程调用则必须反馈插件所支持的协议:

{
"Implements": ["NetworkDriver"]
} 

可用的协议或者说功能如同上面所描述的:

  • 权限控制
  • 网络驱动
  • 卷驱动
  • IP地址管理驱动

除了激活的调用接口每个协议还会额外引入一些它自己支持的一些RPC调用。本文将进一步的讨论VolumeDriver协议,我们将会列举所有VolumeDriver.*形式的远程调用,并且将实际编写一个“Hello World”卷驱动插件。

错误处理

插件必须提供有意义的错误信息给Docker daemon,这样它便可以将它们返回给客户端。错误返回信息格式如下:

{
"Err": string
} 

这应该和HTTP 错误代码400和500一起使用。

卷驱动协议

卷驱动协议不仅简单而且异常强大。第一件需要知道的事情是在握手(/Plugin.Activate)的过程中,插件必须把它们自己注册为卷驱动。

{
"Implements": ["VolumeDriver"]
} 

任何一个卷驱动都需要提供在主机文件系统中可写的路径。

使用卷驱动插件与标准插件的经验很相似。你可以用-d参数在创建一个卷的时候指定使用你的容器驱动。

docker volume create -d=myplugin --name myvolume

或者你可以用-v标志字来创建一个容器时同时启动一个容器,也可以用--volume-driver的标志字来指定你容器驱动插件的名字。

docker run -v myvolume:/my/path/on/container --volume-driver=myplugin alpine sh

写一个”Hello World”卷驱动插件

让我们写一个简单的插件,可以用本地的文件系统从/tmp/exampledriver 文件夹中产生卷。简单地说,当客户端请求一个叫做myvolume的卷,这个插件会将那个卷与挂载点/tmp/exampledriver/myvolume 一一对应,并挂载在那个文件夹上。

VolumeDriver协议是由如下总共7个PRC调用和一个激活调用组成:

  • /VolumeDriver.Create
  • /VolumeDriver.Remove
  • /VolumeDriver.Mount
  • /VolumeDriver.Path
  • /VolumeDriver.Unmount
  • /VolumeDriver.Get
  • /VolumeDriver.List

对于这里的每个RPC操作,我们需要实现可以返回完整的JSON数据体的POST端点。你可以转到这里参考完整的规范。

幸运的是,docker/go-plugin-helpers这个项目已经做了很多相关的工作,包含一系列用Go写的帮助实现Docker插件的包。

当我们打算实现一个卷驱动插件时,我们需要在volume包里创建一个结构体来完成对应的volume.Driver接口。

volume.Driver接口定义如下所示:

type Driver interface {
Create(Request) Response
List(Request) Response
Get(Request) Response
Remove(Request) Response
Path(Request) Response
Mount(Request) Response
Unmount(Request) Response
} 

如你所见,这个接口函数与VolumeDriverRPC请求是一一对应的。因此我们可以通过创建我们驱动的结构体开始。

type ExampleDriver struct {
volumes    map[string]string
m          *sync.Mutex
mountPoint string
} 

这其实并不难。我们只是创建了一个具有几个属性的结构体:

  • Volumes:我们将要用这个属性来保存“volume name” => “mountpoint”的键值对
  • m:这只是一个互斥值,用来阻止同一时间不能执行的操作
  • mountPoint:这是我们插件的基本挂载点

为了让我们的结构体实现volume.Driver接口,它需要实现全部的接口函数。

Create

func (d ExampleDriver) Create(r volume.Request) volume.Response {
logrus.Infof("Create volume: %s", r.Name)
d.m.Lock()
defer d.m.Unlock()

if _, ok := d.volumes[r.Name]; ok {
    return volume.Response{}
}

volumePath := filepath.Join(d.mountPoint, r.Name)

_, err := os.Lstat(volumePath)
if err != nil {
    logrus.Errorf("Error %s %v", volumePath, err.Error())
    return volume.Response{Err: fmt.Sprintf("Error: %s: %s", volumePath, err.Error())}
}

d.volumes[r.Name] = volumePath

return volume.Response{}
} 

这个函数当每次一个客户端想要创建一个卷的时候都会被调用。这里的逻辑很简单,当登录之后命令被执行时,我们会锁住mutex,这样的话我们就确定这时没人可以操作volumes字典。当运行结束后,mutex会被自动释放。

然后它会检查卷是否已经存在,如果是的话,我们会只返回一个空的结果来表示卷是可用的。如果卷还不可用,我们会创建一个带有自身挂载点的字符串,检查路径是否可写,并且把它添加到volumes字典中。成功的话,我们将返回一个空结果,或者如果路径是不可写的,我们将会抛出错误。

这个插件不会自动处理目录的创建(虽说这其实很简单),用户可以手动完成。

List

func (d ExampleDriver) List(r volume.Request) volume.Response {
logrus.Info("Volumes list ", r)

volumes := []*volume.Volume{}

for name, path := range d.volumes {
    volumes = append(volumes, &volume.Volume{
        Name:       name,
        Mountpoint: path,
    })
}

return volume.Response{Volumes: volumes}

} 

一个卷插件必须列出注册在自己插件上的所有卷。这个函数基本做的就是——它循环遍历一遍所有的卷,然后把它们放在一个列表中并且返回结果。

Get

func (d ExampleDriver) Get(r volume.Request) volume.Response {
logrus.Info("Get volume ", r)
if path, ok := d.volumes[r.Name]; ok {
    return volume.Response{
        Volume: &volume.Volume{
            Name:       r.Name,
            Mountpoint: path,
        },
    }
}
return volume.Response{
    Err: fmt.Sprintf("volume named %s not found", r.Name),
}
} 

这个函数主要是返回一些关于这个卷的信息。我们在volumes字典中搜索卷的名字并且在结果中返回它的名字和挂载点。

Remove

func (d ExampleDriver) Remove(r volume.Request) volume.Response {
logrus.Info("Remove volume ", r)

d.m.Lock()
defer d.m.Unlock()

if _, ok := d.volumes[r.Name]; ok {
    delete(d.volumes, r.Name)
}

return volume.Response{}
} 

这个函数当客户端请求Docker daemon删除一个卷时会被调用。首先当我们操作volumes字典时需要锁住mutex,然后我们会删除那个卷。

Path

func (d ExampleDriver) Path(r volume.Request) volume.Response {
logrus.Info("Get volume path", r)

if path, ok := d.volumes[r.Name]; ok {
    return volume.Response{
        Mountpoint: path,
    }
}
return volume.Response{}
} 

有些场景下,Docker需要知道一个给定卷名的对应

挂载点

。这就是这个函数的功能——取到卷名并且返回那个卷的挂载点。

Mount

func (d ExampleDriver) Mount(r volume.Request) volume.Response {
logrus.Info("Mount volume ", r)

if path, ok := d.volumes[r.Name]; ok {
    return volume.Response{
        Mountpoint: path,n
    }
}

return volume.Response{}

} 

当某个容器停止,这个函数都会被调用一次。这里,我们在volumes字典中搜索请求的卷名并返回挂载点,这样的话Docker就可以使用它了。

在这个例子中,这个函数的执行过程与Path函数相同。在一个真实的插件中,Mount函数可能要做更多的事情,比如配置资源或为这个资源请求远程的文件系统。

Unmount

func (d ExampleDriver) Unmount(r volume.Request) volume.Response {
logrus.Info("Unmount ", r)
return volume.Response{}
} 

这个函数每当一个容器停止并且Docker不再使用这块卷时会被调用。这里我们不做任何事。一个生产就绪的插件可能会在这个时候注销资源。

Server

现在我们的驱动已经就绪,我们可以创建服务来给Docker daemon提供Unix socket服务。这里空的for循环是为了让main函数处于死循环中,因为服务会到另一个独立的goroutine。

func main() {
driver := NewExampleDriver()
handler := volume.NewHandler(driver)
if err := handler.ServeUnix("root", "driver-example"); err != nil {
    log.Fatalf("Error %v", err)
}

for {

}
} 

这里一个可能可以改进的地方就是可以处理不同的信号,避免异常干扰。

目前,我们还没有实现/Plugin.Activate PRC调用。go-plugin-helpers在我们注册卷处理器的时候会帮我们实现这个。

因为我展示给你的只是最重要的代码块并且忽略了一些部分。你可以从GitHub上clone到完整的代码仓库:

Clone

git clone https://github.com/fntlnz/docker-volume-plugin-example.git

然后你就可以编译你的插件并使用它了。

Build

$ cd docker-volume-plugin-example
$ go build .

Run

这时,我们需要启动插件服务,这样Docker daemon就可以发现它了。

# ./docker-volume-plugin-example

你可以检查插件是否已经创建了unix socket:

# ls -la /run/docker/plugins

会有如下的结果输出:

total 0
drwxr-xr-x. 2 root root  60 Apr 25 12:49 .
drwx------. 6 root root 120 Apr 25 02:13 ..
srw-rw----. 1 root root   0 Apr 25 12:49 driver-example.sock

比较推荐的做法是在开始Docker daemon之前启动你的插件,并且在停止Docker daemon后再停止插件。我通常会在生产环境中遵循这个建议,当在我本地的测试环境中,我通常是在容器里面测试插件的,所以我没有其他选择,必须要在启动Docker之后再启动插件。

使用你的插件

现在你的插件运行起来了,你可以用它来启动一个容器并且指定卷驱动。在启动容器之前,我们需要在挂载点/tmp/exampledriver下创建myvolumename

一个真实生产就绪的插件应该可以做到自动处理挂载点的创建。

$ mkdir /tmp/exampledriver/myvolumename
# docker run -it -v myvolumename:/data --volume-driver=driver-example alpine sh

你可以通过docker volume ls来检查卷是否被创建了,输出结果如下:

DRIVER              VOLUME NAME
local               dcb04fb12e6d914d4b34b7dbfff6c72a98590033e20cb36b481c37cc97aaf162
local               f3b65b1354484f217caa593dc0f93c1a7ea048721f876729f048639bcfea3375
driver-example      myvolumename

现在每个将要放在容器的/data文件夹里的文件都会被写在主机的/tmp/exampledriver/myvolumename文件夹里。

可用的插件

你可以在这里找到很多插件。我最爱的插件有:

  • Flocker:这个插件可以让你的卷“跟随”着你的容器,让你拥有更稳定的容器,因为如数据库等将会保持一致。
  • Netshare plugin:我用这个插件来把NFS文件夹挂载在容器里。它也可以支持EFS和CIFS。
  • Weave Network Plugin:这个可以让你看到一些挂载在相同网络交换机上但在不同地方独立运行的容器。

现在你已经了解了插件的API是如何工作的,然后你就可以自己写个插件来玩啦~棒棒哒!

但你现在还可以做点事情。举个例子,我给你展示了怎么用Golang写的官方插件助手来用Go语言写你的插件。但你可能没用过Golang——你可能使用Rust或Java,甚至JavaScript。如果这样的话,你可以考虑用你的语言写一个插件助手噢。

考虑用你最爱的语言写一个@Docker插件助手吧。”——@fntlnz

原文链接:Extend Docker Via Plugin(翻译:潘丽娜,校对:吴佳兴)

================================================================
译者介绍
潘丽娜,Intel软件工程师。

原文发布时间为:2016-05-11

本文作者:Lynna 

本文来自合作伙伴DockerOne,了解相关信息可以关注DockerOne。

原文标题:使用插件扩展Docker

时间: 2024-09-10 05:20:36

使用插件扩展Docker的相关文章

Yii安装EClientScript插件扩展实现css,js文件代码压缩合并加载功能_php实例

本文实例讲述了Yii安装EClientScript插件扩展实现css,js文件代码压缩合并加载功能.分享给大家供大家参考,具体如下: 扩展插件下载地址,解压后复制到/protected/vendor/ https://github.com/muayyad-alsadi/yii-EClientScript main配置文件配置插件,components里面增加 //js,css代码压缩,合并 'clientScript' => array( 'class' => 'application.ven

jQuery插件扩展extend的简单实现原理_jquery

相信每位前端的小伙伴对jQuery都不陌生吧,它最大的魅力之一就是有大量的插件,去帮助我们更轻松的实现各种功能. 前几天晚上,闲来无事,就自己动手写了个简单的jQuery插件,功能很简单,只是让选定的元素高亮,但是其中的一些思想,还是很值得学习的,可以戳这里查看代码. 本文不聊怎么写jQuery插件,我们聊聊怎么去实现jQuery的插件扩展功能,extend是怎么实现把我们写的插件挂载到jQuery上的.(大牛可以出门右拐......) 我们可以模拟创建一个迷你jQuery. var $ = {

利用jQuery插件扩展识别浏览器内核与外壳的类型和版本的实现代码_jquery

尤其是在当今各种浏览器满天飞(据说仅以IE为内核的浏览器就有200种之多). 小弟今天写了个基于jQuery的插件扩展,主要用于识别浏览器内核与外壳的类型和版本.可识别各种浏览器的内核,并已经支持多种国内主流浏览器. 费话不多说,上我的JavaScript代码:(文件名:jquery.browsertype-1.0.js) 复制代码 代码如下: /** * jQuery插件开发方法二:第一步:插件定义 */ jQuery.myPlugin = { //获得浏览器的内核与外壳的类型和版本 Clie

jQuery插件扩展测试实例_jquery

本文实例讲述了jQuery插件扩展测试方法.分享给大家供大家参考,具体如下: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-

eclipse插件扩展点相关问题

问题描述 以前使用C++编程,现做一项目需要使用插件架构,故希望通过学习Eclipse架构,看能否从中得到一些想法,看了一些插件说明最重要的是插件扩展点,所有插件都是通过实现插件扩展点来添加插件.既然能够通过实现扩展点来添加新的插件,肯定可以添加多个插件实现同一扩展点.例如,如果想添加两个插件,每个插件添加一个菜单,这时Eclipse架构怎样将这两个插件的菜单都显示出来?具体的实现流程如何,请各位高手指导指导!!!之前看过一篇文章,文章中说插件A和插件B,插件A声明扩展点P及其实现规则接口I,插

利用插件扩展Nagios的监控功能

Nagioshttp://www.aliyun.com/zixun/aggregation/23104.html">监控系统对远程主机上服务状态的获取,可以通过一些相应的服务检测命令,例如check_http.check_ftp等来完成.那么,如果要获取远程主机上的本地资源或者属性,例如要监控远程主机上的磁盘利用率.CPU利用率.系统负载时,该如何实现呢?虽然插件中有check_disk.check_load.check_swap之类的工具,但是这些工具仅能获取主机自身信息,而无法获取远程

求救:VS 2005 的插件扩展使用问题

问题描述 我写了个简单的VS2005扩展插件,配置文件如下<?xmlversion="1.0"encoding="UTF-16"standalone="no"?><Extensibilityxmlns="http://schemas.microsoft.com/AutomationExtensibility"><HostApplication><Name>MicrosoftVis

json格式化/压缩工具 Chrome插件扩展版_javascript技巧

安装方法:用chrome浏览器访问 https://chrome.google.com/extensions/detail/pjkoglpbigbjijmncfkcpkcpddnelgbm?hl=zh-cn [json格式化/压缩]工具 chrome下安装 :) 1.建一个新的文件夹 2.建一个名为 manifest.json的文件 3.打开这个 manifest.json文件,可以理解为配置文件 :) 包含以下内容 复制代码 代码如下: { "name": "My Firs

你真的了解Docker吗?——Docker插件机制详解

云栖TechDay活动第十八期中,阿里云容器服务团队的核心成员陈萌辉带来了题为<Docker插件机制详解>的分享,分享中,他结合阿里云容器服务实践介绍了Docker插件的基本原理.实现方法以及插件机制未来的演进. 幻灯片下载地址:https://yq.aliyun.com/attachment/download/?filename=bdefe06ba7a14d7604af5a63a4bcc4f3.pdf 以下为现场分享观点整理. 为什么需要Docker插件?   Docker之所以这么火并且有