富山のホームページ製作会社・グリーク スタッフブログ - ブログ -
  1. グリークトップ
  2. プログラマー
  3. ブログ

Docker 1.12.1 Swarmモードによるクラスタ環境の構築

Index

Docker Swarmモードの概要

Dockerがインストールされたマシン複数台から簡単にクラスタを構築することができます。

feature_image

1.12以前に存在した同名のプラグインとは異なり、Docker本体に統合されました。
新しいバージョンでは追加のセットアップなしでクラスタを構築することができます。
また、以前はサービスディスカバリーをconsulやetcdに依存していましたが、今後は外部ツールも不要になります。

overview

Swarmに参加するノードはmanagerとworkerの役割を持ちます。
基本的ににmanagerはノードの管理やSwarmの操作を行い、workerへコンテナを配置します。
Swarmモードでイメージを動かす場合はサービスという単位になり、1つのサービスで複数のコンテナにスケールさせることができます。
複数コンテナを動かす場合は複数のノードに分散されて、かつ内部ネットワークによるラウンドロビンが行われます。

サービスが外部にポートを公開する場合、Swarm内の全てのノードからアクセス可能になります。
例えばNginxコンテナを80番ポートで公開する場合、どのノードにアクセスしても(別ノードも含め)かならずどこかのコンテナに到達します。

サービスのデプロイはmanagerノードに対して行います。イメージがprivateレジストリに保管されている場合、事前にレジストリへのログインが必要になります。(docker login)
Swarm内ではmanagerノードの認証情報が共有されるため、事前にmanagerでレジストリにログインしていればworkerに対してはログインの操作が不要になります。

Dockerのインストール

Mac

公式のDocker for Macをインストールします。

https://www.docker.com/products/docker#/mac

Linux

Githubのreleasesページにある通り、ワンライナーでインストールができます。
インストーラーはaptやyumをつかってパッケージをインストールするため、更新する場合は
通常通りパッケージシステムを利用します。(Ubuntuであればapt-get upgrade)

$ curl -fsSL https://get.docker.com/ | sh

Vagrantによるテスト環境の構築

Swarmモードの動作を確認するため、Ubuntu Xenialのマシンを4台準備します。

  • node1 – managerノードとして動作させる
  • node2 – manager/workerノードとして動作させる
  • node3 – manager/workerノードとして動作させる
  • node4 – workerノードとして動作させる

この4台でSwarmを構成し、サービスをnode2〜3に配置してみます。
次のようなVagrantファイルを用意します。

# -*- mode: ruby -*-
# vi: set ft=ruby :

PROVISION_DOCKER = <<-SHELL
  apt-get update -y
  apt-get install -y apt-transport-https ca-certificates wget unzip curl
  curl -fsSL https://get.docker.com/ | sh
SHELL

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/xenial64"

  config.vm.define "node1" do |node|
    node.vm.hostname = "node1"
    node.vm.provider "virtualbox" do |vb|
      vb.name = "test-node1"
      vb.memory = "512"
    end
    node.vm.network "public_network"
    node.vm.network :private_network, ip: "192.168.20.2", virtualbox__intnet: "intnet"
    node.vm.provision "shell", inline: PROVISION_DOCKER
  end

  config.vm.define "node2" do |node|
    node.vm.hostname = "node2"
    node.vm.provider "virtualbox" do |vb|
      vb.name = "test-node2"
      vb.memory = "512"
    end
    node.vm.network "public_network"
    node.vm.network :private_network, ip: "192.168.20.3", virtualbox__intnet: "intnet"
    node.vm.network "forwarded_port", guest: 8002, host: 80
    node.vm.provision "shell", inline: PROVISION_DOCKER
  end

  config.vm.define "node3" do |node|
    node.vm.hostname = "node3"
    node.vm.provider "virtualbox" do |vb|
      vb.name = "test-node3"
      vb.memory = "512"
    end
    node.vm.network "public_network"
    node.vm.network :private_network, ip: "192.168.20.4", virtualbox__intnet: "intnet"
    node.vm.network "forwarded_port", guest: 8003, host: 80
    node.vm.provision "shell", inline: PROVISION_DOCKER
  end

  config.vm.define "node4" do |node|
    node.vm.hostname = "node4"
    node.vm.provider "virtualbox" do |vb|
      vb.name = "test-node4"
      vb.memory = "512"
    end
    node.vm.network "public_network"
    node.vm.network :private_network, ip: "192.168.20.5", virtualbox__intnet: "intnet"
    node.vm.network "forwarded_port", guest: 8004, host: 80
    node.vm.provision "shell", inline: PROVISION_DOCKER
  end
