モチベーション

Docker Composeで複数コンテナを管理するでは、複数コンテナからなるシステムをDocker Composeで一括管理可能にした。また、Docker Swarmで複数Dockerホストからクラスタを作るでは、複数のDockerホストからなるコンテナクラスタをDocker Swarmによって実現した。しかし、記事のようにディスカバリサービスとしてDockerHubを利用するのは本番環境では考えられない。また、コンテナ間通信では非推奨のlinkを用いていた。本記事では、これらをより本番での運用に近づけるために、ディスカバリサービスにConsulを、コンテナ間通信にoverlay networkを用いてマルチホストコンテナクラスタを構築する。

クラスタ全体構成

20151220_docker_swarm_with_overlay_nw_architecture

コンテナクラスタの構築手順

Consulを使ったDocker Swarmクラスタの構築手順をさらっとおさらいする。Consulを起動するDockerホストにて以下を実行する。

➤  docker run --name consul -d \
    -p "8500:8500" \
    -h "consul" \
    --restart=always \
    progrium/consul -server -bootstrap

Swarmクラスタを構築する。まずは、Swarm Masterから。

➤  docker-machine create -d virtualbox \
                      --swarm --swarm-master \
                      --swarm-discovery="consul://$(docker-machine ip consul):8500" \
                      --engine-opt="cluster-store=consul://$(docker-machine ip consul):8500" \
                      --engine-opt="cluster-advertise=eth1:2376" swarm-master

つづいて、Swarm Nodeを構築する。

➤  docker-machine create -d virtualbox \
                         --swarm --swarm-discovery="consul://$(docker-machine ip consul):8500" \
                         --engine-opt="cluster-store=consul://$(docker-machine ip consul):8500" \
                         --engine-opt="cluster-advertise=eth1:2376" swarm-node-01

➤  docker-machine create -d virtualbox \
                         --swarm --swarm-discovery="consul://$(docker-machine ip consul):8500" \
                         --engine-opt="cluster-store=consul://$(docker-machine ip consul):8500" \
                         --engine-opt="cluster-advertise=eth1:2376" swarm-node-02

ここまでの作業をVagrantで自動構築するには以下のファイルを使う。(正確には利用するドライバを変更しているので多少異なる)

Vagrant.configure(2) do |config|
  config.vm.define "consul" do |consul|
    consul.vm.provider :virtualbox do |virtualbox, override|
      virtualbox.name = "consul"
      override.vm.boot_timeout = 7200
      override.vm.box = "takanabe/ubuntu1504"
      override.vm.network "private_network", type: "dhcp"
    end

    consul.vm.provision "trigger" do |trigger|
      trigger.fire do
        puts "======= IN TRIGGER SECTION !!======="
        puts "Creating Consul ..."
        `docker-machine status #{@machine.name}`
        if $?.exitstatus != 0
          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

          run "docker-machine -D 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]} \
          #{@machine.name}"




          consul_config = `docker-machine config #{@machine.name}`
          run "docker #{consul_config} run --name consul -d \
                           --publish 8500:8500 \
                           --hostname consul \
                           --restart=always \
                           progrium/consul -server -bootstrap"

        end
      end
    end

    consul.trigger.before :destroy do
      `docker-machine status #{@machine.name}`
      if $?.exitstatus == 0
        run "docker-machine rm #{@machine.name}"
      end
    end
  end #consul end

  config.vm.define "swarm-master" do |master|
    master.vm.provider :virtualbox do |virtualbox, override|
      virtualbox.name = "swarm-master"
      override.vm.boot_timeout = 7200
      override.vm.box = "takanabe/ubuntu1504"
      override.vm.network "private_network", type: "dhcp"
    end

    master.vm.hostname = "swarm-master"

    master.vm.provision "trigger" do |trigger|
      trigger.fire do
        puts "======= IN TRIGGER SECTION !!======="
        puts "Creating Swarm Master ..."
        `docker-machine status #{@machine.name}`
        if $?.exitstatus != 0
          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

          consul_server_address  = `docker-machine ip consul`.chomp
          run "docker-machine -D 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 \
            --swarm-master \
            --swarm-discovery=consul://#{consul_server_address}:8500 \
            --engine-opt=cluster-store=consul://#{consul_server_address}:8500 \
            --engine-opt=cluster-advertise=eth1:2376 \
          #{@machine.name}"

        end
      end
    end

    master.trigger.before :destroy do
      `docker-machine status #{@machine.name}`
      if $?.exitstatus == 0
        run "docker-machine rm #{@machine.name}"
      end
    end
  end #master end

  config.vm.define "swarm-node01" do |node01|
    node01.vm.provider :virtualbox do |virtualbox, override|
      virtualbox.name = "swarm-node01"
      override.vm.boot_timeout = 7200
      override.vm.box = "takanabe/ubuntu1504"
      override.vm.network "private_network", type: "dhcp"
    end

    node01.vm.hostname = "swarm-node01"

    node01.vm.provision "trigger" do |trigger|
      trigger.fire do
        puts "======= IN TRIGGER SECTION !!======="
        puts "Creating Swarm node01..."
        `docker-machine status #{@machine.name}`
        if $?.exitstatus != 0
          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

          consul_server_address  = `docker-machine ip consul`.chomp
          run "docker-machine -D 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 \
            --swarm-discovery=consul://#{consul_server_address}:8500 \
            --engine-opt=cluster-store=consul://#{consul_server_address}:8500 \
            --engine-opt=cluster-advertise=eth1:2376 \
          #{@machine.name}"

        end
      end
    end

    node01.trigger.before :destroy do
      `docker-machine status #{@machine.name}`
      if $?.exitstatus == 0
        run "docker-machine rm #{@machine.name}"
      end
    end
  end #node01 end

  config.vm.define "swarm-node02" do |node02|
    node02.vm.provider :virtualbox do |virtualbox, override|
      virtualbox.name = "swarm-node02"
      override.vm.boot_timeout = 7200
      override.vm.box = "takanabe/ubuntu1504"
      override.vm.network "private_network", type: "dhcp"
    end

    node02.vm.hostname = "swarm-node02"

    node02.vm.provision "trigger" do |trigger|
      trigger.fire do
        puts "======= IN TRIGGER SECTION !!======="
        puts "Creating Swarm node02..."
        `docker-machine status #{@machine.name}`
        if $?.exitstatus != 0
          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

          consul_server_address  = `docker-machine ip consul`.chomp
          run "docker-machine -D 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 \
            --swarm-discovery=consul://#{consul_server_address}:8500 \
            --engine-opt=cluster-store=consul://#{consul_server_address}:8500 \
            --engine-opt=cluster-advertise=eth1:2376 \
          #{@machine.name}"
        end
      end
    end

    node02.trigger.before :destroy do
      `docker-machine status #{@machine.name}`
      if $?.exitstatus == 0
        run "docker-machine rm #{@machine.name}"
      end
    end
  end #node02 end
