Docker Machine v0.5 の Driver Plugins の仕組み

※この記事は Driver Plugins of Docker Machine を基にブログとして書き起こしたものです。

Docker Machine とは

Docker Machine とは、 Docker ホストの管理ツールです。たとえば以下のようにして使います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# VirtualBox VM でローカル環境に demo という名前のホストをつくる
$ docker-machine create --driver virtualbox demo

# 環境変数を設定する
$ eval "$(docker-machine env demo)"

# ホスト一覧を表示する
$ docker-machine ls
NAME ACTIVE DRIVER STATE URL SWARM
demo * virtualbox Running tcp://192.168.99.100:2376

# docker images で demo 内の image 一覧を表示する
$ docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE

# CentOS の image を取得する
$ docker pull centos
...
Status: Downloaded newer image for centos:latest

# 再び demo 内の image 一覧を表示する
$ docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
centos latest e9fa5d3a0d0e 4 weeks ago 172.3 MB

このように簡単に Docker ホストを作成することができます。

Docker Machine の特長

複数のホストを簡単に管理できる

Docker Machine は複数のホストを一括して管理できます。以下のように環境変数を切り替えることで、 Docker コマンドでの接続先を簡単に切り替えることができます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# ホスト一覧を表示する
$ docker-machine ls
NAME ACTIVE DRIVER STATE URL SWARM
default - virtualbox Running tcp://192.168.99.121:2376
demo * virtualbox Running tcp://192.168.99.118:2376
dev - virtualbox Stopped

# ホストを default に切り替える
$ eval "$(docker-machine env default)"

# default が active になった
$ docker-machine ls
NAME ACTIVE DRIVER STATE URL SWARM
default * virtualbox Running tcp://192.168.99.121:2376
demo - virtualbox Running tcp://192.168.99.118:2376
dev - virtualbox Stopped

# default にある image は ubuntu
$ docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
ubuntu latest e9ae3c220b23 4 days ago 187.9 MB

クラウドのホストも簡単に管理できる

もう1つの大きな特長は、クラウドサービスのホストも簡単に作成できることです。たとえば、 Amazon EC2 上にホストを立てるには、以下のようにします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Amazon EC2 にホストをつくる。各環境変数には適切な値を設定しておく
$ docker-machine create \
--driver amazonec2 \
--amazonec2-access-key $AWS_ACCESS_KEY_ID \
--amazonec2-secret-key $AWS_SECRET_ACCESS_KEY \
--amazonec2-vpc-id $AWS_VPC_ID \
--amazonec2-region ap-northeast-1 \
--amazonec2-zone c \
stg

# つくった EC2 インスタンスがホスト一覧に追加される
$ docker-machine ls
NAME ACTIVE DRIVER STATE URL SWARM
demo * virtualbox Running tcp://192.168.99.100:2376
dev - virtualbox Running tcp://192.168.99.102:2376
stg - amazonec2 Running tcp://52.68.147.113:2376

# ssh サブコマンドでそのまま SSH ログインできる
$ docker-machine ssh stg
...
ubuntu@stg:~$

# 削除も簡単
$ docker-machine rm stg
$ docker-machine ls
NAME ACTIVE DRIVER STATE URL SWARM
demo * virtualbox Running tcp://192.168.99.100:2376
dev - virtualbox Running tcp://192.168.99.102:2376

コマンド1発で EC2 のホストをプロビジョニングできるなんて、とても便利じゃないでしょうか。

v0.5 の変更点

2015年11月3日に v0.5 がリリースされました。その中での最大の変更点が、 Driver Plugins という仕組みです。

Driver Plugins