end
  • クラスタを組むためプライベートネットワークを組んでいます。
  • node2〜3は80番をLISTENするため、ホスト側からは8002〜8004でアクセスできるようにしています。

マシンを起動します。

$ vagrant up

NICの選択を求められた場合は選択します。(この場合は1を入力します)
4台とも設定します。

1) en1: Wi-Fi (AirPort)
2) en0: Ethernet
3) en2: Thunderbolt 1
4) en3: Thunderbolt 2
5) p2p0
6) awdl0
7) bridge0
==> node1: When choosing an interface, it is usually the one that is
==> node1: being used to connect to the internet.
    node1: Which interface should the network bridge to?

この4台をつかってSwarmの動作を確認していきます。

Swarmの作成

1台目の設定 (manager)

まずは1台目にログインし、最初のmanagerノードとしてswarmを設定します。

$ vagrant ssh node1

クラスタの作成はswarm initコマンドで行います。

$ docker swarm init --listen-addr 192.168.20.2:2377 --advertise-addr enp0s9
  • --listen-addrを省略した場合は、0.0.0.0:2377となります
  • --advertise-addrはネットワークインターフェースが複数あってswarmの作成に失敗する場合に指定します。
Swarm initialized: current node (0p7lz9f1pq3u6wgjzx1dod68g) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join \
    --token SWMTKN-1-00ghkeiw4icxq7gcztweg1v4q957ltrcsmamrpv268kt1hmrj8-99dqblba4hfcx0iv4z3ttw6o8 \
    192.168.20.2:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

initを実行するとworkerを参加させるための情報が表示されます。あとからdocker swarm join-token workerを実行しても同じ情報を見ることができます。

dockerコマンド全般で下記のようなエラーが表示されることがあります。

Cannot connect to the Docker daemon. Is the docker daemon running on this host?

sudoつけて実行すればエラーは出なくなりますが、一般ユーザーでdockerを操作できるようにも設定できます。(下記コマンド実行後、ubuntuユーザーは再ログインするまで設定が反映されません)

sudo usermod -aG docker ubuntu

managerノードを参加させるための情報を取得します。

$ docker swarm join-token manager
To add a manager to this swarm, run the following command:

    docker swarm join \
    --token SWMTKN-1-00ghkeiw4icxq7gcztweg1v4q957ltrcsmamrpv268kt1hmrj8-1dwczar3zo6sqsrx3ilggx9sm \
    192.168.20.2:2377

ノードの状態を確認します。

$ docker node ls
ID                           HOSTNAME  STATUS  AVAILABILITY  MANAGER STATUS
0p7lz9f1pq3u6wgjzx1dod68g *  node1     Ready   Active        Leader

managerノードが1台参加しただけですが、swarmが作成できました。

2台目以降の設定 (manager/worker)

2台目以降は先程セットアップしたクラスタに参加させます。

$ vagrant ssh node2

既存のクラスタへ参加する場合はswarm joinコマンドです。
アドレス:ポートは自分自身のものではなく、1台目を指定しますのでご注意ください。
※正確には既存のmanagerノードのアドレスなので、managerが複数ある場合はどれを指定してもOKです。

$ docker swarm join \
    --token SWMTKN-1-00ghkeiw4icxq7gcztweg1v4q957ltrcsmamrpv268kt1hmrj8-1dwczar3zo6sqsrx3ilggx9sm \
    192.168.20.2:2377

このマシンにログインしたまま、再度ノードの状態を確認します。

$ docker node ls
ID                           HOSTNAME  STATUS  AVAILABILITY  MANAGER STATUS
0jo3cnui3dp55w1rhyk0hkyml *  node2     Ready   Active        Reachable
0p7lz9f1pq3u6wgjzx1dod68g    node1     Ready   Active        Leader

