はじめに

今さらジローではあるが、自分の中で Docker を絡めたクラスタ構成について気になっているので CoreOS やその関連技術を改めて勉強してみたいと思う。CoreOS 等の各種情報については参考にさせて頂いた記事がとても詳しく参考になるので、そちらを都度確認しつつ理解を深めていきたい。

今回は以下を学びたい。

  • Vagrant で CoreOS ノードを複数起動する
  • etcd と fleet をザクっと触る
  • fleet で Docker コンテナクラスタを管理する
  • 起動したコンテナのサービスを registrator で etcd に登録する
  • etcd に登録したサービスの情報を利用して HAProxy の設定に反映させる

構成

今回、教材として利用する構成は以下の通り。

20150810214038

参考

CoreOS on VirtualBox

vagrant up

git clone する。

$ git clone https://github.com/coreos/coreos-vagrant.git

3 台の CoreOS ノードを起動したいので Vagrantfile の以下を修正。

$ diff -u Vagrantfile.bk Vagrantfile
--- Vagrantfile.bk      2015-08-10 00:14:07.000000000 +0900
+++ Vagrantfile 2015-08-08 18:36:52.000000000 +0900
@@ -9,14 +9,14 @@
 CONFIG = File.join(File.dirname(__FILE__), "config.rb")

 # Defaults for config options defined in CONFIG
-$num_instances = 1
+$num_instances = 3
 $instance_name_prefix = "core"
 $update_channel = "alpha"
 $image_version = "current"
 $enable_serial_logging = false
 $share_home = false
 $vm_gui = false
-$vm_memory = 1024
+$vm_memory = 512
 $vm_cpus = 1
 $shared_folders = {}
 $forwarded_ports = {}

etcd のクラスタリングには discovery.etcd.io を利用したいので token を取得する。

$ curl -s https://discovery.etcd.io/new
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

出力された token を user-data に追記する。

$ diff -u user-data.sample user-data
--- user-data.sample    2015-08-08 18:33:32.000000000 +0900
+++ user-data   2015-08-08 18:46:53.000000000 +0900
@@ -5,12 +5,14 @@
     # generate a new token for each unique cluster from https://discovery.etcd.io/new
     # WARNING: replace each time you 'vagrant destroy'
     #discovery: https://discovery.etcd.io/
+    discovery: https://discovery.etcd.io/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
     addr: $public_ipv4:4001
     peer-addr: $public_ipv4:7001
   etcd2:
     #generate a new token for each unique cluster from https://discovery.etcd.io/new
     #discovery: https://discovery.etcd.io/
     # multi-region and multi-cloud deployments need to use $public_ipv4
+    # discovery: https://discovery.etcd.io/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
     advertise-client-urls: http://$public_ipv4:2379
     initial-advertise-peer-urls: http://$private_ipv4:2380
     # listen on both the official ports and the legacy ports

vagrant up する。

$ vagrant up

起動したら各ノードにログインする。

$ vagrant ssh core-01
$ vagrant ssh core-02
$ vagrant ssh core-03

いずれかのノードにて etcd のクラスタが構成されているかを確認する。

core@core-01 ~ $ etcdctl --version
etcdctl version 2.1.1
core@core-01 ~ $ etcdctl cluster-health
cluster is healthy
member 7aa2819a5e2a4c9cac1e3d4b87760f7f is healthy
member bc1eb30f9eb844648110fec1455db88e is healthy
member e1103feeae8744c9ab93dc662803ea66 is healthy

fleet でサービスを管理する

fleet クラスタ構成の確認

fleetctl list-machines を実行してクラスタの構成を確認する。

core@core-01 ~ $ fleetctl list-machines
MACHINE         IP              METADATA
7aa2819a...     172.17.8.101    -
bc1eb30f...     172.17.8.103    -
e1103fee...     172.17.8.102    -

unit ファイルには systemd の知識が必要になる…

