※この記事は 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 $ 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 REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE $ docker pull centos ... Status: Downloaded newer image for  centos:latest $ 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 $ eval  "$(docker-machine env default) "  $ 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 $ 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 $ 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 $ 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 $ 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 $ 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 $ 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 &; 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 $ 
 
さすがにこんな早さではメインプロセスと通信するのに十分な時間があるとは思えません。ということでこのことも含めて、 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                  	Client          *InternalClient           } 
 
この構造体がプラグインへの 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 func  NewRpcClientDriver (rawDriverData []byte , driverName string )  (*RpcClientDriver, error)   {	mcnName := ""  	 	p, err := localbinary.NewLocalBinaryPlugin(driverName) 	if  err != nil  { 		return  nil , err 	} 	 	go  func ()   { 		if  err := p.Serve(); err != nil  { 			 			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 	} 	 	c := &RpcClientDriver{ 		Client:          NewInternalClient(rpcclient), 		heartbeatDoneCh: make (chan  bool ), 	} 	 	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) 	 	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) 	 	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 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  (	 	heartbeatTimeout = 500  * time.Millisecond ) func  RegisterDriver (d drivers.Driver)   {	 	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 ) 	 	rpcd := rpcdriver.NewRpcServerDriver(d) 	rpc.Register(rpcd) 	rpc.HandleHTTP() 	 	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 ) 		 		case  <-rpcd.HeartbeatCh: 			continue  		 		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  	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 interfacedrivers.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 type  Driver interface  {	 	Create() error 	 	DriverName() string  	 	 	GetCreateFlags() []mcnflag.Flag 	 	 	GetIP() (string , error) 	 	GetMachineName() string  	 	GetSSHHostname() (string , error) 	 	GetSSHKeyPath() string  	 	GetSSHPort() (int , error) 	 	GetSSHUsername() string  	 	 	GetURL() (string , error) 	 	GetState() (state.State, error) 	 	Kill() error 	 	PreCreateCheck() error 	 	Remove() error 	 	 	Restart() error 	 	 	SetConfigFromFlags(opts DriverOptions) error 	 	Start() error 	 	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                 	*drivers.BaseDriver 	CPU                 int      	Memory              int      	DiskSize            int      	Boot2DockerURL      string   	Boot2DockerImportVM string   	... 	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)   {	 	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   {	 	b2dutils := mcnutils.NewB2dUtils(d.StorePath) 	if  err := b2dutils.CopyIsoToMachineDir(d.Boot2DockerURL, d.MachineName); err != nil  { 		return  err 	} 	 	if  d.IsVTXDisabled() { 		 		 		 		 		log.Warn("This computer doesn't have VT-X/AMD-v enabled. Enabling it in the BIOS is mandatory." ) 	} 	log.Infof("Creating VirtualBox VM..." ) 	 	if  d.Boot2DockerImportVM != ""  { 		name := d.Boot2DockerImportVM 		 		_ = 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 		} 	} 	 	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  	} 	 	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 	} 	 	if  err := d.vbm("storagectl" , d.MachineName, 		"--name" , "SATA" , 		"--add" , "sata" , 		"--hostiocache" , "on" ); err != nil  { 		return  err 	} 	 	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 	} 	 	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   	switch  runtime.GOOS { 	case  "windows" : 		shareName = "c/Users"  		shareDir = "c:\\Users"  	case  "darwin" : 		shareName = "Users"  		shareDir = "/Users"  		 	} 	 	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 == ""  { 				 				shareName = strings.TrimLeft(shareDir, "/" ) 				 				 			} 			 			if  err := d.vbm("sharedfolder" , "add" , d.MachineName, "--name" , shareName, "--hostpath" , shareDir, "--automount" ); err != nil  { 				return  err 			} 			 			if  err := d.vbm("setextradata" , d.MachineName, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/" +shareName, "1" ); err != nil  { 				return  err 			} 		} 	} 	log.Infof("Starting VirtualBox VM..." ) 	return  d.Start() } 
 
このように VBoxManage createvm や VBoxManage 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   	AMI                 string   	SSHKeyID            int  	KeyName             string  	InstanceId          string  	InstanceType        string   	PrivateIPAddress    string  	SecurityGroupId     string  	SecurityGroupName   string  	ReservationId       string  	RootSize            int64  	IamInstanceProfile  string  	VpcId               string  	SubnetId            string  	Zone                string   	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 のコードは比較的読みやすいので、とても勉強になりました。