ノードが増えていることが確認できます。これは1台目のマシンでも同じ結果になります。
*マークは現在操作しているノードを表します。

同様に3台目、4台目もクラスタに参加させるとこのようになります。

ID                           HOSTNAME  STATUS  AVAILABILITY  MANAGER STATUS
0jo3cnui3dp55w1rhyk0hkyml    node2     Ready   Active        Reachable
0p7lz9f1pq3u6wgjzx1dod68g *  node1     Ready   Active        Leader
681nqi767fqk2py5rc5ouhquj    node3     Ready   Active        Reachable
dmzwggyecxwgfzxth8kf00it8    node4     Ready   Active        Reachable

ノードの役割

ここまでの例でnodeの役割としてmangerworkerの2種類あると簡単に紹介してきました。
具体的に役割の違いを説明します。

managerの役割

アプリケーションをデプロイする場合には、managerノードにサービスを定義します。
managerはタスクをworkerに振り分けます。
特に指定をしなければmanager自身もworkerとして動作し、タスク振り分けの対象となります。
したがって先程構築した4台は全てがmasterであり、かつworkerであります。

managerの中からは1台リーダーが選出されます。
リーダーがダウンした場合は生存しているmanagerから再度リーダーが選出されます。
ノード間はGossip Protocolによる情報共有が行われます。

workerの役割

workerはmanagerからのタスクを受け取り実行します。

manager専用ノードとしての設定

デフォルトの動作ではクラスタに参加した時点でmanagerであり、workerです。
managerとして動かしたいが、コンテナを展開したくない場合はAvailabilityを変更します。

node1をmanager専用にする場合、availabilityをdrainに変更します。

$ docker node update --availability drain node1
$ docker node ls
ID                           HOSTNAME  STATUS  AVAILABILITY  MANAGER STATUS
0jo3cnui3dp55w1rhyk0hkyml    node2     Ready   Active        Reachable
0p7lz9f1pq3u6wgjzx1dod68g *  node1     Ready   Drain         Leader
681nqi767fqk2py5rc5ouhquj    node3     Ready   Active        Reachable
dmzwggyecxwgfzxth8kf00it8    node4     Ready   Active        Reachable

これによりこのノードはタスクを振り分けられることがなくなります。

worker専用ノードとしての設定

managerの役割が不要なノードは権限を降格させることで、worker専用として動作させることができます。
この場合は役割を変更するコマンドを用います。

node4をworker専用に変更します。

$ docker node demote node4
$ docker node ls
ID                           HOSTNAME  STATUS  AVAILABILITY  MANAGER STATUS
0jo3cnui3dp55w1rhyk0hkyml    node2     Ready   Active        Reachable
0p7lz9f1pq3u6wgjzx1dod68g *  node1     Ready   Drain         Leader
681nqi767fqk2py5rc5ouhquj    node3     Ready   Active        Reachable
dmzwggyecxwgfzxth8kf00it8    node4     Ready   Active  

これによりnode4はmanagerから離脱してworkerとしてのみ動作するようになります。

Swarmへのサービス配置

managerノードのどれかにログインして、docker serviceコマンドをつかってサービスを配置することができます。
ただしいくつか制約があり、既存のイメージをSwarmを配置しようと考えているかたは以下の点に注意が必要です。

制約・注意点など

1) Dockerイメージはレジストリに配置する必要があります

イメージをサービスとして登録する場合、各workerはDocker Registryからイメージをpullすることでコンテナを展開します。したがってDockerfileから直接サービスを起動することはできません。
通常はDockerhubを利用することになると思いますが、無料の範囲ではprivateリポジトリの数が足りなくなることがほとんどだと思います。そうなった場合は自前でprivateレジストリを構築する必要があります。

Docker Registryを構築するためのイメージがありますので、別途次のようにコンテナを起動すればとりあえずレジストリを用意することはできます。

$ docker run -d -p 5000:5000 --restart=always --name registry registry:2

2) docker-composeとの互換性はありません