fleetctl でサービスを管理する場合に unit という単位でサービスを管理することになるが、この unit 設定ファイルが systemd の unit 設定ファイルに fleet 独自の [X-Fleet] というセクションを加えた内容になる。以下は Docker コンテナを各 CoreOS ノードで起動することを前提とした unit ファイル。尚、詳細な記述方法や各オプションの意味についてはこちらこちら記事にとても詳しく記載されている。有難うございます。

[Unit]
Description=kappa-test
After=docker.service
Requires=docker.service

[Service]
ExecStartPre=-/usr/bin/docker kill kappa-test-%i
ExecStartPre=-/usr/bin/docker rm kappa-test-%i
ExecStart=/usr/bin/docker run --name=kappa-test-%i --hostname=kappa-test-%i -p 8000 centos:centos6 /bin/sh -c 'cd /tmp/ ; hostname -s > index.html ; python -m SimpleHTTPServer'
ExecStop=/usr/bin/docker stop kappa-test-%i

[Install]
WantedBy=multi-user.target

[X-Fleet]
X-Conflicts=kappa-test@*.service

上記例の %i については sysytemd.unit の man ページによると「ユニット名の中の “@” 文字とサフィックスの間の文字列を参照する」と記載されており、いまいち「ピン」と来ていないが実際にサービスを start させてコンテナを起動させてみると 1 から 3 の数値がコンテナ名等に展開されていることから、サービス起動時に各ノードに割り当てられる番号を %i という変数で参照することが出来るようだ。(※後述するが、厳密にはノードでは無くインスタンスに割り当てられた instance suffix を参照することが出来る)また、[X-Fleet] セクションの X-Conflicts オプションではユニット名をグロブマッチさせて同じユニットが起動しないように設定している。

詳細については Template unit files が詳しく解説されている。

上記例を kappa-test@.service というファイル名で保存しておいて、以降の手順を進める。

unit ファイルの登録と確認

先ほど保存した kappa-test@.service ファイルを fleetctl submit コマンドを利用して fleet に登録する。

fleetctl submit kappa-test@.service

登録内容(unit 一覧や内容)を確認する場合には以下のように確認する。

# list-unit-files で登録されている unit ファイル一覧を確認
core@core-01 ~ $ fleetctl list-unit-files --full
UNIT                    HASH                                            DSTATE          STATE           TARGET
kappa-test@.service     559024d01378ac913e63c18dfd75f7a58d1f0bf8        inactive        inactive        -

# cat で登録されている unit ファイルの内容を確認
core@core-01 ~ $ fleetctl cat kappa-test@.service
[Unit]
Description=kappa-test
After=docker.service
Requires=docker.service

[Service]
ExecStartPre=-/usr/bin/docker kill kappa-test-%i
ExecStartPre=-/usr/bin/docker rm kappa-test-%i
ExecStart=/usr/bin/docker run --name=kappa-test-%i --hostname=kappa-test-%i -p 8000 centos:centos6 /bin/sh -c 'cd /tmp/ ; hostname -s > index.html ; python -m SimpleHTTPServer'
ExecStop=/usr/bin/docker stop kappa-test-%i

[Install]
WantedBy=multi-user.target

[X-Fleet]
X-Conflicts=kappa-test-@*.service

ちなみに、登録済みの内容を更新したい場合には以下のような手順が必要になる。上書きは出来ない。

# 一覧確認
core@core-01 ~ $ fleetctl list-unit-files --full
UNIT                    HASH                                            DSTATE          STATE           TARGET
kappa-test@.service     559024d01378ac913e63c18dfd75f7a58d1f0bf8        inactive        inactive        -

# destroy で削除
core@core-01 ~ $ fleetctl destroy kappa-test@.service
Destroyed kappa-test@.service