Docker Machine のプロビジョニングの処理では、ホストのタイプ (VirtualBox なのか EC2 なのかなど) によって、それぞれに対応したドライバが実際の処理をおこないます。 v0.4 までは、その各ドライバはすべて docker-machine バイナリに組み込まれていました。しかし v0.5 から、各ドライバが独立したバイナリとして配布されるようになりました。このことは、以下のようにすると確認できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# docker-machine 関連のバイナリ一覧
$ ls /usr/local/bin | grep docker-machine
docker-machine
docker-machine-driver-amazonec2
docker-machine-driver-azure
docker-machine-driver-digitalocean
docker-machine-driver-exoscale
docker-machine-driver-generic
docker-machine-driver-google
docker-machine-driver-hyperv
docker-machine-driver-none
docker-machine-driver-openstack
docker-machine-driver-rackspace
docker-machine-driver-softlayer
docker-machine-driver-virtualbox
docker-machine-driver-vmwarefusion
docker-machine-driver-vmwarevcloudair
docker-machine-driver-vmwarevsphere

この仕組みが Driver Plugins です。このような仕組みになったことで、ドライバの追加や削除が簡単におこなえるというプラガブル性や、メインバイナリとドライババイナリの開発・配布が独立しておこなえるというモジュール性が高まりました。

しかし一方でバイナリ自体が独立したことで、内部の仕組みは大きく変化しました。 v0.5 からは、このドライバとの通信方法に RPC (Remote Procesure Call) を用いるようになっています。ちなみに本家 Docker のプラグイン機構では一足先にこれが採用されています。

Driver Plugin の起動

このことを確かめてみましょう。以下は OS X でおこなった場合ですので、 Docker デーモンは VirtualBox 上で動いているという前提です。

lsof コマンドを使って、ポートを使用しているプロセスを見てみます。まず通常状態では、 TCP ポートを使用している “docker” の名前がつくプロセスはいません。

1
2
3
4
# 通常はポートを使用している "docker" の名前がつくプロセスはいない
$ lsof -nP -iTCP -sTCP:LISTEN | grep docker

$

続いて docker-machine ls コマンドを実行してみます。すると、これまでなかったプロセスが立ち上がっていることがわかります。

1
2
3
4
5
6
7
8
9
10
11
12
# docker-machine ls コマンドを実行すると、各ホストに対応するプラグインが RPC サーバとして立ち上がる
$ docker-machine ls &; sleep 0.5 && lsof -nP -iTCP -sTCP:LISTEN | grep docker
[1] 65804
docker-ma 65806 skatsuta 3u IPv4 0xc8646a3a5297e9ef 0t0 TCP 127.0.0.1:53372 (LISTEN)
docker-ma 65807 skatsuta 3u IPv4 0xc8646a3a573eb4af 0t0 TCP 127.0.0.1:53376 (LISTEN)
docker-ma 65808 skatsuta 3u IPv4 0xc8646a3a4ad3ff4f 0t0 TCP 127.0.0.1:53380 (LISTEN)

NAME ACTIVE DRIVER STATE URL SWARM
default - virtualbox Stopped
demo * virtualbox Running tcp://192.168.99.100:2376
dev - virtualbox Running tcp://192.168.99.102:2376
[1] + 65804 done docker-machine ls

名前が切れていて docker-ma までしか表示されていませんが、実は3つの docker-machine-driver-virtualbox プロセスが起動しています。
これらは RPC サーバとして、メインプロセスからの RPC リクエストを受け付け、ホストのプロビジョニング処理をおこなっています。

ではプラグインを直接起動してみたらどうなるのでしょうか? やってみると以下のようなエラーメッセージが表示され、起動に失敗してしまいます。

1
2
3
4
$ docker-machine-driver-virtualbox
This is a Docker Machine plugin binary.
Plugin binaries are not intended to be invoked directly.
Please use this plugin through the main 'docker-machine' binary.

実は以下のようにすると起動できますが、このプロセスもすぐ終了してしまいます。

1
2
3
4
$ MACHINE_PLUGIN_TOKEN=42 docker-machine-driver-virtualbox
127.0.0.1:59375
# この間1秒以内
$

さすがにこんな早さではメインプロセスと通信するのに十分な時間があるとは思えません。ということでこのことも含めて、 Docker Machine v0.5 の Driver Plugins の内部実装を追ってみることにしましょう。