一度に複数のコンテナを作成し、コンテナ間通信をさせたりログを集約したい場合、docker-composeをつかうのではないでしょうか?
Swarmモードではdocker-compose.ymlをつかってサービスを定義することができません。

例外として**docker stack**というものがあります。docker-compose.ymlからスタックと呼ばれる
サービスの集合を作成し、一括デプロイができます。
ただ、各サービスのインスタンス数が設定できなかったり、docker-composeのように依存関係を定義することができません。
serviceの性質上、コンテナ同士の依存関係をもたせられないことは自然なことですが、初期インスタンス数の設定ができない点は非常に不便であると思います。
自動デプロイツールなどでサービスを更新するたびに毎回インスタンス数を変えられるかもしれませんが、通常は負荷に応じてインスタンス数を増減させると思いますので、それが毎回リセットされるのは厳しいのではないかと思います。

現状でdocker stack deployを本番環境に導入することはおすすめできません。

サービスの起動

まずはSwarmがどのような動きをするか確認するために、nginxを動かしてみます。
6つのレプリカを作成し、どのようにノードに配置されるか見てみます。

サービスの登録はdocker serviceコマンドを使用します。

$ docker service create --publish 80:80 --replicas 6 --name nginx nginx:latest

出力されるのはサービスIDです。このわかりにくい文字列の代わりに、これ以降は--name nginxで指定したnginxという名前を使います。

1rsljrs94cbi6j14vnzpfsgxw

それではサービスの状態を確認してみます。docker service lsコマンドで概要を確認することができます。
実行直後はまだレプリカ数が0の状態です。

$ docker service ls
ID            NAME   REPLICAS  IMAGE         COMMAND
1rsljrs94cbi  nginx  0/6       nginx:latest  

しばらく待つとこのように6つのコンテナが起動されます。

$ docker service ls
ID            NAME   REPLICAS  IMAGE         COMMAND
1rsljrs94cbi  nginx  6/6       nginx:latest  

より詳しく配置状況を確認するためにdocker service psコマンドを使います。

$ docker service ps nginx
ID                         NAME     IMAGE         NODE   DESIRED STATE  CURRENT STATE          ERROR
559hnrne5vkgp5t5cgu2wxm1l  nginx.1  nginx:latest  node4  Running        Running 8 minutes ago  
co4zz8us78h9tgxs057u13osn  nginx.2  nginx:latest  node3  Running        Running 8 minutes ago  
e4vfi78chjv9kjipdevom9hjw  nginx.3  nginx:latest  node4  Running        Running 8 minutes ago  
f4xhrquruvswo3tckm58mm55q  nginx.4  nginx:latest  node2  Running        Running 8 minutes ago  
77funqjlogu9sg7k6b5ag5y78  nginx.5  nginx:latest  node3  Running        Running 8 minutes ago  
4vhtjch6oahyaacwft94vxp0j  nginx.6  nginx:latest  node2  Running        Running 8 minutes ago  

ご覧の通り、コンテナが各ノードに均等に配置されていることがわかります。
仮にこの中の1台がダウンした場合、レプリカは4となります。Swarmはこういった場合に、サービスで定義されたレプリカ数6を維持するために生存しているノードへ新たに2つコンテナを配置します。

このサービスはコンテナの80番をホスト(この場合はVagrantマシンのこと)の80番にマップします。
Vagrantの設定とあわせて関係を整理すると次のようになります。

あなたのMac:8002 <--> node2:80 <--> nginx:80
あなたのMac:8003 <--> node3:80 <--> nginx:80
あなたのMac:8004 <--> node4:80 <--> nginx:80

ブラウザでhttp://localhost:8002http://localhost:8003http://localhost:8004へアクセスするといずれもnginxのデフォルトページが表示されると思います。
Docker Swarm内部でオーバーレイネットワークが構築され、各ノードの80番はどこかのコンテナの80番へ到達します。
このときノードをまたがってアクセスこともあります。
サービスの定義時に指定しなければデフォルトでingressネットワークが選択されるため、このような動作になります。ネットワークはdocker network lsで確認できます。

