※この記事は 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 のコードは比較的読みやすいので、とても勉強になりました。