# 既にサービスが起動している場合には stop でサービスを停止して unload してから destroy する
# stop
$ fleetctl stop kappa-test@{1..3}.service
Unit kappa-test@1.service loaded on 7aa2819a.../172.17.8.101
Unit kappa-test@3.service loaded on e1103fee.../172.17.8.102
Unit kappa-test@2.service loaded on bc1eb30f.../172.17.8.103
# unload
core@core-01 ~ $ fleetctl unload kappa-test@{1..3}.service
Unit kappa-test@1.service inactive
Unit kappa-test@2.service inactive
Unit kappa-test@3.service inactive
# destroy
core@core-01 ~ $ fleetctl destroy kappa-test@.service
Destroyed kappa-test@.service
core@core-01 ~ $ fleetctl destroy kappa-test@{1..3}.service
Destroyed kappa-test@1.service
Destroyed kappa-test@2.service
Destroyed kappa-test@3.service

# submit で再登録
core@core-01 ~ $ fleetctl submit kappa-test@.service

サービスの起動

登録した unit のサービスを起動する。サービスの起動には fleetctl start を利用する。

core@core-01 ~ $ fleetctl start kappa-test@1.service
Unit kappa-test@1.service launched on 7aa2819a.../172.17.8.101
core@core-01 ~ $ fleetctl start kappa-test@2.service
Unit kappa-test@2.service launched on bc1eb30f.../172.17.8.103
core@core-01 ~ $ fleetctl start kappa-test@3.service
Unit kappa-test@3.service launched on e1103fee.../172.17.8.102

fleetctl start の引数として kappa-test\@1.service 〜 kappa-test\@3.service を渡して起動している。この引数については以下のような意味がある。

  • @ はテンプレートから起動する(テンプレート:kappa-test\@.service)
  • 13 はサービスを起動するインスタンス(CoreOS ノードと必ずイコールではない)を識別する為の任意につけた番号となり、unit ファイル内から %i で参照することが出来る
    fleetctl list-units で確認する。
core@core-01 ~ $ fleetctl list-units
UNIT                    MACHINE                         ACTIVE  SUB
kappa-test@1.service    7aa2819a.../172.17.8.101        active  running
kappa-test@2.service    bc1eb30f.../172.17.8.103        active  running
kappa-test@3.service    e1103fee.../172.17.8.102        active  running

fleetctl status でも確認する。

core@core-01 ~ $ fleetctl status kappa-test@{1..3}.service
● kappa-test@1.service - kappa-test
   Loaded: loaded (/run/fleet/units/kappa-test@1.service; linked-runtime; vendor preset: disabled)
   Active: active (running) since Sun 2015-08-09 23:15:50 UTC; 26min ago
  Process: 19214 ExecStartPre=/usr/bin/docker rm kappa-test-%i (code=exited, status=0/SUCCESS)
  Process: 19206 ExecStartPre=/usr/bin/docker kill kappa-test-%i (code=exited, status=0/SUCCESS)
 Main PID: 19220 (docker)
   CGroup: /system.slice/system-kappax2dtest.slice/kappa-test@1.service
           └─19220 /usr/bin/docker run --name=kappa-test-1 --hostname=kappa-test-1 -p 8000 centos:centos6 /bin/sh -c cd /tmp/ ; hostname -s > index.html ; python -m SimpleHTTPServer

Aug 09 23:15:50 core-01 systemd[1]: Starting kappa-test...
Aug 09 23:15:50 core-01 docker[19206]: kappa-test-1
Aug 09 23:15:50 core-01 docker[19214]: kappa-test-1
Aug 09 23:15:50 core-01 systemd[1]: Started kappa-test.

● kappa-test@2.service - kappa-test
   Loaded: loaded (/run/fleet/units/kappa-test@2.service; linked-runtime; vendor preset: disabled)
   Active: active (running) since Sun 2015-08-09 23:16:00 UTC; 25min ago
  Process: 20600 ExecStartPre=/usr/bin/docker rm kappa-test-%i (code=exited, status=0/SUCCESS)
  Process: 20591 ExecStartPre=/usr/bin/docker kill kappa-test-%i (code=exited, status=0/SUCCESS)
 Main PID: 20609 (docker)
   CGroup: /system.slice/system-kappax2dtest.slice/kappa-test@2.service
           └─20609 /usr/bin/docker run --name=kappa-test-2 --hostname=kappa-test-2 -p 8000 centos:centos6 /bin/sh -c cd /tmp/ ; hostname -s > index.html ; python -m SimpleHTTPServer