end

以降はこのVagrantfileでクラスタを構築したことを前提で動作確認をする。

クラスタの状態を確認する

Dockerホストを確認する。

➤  docker-machine ls
NAME           ACTIVE   DRIVER    STATE     URL                        SWARM                   ERRORS
consul         -        generic   Running   tcp://172.28.128.3:2376
swarm-master   -        generic   Running   tcp://172.28.128.4:2376    swarm-master (master)
swarm-node01   -        generic   Running   tcp://172.28.128.5:2376    swarm-master
swarm-node02   -        generic   Running   tcp://172.28.128.6:2376    swarm-master

クラスタの情報を確認するにはswarm-masterの環境変数を使う。

➤  eval "$(docker-machine env --swarm swarm-master)"
➤  docker info
Containers: 4
Images: 3
Role: primary
Strategy: spread
Filters: health, port, dependency, affinity, constraint
Nodes: 3
 swarm-master: 172.28.128.4:2376
  └ Status: Healthy
  └ Containers: 2
  └ Reserved CPUs: 0 / 1
  └ Reserved Memory: 0 B / 513.5 MiB
  └ Labels: executiondriver=native-0.2, kernelversion=3.19.0-37-generic, operatingsystem=Ubuntu 15.04, provider=generic, storagedriver=aufs
 swarm-node01: 172.28.128.5:2376
  └ Status: Healthy
  └ Containers: 1
  └ Reserved CPUs: 0 / 1
  └ Reserved Memory: 0 B / 513.5 MiB
  └ Labels: executiondriver=native-0.2, kernelversion=3.19.0-37-generic, operatingsystem=Ubuntu 15.04, provider=generic, storagedriver=aufs
 swarm-node02: 172.28.128.6:2376
  └ Status: Healthy
  └ Containers: 1
  └ Reserved CPUs: 0 / 1
  └ Reserved Memory: 0 B / 513.5 MiB
  └ Labels: executiondriver=native-0.2, kernelversion=3.19.0-37-generic, operatingsystem=Ubuntu 15.04, provider=generic, storagedriver=aufs
CPUs: 3
Total Memory: 1.505 GiB
Name: 4bb9ee961926

consulが管理しているSwarmノードの情報を知らべられる。

