使用插件扩展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端点。你可以转到这里(https://docs.docker.com/engine/extend/plugins_volume/)参考完整的规范。
幸运的是,docker/go-plugin-helpers(https://github.com/docker/go-plugins-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
文件夹里。
可用的插件
你可以在这里(https://docs.docker.com/engine/extend/plugins/)找到很多插件。我最爱的插件有:
Flocker:这个插件可以让你的卷“跟随”着你的容器,让你拥有更稳定的容器,因为如数据库等将会保持一致。
Netshare plugin:我用这个插件来把NFS文件夹挂载在容器里。它也可以支持EFS和CIFS。
Weave Network Plugin:这个可以让你看到一些挂载在相同网络交换机上但在不同地方独立运行的容器。
现在你已经了解了插件的API是如何工作的,然后你就可以自己写个插件来玩啦~棒棒哒!
但你现在还可以做点事情。举个例子,我给你展示了怎么用Golang写的官方插件助手来用Go语言写你的插件。但你可能没用过Golang——你可能使用Rust或Java,甚至JavaScript。如果这样的话,你可以考虑用你的语言写一个插件助手噢。
考虑用你最爱的语言写一个@Docker插件助手吧。”——@fntlnz
感谢吴佳兴对文章的审校。
@Container容器技术大会正在火热报名中,知名公司的Docker、Kubernetes、Mesos应用案例,点击下图可查看大会具体内容。
本文为翻译文章,点击左下角阅读原文链接,查看原文。
首页 - Docker 的更多文章:
使用插件扩展Docker的更多相关文章
- QML插件扩展2(基于C++的插件扩展)
上一节介绍了纯QML的插件扩展方式,这种扩展方式基本满足大部分的扩展需求,下面开始介绍比较小众的基于C++的扩展 (一)更新插件工程 1.更新MyPlugin工程下的qmldir文件,加入plugin ...
- QML插件扩展(一)
准备分两节来介绍QML扩展插件,分别为 (一)基于QML文件的扩展方式 (二)基于C++的插件扩展 这篇先介绍基于QML的插件扩展. 先介绍几个基本概念: qmldir: 用于组织自定义的QML插件, ...
- [Unity]Unity3D编辑器插件扩展和组件扩展
1. 插件扩展 1.1. 命名空间 using UnityEditor; using UnityEngine; //非必需,常用到 1.2. 使用语法 [MenuItem("Assets/M ...
- 百度umeditor富文本编辑器插件扩展
富文本编辑器在WEB开发中经常用到,个人比较喜欢用百度出的ueditor这款,ueditor这款本身支持插件扩展的,但是ueditor的mini版本 umeditor 就没有那么方便了,不过找了很多资 ...
- Chrome插件(扩展)开发全攻略
[干货]Chrome插件(扩展)开发全攻略:https://www.cnblogs.com/liuxianan/p/chrome-plugin-develop.html
- FireFox 浏览器插件/扩展开发学习
2014-11-08 内容存档在evernote,笔记名"FireFox 浏览器插件/扩展开发学习"
- jQuery的noConflict以及插件扩展
一.noConflict函数 JavaScript有很多插件,如果jQuery对象的$与其他插件冲突,我们可以使用noConflict()方法去掉$或者使用其他的符号代替 注:noConflict() ...
- jQuery笔记之工具方法extend插件扩展
jQuery工具方法 $.extend()插件扩展(工具方法) $.fn.extend()插件扩展(实例方法) 浅度克隆.深度克隆 两个方法基本是一样的,唯一不同的就是调用方式不一样 -------- ...
- Chrome插件(扩展)
[干货]Chrome插件(扩展)开发全攻略 写在前面 我花了将近一个多月的时间断断续续写下这篇博文,并精心写下完整demo,写博客的辛苦大家懂的,所以转载务必保留出处.本文所有涉及到的大部分代码均 ...
随机推荐
- git branch & checkout fetch 的使用和冲突解决
git branch & checkout fetch 的使用和冲突解决 branch git branch 查看本地分支 git branch -v 查看本地分支的具体信息(commi ...
- springboot拦截器中获取配置文件值
package com.zhx.web.interceptor; import com.zhx.util.other.IpUtil; import org.slf4j.Logger; import o ...
- 函数模拟sort快排
设计一个对一维数组进行排序的sort函数,并调用它实现数组排序 思路:函数调用不止调用一个,最主要对函数不熟悉: #include<stdio.h> #define N 10 int ma ...
- TF:利用TF的train.Saver将训练好的variables(W、b)保存到指定的index、meda文件—Jason niu
import tensorflow as tf import numpy as np W = tf.Variable([[2,1,8],[1,2,5]], dtype=tf.float32, name ...
- gradle修改apk包名和apk文件名
需求1:根据渠道不同给包名添加不同的后缀名 方案: //先定义默认包名,用来复用 def packageName = "xxx.xxxx.xxxx" defaultConfig { ...
- Django 学习第三天——模板变量及模板过滤器
一.模板路径的查找: 查找顺序:(现在哪找到就用那个) 首先在主目录的 setting.py 文件里的 TEMPLATES 中的 DIRS 里找: 其次如果 DIRS 中的 APP_DIRS : 'T ...
- ORM(一)
1.什么是ORM ORM,即Object-Relational Mapping(对象关系映射),它的作用是在关系型数据库和业务实体对象之间作一个映射,这样,我们在具体的操作业务对象的时候,就不需要再去 ...
- DOS简单文件指令
DOS和Windows最大的不同在于DOS命令方式操作,所以使用者需要记住大量命令及其格式使用方法,DOS命令分为内部命令和外部命令, 内部命令是随每次启动的COMMAND.COM装入并常驻内存,而外 ...
- 洛谷.4655.[CEOI2017]Building Bridges(DP 斜率优化 CDQ分治)
LOJ 洛谷 \(f_i=s_{i-1}+h_i^2+\min\{f_j-s_j+h_j^2-2h_i2h_j\}\),显然可以斜率优化. \(f_i-s_{i-1}-h_i^2+2h_ih_j=f_ ...
- Codeforces.GYM100548G.The Problem to Slow Down You(回文树)
题目链接 \(Description\) 给定两个串\(S,T\),求两个串有多少对相同回文子串. \(|S|,|T|\leq 2\times 10^5\). \(Solution\) 好菜啊QAQ ...