Aug 09 23:16:00 core-03 systemd[1]: Starting kappa-test...
Aug 09 23:16:00 core-03 docker[20591]: kappa-test-2
Aug 09 23:16:00 core-03 docker[20600]: kappa-test-2
Aug 09 23:16:00 core-03 systemd[1]: Started kappa-test.

● kappa-test@3.service - kappa-test
   Loaded: loaded (/run/fleet/units/kappa-test@3.service; linked-runtime; vendor preset: disabled)
   Active: active (running) since Sun 2015-08-09 23:16:10 UTC; 25min ago
  Process: 21104 ExecStartPre=/usr/bin/docker rm kappa-test-%i (code=exited, status=0/SUCCESS)
  Process: 21095 ExecStartPre=/usr/bin/docker kill kappa-test-%i (code=exited, status=0/SUCCESS)
 Main PID: 21110 (docker)
   CGroup: /system.slice/system-kappax2dtest.slice/kappa-test@3.service
           └─21110 /usr/bin/docker run --name=kappa-test-3 --hostname=kappa-test-3 -p 8000 centos:centos6 /bin/sh -c cd /tmp/ ; hostname -s > index.html ; python -m SimpleHTTPServer

Aug 09 23:16:10 core-02 systemd[1]: Starting kappa-test...
Aug 09 23:16:10 core-02 docker[21095]: kappa-test-3
Aug 09 23:16:10 core-02 docker[21104]: kappa-test-3
Aug 09 23:16:10 core-02 systemd[1]: Started kappa-test.

念のためにコンテナにアクセスして確認する。

# コンテナを確認
core@core-01 ~ $ docker ps
CONTAINER ID        IMAGE                      COMMAND                CREATED             STATUS              PORTS                                            NAMES
e193b197dc14        centos:centos6             "/bin/sh -c 'cd /tmp   28 minutes ago      Up 28 minutes       0.0.0.0:32771->8000/tcp                          kappa-test-1
# コンテナで起動している Web サーバーにアクセス
core@core-01 ~ $ curl localhost:32771
kappa-test-1

サービス登録

せっかくなので registrator で

registrator を使って起動しているコンテナのサービスを etcd に登録してみる。

registrator の起動

registrator 自体も Docker コンテナで提供されているので以下のように起動する。

core@core-01 ~ $ docker run -d --name registrator -h $HOSTNAME -v /var/run/docker.sock:/tmp/docker.sock progrium/registrator -ip=172.17.8.101 etcd://172.17.8.101:4001/services

core@core-02 ~ $ docker run -d --name registrator -h $HOSTNAME -v /var/run/docker.sock:/tmp/docker.sock progrium/registrator -ip=172.17.8.102 etcd://172.17.8.101:4001/services

core@core-03 ~ $ docker run -d --name registrator -h $HOSTNAME -v /var/run/docker.sock:/tmp/docker.sock progrium/registrator -ip=172.17.8.103 etcd://172.17.8.101:4001/services

etcd のエンドポイントを指定する場合には etcd の leader ノードの IP を指定する必要があった。尚、leader ノードの確認は以下のように行う。

core@core-01 ~ $ curl http://127.0.0.1:4001/v2/leader
http://172.17.8.101:7001

登録されたサービスを確認