➤  docker run swarm list consul://$(docker-machine ip consul):8500
172.28.128.4:2376
172.28.128.5:2376
172.28.128.6:2376

期待している環境になっていることが確認できた。

overlay nw + composeでシステムを構築する

RedmineとPostGresSQLのコンテナをComposeを用いてSwarm nodeに分散配置して、マルチホスト環境でコンテナを管理する。利用するcomposeファイルは以下のとおり。

  cat docker-compose.yml
db:
  image: postgres:9.5
  environment:
    - POSTGRES_PASSWORD=secret
    - POSTGRES_USER=redmine
    - constraint:node==swarm-node02
redmine:
  image: redmine:2.6.8
  volumes:
    - /srv/docker/redmine/redmine:/home/redmine/data
  ports:
    - "8080:3000"
  environment:
    - constraint:node==swarm-node01

前回と異なり、RedmineとPostGresSQL間の通信にlinkオプションをつけていないのに注目して欲しい。これはコンテナ間通信はVXLANによる overlayネットワークに任せるためだ。 このoverlayネットワークを利用するにはdocker-composeを実行する際に--x-networking--x-network-driver overlayオプションを付ける。

➤  docker-compose --x-networking --x-network-driver overlay up -d
Creating network "composeandswarm" with driver "overlay"
Pulling redmine (redmine:2.6.8)...
swarm-node02: Pulling redmine:2.6.8... : downloaded
swarm-master: Pulling redmine:2.6.8... : downloaded
swarm-node01: Pulling redmine:2.6.8... : downloaded
Creating composeandswarm_redmine_1
Pulling db (postgres:9.5)...
swarm-node02: Pulling postgres:9.5... : downloaded
swarm-master: Pulling postgres:9.5... : downloaded
swarm-node01: Pulling postgres:9.5... : downloaded
Creating composeandswarm_db_1

composeで管理しているコンテナを確認する。

➤  docker-compose ps
        Name                   Command                  State                   Ports
---------------------------------------------------------------------------------------------
composeandswarm_db_1    /docker-entrypoint.sh   Up                      5432/tcp
                        postgres
composeandswarm_redmi   /docker-entrypoint.sh   Up                      172.28.128.5:8080->30
ne_1

Redmine、PostGreSQLコンテナが指定したnode01,node02でそれぞれ起動していることを確認する。

➤  docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                         NAMES
f9995dc07765        postgres:9.5        "/docker-entrypoint.s"   About an hour ago   Up About an hour    5432/tcp                      swarm-node02/composeandswarm_db_1
94ec7d822ac7        redmine:2.6.8       "/docker-entrypoint.s"   About an hour ago   Up About an hour    172.28.128.5:8080->3000/tcp   swarm-node01/composeandswarm_redmine_1

ブラウザを開き、172.28.128.5:8080にアクセスするとRedmineにアクセスできる。

クラスタの状態を確認する

ここまででコンテナクラスタとコンテナのマルチホスト配置が完了した。ここで、クラスタの状態をネットワークの観点から確認する。 クラスタが所持する全ネットワークは以下の通り。composeandswarmという名のoverlayネットワークはcomposeで作られたもの。

➤  docker network ls
NETWORK ID          NAME                           DRIVER
d60b430ccffa        swarm-master/host              host
7b313d0cf852        swarm-node01/none              null
0416227693dd        swarm-node01/docker_gwbridge   bridge
bd0d120a4a26        swarm-node01/bridge            bridge
26772f8d0e77        swarm-node02/host              host
a775f144fcb7        swarm-node02/docker_gwbridge   bridge
db2a4af0fcb8        composeandswarm                overlay
77e34c859d90        swarm-master/none              null
20ab037e4b49        swarm-node02/bridge            bridge
edd9de3b93b5        swarm-node02/none              null
5f5b2c79da0d        swarm-master/bridge            bridge
5ce93a14dfbe        swarm-node01/host              host

swarm-node01のネットワーク調査

swarm-node01のネットワーク情報を確認する。

➤  eval "$(docker-machine env swarm-node01)"
➤  docker network ls
NETWORK ID          NAME                DRIVER
db2a4af0fcb8        composeandswarm     overlay
bd0d120a4a26        bridge              bridge
7b313d0cf852        none                null
5ce93a14dfbe        host                host
0416227693dd        docker_gwbridge     bridge

➤  docker exec composeandswarm_redmine_1 ip addr
1: lo:  mtu 65536 qdisc noqueue state UNKNOWN group default
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
11: eth0:  mtu 1450 qdisc noqueue state UP group default
    link/ether 02:42:0a:00:00:02 brd ff:ff:ff:ff:ff:ff
    inet 10.0.0.2/24 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:aff:fe00:2/64 scope link
       valid_lft forever preferred_lft forever
