Vagrant+Docker Machine Generic Driverを使ってDockerホスト・Swarmクラスタを構築する

[Docker Machine](https://docs.docker.com/machine/)を使用すると簡単に様々な環境にDockerホスト・Swarmクラスタを作成し、ローカルのDockerクライアントから使用できるよう設定することができます。

Docker Machine単体でも使えますが[Vagrant](https://www.vagrantup.com/)と組み合わせて使用することでDocker Machineが未対応の環境にホストやSwarmクラスタを構築することができます。

# TL;DR

VagrantとDocker Machineを組み合わせて使うと以下のようなメリットがあります。

* Docker Machineが未対応の環境にDocker環境を構築できる
* ```vagrant up```コマンドで複数ノードのDockerホスト・Swarmクラスタを構築・削除できる
* VagrantのSynced Folderでリモートのホストとファイル・ディレクトリを同期してくれる

# 環境

動作確認は以下の環境で行いました。

* Mac OSX 10.10.1
* Vagrant 1.7.4
* docker-machine 0.4.1
* vagrant-triggers 0.5.1
* vagrant-cloudstack 1.2.0

# 準備

Vagrant, Docker, Docker Machineをインストールしておきます。
Mac, Windowsであれば[Docker Toolbox](https://www.docker.com/toolbox)を使うと関連ツールをまとめてインストールしてくれるので便利です。

仮想マシン作成・削除のタイミングでDocker Machineのコマンドを実行するため[vagrant-triggers](https://github.com/emyl/vagrant-triggers)をインストールしておきます。

```bash
$ vagrant plugin install vagrant-triggers
```

# VirtualBoxにDockerホストを構築

まずVirtualBoxにDockerホストを構築してみます。

Vagrantfileは次のようになります。

```rb:Vagrantfile
Vagrant.configure(2) do |config|
  # vagrant-triggersをProvisionerとして使用し、
  # 作成した仮想マシンに対してdocker-machine createを実行
  config.vm.provision "trigger" do |trigger|
    trigger.fire do
      # docker-machine statusのexitstatusが0の場合はdocker-machine create実行済み
      `docker-machine status #{@machine.name}`
      if $?.exitstatus != 0
        # 使用するIPとポートを取得
        # VirtualBoxの場合には一度仮想マシンにログインしプライベートネットワークのIPを取得
        # その他のProviderでは@machine.ssh_infoからIP、ポートを取得できる
        if @machine.provider_name == :virtualbox
          ip = `vagrant ssh #{@machine.name} -c "ip addr show dev eth1 | grep -w inet"`
               .match(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/)[0]
          port = 22
        else
          ip = @machine.ssh_info[:host]
          port = @machine.ssh_info[:port]
        end

        # docker-machine createを実行
        # SWARM_DISCOVERY_TOKENが設定されている場合Swarmクラスタ用のオプションを付ける
        run "docker-machine create -d generic \
            --generic-ip-address #{ip} \
            --generic-ssh-key #{@machine.ssh_info[:private_key_path][0]} \
            --generic-ssh-port #{port} \
            --generic-ssh-user #{@machine.ssh_info[:username]} \
            #{"--swarm" unless ENV["SWARM_DISCOVERY_TOKEN"].nil?} \
            #{"--swarm-master" if !ENV["SWARM_DISCOVERY_TOKEN"].nil? && @machine.name.to_s.include?("master")} \
            #{"--swarm-discovery token://#{ENV["SWARM_DISCOVERY_TOKEN"]}" unless ENV["SWARM_DISCOVERY_TOKEN"].nil?} \
            #{@machine.name}"
      end
    end
  end

  # 仮想マシン削除時にはdocker-machine rmを実行しdocker-machineからも削除する
  config.trigger.before :destroy do
    # docker-machine statusのリターンコードが0でない場合は
    # docker-machineに登録されていないか異常が発生しているため削除をスキップ
    `docker-machine status #{@machine.name}`
    if $?.exitstatus == 0
      run "docker-machine rm #{@machine.name}"
    end
  end

  # VirtualBoxを使用する際の設定
  config.vm.provider :virtualbox do |virtualbox, override|
    # Boxは現状Ubuntu 14.04がうまくいきやすい
    override.vm.box = "boxcutter/ubuntu1404"
    # VirtualBoxの場合はDockerクライアントからの接続にプライベートネットワークを使用
    override.vm.network "private_network", type: "dhcp"
  end

  # 作成する仮想マシンを定義
  config.vm.define "docker01" do |d|
  end
end
```

```vagrant up``` で仮想マシンが作成され、Docker Machineに登録されます。

```bash
$ vagrant up
```

登録された仮想マシンは```docker-machine ls``` で確認できます。

```bash
$ docker-machine ls
NAME      ACTIVE   DRIVER       STATE     URL                         SWARM   
docker01              generic      Running   tcp://172.28.128.19:2376 
```

```docker-machine env``` の出力を```eval``` するとDockerクライアントから使用できるようになります。

```bash
$ eval "$(docker-machine env docker01)"
$ docker run --rm hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
535020c3e8ad: Pull complete 
af340544ed62: Pull complete 
Digest: sha256:a68868bfe696c00866942e8f5ca39e3e31b79c1e50feaee4ce5e28df2f051d5c
Status: Downloaded newer image for hello-world:latest

Hello from Docker.
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker Hub account:
 https://hub.docker.com

For more examples and ideas, visit:
 https://docs.docker.com/userguide/

```

```vagrant destroy``` で仮想マシンを削除すると、docker-machineからも削除されます。

```bash
$ vagrant destroy
$ docker-machine ls
```


# VirtualBoxにSwarmクラスタを構築

上記のVagrantfileを使ってSwarmクラスタを構築することも可能です。

今回はmaster1台、agent2台のクラスタを作成してみます。
次のようにVagrantfileの仮想マシンの定義を変更します。（名前に「master」を含む仮想マシンをmasterとして使用するようになっています。）

```rb:Vagrantfile
Vagrant.configure(2) do |config|
  # vagrant-triggersをProvisionerとして使用し、
  # 作成した仮想マシンに対してdocker-machine createを実行
  config.vm.provision "trigger" do |trigger|
    trigger.fire do
      # docker-machine statusのexitstatusが0の場合はdocker-machine create実行済み
      `docker-machine status #{@machine.name}`
      if $?.exitstatus != 0
        # 使用するIPとポートを取得
        # VirtualBoxの場合には一度仮想マシンにログインしプライベートネットワークのIPを取得
        # その他のProviderでは@machine.ssh_infoからIP、ポートを取得できる
        if @machine.provider_name == :virtualbox
          ip = `vagrant ssh #{@machine.name} -c "ip addr show dev eth1 | grep -w inet"`
               .match(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/)[0]
          port = 22
        else
          ip = @machine.ssh_info[:host]
          port = @machine.ssh_info[:port]
        end

        # docker-machine createを実行
        # SWARM_DISCOVERY_TOKENが設定されている場合Swarmクラスタ用のオプションを付ける
        run "docker-machine create -d generic \
            --generic-ip-address #{ip} \
            --generic-ssh-key #{@machine.ssh_info[:private_key_path][0]} \
            --generic-ssh-port #{port} \
            --generic-ssh-user #{@machine.ssh_info[:username]} \
            #{"--swarm" unless ENV["SWARM_DISCOVERY_TOKEN"].nil?} \
            #{"--swarm-master" if !ENV["SWARM_DISCOVERY_TOKEN"].nil? && @machine.name.to_s.include?("master")} \
            #{"--swarm-discovery token://#{ENV["SWARM_DISCOVERY_TOKEN"]}" unless ENV["SWARM_DISCOVERY_TOKEN"].nil?} \
            #{@machine.name}"
      end
    end
  end

  # 仮想マシン削除時にはdocker-machine rmを実行しdocker-machineからも削除する
  config.trigger.before :destroy do
    # docker-machine statusのリターンコードが0でない場合は
    # docker-machineに登録されていないか異常が発生しているため削除をスキップ
    `docker-machine status #{@machine.name}`
    if $?.exitstatus == 0
      run "docker-machine rm #{@machine.name}"
    end
  end

  # VirtualBoxを使用する際の設定
  config.vm.provider :virtualbox do |virtualbox, override|
    # Boxは現状Ubuntu 14.04がうまくいきやすい
    override.vm.box = "boxcutter/ubuntu1404"
    # VirtualBoxの場合はDockerクライアントからの接続にプライベートネットワークを使用
    override.vm.network "private_network", type: "dhcp"
  end

  # 作成する仮想マシンを定義
  config.vm.define "master01" do |d|
  end

  config.vm.define "agent01" do |d|
  end

  config.vm.define "agent02" do |d|
  end
end
```

仮想マシンを作成する前にSwarmクラスタを構築するためのトークンを取得しておきます。

```bash
$ SWARM_DISCOVERY_TOKEN=$(curl -s -X POST https://discovery.hub.docker.com/v1/clusters)
```

この値を使って次のように```vagrant up```コマンドを実行するとSwarmクラスタが構築されます。

```bash
$ SWARM_DISCOVERY_TOKEN=$SWARM_DISCOVERY_TOKEN vagrant up
$ docker-machine ls
NAME       ACTIVE   DRIVER       STATE     URL                         SWARM
agent01             generic      Running   tcp://172.28.128.21:2376    master01
agent02             generic      Running   tcp://172.28.128.22:2376    master01  
master01   *        generic      Running   tcp://172.28.128.20:2376    master01 (master)
```

```docker-machine env --swarm``` を使って環境変数を取得・設定します。

```bash
$ eval "$(docker-machine env --swarm master01)"
```

うまくいっていれば```docker info``` コマンドで3ノードあることが確認できます。

```bash
$ docker info
Containers: 4
Images: 3
Role: primary
Strategy: spread
Filters: affinity, health, constraint, port, dependency
Nodes: 3
 agent01: 172.28.128.24:2376
  └ Containers: 1
  └ Reserved CPUs: 0 / 1
  └ Reserved Memory: 0 B / 514.2 MiB
  └ Labels: executiondriver=native-0.2, kernelversion=3.16.0-30-generic, operatingsystem=Ubuntu 14.04.2 LTS, provider=generic, storagedriver=aufs
 agent02: 172.28.128.25:2376
  └ Containers: 1
  └ Reserved CPUs: 0 / 1
  └ Reserved Memory: 0 B / 514.2 MiB
  └ Labels: executiondriver=native-0.2, kernelversion=3.16.0-30-generic, operatingsystem=Ubuntu 14.04.2 LTS, provider=generic, storagedriver=aufs
 master01: 172.28.128.23:2376
  └ Containers: 2
  └ Reserved CPUs: 0 / 1
  └ Reserved Memory: 0 B / 514.2 MiB
  └ Labels: executiondriver=native-0.2, kernelversion=3.16.0-30-generic, operatingsystem=Ubuntu 14.04.2 LTS, provider=generic, storagedriver=aufs
CPUs: 3
Total Memory: 1.507 GiB
Name: a4fbe0415d5f
```

```docker ps -a```でswarm用のコンテナが起動していることを確認できます。

```bash
$ docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                      PORTS                                    NAMES
9d3924175431        swarm:latest        "/swarm join --advert"   5 minutes ago       Up 5 minutes                2375/tcp                                 master01/swarm-agent
a4fbe0415d5f        swarm:latest        "/swarm manage --tlsv"   5 minutes ago       Up 5 minutes                2375/tcp, 172.28.128.23:3376->3376/tcp   master01/swarm-agent-master
056fbce9741c        swarm:latest        "/swarm join --advert"   16 minutes ago      Up 16 minutes               2375/tcp                                 agent02/swarm-agent
3da76b011711        swarm:latest        "/swarm join --advert"   18 minutes ago      Up 18 minutes               2375/tcp                                 agent01/swarm-agent 
```    

Swarmクラスタでも```docker run``` コマンドでコンテナを起動することができます。        

```bash
$ docker run --rm hello-world

Hello from Docker.
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker Hub account:
 https://hub.docker.com

For more examples and ideas, visit:
 https://docs.docker.com/userguide/
```


# CloudStack/IDCFクラウドの場合

Vagrantを仮想マシン作成に利用することでDocker Machineが未対応の環境にもDocker環境を構築することができます。

例としてIDCFクラウドのCloudStack上にSwarmクラスタを構築してみます。

**2015/12/04 追記:** CloudStackドライバを作成しました。Synced Folderが不要であればCloudStackドライバを使用したほうが手軽です。([Docker Machine CloudStack Driverを使ってDockerホスト・Swarmクラスタを構築する - Qiita](http://qiita.com/atsaki/items/b3064d3f68f8b84acd3d))

CloudStackを使うためのプラグインをインストールします。

```bash
$ vagrant plugin install vagrant-cloudstack
```

Docker Machine Generic DriverはDocker用のポートとして2376を固定で使用するためホストごとに異なるパブリックIPが必要になります。今回は追加IPの料金を節約するため3台の仮想マシンをtesla, henry, pascalゾーンに作成しソースIPを使用します。

Vagrantfileは以下のようになります。環境変数はお使いの環境に合わせて適切に設定してください。

```rb:Vagrantfile
Vagrant.configure(2) do |config|
  # vagrant-triggersをProvisionerとして使用し、
  # 作成した仮想マシンに対してdocker-machine createを実行
  config.vm.provision "trigger" do |trigger|
    trigger.fire do
      # docker-machine statusのexitstatusが0の場合はdocker-machine create実行済み
      `docker-machine status #{@machine.name}`
      if $?.exitstatus != 0
        # 使用するIPとポートを取得
        # VirtualBoxの場合には一度仮想マシンにログインしプライベートネットワークのIPを取得
        # その他のProviderでは@machine.ssh_infoからIP、ポートを取得できる
        if @machine.provider_name == :virtualbox
          ip = `vagrant ssh #{@machine.name} -c "ip addr show dev eth1 | grep -w inet"`
               .match(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/)[0]
          port = 22
        else
          ip = @machine.ssh_info[:host]
          port = @machine.ssh_info[:port]
        end

        # docker-machine createを実行
        # SWARM_DISCOVERY_TOKENが設定されている場合Swarmクラスタ用のオプションを付ける
        run "docker-machine create -d generic \
            --generic-ip-address #{ip} \
            --generic-ssh-key #{@machine.ssh_info[:private_key_path][0]} \
            --generic-ssh-port #{port} \
            --generic-ssh-user #{@machine.ssh_info[:username]} \
            #{"--swarm" unless ENV["SWARM_DISCOVERY_TOKEN"].nil?} \
            #{"--swarm-master" if !ENV["SWARM_DISCOVERY_TOKEN"].nil? && @machine.name.to_s.include?("master")} \
            #{"--swarm-discovery token://#{ENV["SWARM_DISCOVERY_TOKEN"]}" unless ENV["SWARM_DISCOVERY_TOKEN"].nil?} \
            #{@machine.name}"
      end
    end
  end

  # 仮想マシン削除時にはdocker-machine rmを実行しdocker-machineからも削除する
  config.trigger.before :destroy do
    # docker-machine statusのリターンコードが0でない場合は
    # docker-machineに登録されていないか異常が発生しているため削除をスキップ
    `docker-machine status #{@machine.name}`
    if $?.exitstatus == 0
      run "docker-machine rm #{@machine.name}"
    end
  end

  # VirtualBoxを使用する際の設定
  config.vm.provider :virtualbox do |virtualbox, override|
    # Boxは現状Ubuntu 14.04がうまくいきやすい
    override.vm.box = "boxcutter/ubuntu1404"
    # VirtualBoxの場合はDockerクライアントからの接続にプライベートネットワークを使用
    override.vm.network "private_network", type: "dhcp"
  end

  # IDCFクラウドを使用する際の設定
  config.vm.provider :cloudstack do |cloudstack, override|
    # Ubuntu 14.04のテンプレートを使用
    override.vm.box  = "Ubuntu Server 14.04 LTS 64-bit"

    cloudstack.host    = "compute.jp-east.idcfcloud.com"
    cloudstack.path    = "/client/api"
    cloudstack.port    = "443"
    cloudstack.scheme  = "https"

    cloudstack.api_key    = "#{ENV['CLOUDSTACK_API_KEY']}"
    cloudstack.secret_key = "#{ENV['CLOUDSTACK_SECRET_KEY']}"

    cloudstack.service_offering_name = "light.S1"

    # Firewallを開放する際に許可するCIDR
    # vagrant-cloudstack 1.2.0からopenfirewallで開放されるルールに自動で設定される
    cloudstack.pf_trusted_networks = "#{ENV['CLOUDSTACK_PF_TRUSTED_NETWORKS'] || "0.0.0.0/0"}"

    cloudstack.keypair            = "#{ENV['CLOUDSTACK_SSH_KEYPAIR']}"
    override.ssh.private_key_path = "#{ENV['VAGRANT_SSH_PRIVATE_KEY']}"

    # requirettyが有効なテンプレートを使う際の設定
    # http://qiita.com/atsaki/items/467ce1569fbc2cb11cb8
    override.ssh.username         = "root"
    override.ssh.pty              = true 
    config.vm.synced_folder ".", "/vagrant", disabled: true
  end

  # 作成する仮想マシンを定義
  config.vm.define "master01" do |vm|
    vm.vm.provider :cloudstack do |cloudstack|
      cloudstack.zone_name = "#{ENV['MASTER01_ZONE']}"
      # SSH用のポートフォワーディングを設定
      # vagrant-cloudstack 1.2.0から自動でランダムなポートを使ってくれるようになった
      cloudstack.pf_ip_address   = "#{ENV['MASTER01_PUBLIC_IP_ADDRESS']}"
      # Docker, Swarm用のポートのポートフォワーディングを設定
      cloudstack.port_forwarding_rules = [2376, 3376].map { |port|
        {
          :ipaddress => "#{ENV['MASTER01_PUBLIC_IP_ADDRESS']}",
          :protocol => "tcp",
          :publicport => port,
          :privateport  => port,
          :openfirewall => true
        }
      }
    end
  end

  config.vm.define "agent01" do |vm|
    vm.vm.provider :cloudstack do |cloudstack|
      cloudstack.zone_name = "#{ENV['AGENT01_ZONE']}"
      cloudstack.pf_ip_address   = "#{ENV['AGENT01_PUBLIC_IP_ADDRESS']}"
      cloudstack.port_forwarding_rules = [2376].map { |port|
        {
          :ipaddress => "#{ENV['AGENT01_PUBLIC_IP_ADDRESS']}",
          :protocol => "tcp",
          :publicport => port,
          :privateport  => port,
          :openfirewall => true
        }
      }
    end
  end

  config.vm.define "agent02" do |vm|
    vm.vm.provider :cloudstack do |cloudstack|
      cloudstack.zone_name = "#{ENV['AGENT02_ZONE']}"
      cloudstack.pf_ip_address   = "#{ENV['AGENT02_PUBLIC_IP_ADDRESS']}"
      cloudstack.port_forwarding_rules = [2376].map { |port|
        {
          :ipaddress => "#{ENV['AGENT02_PUBLIC_IP_ADDRESS']}",
          :protocol => "tcp",
          :publicport => port,
          :privateport  => port,
          :openfirewall => true
        }
      }
    end
  end
end
```

トークンを取得しなおして```vagrant up```を実行するとIDCFクラウド上にSwarmクラスタが構築されます。
（```--no-parallel``` をつけないと```docker-machine create```が全ての仮想マシンに対して実行されず正しくクラスタが構築できなかった。）

```bash
$ SWARM_DISCOVERY_TOKEN=$(curl -s -X POST https://discovery.hub.docker.com/v1/clusters)
$ SWARM_DISCOVERY_TOKEN=$SWARM_DISCOVERY_TOKEN vagrant up --provider=cloudstack --no-parallel
```

# Synced Folderとボリュームマウント

Vagrantは作成した仮想マシンと[Synced Folder](https://docs.vagrantup.com/v2/synced-folders/)でディレクトリ・ファイルを同期することができます。

デフォルトでローカルのカレントディレクトリをリモートの/vagrantと同期しているため、次のように```-v``` オプションを使ってローカルのファイルをリモートの仮想マシン上で利用することができます。

```bash
$ docker run  --rm -v /vagrant:/data  ubuntu head /data/Vagrantfile
Vagrant.configure(2) do |config|
  # vagrant-triggersをProvisionerとして使用し、
  # 作成した仮想マシンに対してdocker-machine createを実行
  config.vm.provision "trigger" do |trigger|
    trigger.fire do
      # docker-machine statusのexitstatusが0の場合はdocker-machine create実行済み
      `docker-machine status #{@machine.name}`
      if $?.exitstatus != 0
        # 使用するIPとポートを取得
        # VirtualBoxの場合には一度仮想マシンにログインしプライベートネットワークのIPを取得
```

rsyncを使用している場合、同期はローカルからリモートへの一方向です。Swarmクラスタのホスト間の同期は行われないので、何れかのホストで変更を加えるとホストごとに中身が異なる状態になります。