# registrator によって登録されているサービスを確認
core@core-01 ~ $ curl -s GET http://127.0.0.1:4001/v2/keys/services | jq .
{
  "action": "get",
  "node": {
    "key": "/services",
    "dir": true,
    "nodes": [
      {
        "key": "/services/centos",
        "dir": true,
        "modifiedIndex": 81615,
        "createdIndex": 81615
      },
      {
        "key": "/services/docker-discover-1936",
        "dir": true,
        "modifiedIndex": 81796,
        "createdIndex": 81796
      },
      {
        "key": "/services/docker-discover-8000",
        "dir": true,
        "modifiedIndex": 81797,
        "createdIndex": 81797
      }
    ],
    "modifiedIndex": 81615,
    "createdIndex": 81615
  }
}

# centos というキーに登録されているようなので確認する
core@core-01 ~ $ curl -s GET http://127.0.0.1:4001/v2/keys/services/centos | jq .
{
  "action": "get",
  "node": {
    "key": "/services/centos",
    "dir": true,
    "nodes": [
      {
        "key": "/services/centos/core-02:kappa-test-3:8000",
        "value": "172.17.8.102:32771",
        "modifiedIndex": 126869,
        "createdIndex": 126869
      },
      {
        "key": "/services/centos/core-01:kappa-test-1:8000",
        "value": "172.17.8.101:32771",
        "modifiedIndex": 126833,
        "createdIndex": 126833
      },
      {
        "key": "/services/centos/core-03:kappa-test-2:8000",
        "value": "172.17.8.103:32771",
        "modifiedIndex": 126851,
        "createdIndex": 126851
      }
    ],
    "modifiedIndex": 81615,
    "createdIndex": 81615
  }
}

docker-discover でサービス検出

docker-discover とは

etcd に登録されているサービスの情報を元にして HAProxy の設定を自動で生成して HAProxy を再起動する…こちらも Docker コンテナ。HAProxy も同梱されている。

ちょっと修正

どうやら docker-discover は docker-register という、これまたコンテナが etcd に登録する情報を利用することが前提の作りになっているようなので registrator で登録した情報を利用しようとした場合に少し修正が必要だったので、fork させて頂いて修正したものが以下。

俺の docker-discover 起動

core@core-01 ~ $ docker run -d --net host --name docker-discover -e ETCD_HOST=172.17.8.101:4001 -e SERVICE_PORT=8000 -p 8000:8000 -p 1936:1936 -t inokappa/docker-discover

core@core-02 ~ $ docker run -d --net host --name docker-discover -e ETCD_HOST=172.17.8.101:4001 -e SERVICE_PORT=8000 -p 8000:8000 -p 1936:1936 -t inokappa/docker-discover

core@core-03 ~ $ docker run -d --net host --name docker-discover -e ETCD_HOST=172.17.8.101:4001 -e SERVICE_PORT=8000 -p 8000:8000 -p 1936:1936 -t inokappa/docker-discover

-e SERVICE_PORT=8000 で HAProxy で公開するポートを指定する。

確認

正常。

core@core-01 ~ $ curl localhost:8000
kappa-test-1
core@core-01 ~ $ curl localhost:8000
kappa-test-2
core@core-01 ~ $ curl localhost:8000
kappa-test-3

1 インスタンスを停止(サービスを停止)してみる。

core@core-01 ~ $ fleetctl stop kappa-test@2.service
Unit kappa-test@2.service loaded on bc1eb30f.../172.17.8.103
core@core-01 ~ $ curl localhost:8000
kappa-test-3
core@core-01 ~ $ curl localhost:8000
kappa-test-1
core@core-01 ~ $ curl localhost:8000
kappa-test-3
core@core-01 ~ $ curl localhost:8000
kappa-test-1
core@core-01 ~ $

停止したインスタンスが復帰。

core@core-01 ~ $ fleetctl start kappa-test@2.service
Unit kappa-test@2.service launched on bc1eb30f.../172.17.8.103
core@core-01 ~ $ curl localhost:8000
kappa-test-1
core@core-01 ~ $ curl localhost:8000
kappa-test-2
core@core-01 ~ $ curl localhost:8000
kappa-test-3
core@core-01 ~ $ curl localhost:8000
kappa-test-1
core@core-01 ~ $ curl localhost:8000
kappa-test-2
core@core-01 ~ $ curl localhost:8000
kappa-test-3