14: eth1:  mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.2/16 scope global eth1
       valid_lft forever preferred_lft forever
    inet6 fe80::42:acff:fe12:2/64 scope link
       valid_lft forever preferred_lft forever

これをみるに、eth0でipが10.0.0.2のインタフェースが割り当てられていることが確認できる。ネットワークレンジを指定しない場合、overlayネットワークはデフォルトで10.0.0.0/24のアドレス帯になるので、これがoverlayネットワークで利用されるインターフェースのようだ。

さらにRedmineコンテナから別ホストのPostGreSQLコンテナに向けたPingを打ってみる。

➤  docker exec -it composeandswarm_redmine_1 bash
root@94ec7d822ac7:/usr/src/redmine# ping 10.0.0.3
PING 10.0.0.3 (10.0.0.3): 56 data bytes
64 bytes from 10.0.0.3: icmp_seq=0 ttl=64 time=0.855 ms
64 bytes from 10.0.0.3: icmp_seq=1 ttl=64 time=0.656 ms
64 bytes from 10.0.0.3: icmp_seq=2 ttl=64 time=0.639 ms

無事通信できた。さらに、/etc/hostsを確認するとPostgreSQLのホスト名とip情報が登録されている。

root@94ec7d822ac7:/usr/src/redmine# cat /etc/hosts
10.0.0.2        94ec7d822ac7
127.0.0.1       localhost
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
10.0.0.3        composeandswarm_db_1
10.0.0.3        composeandswarm_db_1.composeandswarm

無論、ホスト名でも名前解決が出来るためPingが通る。

root@94ec7d822ac7:/usr/src/redmine# ping composeandswarm_db_1
PING composeandswarm_db_1 (10.0.0.3): 56 data bytes
64 bytes from 10.0.0.3: icmp_seq=0 ttl=64 time=0.474 ms
64 bytes from 10.0.0.3: icmp_seq=1 ttl=64 time=0.630 ms
64 bytes from 10.0.0.3: icmp_seq=2 ttl=64 time=0.787 ms### node02

swarm-node02のネットワーク調査

同様にswarm-node02のネットワーク情報も確認する。

➤  eval "$(docker-machine env swarm-node02)"
➤  docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
f9995dc07765        postgres:9.5        "/docker-entrypoint.s"   20 minutes ago      Up 20 minutes       5432/tcp            composeandswarm_db_1
1c68829a3cb0        swarm:latest        "/swarm join --advert"   41 minutes ago      Up 41 minutes       2375/tcp            swarm-agent

➤  docker network ls
NETWORK ID          NAME                DRIVER
db2a4af0fcb8        composeandswarm     overlay
26772f8d0e77        host                host
a775f144fcb7        docker_gwbridge     bridge
20ab037e4b49        bridge              bridge
edd9de3b93b5        none                null

この時、overlayネットワークのIDがswarm-node01のoverlayネットワークのIDと同一であることから同じネットワークであることがわかる。 インターフェースを確認するとeth0に10.0.0.3が振られている。

➤  docker exec composeandswarm_db_1 ip addr
1: lo:  mtu 65536 qdisc noqueue state UNKNOWN group default
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
9: eth0:  mtu 1450 qdisc noqueue state UP group default
    link/ether 02:42:0a:00:00:03 brd ff:ff:ff:ff:ff:ff
    inet 10.0.0.3/24 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:aff:fe00:3/64 scope link
       valid_lft forever preferred_lft forever
12: eth1:  mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.2/16 scope global eth1
       valid_lft forever preferred_lft forever
    inet6 fe80::42:acff:fe12:2/64 scope link
       valid_lft forever preferred_lft forever

Redmineへの通信とホスト情報の確認をする。

➤  docker exec -it composeandswarm_db_1 bash

root@f9995dc07765:/# ping 10.0.0.2
PING 10.0.0.2 (10.0.0.2): 56 data bytes
64 bytes from 10.0.0.2: icmp_seq=0 ttl=64 time=2.140 ms
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=1.365 ms
64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=1.340 ms

root@f9995dc07765:/# cat /etc/hosts
10.0.0.3        f9995dc07765
127.0.0.1       localhost
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
10.0.0.2        composeandswarm_redmine_1
10.0.0.2        composeandswarm_redmine_1.composeandswarm

overlayネットワークでコンテナ間通信をする問はホスト名やFQDNが自動で登録されるので、ipは気にしないで運用することになりそうだ。

今回はここまで。

参考

consul + swarm

Docker Swarm + Docker Compose

Network関連情報

consul in production

Name space