使用插件擴展Docker

http://wwwbuild.net/dockerone/241249.html
 
Docker吸引個人,同時也是促使其成功的一個重要方面,是其開箱即用的特性。

「開箱即用」是指什麼呢?簡單來講,安裝好Docker就能夠立刻使用。不須要任何額外的操做,諸如網絡、進程、文件系統隔離等繁瑣事情也不在你擔憂的範圍內。html

不過,通過一段時間的使用,你可能開始會考慮更多——諸如自定義網絡,自定義保留IP地址,分佈式文件系統等等。這些需求會在你將Docker應用到生產或者作進一步準備時候浮現而出。git

幸運的是,Docker不只僅是開箱即用,其中的功能點也是能夠進行調整的。如何調整呢?經過Docker的插件!github

「即便@Docker開箱即用,最終你仍是想要更多。」—— @fntlndocker

什麼是Docker插件?

官方文檔的描述是:數據庫

Docker插件是加強Docker引擎功能的進程外擴展。json

這就表示,插件不會運行在Docker daemon中。你能夠隨時隨地(若是須要能夠在另外一臺主機上)啓動你的插件。你只須要經過Plugin Discovery(咱們後面會深刻討論)通知Docker daemon這兒有一個新的插件可用便可。安全

進程外體系的另外一個優勢就是你甚至能夠不用從新創建一個Docker daemon來增長一個插件。服務器

「你不須要從新編譯@Docker的守護進程來增長一個插件。」 ——fntlnz網絡

你能夠建立帶有以下功能的各類插件:app

受權(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 的更多文章:
相關文章
相關標籤/搜索