ちなみに以下に記載するコード内に適宜日本語によるコメントを書いていますが、これらはすべて僕が追記したものであり、元のソースコードにはありません。本来のソースコードへのリンクも併せて記載しているので、ちゃんと確認したい方はそちらをご覧ください。

RPC Client

まず RPC クライアント側、すなわちメインプロセス側の実装です。まず RPC クライアントの役割を担うのは、以下の RpcClientDriver という構造体です。

libmachine/drivers/rpc/client_driver.go#L19-L23

1
2
3
4
5
type RpcClientDriver struct {
plugin localbinary.DriverPlugin // プラグイン管理オブジェクト
heartbeatDoneCh chan bool // heartbeat 終了メッセージ送信チャンネル
Client *InternalClient // Go の標準ライブラリの RPC クライアントをラップしたもの
}

この構造体がプラグインへの RPC リクエストを担当します。 Heartbeat の送信についてはあとで解説します。

クライアント生成とプラグイン起動

docker-machine のコマンドを実行すると、もろもろの前処理 (フラグのパースやホスト名の確認など) をおこなったあと、ほとんどの場合以下の NewRpcClientDriver() が呼ばれ、 RPC クライアントの生成処理とプラグインの起動処理がおこなわれます。

libmachine/drivers/rpc/client_driver.go#L49-L112

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// rawDriverData: ドライバに関する設定情報が JSON にシリアライズされたもの
// driverName: "virtualbox", "amazonec2" などのドライバ名文字列
func NewRpcClientDriver(rawDriverData []byte, driverName string) (*RpcClientDriver, error) {
mcnName := ""

// バイナリプラグイン管理オブジェクトを生成する
p, err := localbinary.NewLocalBinaryPlugin(driverName)
if err != nil {
return nil, err
}

// 非同期にドライバプラグイン (RPC サーバ) を起動する
go func() {
if err := p.Serve(); err != nil {
// TODO: Is this best approach?
log.Warn(err)
return
}
}()

// プラグインへの接続情報を取得する
addr, err := p.Address()
if err != nil {
return nil, fmt.Errorf("Error attempting to get plugin server address for RPC: %s", err)
}

// プラグインへ接続する
rpcclient, err := rpc.DialHTTP("tcp", addr)
if err != nil {
return nil, err
}

// RPC クライアントオブジェクトを生成する
c := &RpcClientDriver{
Client: NewInternalClient(rpcclient),
heartbeatDoneCh: make(chan bool),
}

// 定期的にプラグインへ heartbeat を送信する goroutine を生成する
go func(c *RpcClientDriver) {
for {
select {
case <-c.heartbeatDoneCh:
return
default:
if err := c.Client.Call("RpcServerDriver.Heartbeat", struct{}{}, nil); err != nil {
log.Warnf("Error attempting heartbeat call to plugin server: %s", err)
c.Close()
return
}
time.Sleep(heartbeatInterval)
}
}
}(c)

// サーバのバージョンを確認する
var version int
if err := c.Client.Call("RpcServerDriver.GetVersion", struct{}{}, &version); err != nil {
return nil, err
}
log.Debug("Using API Version ", version)

// プラグイン情報をクライアントに設定する
if err := c.SetConfigRaw(rawDriverData); err != nil {
return nil, err
}

mcnName = c.GetMachineName()
p.MachineName = mcnName
c.Client.MachineName = mcnName
c.plugin = p

return c, nil
}

まず注目してもらいたいのは、 プラグインの起動処理を別の goroutine がおこなっていることです。このようにすることで、時間のかかるプラグインの起動処理を本処理とは別におこなうことができます。
もう1つは、 途中で heartbeat を送信する goroutine を立ち上げていることです。この goroutine は RpcServerDriver.Heartbeat というリモートメソッド呼び出しにより、プラグイン側に heartbeat を送っています。このことの意味は、あとで RPC サーバ側の解説をするときに説明します。

では、プラグインの起動処理についてもう少し追ってみましょう。