その他メモ

ssh-agent を利用する

以下のようなエラーが出てしまうことがあるが、これは fleetctl が ssh-agent を利用して他のクラスタノードにアクセスを行う為。

core@core-01 ~ $ fleetctl status kappa-test@2.service
Error running remote command: SSH_AUTH_SOCK environment variable is not set. Verify ssh-agent is running. See https://github.com/coreos/fleet/blob/master/Documentation/using-the-client.md for help.

事前にノード間で公開鍵の交換を行っておく必要があり、以下のように ssh-agent も事前じ起動しておく必要がある。

# ssh-agent を実行
core@core-01 ~ $ ssh-agent
SSH_AUTH_SOCK=/tmp/ssh-TyEESXG6GkAI/agent.20079; export SSH_AUTH_SOCK;
SSH_AGENT_PID=20080; export SSH_AGENT_PID;
echo Agent pid 20080;

# 出力された内容を実行
core@core-01 ~ $ SSH_AUTH_SOCK=/tmp/ssh-TyEESXG6GkAI/agent.20079; export SSH_AUTH_SOCK;
core@core-01 ~ $ SSH_AGENT_PID=20080; export SSH_AGENT_PID;
core@core-01 ~ $ echo Agent pid 20080;
Agent pid 20080

# 秘密鍵を登録する
core@core-01 ~ $ ssh-add ~/.ssh/id_rsa
Identity added: /home/core/.ssh/id_rsa (rsa w/o comment)

ssh-agent は初体験。

discovery.etcd.io にアクセスして token の取得を自動化(vagrant up 時に)

vagrant up 時に discovery.etcd.io にアクセスして token を取得するには config.rb を以下のように修正する。

$ diff -u config.rb.sample~ config.rb
--- config.rb.sample~   Sat Mar 14 21:49:50 2015
+++ config.rb   Mon Aug 10 14:47:12 2015
@@ -3,18 +3,19 @@
 # To automatically replace the discovery token on 'vagrant up', uncomment
 # the lines below:
 #
-#if File.exists?('user-data') && ARGV[0].eql?('up')
-#  require 'open-uri'
-#  require 'yaml'
-#
-#  token = open($new_discovery_url).read
-#
-#  data = YAML.load(IO.readlines('user-data')[1..-1].join)
-#  data['coreos']['etcd']['discovery'] = token
-#
-#  yaml = YAML.dump(data)
-#  File.open('user-data', 'w') { |file| file.write("#cloud-confignn#{yaml}") }
-#end
+if File.exists?('user-data') && ARGV[0].eql?('up')
+  require 'open-uri'
+  require 'yaml'
+  require 'openssl'
+
+  token = open($new_discovery_url, :ssl_verify_mode => OpenSSL::SSL::VERIFY_NONE).read
+
+  data = YAML.load(IO.readlines('user-data')[1..-1].join)
+  data['coreos']['etcd']['discovery'] = token
+
+  yaml = YAML.dump(data)
+  File.open('user-data', 'w') { |file| file.write("#cloud-confignn#{yaml}") }
+end
 #

 #

まだまだ…

ザクッと

CoreOS や etcd ましては fleet 等はほぼ初体験でチンプンカンプンだったが、実際に動かすことでザクッと概要を掴めることが出来た。

勉強不足

fleet の unit を扱う場合に systemd の知識が必要になるのでちゃんと抑えておきたい。

サービス検出について

他のツールもありそうだが registrator を使うのは個人的な鉄板。今回検証した環境では etcd のエンドポイント指定が leader ノードの IP でないと正しくサービス登録が行われなかったのは謎。

元記事はこちら

Vagrant + CoreOS + etcd + fleet + docker + registrator + docker-discover メモ