ノード間で通信をさせたい場合は、サービス名を使うことで可能になります。
特になにもしなくてもコンテナ内からサービス名による名前解決ができる状態になっています。
例えば、別にredisのサービスを起動したとして、redisのコンテナ内からnginxにアクセスしたい場合はnginxまたはnginx.1でIPアドレスが引ける状態です。
nginxの場合はラウンドロビンでどこかのコンテナに到達しますし、nginx.1であればnode4のコンテナに到達します。通常は.1などを付けてコンテナを指定することはないかと思います。

globalモードとreplicatedモード

実はSwarmでサービスを動かす場合には動作モードが2つあり、先程のnginxの例はreplicatedモードです。このモードでは指定したレプリカ数が均等に配置されます。特に指定しなければこれがデフォルトとなります。

もう一つはglobalモードで、--mode globalのように指定します。
globalの場合レプリカ数の指定はできず、必ず各ノードに1つのコンテナが配置されます。
具体的な例としては、リバースプロキシ用のnginxを各ノードに1つずつ配置し、アプリケーションはreplicatedモードでスケールさせながら配置する等でしょうか。

globalモードでの動作を確認してみます。既存のサービスはdocker service rmで削除します。

$ docker service rm nginx
$ docker service create --publish 80:80 --mode global --name nginx nginx:latest

配置状況を確認してみると、各ノードに1つずつ配置されていることがわかります。

$ docker service ls
ID            NAME   REPLICAS  IMAGE         COMMAND
281mkruth0mu  nginx  global    nginx:latest  
$ docker service ps nginx
ID                         NAME       IMAGE         NODE   DESIRED STATE  CURRENT STATE                   ERROR
a5o13zhsd3fqc8k3l35790omz  nginx      nginx:latest  node4  Running        Running less than a second ago  
8uooxf9c9iz9y5mct9o6no30j   \_ nginx  nginx:latest  node3  Running        Running less than a second ago  
evajbm1glyc8hpl73xkq23stu   \_ nginx  nginx:latest  node2  Running        Running 26 seconds ago  

イメージの更新とローリングアップデート

日々の運用ではアプリケーション更新などでDockerイメージ再配置が必要になってくると思います。
サービスの更新はdocker service updateコマンドで新しいイメージ(またはタグ)を指定することで各ノードのコンテナの破棄、再生成が行われます。

docker service update --image nginx:1.11.4

デフォルトの動作ではコンテナの削除・再生成が1つずつ順番に行われます。
これによりサービスを停止せずに段階的に最新版に切り替えることができます。
この動作は変更することができ、主に以下の2つのパラメータで調整を行います。

  • --update-delay: コンテナ更新後の待ち時間
  • --update-parallelism: 同時に更新するコンテナ数(デフォルト:1)、0に設定した場合は全てのコンテナを一斉に更新します。

例えば--update-delay 2 --update-parallelism 3を指定した場合、3つのコンテナを同時に更新し、次の3つまで2秒待ちます。

docker service updateは元々サービスの定義を更新するためのものですので、イメージの更新以外にも様々なオプションがあります。(環境変数の追加/削除、公開ポートの追加/削除、レプリカ数の変更..など)

オプションはコマンドのヘルプを確認することをおすすめします。

docker service update --help

高可用性について

SwarmではConsensus ProtocolとしてRaft Protocolが採用されています。
簡単に説明するとSwarmを維持するためには過半数(Quorum)のノードが生存している必要があります。
N=ノード数とすると、(N/2)+1がクラスタの維持に必要な台数です。

ノード数 Quorum 許容されるダウン数
1 0 0
2 2 0
3 2 1
4 3 1
5 3 2
6 4 2
7 4 3

この表からも分かる通り、最低3台managerノードが必要です。
3ノードの構成の場合、2台ダウンするとLeader選出が不可能になり、結果的にサービスの作成や更新が受け付けられなくなります。残った1台に必要なコンテナが全て配置されていればシステムは継続できるかもしれませんが、運次第となってしまいます。

この記事を書いたスタッフ
Kazuyuki Hayashi
Symfony / Node.js / React.js / Swift を中心にテクノロジー系の記事を書きます。趣味はコーヒーです。