プラグインの起動

プラグインの起動処理を実際におこなうのは、以下の LocalBinaryExecutor という構造体です。

libmachine/drivers/plugin/localbinary/plugin.go#L73-L77

1
2
3
4
5
type LocalBinaryExecutor struct {
pluginStdout, pluginStderr io.ReadCloser
DriverName string
binaryPath string
}

この構造体は、以下の Start() メソッドの中で exec.Command()cmd.Start() を実行することで、プラグインのプロセスを起動します。

libmachine/drivers/plugin/localbinary/plugin.go#L105-L132

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func (lbe *LocalBinaryExecutor) Start() (*bufio.Scanner, *bufio.Scanner, error) {
var err error

log.Debugf("Launching plugin server for driver %s", lbe.DriverName)

// プラグインのプロセスを起動するための exec.Cmd を生成する
cmd := exec.Command(lbe.binaryPath)

lbe.pluginStdout, err = cmd.StdoutPipe()
if err != nil {
return nil, nil, fmt.Errorf("Error getting cmd stdout pipe: %s", err)
}

lbe.pluginStderr, err = cmd.StderrPipe()
if err != nil {
return nil, nil, fmt.Errorf("Error getting cmd stderr pipe: %s", err)
}

outScanner := bufio.NewScanner(lbe.pluginStdout)
errScanner := bufio.NewScanner(lbe.pluginStderr)

// 環境変数 $MACHINE_PLUGIN_TOKEN に 42 を設定する
os.Setenv(PluginEnvKey, PluginEnvVal)

// プロセスを起動する
if err := cmd.Start(); err != nil {
return nil, nil, fmt.Errorf("Error starting plugin binary: %s", err)
}

return outScanner, errScanner, nil
}

const (
PluginEnvKey = "MACHINE_PLUGIN_TOKEN"
PluginEnvVal = "42"
)

また、途中で os.Setenv() により、 MACHINE_PLUGIN_TOKEN=42 に設定しているのがわかります。これが先ほどプラグインプロセスを単体で立ち上げてみたときにおこなったことです。

RPC リクエストの送信

では RPC リクエストの送信処理はどうなっているのでしょうか。 RpcClientDriver のメソッドの大部分は、単なる RpcServerDriver へのリモート呼び出しになっています。

libmachine/drivers/rpc/client_driver.go#L256-L278

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func (c *RpcClientDriver) Create() error {
return c.Client.Call("RpcServerDriver.Create", struct{}{}, nil)
}

func (c *RpcClientDriver) Remove() error {
return c.Client.Call("RpcServerDriver.Remove", struct{}{}, nil)
}

func (c *RpcClientDriver) Start() error {
return c.Client.Call("RpcServerDriver.Start", struct{}{}, nil)
}

func (c *RpcClientDriver) Stop() error {
return c.Client.Call("RpcServerDriver.Stop", struct{}{}, nil)
}

Create(), Remove(), Start(), Stop() などの代表的な処理は、全部 RPC サーバ側に丸投げしています。ということで、 RPC サーバ側の実装を見てみましょう。

RPC Server

RPC サーバ側、すなわちプラグインプロセス側の実装を見てみます。

プラグインバイナリの起動処理

まず先にプラグインプロセスが起動されたときに最初に実行される処理を追ってみましょう。例として docker-machine-driver-virtualbox のエントリポイントは以下のようになっています。

cmd/machine-driver-virtualbox.go#L8-L10

1
2
3
4
// docker-machine-driver-virtualbox の main 関数
func main() {
plugin.RegisterDriver(virtualbox.NewDriver("", ""))
}

これだけです。では RegisterDriver() の実装はどうなっているのでしょうか。

libmachine/drivers/plugin/register_driver.go#L17-L56

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
var (
// heartbeat の猶予時間
heartbeatTimeout = 500 * time.Millisecond
)

func RegisterDriver(d drivers.Driver) {
// $MACHINE_PLUGIN_TOKEN != 42 の場合は終了する
if os.Getenv(localbinary.PluginEnvKey) != localbinary.PluginEnvVal {
fmt.Fprintln(os.Stderr, `This is a Docker Machine plugin binary.
Plugin binaries are not intended to be invoked directly.
Please use this plugin through the main 'docker-machine' binary.`)
os.Exit(1)
}

libmachine.SetDebug(true)

// RPC サーバオブジェクトを生成し、登録処理をおこなう
rpcd := rpcdriver.NewRpcServerDriver(d)
rpc.Register(rpcd)
rpc.HandleHTTP()

// ポート番号ランダムで TCP リスナを生成する
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading RPC server: %s\n", err)
os.Exit(1)
}
defer listener.Close()

fmt.Println(listener.Addr())

// リクエストを待ち受ける
go http.Serve(listener, nil)

for {
select {
case <-rpcd.CloseCh:
os.Exit(0)
// heartbeat が送られてきてる間はプロセス継続
case <-rpcd.HeartbeatCh:
continue
// 500ms 以内の間隔で heartbeat が送られてこなければ終了する
case <-time.After(heartbeatTimeout):
os.Exit(1)
}
}
}

まず最初に、 $MACHINE_PLUGIN_TOKEN != 42 の場合はエラーメッセージを表示して os.Exit(1) しています。これが特定の環境変数をつけないとプラグイン単体では起動できなかった理由です。意図しない起動のされ方がなされないように、このようなセーフティーネットが張られているようです。 42 なのは 『銀河ヒッチハイクガイド』 に由来しているのでしょうか。

続いて net.Listen("tcp", "127.0.0.1:0") で利用可能な TCP ポートで listen することを宣言します。 net.Listen() が内部で呼んでいる net.ListenTCP() の仕様として、ポート番号 0 の場合には利用可能な TCP ポートのどれかが割り当てられることになっています。

最後にリクエストを待ち受ける goroutine を起動するとともに、 for-select で無限ループに入ります。ここで先ほどプラグインプロセスが即時終了してしまった理由がわかります。プラグインは rpcd.HeartbeatCh チャンネルにメッセージが送られ続けている間は起動していますが、 500ms 以上メッセージが送られてこないと強制終了してしまうようになっているのです。これにより、メインプロセスが予期せぬ終了をしてしまった場合でも、プラグインプロセスがゾンビ化することを防いでいます。

RPC サーバ

では、 RPC リクエストに対する実際の処理を見てみましょう。 RPC リクエストを受け付けるのは以下の RpcServerDriver という構造体です。この構造体のメソッドが RpcClientDriver から呼び出されていましたね。

libmachine/drivers/rpc/server_driver.go

1
2
3
4
5
type RpcServerDriver struct {
ActualDriver drivers.Driver // 実際の処理をおこなう Driver インターフェース
CloseCh chan bool
HeartbeatCh chan bool
}

各メソッドは以下のようになっていて、実際のところは ActualDriver に処理を委譲していることがわかります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func (r *RpcServerDriver) Create(_, _ *struct{}) error {
return r.ActualDriver.Create()
}

func (r *RpcServerDriver) Remove(_ *struct{}, _ *struct{}) error {
return r.ActualDriver.Remove()
}

func (r *RpcServerDriver) Start(_ *struct{}, _ *struct{}) error {
return r.ActualDriver.Start()
}

func (r *RpcServerDriver) Stop(_ *struct{}, _ *struct{}) error {
return r.ActualDriver.Stop()
}

ではこの ActualDriver の型である drivers.Driver について詳しく見てみましょう。

Driver interface

drivers.Driver はインターフェースであり、ホストへの操作を実際におこなうオブジェクトが実装すべきメソッド群を規定しています。

libmachine/drivers/drivers.go#L11-L73

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// Driver defines how a host is created and controlled. Different types of
// driver represent different ways hosts can be created (e.g. different
// hypervisors, different cloud providers)
type Driver interface {
// Create a host using the driver's config
Create() error

// DriverName returns the name of the driver as it is registered
DriverName() string

// GetCreateFlags returns the mcnflag.Flag slice representing the flags
// that can be set, their descriptions and defaults.
GetCreateFlags() []mcnflag.Flag

// GetIP returns an IP or hostname that this host is available at
// e.g. 1.2.3.4 or docker-host-d60b70a14d3a.cloudapp.net
GetIP() (string, error)

// GetMachineName returns the name of the machine
GetMachineName() string

// GetSSHHostname returns hostname for use with ssh
GetSSHHostname() (string, error)

// GetSSHKeyPath returns key path for use with ssh
GetSSHKeyPath() string

// GetSSHPort returns port for use with ssh
GetSSHPort() (int, error)

// GetSSHUsername returns username for use with ssh
GetSSHUsername() string

// GetURL returns a Docker compatible host URL for connecting to this host
// e.g. tcp://1.2.3.4:2376
GetURL() (string, error)

// GetState returns the state that the host is in (running, stopped, etc)
GetState() (state.State, error)

// Kill stops a host forcefully
Kill() error

// PreCreateCheck allows for pre-create operations to make sure a driver is ready for creation
PreCreateCheck() error

// Remove a host
Remove() error

// Restart a host. This may just call Stop(); Start() if the provider does not
// have any special restart behaviour.
Restart() error

// SetConfigFromFlags configures the driver with the object that was returned
// by RegisterCreateFlags
SetConfigFromFlags(opts DriverOptions) error

// Start a host
Start() error

// Stop a host gracefully
Stop() error
}

Create(), Start(), Stop(), Remove() など、ホストの操作に必要なメソッドが並んでいます。そして、ホストが VirtualBox なのか Amazon EC2 なのかなどに応じて、各ホストに対する具体的な処理の実装が変わってくるわけです。では例として VirtualBox 用の Driver の実装を見てみることにしましょう。

VirtualBox 用の Driver の実装

drivers/virtualbox/virtualbox.go#L49-L61

1
2
3
4
5
6
7
8
9
10
11
type Driver struct {
VBoxManager // vbm(), vbmOut(), vbmOutErr() をもつインターフェース
*drivers.BaseDriver
CPU int // デフォルト 1
Memory int // デフォルト 1024 MB
DiskSize int // デフォルト 20000 MB
Boot2DockerURL string // Boot2Docker のダウンロード URL
Boot2DockerImportVM string // Boot2Docker の ISO のパス
...
NoShare bool // 共有ディレクトリをつくるかどうか
}

ここで重要なのは VBoxManager という埋め込みフィールドです。 VBoxManager はインターフェースになっていて、その実装は VBoxCmdManager に書かれています。

drivers/virtualbox/vbm.go#L51-L80

1
2
3
4
5
6
7
8
9
10
11
func (v *VBoxCmdManager) vbmOutErr(args ...string) (string, string, error) {
// `VBoxManage args...` コマンドを実行する
cmd := exec.Command(vboxManageCmd, args...)
log.Debugf("COMMAND: %v %v", vboxManageCmd, strings.Join(args, " "))
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
...
}

vboxManageCmd は VirtualBox をインストールすると付属してくる VBoxManage という CLI のパスを指していて、これはコマンドラインから VirtualBox の VM を操作できるツールです。つまり、内部的には VBoxManage コマンドを呼び出してそちらに処理をおこなわせているということです。以下の Driver.Create() の実装を見ることで、どのように VBoxManage を呼び出しているのか見てみましょう。

Driver.Create() の実装

一番泥臭さがわかりやすそうだったので、 VirtualBox の VM を立ち上げる処理をおこなう Driver.Create() の実装を詳しく追ってみます。

drivers/virtualbox/virtualbox.go#L229-L417

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
func (d *Driver) Create() error {
// Boot2Docker の ISO を読み込む
b2dutils := mcnutils.NewB2dUtils(d.StorePath)
if err := b2dutils.CopyIsoToMachineDir(d.Boot2DockerURL, d.MachineName); err != nil {
return err
}

// CPU レベルでの仮想化機能が有効化チェックする
if d.IsVTXDisabled() {
// Let's log a warning to warn the user. When the vm is started, logs
// will be checked for an error anyway.
// We could fail right here but the method to check didn't prove being
// bulletproof.
log.Warn("This computer doesn't have VT-X/AMD-v enabled. Enabling it in the BIOS is mandatory.")
}

log.Infof("Creating VirtualBox VM...")

// import b2d VM if requested
if d.Boot2DockerImportVM != "" {
name := d.Boot2DockerImportVM

// make sure vm is stopped
_ = d.vbm("controlvm", name, "poweroff")

diskInfo, err := d.getVMDiskInfo()
if err != nil {
return err
}

if _, err := os.Stat(diskInfo.Path); err != nil {
return err
}

if err := d.vbm("clonehd", diskInfo.Path, d.diskPath()); err != nil {
return err
}

log.Debugf("Importing VM settings...")
vmInfo, err := d.getVMInfo()
if err != nil {
return err
}

d.CPU = vmInfo.CPUs
d.Memory = vmInfo.Memory

log.Debugf("Importing SSH key...")
keyPath := filepath.Join(mcnutils.GetHomeDir(), ".ssh", "id_boot2docker")
if err := mcnutils.CopyFile(keyPath, d.GetSSHKeyPath()); err != nil {
return err
}
} else {
log.Infof("Creating SSH key...")
if err := ssh.GenerateSSHKey(d.GetSSHKeyPath()); err != nil {
return err
}

log.Debugf("Creating disk image...")
if err := d.generateDiskImage(d.DiskSize); err != nil {
return err
}
}

// `VBoxManage createvm` コマンドで VM を作成する
if err := d.vbm("createvm",
"--basefolder", d.ResolveStorePath("."),
"--name", d.MachineName,
"--register"); err != nil {
return err
}

log.Debugf("VM CPUS: %d", d.CPU)
log.Debugf("VM Memory: %d", d.Memory)

cpus := d.CPU
if cpus < 1 {
cpus = int(runtime.NumCPU())
}
if cpus > 32 {
cpus = 32
}

// `VBoxManage modifyvm` コマンドで VM に各種設定を反映させる
if err := d.vbm("modifyvm", d.MachineName,
"--firmware", "bios",
"--bioslogofadein", "off",
"--bioslogofadeout", "off",
"--bioslogodisplaytime", "0",
"--biosbootmenu", "disabled",
"--ostype", "Linux26_64",
"--cpus", fmt.Sprintf("%d", cpus),
"--memory", fmt.Sprintf("%d", d.Memory),
"--acpi", "on",
"--ioapic", "on",
"--rtcuseutc", "on",
"--natdnshostresolver1", "off",
"--natdnsproxy1", "off",
"--cpuhotplug", "off",
"--pae", "on",
"--hpet", "on",
"--hwvirtex", "on",
"--nestedpaging", "on",
"--largepages", "on",
"--vtxvpid", "on",
"--accelerate3d", "off",
"--boot1", "dvd"); err != nil {
return err
}

if err := d.vbm("modifyvm", d.MachineName,
"--nic1", "nat",
"--nictype1", "82540EM",
"--cableconnected1", "on"); err != nil {
return err
}

if err := d.setupHostOnlyNetwork(d.MachineName); err != nil {
return err
}

// `VBoxManage storagectl` コマンドでストレージコントローラを付加する
if err := d.vbm("storagectl", d.MachineName,
"--name", "SATA",
"--add", "sata",
"--hostiocache", "on"); err != nil {
return err
}

// `VBoxManage storageattach` コマンドでストレージを VM にアタッチする
if err := d.vbm("storageattach", d.MachineName,
"--storagectl", "SATA",
"--port", "0",
"--device", "0",
"--type", "dvddrive",
"--medium", d.ResolveStorePath("boot2docker.iso")); err != nil {
return err
}

if err := d.vbm("storageattach", d.MachineName,
"--storagectl", "SATA",
"--port", "1",
"--device", "0",
"--type", "hdd",
"--medium", d.diskPath()); err != nil {
return err
}

// let VBoxService do nice magic automounting (when it's used)
if err := d.vbm("guestproperty", "set", d.MachineName, "/VirtualBox/GuestAdd/SharedFolders/MountPrefix", "/"); err != nil {
return err
}
if err := d.vbm("guestproperty", "set", d.MachineName, "/VirtualBox/GuestAdd/SharedFolders/MountDir", "/"); err != nil {
return err
}

// 共有ディレクトリ名を設定する
var shareName, shareDir string // TODO configurable at some point
switch runtime.GOOS {
case "windows":
shareName = "c/Users"
shareDir = "c:\\Users"
case "darwin":
shareName = "Users"
shareDir = "/Users"
// TODO "linux"
}

// 共有ディレクトリをマウントする
if shareDir != "" && !d.NoShare {
log.Debugf("setting up shareDir")
if _, err := os.Stat(shareDir); err != nil && !os.IsNotExist(err) {
return err
} else if !os.IsNotExist(err) {
if shareName == "" {
// parts of the VBox internal code are buggy with share names that start with "/"
shareName = strings.TrimLeft(shareDir, "/")
// TODO do some basic Windows -> MSYS path conversion
// ie, s!^([a-z]+):[/\\]+!\1/!; s!\\!/!g
}

// woo, shareDir exists! let's carry on!
if err := d.vbm("sharedfolder", "add", d.MachineName, "--name", shareName, "--hostpath", shareDir, "--automount"); err != nil {
return err
}

// enable symlinks
if err := d.vbm("setextradata", d.MachineName, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/"+shareName, "1"); err != nil {
return err
}
}
}

log.Infof("Starting VirtualBox VM...")

return d.Start()
}

このように VBoxManage createvmVBoxManage modifyvm コマンドなどに必要なフラグをつけて呼び出すことで、 VirtualBox ホストのプロビジョニング処理をおこなっていることがわかります。ここでは解説しませんが、最後に呼び出している Driver.Start() の中では VM の起動やネットワークの設定などがおこなわれます。なかなか泥臭さが伝わってきますね。

Amazon EC2 用の Driver の実装

もう1つだけ、 Amazon EC2 用の Driver の実装も見てみましょう。

drivers/amazonec2/amazonec2.go#L23-L69

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type Driver struct {
*drivers.BaseDriver
Id string
AccessKey string
SecretKey string
SessionToken string
Region string // default: us-east-1
AMI string // default: ami-615cb725 (Ubuntu 14.04)
SSHKeyID int
KeyName string
InstanceId string
InstanceType string // default: t2.micro
PrivateIPAddress string
SecurityGroupId string
SecurityGroupName string
ReservationId string
RootSize int64
IamInstanceProfile string
VpcId string
SubnetId string
Zone string // default: a
keyPath string
RequestSpotInstance bool
SpotPrice string
PrivateIPOnly bool
UsePrivateIP bool
Monitoring bool
}

こちらの構造体では、 EC2 に必要な access key や secret key, region, AMI などの情報を保持していることがわかります。具体的なメソッドの実装は解説しませんが、基本的には AWS SDK for Go を介して対応する EC2 の API を呼び出しています。

Summary

僕が今回 Driver Plugins の仕組みを追ってみようと思ったのは、「コンパイル型の静的言語でプラグイン機構を実現するにはどうすればよいのか?」ということを考えていたからでした。スクリプト言語であればプラグイン機構は簡単に実現できるのですが、 Go のように単一バイナリになり、 DLL のような仕組みもない言語において、プラグイン機構を実現する仕組みがわからなかったのです。 RPC を使うというのが常套手段なのはのちのちわかったのですが、具体的な実装を追ってみて腑に落ちた感じです。 Docker Machine のコードは比較的読みやすいので、とても勉強になりました。